Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 5 additions & 5 deletions .github/workflows/publish-kubescape-storage-hotfix.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ concurrency:
env:
KUBESCAPE_STORAGE_SOURCE_COMMIT: b35788b68337134fc2514574cde1ba7f1225fd43
IMAGE: ghcr.io/devantler-tech/platform-kubescape-storage
IMAGE_TAG: v0.0.297-sqlite-contention.9-${{ github.sha }}
IMAGE_TAG: v0.0.297-sqlite-contention.10-${{ github.sha }}
PATCH_PATH: k8s/bases/infrastructure/controllers/kubescape/storage-v0.0.297-sqlite-contention.patch
CLEANUP_PATCH_PATH: k8s/bases/infrastructure/controllers/kubescape/storage-v0.0.297-scan-record-cleanup.patch
SUMMARY_REPAIR_PATCH_PATH: k8s/bases/infrastructure/controllers/kubescape/storage-v0.0.297-summary-metadata-repair.patch
Expand Down Expand Up @@ -89,8 +89,8 @@ jobs:
test "$(git hash-object pkg/registry/file/summary_metadata_repair_test.go)" = 5f0ee570bc1f758f0c2e4dcaae4ee7b5a4ce3717
git apply --check --unidiff-zero "${NATIVE_CODEC_PATCH}"
git apply --unidiff-zero "${NATIVE_CODEC_PATCH}"
test "$(git hash-object pkg/registry/file/summary_metadata_repair.go)" = de8e238132fd0c45ceda47f08013b6a6b07e7e87
test "$(git hash-object pkg/registry/file/summary_metadata_repair_test.go)" = b7b312ad120c5e68b4c6bdb90c41d8f5474be457
test "$(git hash-object pkg/registry/file/summary_metadata_repair.go)" = 8e0b39495ccf9d24de543443fec7169b36cadc5a
test "$(git hash-object pkg/registry/file/summary_metadata_repair_test.go)" = 24ef7781d710ce92453fed46ec92b0dd52830f89
git diff --check
go test -race ./pkg/registry/file

Expand Down Expand Up @@ -155,8 +155,8 @@ jobs:
test "$(git hash-object pkg/registry/file/summary_metadata_repair_test.go)" = 5f0ee570bc1f758f0c2e4dcaae4ee7b5a4ce3717
git apply --check --unidiff-zero "${NATIVE_CODEC_PATCH}"
git apply --unidiff-zero "${NATIVE_CODEC_PATCH}"
test "$(git hash-object pkg/registry/file/summary_metadata_repair.go)" = de8e238132fd0c45ceda47f08013b6a6b07e7e87
test "$(git hash-object pkg/registry/file/summary_metadata_repair_test.go)" = b7b312ad120c5e68b4c6bdb90c41d8f5474be457
test "$(git hash-object pkg/registry/file/summary_metadata_repair.go)" = 8e0b39495ccf9d24de543443fec7169b36cadc5a
test "$(git hash-object pkg/registry/file/summary_metadata_repair_test.go)" = 24ef7781d710ce92453fed46ec92b0dd52830f89
git diff --check

- name: Set up Buildx container driver
Expand Down
5 changes: 5 additions & 0 deletions docs/kubescape-result-coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,11 @@ changes are refused and reported. Payload bytes and scan times are retained. The
under the summary's write lock before later scanner updates, so a failed metadata write can be
recovered on retry without accepting a stale client's update.

The revision 10 publisher compares retained creation times at the second-level precision used by
Kubernetes metadata JSON. Gob retains fractional seconds, so comparing its timestamp directly
with a JSON metadata row can falsely report an identity change. The comparison keeps the payload
intact and retains the name, namespace, UID, version-direction and scan-time guards.

