Merge pull request #191 from replicatedhq/joshs/sidecar-followups

CI runs and metadata corrections
This commit is contained in:
Josh Sandlin
2026-07-30 19:01:56 -04:00
committed by GitHub
15 changed files with 204 additions and 37 deletions
+5
View File
@@ -8,6 +8,11 @@ updates:
schedule:
interval: "daily"
- package-ecosystem: "gomod"
directory: "/sidecar"
schedule:
interval: "weekly"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
+101
View File
@@ -0,0 +1,101 @@
name: CI
on:
pull_request:
branches: [main]
# sidecar/ is the only Go module in this repo; skip CI for changes that
# cannot affect it (web/, ansible/, docs).
paths:
- "sidecar/**"
- ".github/workflows/ci.yaml"
# Cancel in-progress runs for the same ref (PR or branch) when new commits land.
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
defaults:
run:
working-directory: sidecar
jobs:
lint:
name: Lint & format
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version-file: sidecar/go.mod
check-latest: true
cache-dependency-path: sidecar/go.sum
- name: Verify gofmt
run: |
unformatted=$(gofmt -l .)
if [ -n "$unformatted" ]; then
echo "::error::The following files are not gofmt-formatted:"
echo "$unformatted"
echo "Run 'gofmt -w .' to fix."
exit 1
fi
- name: go vet
run: go vet ./...
- name: Verify go.mod is tidy
run: |
go mod tidy
if ! git diff --exit-code -- go.mod go.sum; then
echo "::error::go.mod/go.sum are not tidy. Run 'go mod tidy' and commit the result."
exit 1
fi
- name: golangci-lint
uses: golangci/golangci-lint-action@v9
with:
version: v2.12.2
# `defaults.run` does not apply to actions, so point it at the module.
working-directory: sidecar
test:
name: Test
runs-on: ubuntu-latest
# No service container: internal/redistest starts its own Redis on the
# runner's Docker daemon. GitHub sets CI=true, which makes it fail rather
# than skip, so the persistence layer cannot drop out of this job.
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version-file: sidecar/go.mod
check-latest: true
cache-dependency-path: sidecar/go.sum
- name: Run tests (race + coverage)
run: go test -race -coverprofile=cover.out -covermode=atomic ./...
- name: Coverage summary
run: go tool cover -func=cover.out | tail -1
build:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version-file: sidecar/go.mod
check-latest: true
cache-dependency-path: sidecar/go.sum
- name: go build
env:
CGO_ENABLED: "0"
run: go build -ldflags="-s -w" ./...
+15 -14
View File
@@ -101,22 +101,23 @@ jobs:
runs-on: ubuntu-latest
needs: deploy
steps:
- name: Test push and pull
- name: Test push, pull, and expiry
run: |
IMAGE="ttl.sh/smoke-${{ github.sha }}:5m"
echo "🧪 Testing with image: $IMAGE"
# Use busybox - tiny image (~1MB)
IMAGE="ttl.sh/smoke-${{ github.sha }}:1m"
docker pull busybox:latest
docker tag busybox:latest "$IMAGE"
echo "📤 Pushing image..."
docker push "$IMAGE"
# Remove local copy to force pull from registry
docker rmi "$IMAGE"
echo "📥 Pulling image back..."
docker rmi "$IMAGE" busybox:latest
docker pull "$IMAGE"
echo "✅ Push/pull test passed!"
echo "✅ Push/pull passed"
docker rmi "$IMAGE"
sleep 90
if docker pull "$IMAGE"; then
echo "❌ image still pullable after TTL"
exit 1
fi
echo "✅ Image expired as expected"
+37
View File
@@ -0,0 +1,37 @@
name: Vulnerability check
on:
pull_request:
branches: [main]
# sidecar/ is the only Go module in this repo.
paths:
- "sidecar/**"
- ".github/workflows/govulncheck.yaml"
schedule:
# Weekly scan so newly disclosed CVEs in pinned deps surface even
# without a code change. Mondays at 07:00 UTC.
- cron: "0 7 * * 1"
concurrency:
group: govulncheck-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
govulncheck:
name: govulncheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
# The action still installs govulncheck itself at @latest.
- name: Run govulncheck
uses: golang/govulncheck-action@v1
with:
go-version-file: sidecar/go.mod
check-latest: true
cache-dependency-path: sidecar/go.sum
work-dir: sidecar
repo-checkout: false
+1 -1
View File
@@ -65,7 +65,7 @@ ARG BUILD_DATE
LABEL org.opencontainers.image.title="zot-ephemeral-ttl" \
org.opencontainers.image.description="Tag-driven TTL sidecar for zot. Subscribes to image.updated CloudEvents and DELETEs expired manifests." \
org.opencontainers.image.source="https://github.com/nullbytelabs/zot-ephemeral-ttl" \
org.opencontainers.image.source="https://github.com/replicatedhq/ttl.sh" \
org.opencontainers.image.licenses="Apache-2.0" \
org.opencontainers.image.version="${VERSION}" \
org.opencontainers.image.revision="${COMMIT}" \
+2 -2
View File
@@ -9,8 +9,8 @@ import (
"os/signal"
"syscall"
"github.com/nullbytelabs/zot-ephemeral-ttl/internal/app"
"github.com/nullbytelabs/zot-ephemeral-ttl/internal/config"
"github.com/replicatedhq/ttl.sh/sidecar/internal/app"
"github.com/replicatedhq/ttl.sh/sidecar/internal/config"
)
// Build metadata, set via -ldflags "-X main.version=..." in the Dockerfile.
+1 -1
View File
@@ -1,4 +1,4 @@
module github.com/nullbytelabs/zot-ephemeral-ttl
module github.com/replicatedhq/ttl.sh/sidecar
go 1.26
+25 -7
View File
@@ -13,11 +13,11 @@ import (
"net/http"
"time"
"github.com/nullbytelabs/zot-ephemeral-ttl/internal/config"
"github.com/nullbytelabs/zot-ephemeral-ttl/internal/reaper"
"github.com/nullbytelabs/zot-ephemeral-ttl/internal/registry"
"github.com/nullbytelabs/zot-ephemeral-ttl/internal/server"
"github.com/nullbytelabs/zot-ephemeral-ttl/internal/store"
"github.com/replicatedhq/ttl.sh/sidecar/internal/config"
"github.com/replicatedhq/ttl.sh/sidecar/internal/reaper"
"github.com/replicatedhq/ttl.sh/sidecar/internal/registry"
"github.com/replicatedhq/ttl.sh/sidecar/internal/server"
"github.com/replicatedhq/ttl.sh/sidecar/internal/store"
)
// storeBackend is the union of persistence capabilities app injects into the
@@ -56,8 +56,18 @@ func Run(ctx context.Context, cfg config.Config) error {
ReadHeaderTimeout: 10 * time.Second,
}
// The sweep loop gets its own cancellable context so it can be stopped on
// the way out even when shutdown was triggered by a server error rather
// than by ctx.
reaperCtx, stopReaper := context.WithCancel(ctx)
defer stopReaper()
rp := reaper.New(cfg.SweepInterval, st, registry.New(cfg.ZotURL))
go rp.Run(ctx)
reaperDone := make(chan struct{})
go func() {
defer close(reaperDone)
rp.Run(reaperCtx)
}()
serveErr := make(chan error, 1)
go func() {
@@ -76,5 +86,13 @@ func Run(ctx context.Context, cfg config.Config) error {
log.Printf("shutting down")
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
return httpSrv.Shutdown(shutdownCtx)
shutdownErr := httpSrv.Shutdown(shutdownCtx)
// Wait for an in-flight sweep to unwind before returning, so it cannot
// still be talking to Redis when the deferred Close runs. Cancelling
// reaperCtx aborts the delete request in flight, so this does not block for
// long.
stopReaper()
<-reaperDone
return shutdownErr
}
+2 -2
View File
@@ -6,8 +6,8 @@ import (
"testing"
"time"
"github.com/nullbytelabs/zot-ephemeral-ttl/internal/config"
"github.com/nullbytelabs/zot-ephemeral-ttl/internal/redistest"
"github.com/replicatedhq/ttl.sh/sidecar/internal/config"
"github.com/replicatedhq/ttl.sh/sidecar/internal/redistest"
)
// The startup-path test needs a real Redis; redistest starts one per package.
+1 -1
View File
@@ -9,7 +9,7 @@ import (
"log"
"time"
"github.com/nullbytelabs/zot-ephemeral-ttl/internal/store"
"github.com/replicatedhq/ttl.sh/sidecar/internal/store"
)
// Store is the subset of the persistence layer the reaper needs.
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"testing"
"time"
"github.com/nullbytelabs/zot-ephemeral-ttl/internal/store"
"github.com/replicatedhq/ttl.sh/sidecar/internal/store"
)
// In-memory fakes for the reaper's two dependencies. No real DB, no real HTTP.
+8 -3
View File
@@ -21,7 +21,11 @@ func New(baseURL string) *Client {
return &Client{
baseURL: baseURL,
http: &http.Client{
Timeout: 30 * time.Second,
// A manifest delete is a metadata operation against a zot running
// alongside this process, so it should be fast. Failing quickly
// keeps one wedged tag from stalling the rest of the sweep; the row
// stays in the store and is retried on the next tick.
Timeout: 5 * time.Second,
},
}
}
@@ -41,11 +45,12 @@ func (c *Client) DeleteManifest(ctx context.Context, repo, tag string) error {
return err
}
defer func() { _ = resp.Body.Close() }()
_, _ = io.Copy(io.Discard, resp.Body)
body, _ := io.ReadAll(resp.Body)
switch resp.StatusCode {
case http.StatusOK, http.StatusAccepted, http.StatusNoContent, http.StatusNotFound:
return nil
default:
return fmt.Errorf("DELETE %s -> %d", endpoint, resp.StatusCode)
// zot describes the failure in the body; %q keeps it on one log line.
return fmt.Errorf("DELETE %s -> %d: %q", endpoint, resp.StatusCode, body)
}
}
+2 -2
View File
@@ -8,8 +8,8 @@ import (
"net/http"
"time"
"github.com/nullbytelabs/zot-ephemeral-ttl/internal/events"
"github.com/nullbytelabs/zot-ephemeral-ttl/internal/ttl"
"github.com/replicatedhq/ttl.sh/sidecar/internal/events"
"github.com/replicatedhq/ttl.sh/sidecar/internal/ttl"
)
// Store is the subset of the persistence layer the server needs.
+2 -2
View File
@@ -12,8 +12,8 @@ import (
"testing"
"time"
"github.com/nullbytelabs/zot-ephemeral-ttl/internal/events"
"github.com/nullbytelabs/zot-ephemeral-ttl/internal/store"
"github.com/replicatedhq/ttl.sh/sidecar/internal/events"
"github.com/replicatedhq/ttl.sh/sidecar/internal/store"
)
// Server.Store is documented as safe for concurrent use, so the fake locks its
+1 -1
View File
@@ -9,7 +9,7 @@ import (
"github.com/redis/go-redis/v9"
"github.com/nullbytelabs/zot-ephemeral-ttl/internal/redistest"
"github.com/replicatedhq/ttl.sh/sidecar/internal/redistest"
)
// These tests run against a real Redis, started for the package by redistest.