From 127f47ed96422e82b5eb6ac622802beba81e519c Mon Sep 17 00:00:00 2001 From: Kevin Conner Date: Mon, 12 Jan 2026 08:18:17 -0800 Subject: [PATCH 1/2] feat: add checkpointing into the backfill so we can resume from the last completed, contiguous index Signed-off-by: Kevin Conner --- cmd/backfill-index/main.go | 331 +++++++++++++++++++++++++++++-------- tests/backfill-test.sh | 152 ++++++++++++++++- 2 files changed, 416 insertions(+), 67 deletions(-) diff --git a/cmd/backfill-index/main.go b/cmd/backfill-index/main.go index 6169b2a57..b685e06f1 100644 --- a/cmd/backfill-index/main.go +++ b/cmd/backfill-index/main.go @@ -19,10 +19,18 @@ we need to backfill missing entries into the database for the search API. It can also be used to populate an index storage backend from scratch. + The tool supports automatic checkpointing for both Redis and MySQL backends to allow + resuming interrupted backfills and to maintain progress across scheduled runs. + Use --checkpoint-interval to control how frequently checkpoints are saved + (e.g., --checkpoint-interval 100 saves every 100 entries). On subsequent runs, + the tool automatically continues from the last checkpoint unless --reset-checkpoint is set. + Checkpoints persist indefinitely for scheduled jobs. + To run: go run cmd/backfill-index/main.go --rekor-address
\ - --hostname --port --concurrency \ - --start --end [--dry-run] + --redis-hostname --redis-port --concurrency \ + --start --end \ + [--checkpoint-interval ] [--reset-checkpoint] [--checkpoint-key ] [--dry-run] */ package main @@ -31,7 +39,9 @@ import ( "bytes" "context" "crypto/tls" + "database/sql" "encoding/base64" + "encoding/json" "errors" "flag" "fmt" @@ -79,7 +89,15 @@ const ( PRIMARY KEY(PK), UNIQUE(EntryKey, EntryUUID) )` - maxInsertErrors = 5 + mysqlCheckpointTableStmt = `CREATE TABLE IF NOT EXISTS BackfillCheckpoint ( + CheckpointKey VARCHAR(255) NOT NULL, + LastCompletedIndex INT NOT NULL, + LastUpdated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY(CheckpointKey) + )` + mysqlCheckpointSaveStmt = `REPLACE INTO BackfillCheckpoint (CheckpointKey, LastCompletedIndex) VALUES (:key, :lastIndex)` + mysqlCheckpointLoadStmt = `SELECT LastCompletedIndex, LastUpdated FROM BackfillCheckpoint WHERE CheckpointKey = ?` + mysqlCheckpointDeleteStmt = `DELETE FROM BackfillCheckpoint WHERE CheckpointKey = ?` ) type provider int @@ -92,6 +110,10 @@ const ( type indexClient interface { idempotentAddToIndex(ctx context.Context, key, value string) error + saveCheckpoint(ctx context.Context, checkpointKey string, state checkpointState) error + loadCheckpoint(ctx context.Context, checkpointKey string) (*checkpointState, error) + deleteCheckpoint(ctx context.Context, checkpointKey string) error + supportsCheckpointing() bool } type redisClient struct { @@ -102,6 +124,15 @@ type mysqlClient struct { client *sqlx.DB } +type checkpointState struct { + LastCompletedIndex int `json:"last_completed_index"` + LastUpdated time.Time `json:"last_updated"` +} + +type checkpointUpdate struct { + index int +} + type headers map[string][]string func (h *headers) String() string { @@ -139,6 +170,9 @@ var ( versionFlag = flag.Bool("version", false, "Print the current version of Backfill MySQL") concurrency = flag.Int("concurrency", 1, "Number of workers to use for backfill") dryRun = flag.Bool("dry-run", false, "Dry run - don't actually insert into MySQL") + checkpointInterval = flag.Int("checkpoint-interval", 100, "Save checkpoint every N entries (0 to disable)") + resetCheckpoint = flag.Bool("reset-checkpoint", false, "Clear checkpoint and start from --start value") + checkpointKey = flag.String("checkpoint-key", "", "Custom Redis key for checkpoint (default: \"default\")") ) func main() { @@ -181,6 +215,9 @@ func main() { if *rekorAddress == "" { log.Fatal("rekor-address must be set") } + if *checkpointInterval < 0 { + log.Fatal("checkpoint-interval must be >= 0") + } log.Printf("running backfill index Version: %s GitCommit: %s BuildDate: %s", versionInfo.GitVersion, versionInfo.GitCommit, versionInfo.BuildDate) @@ -233,6 +270,9 @@ func getIndexClient(backend provider) (indexClient, error) { if _, err = dbClient.Exec(mysqlCreateTableStmt); err != nil { return nil, err } + if _, err = dbClient.Exec(mysqlCheckpointTableStmt); err != nil { + return nil, err + } return &mysqlClient{client: dbClient}, nil default: return nil, fmt.Errorf("could not create client for unexpected provider") @@ -245,36 +285,95 @@ func populate(indexClient indexClient, rekorClient *rekorclient.Rekor) (err erro group, ctx := errgroup.WithContext(ctx) group.SetLimit(*concurrency) - type result struct { - index int - parseErrs []error - insertErrs []error - } - var resultChan = make(chan result) - parseErrs := make([]int, 0) - insertErrs := make([]int, 0) - runningInsertErrs := 0 + var checkpointKeyName string + var checkpointChan chan checkpointUpdate + var checkpointDone chan struct{} + var useCheckpointing = *checkpointInterval > 0 && indexClient.supportsCheckpointing() + originalStartIndex := *startIndex - go func() { - for r := range resultChan { - if len(r.parseErrs) > 0 { - parseErrs = append(parseErrs, r.index) + if useCheckpointing { + checkpointKeyName = *checkpointKey + if checkpointKeyName == "" { + checkpointKeyName = "default" + } + + if *resetCheckpoint { + if err := indexClient.deleteCheckpoint(ctx, checkpointKeyName); err != nil { + log.Printf("Warning: failed to delete checkpoint: %v", err) + } else { + log.Println("Checkpoint reset - starting fresh") } - if len(r.insertErrs) > 0 { - insertErrs = append(insertErrs, r.index) + } else { + checkpoint, err := indexClient.loadCheckpoint(ctx, checkpointKeyName) + switch { + case err != nil: + log.Printf("Warning: failed to load checkpoint: %v", err) + case checkpoint != nil: + log.Printf("Resuming from checkpoint: last completed index %d", checkpoint.LastCompletedIndex) + *startIndex = checkpoint.LastCompletedIndex + 1 + if *startIndex > *endIndex { + log.Printf("Checkpoint at %d is already past end index %d - nothing to do", checkpoint.LastCompletedIndex, *endIndex) + return nil + } + log.Printf("Processing entries from index %d to %d", *startIndex, *endIndex) + default: + log.Printf("No checkpoint found - starting fresh from index %d to %d", *startIndex, *endIndex) } } - }() - defer func() { - close(resultChan) - if len(parseErrs) > 0 { - err = fmt.Errorf("failed to parse %d entries: %v", len(parseErrs), parseErrs) - } - if len(insertErrs) > 0 { - err = fmt.Errorf("failed to insert/remove %d entries: %v", len(insertErrs), insertErrs) - } - }() + checkpointChan = make(chan checkpointUpdate, *concurrency*2) + checkpointDone = make(chan struct{}) + + go func() { + defer close(checkpointDone) + + completedIndices := make(map[int]bool) + highestCompleted := *startIndex - 1 + saveCounter := 0 + + for update := range checkpointChan { + completedIndices[update.index] = true + + // Find highest contiguous completed index + if update.index == highestCompleted+1 { + for i := update.index; completedIndices[i]; i++ { + highestCompleted = i + delete(completedIndices, i) + } + } + + saveCounter++ + + if saveCounter >= *checkpointInterval { + state := checkpointState{ + LastCompletedIndex: highestCompleted, + LastUpdated: time.Now(), + } + saveCtx, saveCancel := context.WithTimeout(context.Background(), 5*time.Second) + if err := indexClient.saveCheckpoint(saveCtx, checkpointKeyName, state); err != nil { + // If save fails we will try to catch it next time + log.Printf("Warning: failed to save checkpoint: %v", err) + } else { + log.Printf("Checkpoint saved: last completed index %d", highestCompleted) + } + saveCancel() + saveCounter = 0 + } + } + + state := checkpointState{ + LastCompletedIndex: highestCompleted, + LastUpdated: time.Now(), + } + saveCtx, saveCancel := context.WithTimeout(context.Background(), 5*time.Second) + if err := indexClient.saveCheckpoint(saveCtx, checkpointKeyName, state); err != nil { + log.Printf("Warning: failed to save final checkpoint: %v", err) + } else { + log.Printf("Final checkpoint saved: last completed index %d", highestCompleted) + } + saveCancel() + }() + } for i := *startIndex; i <= *endIndex; i++ { index := i // capture loop variable for closure @@ -287,69 +386,61 @@ func populate(indexClient indexClient, rekorClient *rekorclient.Rekor) (err erro if errors.Is(err, context.Canceled) { return nil } - return fmt.Errorf("retrieving log uuid by index: %v", err) + return fmt.Errorf("retrieving log uuid by index %d: %w", index, err) } - var parseErrs []error - var insertErrs []error - defer func() { - if len(insertErrs) != 0 || len(parseErrs) != 0 { - fmt.Printf("Errors with log index %d:\n", index) - for _, e := range insertErrs { - fmt.Println(e) - } - for _, e := range parseErrs { - fmt.Println(e) - } - } else { - fmt.Printf("Completed log index %d\n", index) - } - }() + for uuid, entry := range resp.Payload { // uuid is the global UUID - tree ID and entry UUID e, _, _, err := unmarshalEntryImpl(entry.Body.(string)) if err != nil { - parseErrs = append(parseErrs, fmt.Errorf("error unmarshalling entry for %s: %w", uuid, err)) - continue + return fmt.Errorf("error unmarshalling entry at index %d for %s: %w", index, uuid, err) } keys, err := e.IndexKeys() if err != nil { - parseErrs = append(parseErrs, fmt.Errorf("error building index keys for %s: %w", uuid, err)) - continue + return fmt.Errorf("error building index keys at index %d for %s: %w", index, uuid, err) } for _, key := range keys { if err := indexClient.idempotentAddToIndex(ctx, key, uuid); err != nil { if errors.Is(err, context.Canceled) { return nil } - insertErrs = append(insertErrs, fmt.Errorf("error inserting UUID %s with key %s: %w", uuid, key, err)) - runningInsertErrs++ - if runningInsertErrs > maxInsertErrors { - resultChan <- result{ - index: index, - parseErrs: parseErrs, - insertErrs: insertErrs, - } - return fmt.Errorf("too many insertion errors") - } - continue + return fmt.Errorf("error inserting UUID %s with key %s at index %d: %w", uuid, key, index, err) } fmt.Printf("Uploaded entry %s, index %d, key %s\n", uuid, index, key) } } - resultChan <- result{ - index: index, - parseErrs: parseErrs, - insertErrs: insertErrs, + + if useCheckpointing { + select { + case checkpointChan <- checkpointUpdate{index: index}: + case <-ctx.Done(): + return nil + } } + fmt.Printf("Completed log index %d\n", index) return nil }) } err = group.Wait() + + if useCheckpointing { + close(checkpointChan) + <-checkpointDone + } + if err != nil { - return fmt.Errorf("error running backfill: %v", err) + if useCheckpointing { + log.Printf("Backfill failed with error (checkpoint saved, resume from last checkpoint on next run): %v", err) + } + return fmt.Errorf("error running backfill: %w", err) + } + + if useCheckpointing { + log.Printf("Backfill complete: processed %d entries, checkpoint persists for next run", *endIndex-originalStartIndex+1) + } else { + fmt.Println("Backfill complete") } - fmt.Println("Backfill complete") return nil } @@ -386,6 +477,61 @@ func (c *redisClient) idempotentAddToIndex(ctx context.Context, key, value strin return err } +// formatRedisCheckpointKey generates a Redis-specific key format for checkpoint storage +func (c *redisClient) formatRedisCheckpointKey(checkpointKey string) string { + return fmt.Sprintf("backfill/checkpoint/%s", checkpointKey) +} + +func (c *redisClient) saveCheckpoint(ctx context.Context, checkpointKey string, state checkpointState) error { + if *dryRun { + return nil + } + redisKey := c.formatRedisCheckpointKey(checkpointKey) + data, err := json.Marshal(state) + if err != nil { + return fmt.Errorf("marshaling checkpoint state: %w", err) + } + err = c.client.Set(ctx, redisKey, data, 0).Err() + if err != nil { + return fmt.Errorf("saving checkpoint to Redis: %w", err) + } + return nil +} + +func (c *redisClient) loadCheckpoint(ctx context.Context, checkpointKey string) (*checkpointState, error) { + redisKey := c.formatRedisCheckpointKey(checkpointKey) + data, err := c.client.Get(ctx, redisKey).Result() + if err == redis.Nil { + return nil, nil + } + if err != nil { + return nil, fmt.Errorf("loading checkpoint from Redis: %w", err) + } + + var state checkpointState + if err := json.Unmarshal([]byte(data), &state); err != nil { + return nil, fmt.Errorf("unmarshaling checkpoint state: %w", err) + } + return &state, nil +} + +func (c *redisClient) deleteCheckpoint(ctx context.Context, checkpointKey string) error { + if *dryRun { + return nil + } + redisKey := c.formatRedisCheckpointKey(checkpointKey) + err := c.client.Del(ctx, redisKey).Err() + if err != nil { + return fmt.Errorf("deleting checkpoint from Redis: %w", err) + } + log.Printf("Checkpoint deleted: %s", redisKey) + return nil +} + +func (c *redisClient) supportsCheckpointing() bool { + return true +} + func (c *mysqlClient) idempotentAddToIndex(ctx context.Context, key, value string) error { if *dryRun { return nil @@ -393,3 +539,58 @@ func (c *mysqlClient) idempotentAddToIndex(ctx context.Context, key, value strin _, err := c.client.NamedExecContext(ctx, mysqlWriteStmt, map[string]any{"key": key, "uuid": value}) return err } + +func (c *mysqlClient) saveCheckpoint(ctx context.Context, checkpointKey string, state checkpointState) error { + if *dryRun { + return nil + } + _, err := c.client.NamedExecContext(ctx, mysqlCheckpointSaveStmt, map[string]any{ + "key": checkpointKey, + "lastIndex": state.LastCompletedIndex, + }) + if err != nil { + return fmt.Errorf("saving checkpoint to MySQL: %w", err) + } + return nil +} + +func (c *mysqlClient) loadCheckpoint(ctx context.Context, checkpointKey string) (*checkpointState, error) { + var state checkpointState + var lastUpdatedBytes []byte + err := c.client.QueryRowContext(ctx, mysqlCheckpointLoadStmt, checkpointKey).Scan( + &state.LastCompletedIndex, + &lastUpdatedBytes, + ) + if err == sql.ErrNoRows { + return nil, nil + } + if err != nil { + return nil, fmt.Errorf("loading checkpoint from MySQL: %w", err) + } + + if len(lastUpdatedBytes) > 0 { + parsedTime, err := time.Parse("2006-01-02 15:04:05", string(lastUpdatedBytes)) + if err != nil { + return nil, fmt.Errorf("parsing LastUpdated timestamp: %w", err) + } + state.LastUpdated = parsedTime + } + + return &state, nil +} + +func (c *mysqlClient) deleteCheckpoint(ctx context.Context, checkpointKey string) error { + if *dryRun { + return nil + } + _, err := c.client.ExecContext(ctx, mysqlCheckpointDeleteStmt, checkpointKey) + if err != nil { + return fmt.Errorf("deleting checkpoint from MySQL: %w", err) + } + log.Printf("Checkpoint deleted: %s", checkpointKey) + return nil +} + +func (c *mysqlClient) supportsCheckpointing() bool { + return true +} diff --git a/tests/backfill-test.sh b/tests/backfill-test.sh index 2076999fd..3c657641f 100755 --- a/tests/backfill-test.sh +++ b/tests/backfill-test.sh @@ -50,7 +50,18 @@ remove_keys() { set +e } +clear_checkpoint() { + local checkpoint_key=${1:-default} + if [ "$INDEX_BACKEND" == "redis" ] ; then + redis_cli DEL "backfill/checkpoint/$checkpoint_key" 2>/dev/null || true + else + mysql_cli -e "DELETE FROM BackfillCheckpoint WHERE CheckpointKey='$checkpoint_key';" 2>/dev/null || true + fi +} + check_all_entries() { + local expected_redis=${1:-21} # 20 index keys + 1 checkpoint key + local expected_mysql=${2:-26} set -e for artifact in "${!expected_artifacts[@]}" ; do local expected_uuids="${expected_artifacts[$artifact]}" @@ -113,10 +124,10 @@ check_all_entries() { local expected_size local actual_size if [ "${INDEX_BACKEND}" == "redis" ] ; then - expected_size=20 + expected_size=$expected_redis actual_size=$(redis_cli DBSIZE) else - expected_size=26 + expected_size=$expected_mysql actual_size=$(mysql_cli -NB -e "SELECT COUNT(*) FROM EntryIndex;") fi if [ $expected_size -ne $actual_size ] ; then @@ -174,6 +185,9 @@ echo echo "##### Scenario 2: backfill last half of entries #####" echo +# Clear checkpoint since we're modifying the index +clear_checkpoint + remove_keys $(seq 6 12) # searching for artifact 0-5 should succeed, but searching for later artifacts should fail @@ -190,6 +204,9 @@ echo echo "##### Scenario 3: backfill sparse entries #####" echo +# Clear checkpoint since we're modifying the index +clear_checkpoint + remove_keys $(seq 2 2 12) # searching for odd artifacts should succeed, but searching for even artifacts should fail unless it was re-signed @@ -212,3 +229,134 @@ run_backfill $end_index check_all_entries echo "Scenario 4: SUCCESS" + +echo +echo "##### Scenario 5: checkpoint and resume #####" +echo + +# Start fresh, make sure we have a clean index +if [ "$INDEX_BACKEND" == "redis" ] ; then + redis_cli FLUSHALL +else + mysql_cli -e "DELETE FROM EntryIndex; DELETE FROM BackfillCheckpoint;" 2>/dev/null || true +fi + +echo "Initial backfill from 0 to $end_index with checkpointing" +set -e +if [ "$INDEX_BACKEND" == "redis" ] ; then + go run cmd/backfill-index/main.go --rekor-address $REKOR_ADDRESS \ + --redis-hostname $REDIS_HOST --redis-port $REDIS_PORT --redis-password $REDIS_PASSWORD \ + --concurrency 5 --start 0 --end $end_index \ + --checkpoint-interval 2 +else + go run cmd/backfill-index/main.go --rekor-address $REKOR_ADDRESS \ + --mysql-dsn "${MYSQL_USER}:${MYSQL_PASSWORD}@tcp(${MYSQL_HOST}:${MYSQL_PORT})/${MYSQL_DB}" \ + --concurrency 5 --start 0 --end $end_index \ + --checkpoint-interval 2 +fi +set +e + +# Capture initial checkpoint value for later +initial_checkpoint=$end_index + +echo "Adding more entries to the log..." +for i in $(seq 13 20) ; do + minisign -GW -p $testdir/mini${i}.pub -s $testdir/mini${i}.key + echo test${i} > $testdir/blob${i} + minisign -S -s $testdir/mini${i}.key -m $testdir/blob${i} + rekor_out=$(rekor-cli --rekor_server $REKOR_ADDRESS upload \ + --artifact $testdir/blob${i} \ + --pki-format=minisign \ + --public-key $testdir/mini${i}.pub \ + --signature $testdir/blob${i}.minisig \ + --format json) + uuid=$(echo $rekor_out | jq -r .Location | cut -d '/' -f 6) + expected_keys["$testdir/mini${i}.pub"]=$uuid + expected_artifacts["$testdir/blob${i}"]=$uuid +done + +set -e +loginfo=$(rekor-cli --rekor_server $REKOR_ADDRESS loginfo --format=json) +let new_end_index=$(echo $loginfo | jq .ActiveTreeSize)-1 +set +e + +echo "New entries added, log now has indices 0-$new_end_index" + +echo "Running backfill from 0 to $new_end_index (should resume from checkpoint)" +if [ "$INDEX_BACKEND" == "redis" ] ; then + go run cmd/backfill-index/main.go --rekor-address $REKOR_ADDRESS \ + --redis-hostname $REDIS_HOST --redis-port $REDIS_PORT --redis-password $REDIS_PASSWORD \ + --concurrency 5 --start 0 --end $new_end_index \ + --checkpoint-interval 2 2>&1 | tee /tmp/backfill-resume.log +else + go run cmd/backfill-index/main.go --rekor-address $REKOR_ADDRESS \ + --mysql-dsn "${MYSQL_USER}:${MYSQL_PASSWORD}@tcp(${MYSQL_HOST}:${MYSQL_PORT})/${MYSQL_DB}" \ + --concurrency 5 --start 0 --end $new_end_index \ + --checkpoint-interval 2 2>&1 | tee /tmp/backfill-resume.log +fi + +if ! grep -q "Resuming from checkpoint: last completed index $initial_checkpoint" /tmp/backfill-resume.log ; then + echo "Scenario 5: FAILED - Did not resume from checkpoint" + cat /tmp/backfill-resume.log + exit 1 +fi +echo "Verified: Backfill resumed from checkpoint index $initial_checkpoint" + +# Verify all entries are indexed and checkpoint is at the end +# After adding 8 new entries (13-20), total is 21 entries: +# Redis: 20 index + 16 new index + 1 checkpoint = 37 keys +# MySQL: 26 + (8 entries × 2 rows) = 42 rows +check_all_entries 37 42 + +if [ "$INDEX_BACKEND" == "redis" ] ; then + checkpoint_data=$(redis_cli GET "backfill/checkpoint/default") + final_checkpoint=$(echo $checkpoint_data | jq -r .last_completed_index) +else + final_checkpoint=$(mysql_cli -NB -e "SELECT LastCompletedIndex FROM BackfillCheckpoint WHERE CheckpointKey='default'") +fi + +if [ "$final_checkpoint" != "$new_end_index" ] ; then + echo "Scenario 5: FAILED - Checkpoint should be at end index $new_end_index, got $final_checkpoint" + exit 1 +fi +echo "Checkpoint at index $final_checkpoint (end of log)" + +echo "Testing --reset-checkpoint flag" +reset_end_index=5 +if [ "$INDEX_BACKEND" == "redis" ] ; then + go run cmd/backfill-index/main.go --rekor-address $REKOR_ADDRESS \ + --redis-hostname $REDIS_HOST --redis-port $REDIS_PORT --redis-password $REDIS_PASSWORD \ + --concurrency 5 --start 0 --end $reset_end_index \ + --checkpoint-interval 2 --reset-checkpoint 2>&1 | tee /tmp/backfill-reset.log +else + go run cmd/backfill-index/main.go --rekor-address $REKOR_ADDRESS \ + --mysql-dsn "${MYSQL_USER}:${MYSQL_PASSWORD}@tcp(${MYSQL_HOST}:${MYSQL_PORT})/${MYSQL_DB}" \ + --concurrency 5 --start 0 --end $reset_end_index \ + --checkpoint-interval 2 --reset-checkpoint 2>&1 | tee /tmp/backfill-reset.log +fi + +if grep -q "Resuming from checkpoint" /tmp/backfill-reset.log ; then + echo "Scenario 5: FAILED - Reset flag did not clear checkpoint" + exit 1 +fi +if ! grep -q "Checkpoint reset - starting fresh" /tmp/backfill-reset.log ; then + echo "Scenario 5: FAILED - Did not see checkpoint reset message" + exit 1 +fi + +if [ "$INDEX_BACKEND" == "redis" ] ; then + checkpoint_data=$(redis_cli GET "backfill/checkpoint/default") + reset_checkpoint=$(echo $checkpoint_data | jq -r .last_completed_index) +else + reset_checkpoint=$(mysql_cli -NB -e "SELECT LastCompletedIndex FROM BackfillCheckpoint WHERE CheckpointKey='default'") +fi + +if [ "$reset_checkpoint" != "$reset_end_index" ] ; then + echo "Scenario 5: FAILED - Checkpoint should be at end index $reset_end_index, got $reset_checkpoint" + exit 1 +fi +echo "Checkpoint reset and at index $reset_checkpoint (end of range)" + +check_all_entries 37 42 + +echo "Scenario 5: SUCCESS" From 91abf52446be10f98789b5f64d89c7c1d07f6c1f Mon Sep 17 00:00:00 2001 From: Kevin Conner Date: Mon, 12 Jan 2026 10:01:34 -0800 Subject: [PATCH 2/2] bug: fix workflow disk space issues, only build the current platform, and prune docker volumes between the harness tests. Signed-off-by: Kevin Conner --- .github/workflows/validate-release.yml | 1 + .goreleaser.ci.yml | 96 ++++++++++++++++++++++++++ release/release.mk | 4 +- tests/rekor-harness.sh | 4 ++ 4 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 .goreleaser.ci.yml diff --git a/.github/workflows/validate-release.yml b/.github/workflows/validate-release.yml index 018f82f2a..f5080b5a7 100644 --- a/.github/workflows/validate-release.yml +++ b/.github/workflows/validate-release.yml @@ -67,6 +67,7 @@ jobs: env: PROJECT_ID: honk-fake-project RUNTIME_IMAGE: gcr.io/distroless/static:debug-nonroot + GORELEASER_CONFIG: .goreleaser.ci.yml - name: check binaries run: | diff --git a/.goreleaser.ci.yml b/.goreleaser.ci.yml new file mode 100644 index 000000000..9a8b38101 --- /dev/null +++ b/.goreleaser.ci.yml @@ -0,0 +1,96 @@ +project_name: rekor +version: 2 + +env: + - GO111MODULE=on + - CGO_ENABLED=0 + - DOCKER_CLI_EXPERIMENTAL=enabled + - COSIGN_YES=true + +# Prevents parallel builds from stepping on eachothers toes downloading modules +before: + hooks: + - go mod tidy + - /bin/bash -c 'if [ -n "$(git --no-pager diff --exit-code go.mod go.sum)" ]; then exit 1; fi' +# if running a release we will generate the images in this step +# if running in the CI the CI env va is set by github action runner and we dont run the ko steps +# this is needed because we are generating files that goreleaser was not aware to push to GH project release + - /bin/bash -c 'if [ -z "$CI" ]; then make sign-container-release; fi' + +gomod: + proxy: true + +sboms: + - artifacts: binary + +builds: + - id: rekor-server-linux + binary: rekor-server-linux-{{ .Arch }} + no_unique_dist_dir: true + main: ./cmd/rekor-server + goos: + - linux + goarch: + - amd64 + flags: + - -trimpath + mod_timestamp: '{{ .CommitTimestamp }}' + ldflags: + - "{{ .Env.SERVER_LDFLAGS }}" + + - id: rekor-cli + binary: rekor-cli-{{ .Os }}-{{ .Arch }} + no_unique_dist_dir: true + main: ./cmd/rekor-cli + goos: + - linux + goarch: + - amd64 + flags: + - -trimpath + mod_timestamp: '{{ .CommitTimestamp }}' + ldflags: + - "{{ .Env.CLI_LDFLAGS }}" + +signs: + - id: rekor + signature: "${artifact}.sig" + cmd: cosign + args: ["sign-blob", "--output-signature", "${artifact}.sig", "--key", "gcpkms://projects/{{ .Env.PROJECT_ID }}/locations/{{ .Env.KEY_LOCATION }}/keyRings/{{ .Env.KEY_RING }}/cryptoKeys/{{ .Env.KEY_NAME }}/versions/{{ .Env.KEY_VERSION }}", "${artifact}"] + artifacts: binary + # Keyless + - id: rekor-keyless + signature: "${artifact}-keyless.sig" + certificate: "${artifact}-keyless.pem" + cmd: cosign + args: ["sign-blob", "--output-signature", "${artifact}-keyless.sig", "--output-certificate", "${artifact}-keyless.pem", "${artifact}"] + artifacts: binary + - id: checksum-keyless + signature: "${artifact}-keyless.sig" + certificate: "${artifact}-keyless.pem" + cmd: cosign + args: ["sign-blob", "--output-signature", "${artifact}-keyless.sig", "--output-certificate", "${artifact}-keyless.pem", "${artifact}"] + artifacts: checksum + +archives: + - format: binary + name_template: "{{ .Binary }}" + allow_different_binary_count: true + +checksum: + name_template: "{{ .ProjectName }}_checksums.txt" + +snapshot: + name_template: SNAPSHOT-{{ .ShortCommit }} + +release: + prerelease: allow # remove this when we start publishing non-prerelease or set to auto + draft: true # allow for manual edits + github: + owner: sigstore + name: rekor + footer: | + ### Thanks for all contributors! + + extra_files: + - glob: "./rekor*.yaml" diff --git a/release/release.mk b/release/release.mk index e99dca67b..b95ece474 100644 --- a/release/release.mk +++ b/release/release.mk @@ -2,6 +2,8 @@ # release section ################## +GORELEASER_CONFIG ?= .goreleaser.yml + # used when releasing together with GCP CloudBuild .PHONY: release release: @@ -10,7 +12,7 @@ release: # used when need to validate the goreleaser .PHONY: snapshot snapshot: - CLI_LDFLAGS="$(CLI_LDFLAGS)" SERVER_LDFLAGS="$(SERVER_LDFLAGS)" goreleaser release --skip=sign,publish --snapshot --clean --timeout 120m + CLI_LDFLAGS="$(CLI_LDFLAGS)" SERVER_LDFLAGS="$(SERVER_LDFLAGS)" goreleaser release --skip=sign,publish --snapshot --clean --config $(GORELEASER_CONFIG) --timeout 120m ########################### # sign section diff --git a/tests/rekor-harness.sh b/tests/rekor-harness.sh index 970f0be62..0ac0c391b 100755 --- a/tests/rekor-harness.sh +++ b/tests/rekor-harness.sh @@ -123,6 +123,10 @@ do echo "Tests passed successfully." echo "=======================================================" done + + # Clean up docker resources between server versions to free disk space + echo "Cleaning up docker resources..." + docker system prune -f --volumes done # Since we add two entries to the log for every test, once all tests are run we should have 2*(($NUM_VERSIONS_TO_TEST+1)^2) entries