diff --git a/tests/integration/go.mod b/tests/integration/go.mod index 5e216d2ad..efcfcbe52 100644 --- a/tests/integration/go.mod +++ b/tests/integration/go.mod @@ -13,6 +13,8 @@ require ( github.com/operator-framework/operator-registry v1.50.0 github.com/sirupsen/logrus v1.9.4 gopkg.in/yaml.v3 v3.0.1 + k8s.io/api v0.32.0 + sigs.k8s.io/yaml v1.4.0 ) require ( @@ -125,7 +127,7 @@ require ( gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gotest.tools/v3 v3.5.1 // indirect - k8s.io/api v0.32.0 // indirect + k8s.io/apiextensions-apiserver v0.32.0 // indirect k8s.io/apimachinery v0.32.0 // indirect k8s.io/client-go v0.32.0 // indirect k8s.io/klog/v2 v2.130.1 // indirect @@ -133,5 +135,4 @@ require ( k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect - sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/tests/integration/go.sum b/tests/integration/go.sum index 725b1d96a..831bf6fe6 100644 --- a/tests/integration/go.sum +++ b/tests/integration/go.sum @@ -389,6 +389,7 @@ gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= k8s.io/api v0.32.0 h1:OL9JpbvAU5ny9ga2fb24X8H6xQlVp+aJMFlgtQjR9CE= k8s.io/api v0.32.0/go.mod h1:4LEwHZEf6Q/cG96F3dqR965sYOfmPM7rq81BLgsE0p0= +k8s.io/apiextensions-apiserver v0.32.0/go.mod h1:86hblMvN5yxMvZrZFX2OhIHAuFIMJIZ19bTvzkP+Fmw= k8s.io/apimachinery v0.32.0 h1:cFSE7N3rmEEtv4ei5X6DaJPHHX0C+upp+v5lVPiEwpg= k8s.io/apimachinery v0.32.0/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= k8s.io/client-go v0.32.0 h1:DimtMcnN/JIKZcrSrstiwvvZvLjG0aSxy8PxN8IChp8= diff --git a/tests/integration/signature_test.go b/tests/integration/signature_test.go index 0dcc5bee8..70bada444 100644 --- a/tests/integration/signature_test.go +++ b/tests/integration/signature_test.go @@ -1,9 +1,17 @@ package integration_test import ( + "encoding/json" + "os" "path/filepath" + "strings" . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/yaml" + + "github.com/openshift/oc-mirror/tests/integration/pkg/ocmirror" ) var _ = Describe("signatures", func() { @@ -48,6 +56,9 @@ var _ = Describe("signatures", func() { By("verifying signatures are present in the local registry") expectSignaturesInRegistry(*testRegistry) + By("verifying the signature configmap was generated correctly") + expectSignatureConfigMapGenerated(workDir) + By("running delete with --delete-signatures") result, err = runner.DeletePhaseOne(ctx, filepath.Join(iscDir, discSignatures), workDir, "", testRegistry.Endpoint(), "--delete-signatures") @@ -86,4 +97,109 @@ var _ = Describe("signatures", func() { By("verifying signature tags are still present") expectOnlySignatureTagsRemain(*testRegistry) }) + + It("fails gracefully when it fails to retrieve the release signature", func() { + // Use a plain, unseeded working directory so no cached signature is found, + // forcing oc-mirror to fetch it from OCP_SIGNATURE_URL. + unseededWorkDir, err := os.MkdirTemp("", "oc-mirror-test-*") + Expect(err).NotTo(HaveOccurred()) + defer cleanupWorkDir(unseededWorkDir) + + By("running mirrorToDisk with an unreachable signature server") + failingSigRunner := ocmirror.NewRunner(os.Getenv("OC_MIRROR_BINARY")). + WithEnv([]string{"OCP_SIGNATURE_URL=http://127.0.0.1:1/"}) + result, err := failingSigRunner.MirrorToDisk(ctx, filepath.Join(iscDir, iscSignatures), unseededWorkDir) + + By("verifying oc-mirror failed gracefully instead of panicking") + expectOcMirrorExitCode(result, err, 2, "collection error", "http request") + expectNoTarArchive(unseededWorkDir) + }) + + Describe("release signature configmap", func() { + It("should not generate a signature configmap when --ignore-release-signature is used", func() { + // Use a plain, unseeded working directory to guarantee no signature is ever + // cached, regardless of --ignore-release-signature. + unseededWorkDir, err := os.MkdirTemp("", "oc-mirror-test-*") + Expect(err).NotTo(HaveOccurred()) + defer cleanupWorkDir(unseededWorkDir) + + By("running mirrorToDisk with --ignore-release-signature") + result, err := runner.MirrorToDisk(ctx, filepath.Join(iscDir, iscSignatures), unseededWorkDir, "--ignore-release-signature") + expectOcMirrorCommandSuccess(result, err) + + By("running diskToMirror with --ignore-release-signature") + result, err = runner.DiskToMirror(ctx, filepath.Join(iscDir, iscSignatures), unseededWorkDir, testRegistry.Endpoint(), + "--ignore-release-signature", "--dest-tls-verify=false") + expectOcMirrorCommandSuccess(result, err) + + By("verifying no signature configmap was generated since no signature was ever cached") + expectNoSignatureConfigMap(unseededWorkDir) + }) + }) }) + +// expectSignatureConfigMapGenerated verifies that the release signature configmap +// (both JSON and YAML) was generated in working-dir/cluster-resources with the +// expected static name, and that every binaryData entry in the configmap is backed +// by a cached signature file in working-dir/signatures with matching content. +// Note: the configmap only contains signatures for images that were actually +// mirrored as OCP releases, so not every file on disk is necessarily present in +// the configmap. +func expectSignatureConfigMapGenerated(workDir string) { + crDir := filepath.Join(workDir, dirWorkingDir, dirClusterResources) + + var jsonCM corev1.ConfigMap + jsonData, err := os.ReadFile(filepath.Join(crDir, "signature-configmap.json")) + Expect(err).NotTo(HaveOccurred(), "signature-configmap.json not found") + Expect(json.Unmarshal(jsonData, &jsonCM)).To(Succeed(), "failed to unmarshal signature-configmap.json") + + var yamlCM corev1.ConfigMap + yamlData, err := os.ReadFile(filepath.Join(crDir, "signature-configmap.yaml")) + Expect(err).NotTo(HaveOccurred(), "signature-configmap.yaml not found") + Expect(yaml.Unmarshal(yamlData, &yamlCM)).To(Succeed(), "failed to unmarshal signature-configmap.yaml") + + for _, cm := range []corev1.ConfigMap{jsonCM, yamlCM} { + Expect(cm.Name).To(Equal("mirrored-release-signatures")) + Expect(cm.BinaryData).NotTo(BeEmpty()) + for key := range cm.BinaryData { + Expect(key).To(MatchRegexp(`^sha256-[0-9a-f]{64}-\d+$`), "unexpected binaryData key format: %s", key) + } + } + Expect(jsonCM.BinaryData).To(Equal(yamlCM.BinaryData), "json and yaml signature configmaps disagree on binaryData") + + sigDir := filepath.Join(workDir, dirWorkingDir, "signatures") + entries, err := os.ReadDir(sigDir) + Expect(err).NotTo(HaveOccurred()) + Expect(entries).NotTo(BeEmpty(), "no cached signature files found in %s", sigDir) + + // Verify every configmap entry is backed by a real signature file on disk. + // Key format is "sha256--"; extract the hex digest. + for key, val := range jsonCM.BinaryData { + parts := strings.SplitN(key, "-", 3) + Expect(parts).To(HaveLen(3), "unexpected binaryData key format: %s", key) + digest := parts[1] + + found := false + for _, f := range entries { + if strings.Contains(f.Name(), digest) { + data, err := os.ReadFile(filepath.Join(sigDir, f.Name())) + Expect(err).NotTo(HaveOccurred()) + Expect(val).To(Equal(data), "binaryData content mismatch for key %s and file %s", key, f.Name()) + found = true + break + } + } + Expect(found).To(BeTrue(), "no signature file found for configmap key %s", key) + } +} + +// expectNoSignatureConfigMap verifies that no release signature configmap was generated. +func expectNoSignatureConfigMap(workDir string) { + crDir := filepath.Join(workDir, dirWorkingDir, dirClusterResources) + + _, errJSON := os.Stat(filepath.Join(crDir, "signature-configmap.json")) + Expect(os.IsNotExist(errJSON)).To(BeTrue(), "expected signature-configmap.json to not exist") + + _, errYAML := os.Stat(filepath.Join(crDir, "signature-configmap.yaml")) + Expect(os.IsNotExist(errYAML)).To(BeTrue(), "expected signature-configmap.yaml to not exist") +}