diff --git a/bindata/rbac/rbac.yaml b/bindata/rbac/rbac.yaml index 35260f1f09..6480aa0e4b 100644 --- a/bindata/rbac/rbac.yaml +++ b/bindata/rbac/rbac.yaml @@ -304,6 +304,7 @@ rules: - apiGroups: - config.openshift.io resources: + - clusterimagepolicies - imagedigestmirrorsets - images - networks diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 1c68d8089b..5eb5ab7f75 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -255,6 +255,7 @@ rules: - apiGroups: - config.openshift.io resources: + - clusterimagepolicies - imagedigestmirrorsets - images - networks diff --git a/internal/controller/dataplane/openstackdataplanenodeset_controller.go b/internal/controller/dataplane/openstackdataplanenodeset_controller.go index c625a93359..7a268e98e8 100644 --- a/internal/controller/dataplane/openstackdataplanenodeset_controller.go +++ b/internal/controller/dataplane/openstackdataplanenodeset_controller.go @@ -133,6 +133,7 @@ func (r *OpenStackDataPlaneNodeSetReconciler) GetLogger(ctx context.Context) log // RBAC for ImageContentSourcePolicy and MachineConfig // +kubebuilder:rbac:groups="operator.openshift.io",resources=imagecontentsourcepolicies,verbs=get;list;watch +// +kubebuilder:rbac:groups="config.openshift.io",resources=clusterimagepolicies,verbs=get;list;watch // +kubebuilder:rbac:groups="config.openshift.io",resources=imagedigestmirrorsets,verbs=get;list;watch // +kubebuilder:rbac:groups="config.openshift.io",resources=images,verbs=get;list;watch // +kubebuilder:rbac:groups="machineconfiguration.openshift.io",resources=machineconfigs,verbs=get;list;watch diff --git a/internal/dataplane/inventory.go b/internal/dataplane/inventory.go index 5e16ace54d..e81bf5dde2 100644 --- a/internal/dataplane/inventory.go +++ b/internal/dataplane/inventory.go @@ -145,27 +145,38 @@ func GenerateNodeSetInventory(ctx context.Context, helper *helper.Helper, registryConfig, err := util.GetMCRegistryConf(ctx, helper) if err != nil { // CRD not installed (non-OpenShift or no MCO) - log warning and continue. - // This allows graceful degradation when running on non-OpenShift clusters. // Users can manually configure registries.conf via ansibleVars. if util.IsNoMatchError(err) { - helper.GetLogger().Info("Disconnected environment detected but MachineConfig CRD not available. "+ - "Registry configuration will not be propagated to dataplane nodes. "+ - "You may need to configure registries.conf manually using ansibleVars "+ - "(edpm_podman_disconnected_ocp and edpm_podman_registries_conf).", + helper.GetLogger().Info("MachineConfig CRD not available; registry config will not be propagated", "error", err.Error()) } else { - // CRD exists but resource not found, or other errors (network issues, - // permissions, etc.) - return the error. If MCO is installed but the - // registry MachineConfig doesn't exist, this indicates a misconfiguration. return "", fmt.Errorf("failed to get MachineConfig registry configuration: %w", err) } } else { helper.GetLogger().Info("Mirror registries detected via IDMS/ICSP. Using OCP registry configuration.") - - // Use OCP registries.conf for mirror registry deployments nodeSetGroup.Vars["edpm_podman_registries_conf"] = registryConfig nodeSetGroup.Vars["edpm_podman_disconnected_ocp"] = hasMirrorRegistries } + + mirrorScopes, sourceByMirror, err := util.GetMirrorRegistryScopes(ctx, helper) + if err != nil { + return "", fmt.Errorf("failed to get mirror registries for sigstore verification: %w", err) + } + + sigstorePolicy, err := util.GetSigstoreImagePolicy(ctx, helper, mirrorScopes, sourceByMirror) + if err != nil { + return "", fmt.Errorf("failed to get ClusterImagePolicy for sigstore verification: %w", err) + } + if sigstorePolicy != nil { + nodeSetGroup.Vars["edpm_container_signature_verification"] = true + nodeSetGroup.Vars["edpm_container_signature_registry_mappings"] = sigstorePolicy.RegistryMappings + nodeSetGroup.Vars["edpm_container_signature_cosign_key_data"] = sigstorePolicy.CosignKeyData + if sigstorePolicy.SignedPrefix != "" { + nodeSetGroup.Vars["edpm_container_signature_signed_prefix"] = sigstorePolicy.SignedPrefix + } + } else { + helper.GetLogger().Info("No matching ClusterImagePolicy found; skipping sigstore verification") + } } // add TLS ansible variable diff --git a/internal/dataplane/util/image_registry.go b/internal/dataplane/util/image_registry.go index cc006fc828..5de520075b 100644 --- a/internal/dataplane/util/image_registry.go +++ b/internal/dataplane/util/image_registry.go @@ -5,6 +5,7 @@ import ( "encoding/base64" "encoding/json" "fmt" + "sort" "strings" ocpconfigv1 "github.com/openshift/api/config/v1" @@ -12,8 +13,11 @@ import ( ocpicsp "github.com/openshift/api/operator/v1alpha1" "github.com/openstack-k8s-operators/lib-common/modules/common/helper" corev1 "k8s.io/api/core/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" k8s_errors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" ) @@ -65,6 +69,78 @@ func HasMirrorRegistries(ctx context.Context, helper *helper.Helper) (bool, erro return false, nil } +func sortedSetKeys(set map[string]struct{}) []string { + if len(set) == 0 { + return nil + } + result := make([]string, 0, len(set)) + for k := range set { + result = append(result, k) + } + sort.Strings(result) + return result +} + +// GetMirrorRegistryScopes returns the configured mirror scopes and their +// source registry mapping, preferring IDMS and falling back to ICSP. +// The returned scopes are normalized and de-duplicated for policy matching. +// The sourceByMirror map links each mirror scope back to its IDMS/ICSP source. +func GetMirrorRegistryScopes(ctx context.Context, helper *helper.Helper) ([]string, map[string]string, error) { + idmsList := &ocpconfigv1.ImageDigestMirrorSetList{} + if err := helper.GetClient().List(ctx, idmsList); err != nil { + if !IsNoMatchError(err) { + return nil, nil, err + } + } else { + scopes := map[string]struct{}{} + sourceByMirror := map[string]string{} + for _, idms := range idmsList.Items { + for _, mirrorSet := range idms.Spec.ImageDigestMirrors { + source := normalizeImageScope(string(mirrorSet.Source)) + for _, mirror := range mirrorSet.Mirrors { + m := normalizeImageScope(string(mirror)) + if m != "" { + scopes[m] = struct{}{} + if source != "" { + sourceByMirror[m] = source + } + } + } + } + } + if result := sortedSetKeys(scopes); len(result) > 0 { + return result, sourceByMirror, nil + } + } + + icspList := &ocpicsp.ImageContentSourcePolicyList{} + if err := helper.GetClient().List(ctx, icspList); err != nil { + if !IsNoMatchError(err) { + return nil, nil, err + } + } else { + scopes := map[string]struct{}{} + sourceByMirror := map[string]string{} + for _, icsp := range icspList.Items { + for _, mirrorSet := range icsp.Spec.RepositoryDigestMirrors { + source := normalizeImageScope(mirrorSet.Source) + for _, mirror := range mirrorSet.Mirrors { + m := normalizeImageScope(mirror) + if m != "" { + scopes[m] = struct{}{} + if source != "" { + sourceByMirror[m] = source + } + } + } + } + } + return sortedSetKeys(scopes), sourceByMirror, nil + } + + return nil, nil, nil +} + // IsNoMatchError checks if the error indicates that a CRD/resource type doesn't exist func IsNoMatchError(err error) bool { errStr := err.Error() @@ -151,6 +227,217 @@ func getMachineConfig(ctx context.Context, helper *helper.Helper) (mc.MachineCon return masterMachineConfig, nil } +// RegistryMapping pairs a mirror registry with its upstream source from IDMS/ICSP. +type RegistryMapping struct { + Mirror string `json:"mirror"` + Source string `json:"source"` +} + +// SigstorePolicyInfo contains the EDPM-relevant parts of a ClusterImagePolicy. +// A single RemapIdentity signedPrefix covers all mirrors under the same registry +// root — the container runtime replaces only the prefix, preserving namespace paths. +type SigstorePolicyInfo struct { + RegistryMappings []RegistryMapping + CosignKeyData string + SignedPrefix string +} + +const ( + clusterImagePolicyCRDName = "clusterimagepolicies.config.openshift.io" + clusterImagePolicyGroup = "config.openshift.io" + clusterImagePolicyKind = "ClusterImagePolicy" + clusterImagePolicyV1 = "v1" + clusterImagePolicyV1Alpha1 = "v1alpha1" + publicKeyRootOfTrustPolicyType = "PublicKey" + remapIdentityMatchPolicy = "RemapIdentity" +) + +func normalizeImageScope(scope string) string { + return strings.TrimSuffix(strings.TrimSpace(scope), "/") +} + +func clusterImagePolicyScopeMatchesMirror(policyScope string, mirrorScope string) bool { + policyScope = normalizeImageScope(policyScope) + mirrorScope = normalizeImageScope(mirrorScope) + + if policyScope == "" || mirrorScope == "" { + return false + } + + if strings.HasPrefix(policyScope, "*.") { + mirrorHostPort := strings.SplitN(mirrorScope, "/", 2)[0] + mirrorHost := strings.SplitN(mirrorHostPort, ":", 2)[0] + suffix := strings.TrimPrefix(policyScope, "*") + return strings.HasSuffix(mirrorHost, suffix) + } + + return mirrorScope == policyScope || strings.HasPrefix(mirrorScope, policyScope+"/") +} + +func getServedClusterImagePolicyVersion(ctx context.Context, helper *helper.Helper) (string, error) { + crd := &apiextensionsv1.CustomResourceDefinition{} + if err := helper.GetClient().Get(ctx, types.NamespacedName{Name: clusterImagePolicyCRDName}, crd); err != nil { + if k8s_errors.IsNotFound(err) || IsNoMatchError(err) { + return "", nil + } + return "", err + } + + for _, preferredVersion := range []string{clusterImagePolicyV1, clusterImagePolicyV1Alpha1} { + for _, version := range crd.Spec.Versions { + if version.Name == preferredVersion && version.Served { + return preferredVersion, nil + } + } + } + + return "", nil +} + +func listClusterImagePolicies( + ctx context.Context, + helper *helper.Helper, + version string, +) (*unstructured.UnstructuredList, error) { + // Use an unstructured client here because ClusterImagePolicy may be served as + // either v1 or v1alpha1 depending on the cluster; binding to a typed v1 API + // would fail on clusters that do not serve v1 yet. + policyList := &unstructured.UnstructuredList{} + policyList.SetGroupVersionKind(schema.GroupVersionKind{ + Group: clusterImagePolicyGroup, + Version: version, + Kind: clusterImagePolicyKind + "List", + }) + + if err := helper.GetClient().List(ctx, policyList); err != nil { + if IsNoMatchError(err) { + return nil, nil + } + return nil, err + } + + return policyList, nil +} + +// GetSigstoreImagePolicy checks if OCP has a ClusterImagePolicy configured +// with sigstore signature verification for one of the mirror registries in use. +// sourceByMirror maps each mirror scope to its upstream source registry (from IDMS/ICSP). +// Returns policy info if a relevant policy is found, nil if no policy exists. +// Returns nil without error if the ClusterImagePolicy CRD is not installed. +func GetSigstoreImagePolicy(ctx context.Context, helper *helper.Helper, mirrorScopes []string, sourceByMirror map[string]string) (*SigstorePolicyInfo, error) { + if len(mirrorScopes) == 0 { + return nil, nil + } + + version, err := getServedClusterImagePolicyVersion(ctx, helper) + if err != nil { + return nil, err + } + if version == "" { + return nil, nil + } + + policyList, err := listClusterImagePolicies(ctx, helper, version) + if err != nil { + return nil, err + } + if policyList == nil { + return nil, nil + } + + var matches []string + var match *SigstorePolicyInfo + + for _, policy := range policyList.Items { + if policy.GetName() == "openshift" { + continue + } + + policyType, found, err := unstructured.NestedString(policy.Object, "spec", "policy", "rootOfTrust", "policyType") + if err != nil { + return nil, fmt.Errorf("failed to parse ClusterImagePolicy %s policyType: %w", policy.GetName(), err) + } + if !found || policyType != publicKeyRootOfTrustPolicyType { + continue + } + + keyData, found, err := unstructured.NestedString(policy.Object, "spec", "policy", "rootOfTrust", "publicKey", "keyData") + if err != nil { + return nil, fmt.Errorf("failed to parse ClusterImagePolicy %s keyData: %w", policy.GetName(), err) + } + if !found || len(keyData) == 0 { + continue + } + + scopes, found, err := unstructured.NestedStringSlice(policy.Object, "spec", "scopes") + if err != nil { + return nil, fmt.Errorf("failed to parse ClusterImagePolicy %s scopes: %w", policy.GetName(), err) + } + if !found || len(scopes) == 0 { + continue + } + + signedPrefix := "" + matchPolicy, found, err := unstructured.NestedString(policy.Object, "spec", "policy", "signedIdentity", "matchPolicy") + if err != nil { + return nil, fmt.Errorf("failed to parse ClusterImagePolicy %s matchPolicy: %w", policy.GetName(), err) + } + if found && matchPolicy == remapIdentityMatchPolicy { + signedPrefix, _, err = unstructured.NestedString( + policy.Object, + "spec", "policy", "signedIdentity", "remapIdentity", "signedPrefix", + ) + if err != nil { + return nil, fmt.Errorf("failed to parse ClusterImagePolicy %s signedPrefix: %w", policy.GetName(), err) + } + } + + matchedMirrorScopes := map[string]struct{}{} + for _, scope := range scopes { + policyScope := normalizeImageScope(scope) + if policyScope == "" { + continue + } + + for _, mirrorScope := range mirrorScopes { + if clusterImagePolicyScopeMatchesMirror(policyScope, mirrorScope) { + matchedMirrorScopes[normalizeImageScope(mirrorScope)] = struct{}{} + } + } + } + if len(matchedMirrorScopes) == 0 { + continue + } + + sortedMirrors := sortedSetKeys(matchedMirrorScopes) + + mappings := make([]RegistryMapping, 0, len(sortedMirrors)) + for _, m := range sortedMirrors { + mappings = append(mappings, RegistryMapping{ + Mirror: m, + Source: sourceByMirror[m], + }) + } + + matches = append(matches, fmt.Sprintf("%s (%s)", policy.GetName(), strings.Join(sortedMirrors, ", "))) + match = &SigstorePolicyInfo{ + RegistryMappings: mappings, + CosignKeyData: keyData, + SignedPrefix: signedPrefix, + } + } + + if len(matches) > 1 { + sort.Strings(matches) + return nil, fmt.Errorf( + "expected exactly one ClusterImagePolicy matching mirror registries, found %d: %s", + len(matches), strings.Join(matches, ", "), + ) + } + + return match, nil +} + // GetMirrorRegistryCACerts retrieves CA certificates from image.config.openshift.io/cluster. // Returns nil without error if: // - not on OpenShift (Image CRD doesn't exist) diff --git a/internal/dataplane/util/image_registry_test.go b/internal/dataplane/util/image_registry_test.go index 21155078fb..a1bd93c493 100644 --- a/internal/dataplane/util/image_registry_test.go +++ b/internal/dataplane/util/image_registry_test.go @@ -20,6 +20,7 @@ import ( "context" "encoding/base64" "errors" + "fmt" "testing" . "github.com/onsi/gomega" //revive:disable:dot-imports @@ -30,8 +31,11 @@ import ( "github.com/openstack-k8s-operators/lib-common/modules/common/helper" corev1 "github.com/openstack-k8s-operators/openstack-operator/api/core/v1beta1" k8s_corev1 "k8s.io/api/core/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/kubernetes/fake" "k8s.io/client-go/kubernetes/scheme" ctrl "sigs.k8s.io/controller-runtime" @@ -44,6 +48,7 @@ import ( func setupTestHelper(includeOpenShiftCRDs bool, objects ...client.Object) *helper.Helper { s := runtime.NewScheme() _ = scheme.AddToScheme(s) + _ = apiextensionsv1.AddToScheme(s) _ = corev1.AddToScheme(s) _ = k8s_corev1.AddToScheme(s) @@ -237,6 +242,135 @@ func TestHasMirrorRegistries_CRDsNotInstalled(t *testing.T) { g.Expect(hasMirrors).To(BeFalse(), "Should return false when CRDs don't exist (graceful degradation)") } +func TestGetMirrorRegistryScopes(t *testing.T) { + g := NewWithT(t) + ctx := context.Background() + + idms := &ocpidms.ImageDigestMirrorSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-idms", + }, + Spec: ocpidms.ImageDigestMirrorSetSpec{ + ImageDigestMirrors: []ocpidms.ImageDigestMirrors{ + { + Source: "registry.redhat.io/rhosp-dev-preview", + Mirrors: []ocpidms.ImageMirror{ + "mirror.example.com:5000/rhosp-dev-preview", + "mirror.example.com:5000/rhosp-dev-preview", + }, + }, + }, + }, + } + icsp := &ocpicsp.ImageContentSourcePolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-icsp", + }, + Spec: ocpicsp.ImageContentSourcePolicySpec{ + RepositoryDigestMirrors: []ocpicsp.RepositoryDigestMirrors{ + { + Source: "quay.io/openstack-k8s-operators", + Mirrors: []string{"mirror.example.com:5000/openstack-k8s-operators/"}, + }, + }, + }, + } + + h := setupTestHelper(true, idms, icsp) + + scopes, sourceByMirror, err := GetMirrorRegistryScopes(ctx, h) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(scopes).To(Equal([]string{"mirror.example.com:5000/rhosp-dev-preview"})) + g.Expect(sourceByMirror).To(Equal(map[string]string{ + "mirror.example.com:5000/rhosp-dev-preview": "registry.redhat.io/rhosp-dev-preview", + })) +} + +func TestGetMirrorRegistryScopes_FallsBackToICSP(t *testing.T) { + g := NewWithT(t) + ctx := context.Background() + + icsp := &ocpicsp.ImageContentSourcePolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-icsp", + }, + Spec: ocpicsp.ImageContentSourcePolicySpec{ + RepositoryDigestMirrors: []ocpicsp.RepositoryDigestMirrors{ + { + Source: "quay.io/openstack-k8s-operators", + Mirrors: []string{"mirror.example.com:5000/openstack-k8s-operators/"}, + }, + }, + }, + } + + h := setupTestHelper(true, icsp) + + scopes, sourceByMirror, err := GetMirrorRegistryScopes(ctx, h) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(scopes).To(Equal([]string{"mirror.example.com:5000/openstack-k8s-operators"})) + g.Expect(sourceByMirror).To(Equal(map[string]string{ + "mirror.example.com:5000/openstack-k8s-operators": "quay.io/openstack-k8s-operators", + })) +} + +func TestGetMirrorRegistryScopes_MultipleIDMS(t *testing.T) { + g := NewWithT(t) + ctx := context.Background() + + idms1 := &ocpidms.ImageDigestMirrorSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "idms-one", + }, + Spec: ocpidms.ImageDigestMirrorSetSpec{ + ImageDigestMirrors: []ocpidms.ImageDigestMirrors{ + { + Source: "registry.redhat.io/rhoso", + Mirrors: []ocpidms.ImageMirror{"mirror.example.com:5000/rhoso"}, + }, + }, + }, + } + idms2 := &ocpidms.ImageDigestMirrorSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "idms-two", + }, + Spec: ocpidms.ImageDigestMirrorSetSpec{ + ImageDigestMirrors: []ocpidms.ImageDigestMirrors{ + { + Source: "registry.redhat.io/rhoso-operators", + Mirrors: []ocpidms.ImageMirror{"mirror.example.com:5000/rhoso-operators"}, + }, + }, + }, + } + + h := setupTestHelper(true, idms1, idms2) + + scopes, sourceByMirror, err := GetMirrorRegistryScopes(ctx, h) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(scopes).To(Equal([]string{ + "mirror.example.com:5000/rhoso", + "mirror.example.com:5000/rhoso-operators", + })) + g.Expect(sourceByMirror).To(Equal(map[string]string{ + "mirror.example.com:5000/rhoso": "registry.redhat.io/rhoso", + "mirror.example.com:5000/rhoso-operators": "registry.redhat.io/rhoso-operators", + })) +} + +func TestGetMirrorRegistryScopes_CRDsNotInstalled(t *testing.T) { + g := NewWithT(t) + ctx := context.Background() + + h := setupTestHelper(false) + + scopes, sourceByMirror, err := GetMirrorRegistryScopes(ctx, h) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(scopes).To(BeNil()) + g.Expect(sourceByMirror).To(BeNil()) +} + // Test GetMCRegistryConf scenarios func TestGetMCRegistryConf_Success(t *testing.T) { g := NewWithT(t) @@ -544,3 +678,268 @@ func TestGetMirrorRegistryCACerts_ConfigMapNotFound(t *testing.T) { g.Expect(err).ToNot(HaveOccurred()) g.Expect(caCerts).To(BeNil()) } + +func newSigstorePolicy( + version string, + name string, + scopes []string, + keyData string, + matchPolicy string, + signedPrefix string, +) *unstructured.Unstructured { + rawScopes := make([]interface{}, 0, len(scopes)) + for _, scope := range scopes { + rawScopes = append(rawScopes, scope) + } + + raw := map[string]interface{}{ + "apiVersion": fmt.Sprintf("config.openshift.io/%s", version), + "kind": "ClusterImagePolicy", + "metadata": map[string]interface{}{ + "name": name, + }, + "spec": map[string]interface{}{ + "scopes": rawScopes, + "policy": map[string]interface{}{ + "rootOfTrust": map[string]interface{}{ + "policyType": "PublicKey", + "publicKey": map[string]interface{}{ + "keyData": base64.StdEncoding.EncodeToString([]byte(keyData)), + }, + }, + "signedIdentity": map[string]interface{}{ + "matchPolicy": matchPolicy, + }, + }, + }, + } + + if matchPolicy == "RemapIdentity" { + rawPolicy := raw["spec"].(map[string]interface{})["policy"].(map[string]interface{}) + rawPolicy["signedIdentity"].(map[string]interface{})["remapIdentity"] = map[string]interface{}{ + "prefix": scopes[0], + "signedPrefix": signedPrefix, + } + } + + policy := &unstructured.Unstructured{Object: raw} + policy.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "config.openshift.io", + Version: version, + Kind: "ClusterImagePolicy", + }) + + return policy +} + +func newClusterImagePolicyCRD(servedVersions ...string) *apiextensionsv1.CustomResourceDefinition { + versions := make([]apiextensionsv1.CustomResourceDefinitionVersion, 0, len(servedVersions)) + for i, version := range servedVersions { + versions = append(versions, apiextensionsv1.CustomResourceDefinitionVersion{ + Name: version, + Served: true, + Storage: i == 0, + }) + } + + return &apiextensionsv1.CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{Name: "clusterimagepolicies.config.openshift.io"}, + Spec: apiextensionsv1.CustomResourceDefinitionSpec{ + Group: "config.openshift.io", + Names: apiextensionsv1.CustomResourceDefinitionNames{ + Kind: "ClusterImagePolicy", + Plural: "clusterimagepolicies", + }, + Scope: apiextensionsv1.ClusterScoped, + Versions: versions, + }, + } +} + +func TestGetSigstoreImagePolicy_WithRemapIdentity(t *testing.T) { + g := NewWithT(t) + ctx := context.Background() + + policy := newSigstorePolicy( + "v1", + "test-policy", + []string{"local-registry.example.com:5000"}, + "test-public-key", + "RemapIdentity", + "registry.example.com/vendor", + ) + + h := setupTestHelper(true, newClusterImagePolicyCRD("v1"), policy) + + sourceByMirror := map[string]string{ + "local-registry.example.com:5000": "registry.example.com/vendor", + } + result, err := GetSigstoreImagePolicy(ctx, h, []string{"local-registry.example.com:5000"}, sourceByMirror) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(result).ToNot(BeNil()) + g.Expect(result.RegistryMappings).To(Equal([]RegistryMapping{ + {Mirror: "local-registry.example.com:5000", Source: "registry.example.com/vendor"}, + })) + g.Expect(result.CosignKeyData).To(Equal(base64.StdEncoding.EncodeToString([]byte("test-public-key")))) + g.Expect(result.SignedPrefix).To(Equal("registry.example.com/vendor")) +} + +func TestGetSigstoreImagePolicy(t *testing.T) { + g := NewWithT(t) + ctx := context.Background() + + policy := newSigstorePolicy( + "v1alpha1", + "test-policy", + []string{"local-registry.example.com:5000"}, + "test-public-key", + "MatchRepoDigestOrExact", + "", + ) + + h := setupTestHelper(true, newClusterImagePolicyCRD("v1alpha1"), policy) + + result, err := GetSigstoreImagePolicy(ctx, h, []string{"local-registry.example.com:5000"}, nil) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(result).ToNot(BeNil()) + g.Expect(result.RegistryMappings).To(Equal([]RegistryMapping{ + {Mirror: "local-registry.example.com:5000"}, + })) + g.Expect(result.CosignKeyData).To(Equal(base64.StdEncoding.EncodeToString([]byte("test-public-key")))) + g.Expect(result.SignedPrefix).To(BeEmpty()) +} + +func TestGetSigstoreImagePolicy_ReturnsAllMatchingMirrorScopes(t *testing.T) { + g := NewWithT(t) + ctx := context.Background() + + policy := newSigstorePolicy( + "v1alpha1", + "test-policy", + []string{"mirror.example.com:5000"}, + "test-public-key", + "MatchRepoDigestOrExact", + "", + ) + + h := setupTestHelper(true, newClusterImagePolicyCRD("v1alpha1"), policy) + + sourceByMirror := map[string]string{ + "mirror.example.com:5000/rhoso": "registry.redhat.io/rhoso", + "mirror.example.com:5000/rhoso-operators": "registry.redhat.io/rhoso-operators", + } + result, err := GetSigstoreImagePolicy(ctx, h, []string{ + "mirror.example.com:5000/rhoso", + "mirror.example.com:5000/rhoso-operators", + }, sourceByMirror) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(result).ToNot(BeNil()) + g.Expect(result.RegistryMappings).To(Equal([]RegistryMapping{ + {Mirror: "mirror.example.com:5000/rhoso", Source: "registry.redhat.io/rhoso"}, + {Mirror: "mirror.example.com:5000/rhoso-operators", Source: "registry.redhat.io/rhoso-operators"}, + })) + g.Expect(result.CosignKeyData).To(Equal(base64.StdEncoding.EncodeToString([]byte("test-public-key")))) +} + +func TestGetSigstoreImagePolicy_IgnoresNonMatchingPolicies(t *testing.T) { + g := NewWithT(t) + ctx := context.Background() + + policy := newSigstorePolicy( + "v1alpha1", + "other-policy", + []string{"other-registry.example.com:5000"}, + "test-public-key", + "MatchRepoDigestOrExact", + "", + ) + + h := setupTestHelper(true, newClusterImagePolicyCRD("v1alpha1"), policy) + + result, err := GetSigstoreImagePolicy(ctx, h, []string{"mirror.example.com:5000/openstack-k8s-operators"}, nil) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(result).To(BeNil()) +} + +func TestGetSigstoreImagePolicy_ReturnsErrorForAmbiguousPolicies(t *testing.T) { + g := NewWithT(t) + ctx := context.Background() + + policy1 := newSigstorePolicy( + "v1alpha1", + "policy-one", + []string{"mirror.example.com:5000/openstack-k8s-operators"}, + "key-one", + "MatchRepoDigestOrExact", + "", + ) + policy2 := newSigstorePolicy( + "v1alpha1", + "policy-two", + []string{"mirror.example.com:5000/openstack-k8s-operators"}, + "key-two", + "MatchRepoDigestOrExact", + "", + ) + + h := setupTestHelper(true, newClusterImagePolicyCRD("v1alpha1"), policy1, policy2) + + result, err := GetSigstoreImagePolicy(ctx, h, []string{"mirror.example.com:5000/openstack-k8s-operators"}, nil) + g.Expect(err).To(HaveOccurred()) + g.Expect(err.Error()).To(ContainSubstring("expected exactly one ClusterImagePolicy matching mirror registries")) + g.Expect(result).To(BeNil()) +} + +func TestGetSigstoreImagePolicy_CRDNotInstalled(t *testing.T) { + g := NewWithT(t) + ctx := context.Background() + + h := setupTestHelper(false) + + result, err := GetSigstoreImagePolicy(ctx, h, []string{"mirror.example.com:5000/openstack-k8s-operators"}, nil) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(result).To(BeNil()) +} + +func TestGetSigstoreImagePolicy_WildcardScopeMatch(t *testing.T) { + g := NewWithT(t) + ctx := context.Background() + + policy := newSigstorePolicy( + "v1", + "wildcard-policy", + []string{"*.example.com"}, + "test-public-key", + "MatchRepoDigestOrExact", + "", + ) + + h := setupTestHelper(true, newClusterImagePolicyCRD("v1"), policy) + + result, err := GetSigstoreImagePolicy(ctx, h, []string{"mirror.example.com:5000/rhoso"}, nil) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(result).ToNot(BeNil()) + g.Expect(result.RegistryMappings).To(Equal([]RegistryMapping{ + {Mirror: "mirror.example.com:5000/rhoso"}, + })) +} + +func TestGetSigstoreImagePolicy_SkipsOpenshiftPolicy(t *testing.T) { + g := NewWithT(t) + ctx := context.Background() + + openshiftPolicy := newSigstorePolicy( + "v1", + "openshift", + []string{"mirror.example.com:5000"}, + "openshift-key", + "MatchRepoDigestOrExact", + "", + ) + + h := setupTestHelper(true, newClusterImagePolicyCRD("v1"), openshiftPolicy) + + result, err := GetSigstoreImagePolicy(ctx, h, []string{"mirror.example.com:5000/rhoso"}, nil) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(result).To(BeNil()) +}