diff --git a/.github/workflows/publish-kubescape-storage-hotfix.yaml b/.github/workflows/publish-kubescape-storage-hotfix.yaml index 420e848485..c4b174f82e 100644 --- a/.github/workflows/publish-kubescape-storage-hotfix.yaml +++ b/.github/workflows/publish-kubescape-storage-hotfix.yaml @@ -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 @@ -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 @@ -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 diff --git a/docs/kubescape-result-coverage.md b/docs/kubescape-result-coverage.md index 47d5f68775..18ad819ae3 100644 --- a/docs/kubescape-result-coverage.md +++ b/docs/kubescape-result-coverage.md @@ -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 diff --git a/k8s/bases/infrastructure/controllers/kubescape/storage-v0.0.297-summary-native-codec.patch b/k8s/bases/infrastructure/controllers/kubescape/storage-v0.0.297-summary-native-codec.patch index 7248d7e7de..710e2cb196 100644 --- a/k8s/bases/infrastructure/controllers/kubescape/storage-v0.0.297-summary-native-codec.patch +++ b/k8s/bases/infrastructure/controllers/kubescape/storage-v0.0.297-summary-native-codec.patch @@ -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 ( @@ -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 { @@ -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()) ++ } ++ }) ++ } ++ } ++} diff --git a/scripts/tests/test-kubescape-storage-hotfix.sh b/scripts/tests/test-kubescape-storage-hotfix.sh index 3064c1d2d0..331d983bb9 100755 --- a/scripts/tests/test-kubescape-storage-hotfix.sh +++ b/scripts/tests/test-kubescape-storage-hotfix.sh @@ -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}" ||