Skip to content
Open
Show file tree
Hide file tree
Changes from 50 commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
12a57e0
test(storage): add Tier A work budgets (TestWorkBudget) with a checke…
matthyx Sep 8, 2026
564ff4d
test(storage): add Tier B paired A/B (make perf-ab) with fixed work a…
matthyx Sep 8, 2026
715df7f
feat(storage): schema migrations 3-4 (rv/uid columns, payloads table)…
matthyx Sep 8, 2026
3ef25e7
feat(metrics): series for the ContainerProfile SQLite backend
matthyx Sep 8, 2026
a04af90
feat(storage): write gate and supervised PASSIVE checkpointer for the…
matthyx Sep 8, 2026
58c59d1
feat(storage): ObjectStore, the SQLite-native fully-ACID ContainerPro…
matthyx Sep 8, 2026
d786af3
feat(storage): kind-ownership guard in the legacy StorageImpl (INV-4)
matthyx Sep 8, 2026
2efa212
feat: ContainerProfileSqliteBackend flag (prototype wiring)
matthyx Sep 8, 2026
ae52663
test(storage): ObjectStore unit, gate, checkpointer and INV-1/INV-4/I…
matthyx Sep 8, 2026
cdea399
test(storage): differential harness, legacy StorageImpl vs ObjectStore
matthyx Sep 8, 2026
71dcf88
test(storage): rapid INV-2 state machine with crash injection at ever…
matthyx Sep 8, 2026
9156513
Merge branch 'feat/step1-l-measurement-harness' into prototype/full-a…
matthyx Sep 8, 2026
fe55c65
test(storage): Tier B backend A/B on one commit; INV-4 via the statem…
matthyx Sep 8, 2026
a6e718a
fix(storage): space PASSIVE checkpoints; GetSbom reuses the caller's …
matthyx Sep 8, 2026
2794238
fix(storage): fullSpec LIST pages metadata by rowid before joining pa…
matthyx Sep 9, 2026
38a92d1
fix(storage): GetSbom answers an absent SBOM from the metadata row, n…
matthyx Sep 9, 2026
27754d7
test(storage): compare consolidation creationTimestamp by precision, …
matthyx Sep 9, 2026
ef41d2d
Merge origin/main (Lane 0) into prototype/full-acid-containerprofile
matthyx Sep 9, 2026
64b4130
fix(storage): close X-A race window in objectStoreCPStorage.SaveConta…
matthyx Sep 9, 2026
94ce422
test(storage): AC-G2 read-entry-point × key-state matrix, flag-off to…
matthyx Sep 9, 2026
d21d0b2
test(storage): AC-G1 write-gate invariant harness, T-G1, AC-G2(on) — …
matthyx Sep 9, 2026
0641a24
feat(storage): hoist the write gate out of the ObjectStore; gate API …
matthyx Sep 9, 2026
0907aba
feat(storage): route saveObject, deleteLocked and the self-repair del…
matthyx Sep 9, 2026
0d44645
feat(storage): the shard commit runs on the gate's connection, no poo…
matthyx Sep 9, 2026
762ed8c
feat(storage): the cleanup tick's row delete and sidecar migration go…
matthyx Sep 9, 2026
3fe6456
test(storage): T-G2 re-entrancy, T-G5 pool bound, T-G6 shutdown, RC-6…
matthyx Sep 9, 2026
d441df6
test(storage): Tier B carries legacy-kind traffic through the shared …
matthyx Sep 9, 2026
826de46
fix(storage): close the gate after the server drains; owns() bounded …
matthyx Sep 9, 2026
a0bff3a
fix(storage): GeneratedNetworkPolicyStorage reads ContainerProfiles t…
matthyx Sep 9, 2026
7bc270b
fix(storage): the ContainerProfile cleanup arm runs from the processo…
matthyx Sep 9, 2026
680beac
feat(storage): startup ContainerProfile data migration into the Objec…
matthyx Sep 9, 2026
dc65ad2
feat(storage): cpexport, the reverse export for the export-then-downg…
matthyx Sep 9, 2026
ac7e7e7
fix(storage): the migration's staging-file sweep never matches a payl…
matthyx Sep 9, 2026
951eb4d
test(storage): ObjectStore same-key concurrency, 5,000-row migration/…
matthyx Sep 10, 2026
607af97
fix(storage): consolidation's retry reserves the series so a back-to-…
matthyx Sep 10, 2026
c646400
fix(storage): Tier B updaters perform a real mutation; LOAD_HOT_KEYS …
matthyx Sep 10, 2026
94e489b
tune(storage): shrink keyReserveWaitMax to 200ms to recover update-p9…
matthyx Sep 10, 2026
f23d831
tune(storage): shrink keyReserveWaitMax further to 50ms
matthyx Sep 11, 2026
4cbb2d2
fix(storage): split the reservation wait into writer-vs-retry and ret…
matthyx Sep 11, 2026
fb61576
fix(storage): exclude un-consolidated TS rows from ContainerProfile List
matthyx Sep 11, 2026
ed84722
fix(storage): settle before load1 sampling in the Tier B harness
matthyx Sep 11, 2026
72112c7
fix(storage): avoid truncating resourceVersion to int in migration lo…
matthyx Sep 11, 2026
9bc0f69
fix(storage): cpexport reconciles stale legacy files of keys deleted …
matthyx Sep 11, 2026
864b134
fix(storage): a panic mid-staging discards the tick's write set inste…
matthyx Sep 11, 2026
8493153
fix(storage): maintenance starts only after cleanup ownership is wire…
matthyx Sep 11, 2026
25f4e84
fix(storage): a missing payload row must not truncate full-spec LIST …
matthyx Sep 11, 2026
cf3e8fa
fix(storage): a filesystem error during the migration sweep must fail…
matthyx Sep 11, 2026
fec270c
style(storage): gofmt the changed files flagged on PR #402
matthyx Sep 11, 2026
f524ae9
style(storage): fix space-before-tab whitespace in the is_time_series…
matthyx Sep 11, 2026
c8f7377
fix(storage): StartMaintenance's loop is now stoppable, closing a tes…
matthyx Sep 11, 2026
408fcc0
fix(storage): an uncleaned root with a trailing slash must not delete…
matthyx Sep 11, 2026
0cedf43
fix(storage): a filesystem error during stale-file reconciliation mus…
matthyx Sep 11, 2026
4ad4bb4
fix(storage): the cleanup Deleted-event test waits for async dispatch…
matthyx Sep 11, 2026
8aa3e34
fix(storage): round-3 review -- general key derivation, Read-failure …
matthyx Sep 11, 2026
087df31
fix(storage): clear golangci-lint findings from CI (errcheck, govet, …
matthyx Sep 14, 2026
2942a88
fix(storage): rollback-safety guard -- flag-off no longer destroys Co…
matthyx Sep 14, 2026
e513ec5
fix(storage): preserve rollback data across legacy error paths
matthyx Sep 14, 2026
9b4d555
test(storage): replace AC-G2 timing windows with gate ordering checks
matthyx Sep 14, 2026
bff3bf2
Merge main into prototype/full-acid-containerprofile
matthyx Sep 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions .github/workflows/perf-ab.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
name: perf-ab

# Tier B of the storage measurement harness (hack/perf-ab.sh): a paired A/B of
# the branch against its merge-base, rebuilt in the same run on the same
# runner. GitHub runners are noisy, so INCONCLUSIVE is expected often here and
# is reported, not hidden; a local run on a quiet machine is the authoritative
# one. Only a REGRESSION verdict fails the check.

on:
workflow_dispatch:
inputs:
base:
description: "Commit to compare against (default: merge-base with origin/main)"
required: false
type: string
pairs:
description: "Interleaved (base, head) rounds"
required: false
type: string
default: "10"
schedule:
- cron: "17 3 * * *"
pull_request:
types: [opened, synchronize, reopened, labeled]

jobs:
perf-ab:
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'perf')
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true
- name: Resolve base
id: base
run: |
if [ -n "${{ inputs.base }}" ]; then
echo "sha=${{ inputs.base }}" >> "$GITHUB_OUTPUT"
elif [ "${{ github.event_name }}" = "schedule" ]; then
echo "sha=$(git rev-list -n 1 --before='1 day ago' origin/main)" >> "$GITHUB_OUTPUT"
else
echo "sha=$(git merge-base origin/main HEAD)" >> "$GITHUB_OUTPUT"
fi
- name: Paired A/B
id: ab
env:
BASE: ${{ steps.base.outputs.sha }}
PAIRS: ${{ inputs.pairs || '10' }}
PERF_AB_OUT_DIR: ${{ runner.temp }}/perf-ab
PERF_AB_ALLOW_NOISY: "1"
run: |
set +e
make perf-ab
echo "rc=$?" >> "$GITHUB_OUTPUT"
- name: Report verdict
if: always()
env:
OUT: ${{ runner.temp }}/perf-ab
RC: ${{ steps.ab.outputs.rc }}
run: |
{
echo "## perf-ab: $(cat "$OUT/verdict.txt" 2>/dev/null || echo 'no verdict (driver failed)')"
echo
echo "base \`${{ steps.base.outputs.sha }}\` vs head \`${{ github.sha }}\`, exit $RC (0 pass, 1 regression, 2 config mismatch, 3 inconclusive, 4 underpowered)"
echo
if [ -f "$OUT/verdict-table.txt" ]; then echo '```'; cat "$OUT/verdict-table.txt"; echo '```'; fi
if [ -f "$OUT/schedule.txt" ]; then echo '<details><summary>schedule</summary>'; echo; echo '```'; cat "$OUT/schedule.txt"; echo '```'; echo '</details>'; fi
} >> "$GITHUB_STEP_SUMMARY"
- name: Upload rounds
if: always()
uses: actions/upload-artifact@v4
with:
name: perf-ab-${{ github.run_id }}
path: ${{ runner.temp }}/perf-ab
if-no-files-found: ignore
- name: Fail on regression
if: steps.ab.outputs.rc == '1'
run: exit 1
13 changes: 13 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,16 @@
The Kubescape project manages this document in the central project repository.