Publication does not deploy this image. Before a separate immutable image-pin change, verify
its signed digest, a recovery path for the retained store and the repeated split observations.
Preserve the chart's single-writer `Recreate` rollout. After deployment, retain the reconciliation
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
diff --git a/pkg/registry/file/summary_metadata_repair.go b/pkg/registry/file/summary_metadata_repair.go
index 3f044057cba5614fdbcca8104f27cdb086641d29..de8e238132fd0c45ceda47f08013b6a6b07e7e87 100644
index 3f044057cba5614fdbcca8104f27cdb086641d29..8e0b39495ccf9d24de543443fec7169b36cadc5a 100644
--- a/pkg/registry/file/summary_metadata_repair.go
+++ b/pkg/registry/file/summary_metadata_repair.go
@@ -15,0 +16 @@ import (
Expand Down Expand Up @@ -39,14 +39,21 @@ index 3f044057cba5614fdbcca8104f27cdb086641d29..de8e238132fd0c45ceda47f08013b6a6
@@ -160 +184 @@ func reconcileSummaryMetadataRow(ctx context.Context, fs afero.Fs, root string,
- err = gob.NewDecoder(io.LimitReader(f, 8<<20)).Decode(&stored)
+ err = gob.NewDecoder(io.LimitReader(f, 8<<20)).Decode(storedObject)
@@ -193 +217 @@ func reconcileSummaryMetadataRow(ctx context.Context, fs afero.Fs, root string,
@@ -168 +192,5 @@ func reconcileSummaryMetadataRow(ctx context.Context, fs afero.Fs, root string,
- if stored.Name != row.name || stored.Namespace != row.namespace || stored.UID != listed.UID || !stored.CreationTimestamp.Equal(&listed.CreationTimestamp) {
+ // Metadata JSON uses Kubernetes RFC3339 second precision, while Gob retains
+ // nanoseconds. Compare the retained time through that same metadata codec;
+ // do not alter the payload or relax UID, name, namespace or version checks.
+ storedCreated := stored.CreationTimestamp.Rfc3339Copy()
+ if stored.Name != row.name || stored.Namespace != row.namespace || stored.UID != listed.UID || !storedCreated.Equal(&listed.CreationTimestamp) {
@@ -193 +221 @@ func reconcileSummaryMetadataRow(ctx context.Context, fs afero.Fs, root string,
- metadata := extractFields(&stored, []string{"ObjectMeta", "SchemaVersion"})
+ metadata := extractFields(storedObject, []string{"ObjectMeta", "SchemaVersion"})
@@ -210 +234 @@ func reconcileSummaryMetadataRow(ctx context.Context, fs afero.Fs, root string,
@@ -210 +238 @@ func reconcileSummaryMetadataRow(ctx context.Context, fs afero.Fs, root string,
- return &stored, nil
+ return storedObject, nil
diff --git a/pkg/registry/file/summary_metadata_repair_test.go b/pkg/registry/file/summary_metadata_repair_test.go
index 5f0ee570bc1f758f0c2e4dcaae4ee7b5a4ce3717..b7b312ad120c5e68b4c6bdb90c41d8f5474be457 100644
index 5f0ee570bc1f758f0c2e4dcaae4ee7b5a4ce3717..24ef7781d710ce92453fed46ec92b0dd52830f89 100644
--- a/pkg/registry/file/summary_metadata_repair_test.go
+++ b/pkg/registry/file/summary_metadata_repair_test.go
@@ -45,0 +46,8 @@ func newSplitSummary(t *testing.T) summaryRepairFixture {
Expand Down Expand Up @@ -215,3 +222,63 @@ index 5f0ee570bc1f758f0c2e4dcaae4ee7b5a4ce3717..b7b312ad120c5e68b4c6bdb90c41d8f5
+ }
+}
+
@@ -243,0 +394,59 @@ func TestSummaryMetadataRetryRepairsSplitBeforeVersionConflict(t *testing.T) {
+
+// Metadata JSON and retained Gob payloads must agree at Kubernetes' wire precision.
+// saveObject preserves fractional seconds in Gob but metav1.Time JSON emits whole seconds.
+func TestSummaryMetadataRepairCreationTimestampWirePrecision(t *testing.T) {
+ for _, native := range []bool{false, true} {
+ for _, coherent := range []bool{false, true} {
+ t.Run(fmt.Sprintf("native=%t/coherent=%t", native, coherent), func(t *testing.T) {
+ meta := metav1.ObjectMeta{
+ Name: "deployment-example-app", Namespace: "example", UID: types.UID("retained-identity"),
+ CreationTimestamp: metav1.NewTime(time.Date(2026, 9, 1, 0, 0, 0, 123456789, time.UTC)),
+ Annotations: map[string]string{"kubescape.io/timestamp": "1788220800"},
+ }
+ var obj runtime.Object = &v1beta1.VulnerabilityManifestSummary{ObjectMeta: meta}
+ if native {
+ obj = &softwarecomposition.VulnerabilityManifestSummary{ObjectMeta: meta}
+ }
+ f := newSplitSummaryObject(t, obj)
+ if coherent {
+ require.NoError(t, f.store.saveObject(f.conn, summaryRepairKey, obj, nil, ""))
+ }
+ payload, err := afero.ReadFile(f.fs, filepath.Join(f.root, summaryRepairKey)+GobExt)
+ require.NoError(t, err)
+ report, err := ReconcileVulnerabilitySummaryMetadata(context.Background(), f.fs, f.root, f.pool)
+ require.NoError(t, err)
+ if coherent {
+ require.Equal(t, SummaryMetadataRepairReport{Examined: 1, Unchanged: 1}, report)
+ } else {
+ require.Equal(t, SummaryMetadataRepairReport{Examined: 1, Repaired: 1}, report)
+ }
+ retained, err := afero.ReadFile(f.fs, filepath.Join(f.root, summaryRepairKey)+GobExt)
+ require.NoError(t, err)
+ require.Equal(t, payload, retained, "the fractional timestamp in the retained payload must stay intact")
+ metadata, err := ReadMetadata(f.conn, summaryRepairKey)
+ require.NoError(t, err)
+ var listed metav1.Object
+ if native {
+ listed = &softwarecomposition.VulnerabilityManifestSummary{}
+ } else {
+ listed = &v1beta1.VulnerabilityManifestSummary{}
+ }
+ require.NoError(t, json.Unmarshal(metadata, listed))
+ require.Equal(t, types.UID("retained-identity"), listed.GetUID())
+ require.Equal(t, "2026-09-01T00:00:00Z", listed.GetCreationTimestamp().UTC().Format(time.RFC3339Nano))
+ out := obj.DeepCopyObject()
+ require.NoError(t, f.store.GuaranteedUpdate(context.Background(), summaryRepairKey, out, false, nil,
+ func(input runtime.Object, _ storage.ResponseMeta) (runtime.Object, *uint64, error) {
+ current := input.DeepCopyObject()
+ current.(metav1.Object).GetAnnotations()["kubescape.io/timestamp"] = "1788393600"
+ return current, nil, nil
+ }, listed.(runtime.Object)))
+ if coherent {
+ require.Equal(t, "4", out.(metav1.Object).GetResourceVersion())
+ } else {
+ require.Equal(t, "3", out.(metav1.Object).GetResourceVersion())
+ }
+ })
+ }
+ }
+}
2 changes: 1 addition & 1 deletion scripts/tests/test-kubescape-storage-hotfix.sh
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ command -v yq >/dev/null 2>&1 || fail 'yq v4 is required'
fail 'the workflow image destination drifted'
# The literal GitHub expression is the contract.
# shellcheck disable=SC2016
[[ "$(yq -er '.env.IMAGE_TAG' "${workflow}")" == 'v0.0.297-sqlite-contention.9-${{ github.sha }}' ]] ||
[[ "$(yq -er '.env.IMAGE_TAG' "${workflow}")" == 'v0.0.297-sqlite-contention.10-${{ github.sha }}' ]] ||
fail 'the workflow image tag must identify the native-metadata codec repair revision'

grep -qF 'repository: kubescape/storage' "${workflow}" ||
Expand Down
Loading