Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34b73c9411 |
+3
-24
@@ -5,16 +5,7 @@
|
|||||||
FROM golangci/golangci-lint:v2.14.0@sha256:ad862ba6b3798cbe0fd9fd7408d498fd74fbd2623a92406b2fd3898faf0bf98f AS lint
|
FROM golangci/golangci-lint:v2.14.0@sha256:ad862ba6b3798cbe0fd9fd7408d498fd74fbd2623a92406b2fd3898faf0bf98f AS lint
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
# Without the time limit a stalled connection to the module proxy holds
|
RUN go mod download
|
||||||
# this step, and every build on the host sharing it, until the kernel
|
|
||||||
# drops the connection, which can take an hour. The limit is well above
|
|
||||||
# the three minutes a working download can take on a busy build host.
|
|
||||||
RUN for attempt in 1 2 3; do \
|
|
||||||
timeout 300 go mod download && exit 0; \
|
|
||||||
echo "go mod download attempt $attempt failed or ran over 300s" >&2; \
|
|
||||||
done; \
|
|
||||||
echo "module download failed: giving up after 3 attempts" >&2; \
|
|
||||||
exit 1
|
|
||||||
COPY . .
|
COPY . .
|
||||||
# `golangci-lint run` silently ignores an unknown top-level key in
|
# `golangci-lint run` silently ignores an unknown top-level key in
|
||||||
# .golangci.yml, such as a misspelt `linters:`; `config verify` fails on it.
|
# .golangci.yml, such as a misspelt `linters:`; `config verify` fails on it.
|
||||||
@@ -27,13 +18,7 @@ RUN golangci-lint run --config .golangci.yml ./...
|
|||||||
FROM golang:1.26.1@sha256:cd78d88e00afadbedd272f977d375a6247455f3a4b1178f8ae8bbcb201743a8a AS test
|
FROM golang:1.26.1@sha256:cd78d88e00afadbedd272f977d375a6247455f3a4b1178f8ae8bbcb201743a8a AS test
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
# Limited and retried as in the lint phase above.
|
RUN go mod download
|
||||||
RUN for attempt in 1 2 3; do \
|
|
||||||
timeout 300 go mod download && exit 0; \
|
|
||||||
echo "go mod download attempt $attempt failed or ran over 300s" >&2; \
|
|
||||||
done; \
|
|
||||||
echo "module download failed: giving up after 3 attempts" >&2; \
|
|
||||||
exit 1
|
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN go test -timeout 90s -race -cover ./... || \
|
RUN go test -timeout 90s -race -cover ./... || \
|
||||||
{ echo "--- Rerunning with -v for details ---"; \
|
{ echo "--- Rerunning with -v for details ---"; \
|
||||||
@@ -51,13 +36,7 @@ RUN apk add --no-cache git
|
|||||||
RUN git config --system --add safe.directory /src
|
RUN git config --system --add safe.directory /src
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
# Limited and retried as in the lint phase above.
|
RUN go mod download
|
||||||
RUN for attempt in 1 2 3; do \
|
|
||||||
timeout 300 go mod download && exit 0; \
|
|
||||||
echo "go mod download attempt $attempt failed or ran over 300s" >&2; \
|
|
||||||
done; \
|
|
||||||
echo "module download failed: giving up after 3 attempts" >&2; \
|
|
||||||
exit 1
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# The VERSION build arg when one is given, otherwise
|
# The VERSION build arg when one is given, otherwise
|
||||||
|
|||||||
@@ -339,9 +339,7 @@ local index alone, and still exits zero.
|
|||||||
|
|
||||||
**`snapshot purge`**: Remove old snapshots based on criteria. Retention is
|
**`snapshot purge`**: Remove old snapshots based on criteria. Retention is
|
||||||
per-snapshot-name (`--keep-latest` keeps the latest of each name, not the
|
per-snapshot-name (`--keep-latest` keeps the latest of each name, not the
|
||||||
latest globally). If a snapshot cannot be removed from the local index, the
|
latest globally).
|
||||||
command stops there, leaves that snapshot's metadata on the destination store
|
|
||||||
and exits non-zero; `snapshot create --prune` fails the same way.
|
|
||||||
* `--keep-latest`: Keep only the most recent snapshot of each name
|
* `--keep-latest`: Keep only the most recent snapshot of each name
|
||||||
* `--older-than <duration>`: Remove snapshots older than duration (e.g. `30d`,
|
* `--older-than <duration>`: Remove snapshots older than duration (e.g. `30d`,
|
||||||
`4w`, `6mo`, `1y`; `m` is minutes, `mo` is months)
|
`4w`, `6mo`, `1y`; `m` is minutes, `mo` is months)
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
package main_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestDockerfileBoundsEveryModuleDownload fails unless every
|
|
||||||
// `go mod download` in the Dockerfile runs under a time limit, is tried
|
|
||||||
// a fixed number of times, and then fails the build with a message that
|
|
||||||
// names the module download (issue #351). Like the version tests, this
|
|
||||||
// parses the Dockerfile; that a stalled download really is cut off and
|
|
||||||
// retried was checked by hand.
|
|
||||||
func TestDockerfileBoundsEveryModuleDownload(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
var downloads []string
|
|
||||||
|
|
||||||
for _, instruction := range instructions(t, productDockerfile) {
|
|
||||||
if strings.Contains(instruction, "go mod download") {
|
|
||||||
downloads = append(downloads, instruction)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
require.NotEmpty(t, downloads, "%s must download modules",
|
|
||||||
productDockerfile)
|
|
||||||
|
|
||||||
for _, download := range downloads {
|
|
||||||
assert.Contains(t, download, "timeout 300 go mod download",
|
|
||||||
"%s: a module download must have a time limit: %s",
|
|
||||||
productDockerfile, download)
|
|
||||||
assert.Contains(t, download, "for attempt in 1 2 3",
|
|
||||||
"%s: a module download must be retried: %s",
|
|
||||||
productDockerfile, download)
|
|
||||||
assert.Contains(t, download, "module download failed",
|
|
||||||
"%s: a module download that never succeeds must say so: %s",
|
|
||||||
productDockerfile, download)
|
|
||||||
assert.True(t, strings.HasSuffix(download, "exit 1"),
|
|
||||||
"%s: a module download that never succeeds must fail the"+
|
|
||||||
" build: %s", productDockerfile, download)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
package cli //nolint:testpackage // shares runEntry and the hermetic config helpers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"database/sql"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"sneak.berlin/go/vaultik/internal/database"
|
|
||||||
"sneak.berlin/go/vaultik/internal/snapshot"
|
|
||||||
"sneak.berlin/go/vaultik/internal/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
// The `snapshot purge` command word and flags used below.
|
|
||||||
const (
|
|
||||||
cmdPurge = "purge"
|
|
||||||
flagKeepLatest = "--keep-latest"
|
|
||||||
flagForce = "--force"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestEntrySnapshotPurgeKeepsMetadataWhenLocalIndexDeleteFails makes
|
|
||||||
// removing a snapshot from the local index fail and checks that
|
|
||||||
// `snapshot purge` exits non-zero and leaves that snapshot's metadata on
|
|
||||||
// the destination store. It used to log the error, delete the metadata
|
|
||||||
// anyway and exit 0.
|
|
||||||
//
|
|
||||||
//nolint:paralleltest // replaces os.Args, os.Stdout, os.Stderr and the xdg globals
|
|
||||||
func TestEntrySnapshotPurgeKeepsMetadataWhenLocalIndexDeleteFails(t *testing.T) {
|
|
||||||
ctx := context.Background()
|
|
||||||
configPath := writeHermeticPruneConfig(t, false)
|
|
||||||
|
|
||||||
// writeHermeticPruneConfig puts the index and the store directory
|
|
||||||
// beside the config file.
|
|
||||||
dir := filepath.Dir(configPath)
|
|
||||||
|
|
||||||
// Two snapshots of the same name: --keep-latest purges the older.
|
|
||||||
olderStart := time.Date(2026, 5, 1, 9, 0, 0, 0, time.UTC)
|
|
||||||
newerStart := olderStart.Add(time.Hour)
|
|
||||||
olderID := "test-host_test_" + olderStart.Format(time.RFC3339)
|
|
||||||
newerID := "test-host_test_" + newerStart.Format(time.RFC3339)
|
|
||||||
|
|
||||||
db, err := database.New(ctx, filepath.Join(dir, "index.sqlite"))
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
repos := database.NewRepositories(db)
|
|
||||||
|
|
||||||
manifestPaths := make(map[string]string)
|
|
||||||
|
|
||||||
for id, startedAt := range map[string]time.Time{
|
|
||||||
olderID: olderStart,
|
|
||||||
newerID: newerStart,
|
|
||||||
} {
|
|
||||||
completedAt := startedAt.Add(time.Minute)
|
|
||||||
snap := &database.Snapshot{
|
|
||||||
ID: types.SnapshotID(id),
|
|
||||||
Hostname: "test-host",
|
|
||||||
VaultikVersion: "test",
|
|
||||||
StartedAt: startedAt,
|
|
||||||
CompletedAt: &completedAt,
|
|
||||||
}
|
|
||||||
|
|
||||||
err = repos.WithTx(ctx, func(ctx context.Context, tx *sql.Tx) error {
|
|
||||||
return repos.Snapshots.Create(ctx, tx, snap)
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
manifestPaths[id] = filepath.Join(dir, "store", "metadata",
|
|
||||||
snapshot.RemoteSnapshotKey(id), "manifest.json.zst")
|
|
||||||
require.NoError(t, os.MkdirAll(filepath.Dir(manifestPaths[id]), 0o750))
|
|
||||||
require.NoError(t, os.WriteFile(manifestPaths[id], []byte("stub"), 0o600))
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = db.Conn().ExecContext(ctx, `
|
|
||||||
CREATE TRIGGER refuse_snapshot_delete BEFORE DELETE ON snapshots
|
|
||||||
BEGIN SELECT RAISE(ABORT, 'simulated index error'); END`)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NoError(t, db.Close())
|
|
||||||
|
|
||||||
code, _, stderr := runEntry(t,
|
|
||||||
flagConfig, configPath, cmdSnapshot, cmdPurge, flagKeepLatest, flagForce)
|
|
||||||
|
|
||||||
assert.Equal(t, 1, code)
|
|
||||||
assert.Contains(t, stderr, "simulated index error")
|
|
||||||
assert.FileExists(t, manifestPaths[olderID],
|
|
||||||
"a snapshot still in the local index keeps its metadata")
|
|
||||||
}
|
|
||||||
@@ -627,8 +627,7 @@ func selectSnapshotsToPurge(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// confirmAndExecutePurge shows deletion candidates, confirms with the
|
// confirmAndExecutePurge shows deletion candidates, confirms with the
|
||||||
// user, and deletes snapshots. It stops with an error at the first
|
// user, and deletes snapshots.
|
||||||
// snapshot it cannot delete.
|
|
||||||
func (v *Vaultik) confirmAndExecutePurge(
|
func (v *Vaultik) confirmAndExecutePurge(
|
||||||
toDelete []SnapshotInfo, force, quiet bool,
|
toDelete []SnapshotInfo, force, quiet bool,
|
||||||
) error {
|
) error {
|
||||||
@@ -670,11 +669,10 @@ func (v *Vaultik) confirmAndExecutePurge(
|
|||||||
snapshotID := snap.ID.String()
|
snapshotID := snap.ID.String()
|
||||||
log.Info("Deleting snapshot", "id", snapshotID)
|
log.Info("Deleting snapshot", "id", snapshotID)
|
||||||
|
|
||||||
// Stop before the remote delete: a snapshot still in the local
|
|
||||||
// index must keep its metadata on the destination store.
|
|
||||||
err := v.deleteSnapshotFromLocalDB(snapshotID)
|
err := v.deleteSnapshotFromLocalDB(snapshotID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("deleting snapshot %s from local index: %w", snapshotID, err)
|
log.Error("Failed to delete from local database",
|
||||||
|
"snapshot_id", snapshotID, "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = v.deleteRemoteSnapshotByKey(snapshot.RemoteSnapshotKey(snapshotID))
|
_, err = v.deleteRemoteSnapshotByKey(snapshot.RemoteSnapshotKey(snapshotID))
|
||||||
|
|||||||
Reference in New Issue
Block a user