Go to the [centralized CONTRIBUTING.md](https://github.com/kubescape/project-governance/blob/main/CONTRIBUTING.md)

## Storage hot-path changes

A PR touching `pkg/registry/file/{storage,singlewriter,containerprofile_*,sqlite}.go`
must carry two numbers (see `docs/features/storage-measurement-harness.md`):

- **Work budgets (Tier A).** `go test ./...` includes `TestWorkBudget`. If the
golden `pkg/registry/file/testdata/workbudget.golden.json` changes, the
commit message states each row's delta and why; the reviewer checks the
attribution.
- **Paired A/B (Tier B).** Run `make perf-ab` on a quiet machine and quote its
verdict line verbatim in the PR description. `INCONCLUSIVE` and
`UNDERPOWERED` are not `PASS`.
9 changes: 8 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,21 @@ TAG?=test
IMAGE?=quay.io/kubescape/$(BINARY_NAME)


.PHONY: build test docker-build docker-push
.PHONY: build test perf-ab docker-build docker-push

build:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o $(BINARY_NAME)

test:
go test ./...

# Paired A/B of HEAD against its merge-base on this machine (Tier B of the
# storage measurement harness). Mandatory before SHIP for any change under
# pkg/registry/file/{storage,singlewriter,containerprofile_*,sqlite}.go; quote
# its verdict line in the PR. BASE=<sha> and PAIRS=<n> override the defaults.
perf-ab:
hack/perf-ab.sh

docker-build:
docker buildx build --platform linux/amd64 -t $(IMAGE):$(TAG) --load -f $(DOCKERFILE_PATH) .
docker-push:
Expand Down
6 changes: 6 additions & 0 deletions build/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ RUN --mount=target=. \
--mount=type=cache,target=/go/pkg \
GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /out/migration ./cmd/migration/main.go

RUN --mount=target=. \
--mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/go/pkg \
GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /out/cpexport ./cmd/cpexport/main.go

RUN --mount=target=. \
--mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/go/pkg \
Expand All @@ -18,6 +23,7 @@ FROM gcr.io/distroless/static-debian13:nonroot

COPY --from=builder /out/storage /usr/bin/storage
COPY --from=builder /out/migration /usr/bin/migration
COPY --from=builder /out/cpexport /usr/bin/cpexport
ARG image_version
ENV RELEASE=$image_version

Expand Down
65 changes: 65 additions & 0 deletions cmd/cpexport/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package main

// cpexport writes every ContainerProfile the SQLite-native backend
// (config.ContainerProfileSqliteBackend) holds in the payloads table back
// as the legacy gob payload file an older storage binary expects. It is the
// first half of the rollback order of
// .omc/plans/full-acid-storage-architecture.md §8.4 — export, THEN
// downgrade — because an older binary's get() deletes the metadata row of
// any ContainerProfile whose file is missing, destroying the object.
//
// Run it with the storage server stopped (scale the deployment to 0, then
// run it against the PVC), never alongside a serving process: a write that
// lands after a key was exported leaves the old binary a stale file.
//
// cpexport [-db /data/metadata.sq3] [-root /data] [-dry-run]

import (
"context"
"flag"
"fmt"
"os"
"path/filepath"
"time"

"github.com/kubescape/storage/pkg/apis/softwarecomposition/install"
"github.com/kubescape/storage/pkg/registry/file"
"github.com/spf13/afero"
"k8s.io/apimachinery/pkg/runtime"
)

func main() {
root := flag.String("root", file.DefaultStorageRoot, "storage root directory holding the payload files")
db := flag.String("db", "", "SQLite database path (default <root>/metadata.sq3)")
dryRun := flag.Bool("dry-run", false, "decode and count without writing any file")
batch := flag.Int("batch", file.DefaultMigrationBatchSize, "rows read per query")
flag.Parse()
if *db == "" {
*db = filepath.Join(*root, "metadata.sq3")
}
// The pool retries an unopenable database every 5 s until ctx expires
// (sqlitemigration.Pool.Take); a missing database is always a wrong
// -db/-root, not something to wait an hour for.
if _, err := os.Stat(*db); err != nil {
fmt.Fprintf(os.Stderr, "cpexport: database: %v\n", err)
os.Exit(1)
}

sch := runtime.NewScheme()
install.Install(sch)
pool := file.NewPoolWithOptions(*db, file.PoolOptions{Size: 2, BusyTimeout: file.DefaultBusyTimeout})
defer func() { _ = pool.Close() }()

ctx, cancel := context.WithTimeout(context.Background(), time.Hour)
defer cancel()
report, err := file.ExportContainerProfiles(ctx, pool, afero.NewOsFs(), *root, sch, file.ContainerProfileExportOptions{BatchSize: *batch, DryRun: *dryRun})
if err != nil {
fmt.Fprintf(os.Stderr, "cpexport: %v\n", err)
os.Exit(1)
}
fmt.Printf("cpexport: exported=%d legacySkipped=%d undecodable=%d dryRun=%v elapsed=%s\n",
report.Exported, report.LegacySkipped, report.Undecodable, report.DryRun, report.Elapsed)
if report.Undecodable > 0 {
os.Exit(2)
}
}
Loading
Loading