diff --git a/cmd/machine-config-controller/start.go b/cmd/machine-config-controller/start.go index e151a5d3b7..b1f7bcac3b 100644 --- a/cmd/machine-config-controller/start.go +++ b/cmd/machine-config-controller/start.go @@ -172,6 +172,8 @@ func createControllers(ctx *ctrlcommon.ControllerContext) []ctrlcommon.Controlle ctx.ConfigInformerFactory.Config().V1().Images(), ctx.ConfigInformerFactory.Config().V1().ImageDigestMirrorSets(), ctx.ConfigInformerFactory.Config().V1().ImageTagMirrorSets(), + ctx.ConfigInformerFactory.Config().V1alpha1().ImagePolicies(), + ctx.ConfigInformerFactory.Config().V1alpha1().ClusterImagePolicies(), ctx.OperatorInformerFactory.Operator().V1alpha1().ImageContentSourcePolicies(), ctx.ConfigInformerFactory.Config().V1().ClusterVersions(), ctx.ClientBuilder.KubeClientOrDie("container-runtime-config-controller"), diff --git a/docs/ClusterImagePolicyDesign.md b/docs/ClusterImagePolicyDesign.md new file mode 100644 index 0000000000..d844effd7d --- /dev/null +++ b/docs/ClusterImagePolicyDesign.md @@ -0,0 +1,48 @@ +## Summary +ClusterImagePolicy and ImagePolicy are CRDs that managed by ContainerRuntimeConfig controller. These CRDs allow setting up configurations for CRI-O to verify the container images signed using [Sigstore](https://www.sigstore.dev/) tools. + +## Goals +Generating corresponding CRI-O configuration files for image signature verification. Rollout ClusterImagePolicy to `/etc/containers/policy.json` for cluster wide configuration. Roll out ImagePolicy to `/etc/crio/policies/.json` for pod namespace-separated signature policies configuration. + +## Non-Goals +Rolling out configuration for OCP payload repositories. The (super scope of) OCP payload repositories will not be written to the configuration files. + +## CRD +[ClusterImagePolicy CRD](https://github.com/openshift/api/blob/master/config/v1alpha1/0000_10_config-operator_01_clusterimagepolicy-TechPreviewNoUpgrade.crd.yaml) + +[ImagePolicy CRD](https://github.com/openshift/api/blob/master/config/v1alpha1/0000_10_config-operator_01_imagepolicy-TechPreviewNoUpgrade.crd.yaml) + +## Example + +## Validation and Troubleshooting + +## Implementation Details +The ContainerRuntimeConfigController would perform the following steps: + +1. Validate the ClusterImagePolicy and ImagePolicy objects on the cluster. Follow the table below to ignore the conflicting scopes. + +| |process the policies from the CRs | | | | +|----------------------------------------------------------------------------------------------------------------- |------------------------------------------------ |----------------------------------------------------------------------------------- |--- |--- | +| same scope in different CRs | ImagePolicy | ClusterImagePolicy | | | +| ClusterImagePolicy ImagePolicy (scope in the ClusterImagePolicy is equal to or broader than in the ImagePolicy) | Do not deploy non-global policy for this scope | Write the cluster policy to `/etc/containers/policy.json` and `.json` | | | +| ClusterImagePolicy ClusterImagePolicy | N/A | Append the policy to existing `etc/containers/policy.json` | | | +| ImagePolicy ImagePolicy | append the policy to .json | N/A | | | + +2. Render the current MachineConfigs (storage.files.contents[policy.json]) into the originalPolicyIgn + +3. Serialize the cluster level policies to `policy.json`. + +4. Copy the cluster policy.json to `.json`, serialize the namespace level policies to `.json`. + +5. Add registries configuration to `/etc/containers/registries.d/sigstore-registries.yaml`. This configuration is used to specify the sigstore is being used as the image signature verification backend. + +6. Update the ignition file `/etc/containers/policy.json` within the `99--generated-registries` MachineConfig. + +7. Create or Update the ignition file `/etc/crio/policies/.json` within the `99--generated-imagepolicies` MachineConfig. + +After deletion all of the ClusterImagePolicy or the ImagePolicy instance the config will be reverted to the original policy.json. + +## See Also +see **[containers-policy.json(5)](https://github.com/containers/image/blob/main/docs/containers-policy.json.5.md)**, **[containers-registries.d(5)](https://github.com/containers/image/blob/main/docs/containers-registries.d.5.md)** for more information. + + diff --git a/go.mod b/go.mod index caf9926b5c..d8d8149a92 100644 --- a/go.mod +++ b/go.mod @@ -27,7 +27,7 @@ require ( github.com/google/renameio v0.1.0 github.com/imdario/mergo v0.3.13 github.com/opencontainers/go-digest v1.0.0 - github.com/openshift/api v0.0.0-20240205144533-7162acc29bb6 + github.com/openshift/api v0.0.0-20240124164020-e2ce40831f2e github.com/openshift/client-go v0.0.0-20240104132419-223261fd8630 github.com/openshift/cluster-config-operator v0.0.0-alpha.0.0.20231213185242-e4dc676febfe github.com/openshift/library-go v0.0.0-20231020125034-5a2d9fe760b3 @@ -330,3 +330,7 @@ require ( ) replace k8s.io/kube-openapi => github.com/openshift/kube-openapi v0.0.0-20230816122517-ffc8f001abb0 + +replace github.com/openshift/api => github.com/QiWang19/api v0.0.0-20240210054700-a95bb144f44f + +replace github.com/openshift/client-go => github.com/QiWang19/client-go v0.0.0-20240210061104-d13d84b73765 diff --git a/go.sum b/go.sum index 42820aab6e..c8f6e2e5fb 100644 --- a/go.sum +++ b/go.sum @@ -72,6 +72,10 @@ github.com/OpenPeeDeeP/depguard/v2 v2.1.0 h1:aQl70G173h/GZYhWf36aE5H0KaujXfVMnn/ github.com/OpenPeeDeeP/depguard/v2 v2.1.0/go.mod h1:PUBgk35fX4i7JDmwzlJwJ+GMe6NfO1723wmJMgPThNQ= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/QiWang19/api v0.0.0-20240210054700-a95bb144f44f h1:V8tMeJUo24MoTX0Ac8Ud6y7AgcYJ53fk8av43ylbqN4= +github.com/QiWang19/api v0.0.0-20240210054700-a95bb144f44f/go.mod h1:CxgbWAlvu2iQB0UmKTtRu1YfepRg1/vJ64n2DlIEVz4= +github.com/QiWang19/client-go v0.0.0-20240210061104-d13d84b73765 h1:/el4UT01Tg+5nnQaZSLzeZYBmVLhaFSDLqdp+lHFa9w= +github.com/QiWang19/client-go v0.0.0-20240210061104-d13d84b73765/go.mod h1:abbgYykRixaLLqDXiSdoMd8/sIm5bE1kfaMU1rGHJ7c= github.com/ajeddeloh/go-json v0.0.0-20170920214419-6a2fe990e083/go.mod h1:otnto4/Icqn88WCcM4bhIJNSgsh9VLBuspyyCfvof9c= github.com/ajeddeloh/go-json v0.0.0-20200220154158-5ae607161559 h1:4SPQljF/GJ8Q+QlCWMWxRBepub4DresnOm4eI2ebFGc= github.com/ajeddeloh/go-json v0.0.0-20200220154158-5ae607161559/go.mod h1:otnto4/Icqn88WCcM4bhIJNSgsh9VLBuspyyCfvof9c= @@ -689,10 +693,6 @@ github.com/opencontainers/runc v1.1.10 h1:EaL5WeO9lv9wmS6SASjszOeQdSctvpbu0DdBQB github.com/opencontainers/runc v1.1.10/go.mod h1:+/R6+KmDlh+hOO8NkjmgkG9Qzvypzk0yXxAPYYR65+M= github.com/opencontainers/runtime-spec v1.1.0 h1:HHUyrt9mwHUjtasSbXSMvs4cyFxh+Bll4AjJ9odEGpg= github.com/opencontainers/runtime-spec v1.1.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/openshift/api v0.0.0-20240205144533-7162acc29bb6 h1:OrjG0Lt0pXNYd6LUfGD3BYoBWXhfRMAMK0LYu8pdyqQ= -github.com/openshift/api v0.0.0-20240205144533-7162acc29bb6/go.mod h1:CxgbWAlvu2iQB0UmKTtRu1YfepRg1/vJ64n2DlIEVz4= -github.com/openshift/client-go v0.0.0-20240104132419-223261fd8630 h1:JQ/TO3bSDowReecFDvz+Nr0Fx8aoJI0zdo01y2W5NKk= -github.com/openshift/client-go v0.0.0-20240104132419-223261fd8630/go.mod h1:8W4atsD8vBtEK0qplKpFWo+7XsQwzHTlqL7o/XpagRM= github.com/openshift/cluster-config-operator v0.0.0-alpha.0.0.20231213185242-e4dc676febfe h1:wDQtyIbJJIoif2Ux0S+9MJWIWEGV0oG+iLm8WtqwdSw= github.com/openshift/cluster-config-operator v0.0.0-alpha.0.0.20231213185242-e4dc676febfe/go.mod h1:SGUtv1pKZSzSVr2YCxXFvhE+LbGfI+vcetEhNicKayw= github.com/openshift/kube-openapi v0.0.0-20230816122517-ffc8f001abb0 h1:GPlAy197Jkr+D0T2FNWanamraTdzS/r9ZkT29lxvHaA= diff --git a/install/0000_10_config-operator_01_clusterimagepolicy-CustomNoUpgrade.crd.yaml b/install/0000_10_config-operator_01_clusterimagepolicy-CustomNoUpgrade.crd.yaml new file mode 100644 index 0000000000..79c1767755 --- /dev/null +++ b/install/0000_10_config-operator_01_clusterimagepolicy-CustomNoUpgrade.crd.yaml @@ -0,0 +1,395 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + api-approved.openshift.io: https://github.com/openshift/api/pull/1457 + include.release.openshift.io/ibm-cloud-managed: "true" + include.release.openshift.io/self-managed-high-availability: "true" + include.release.openshift.io/single-node-developer: "true" + release.openshift.io/feature-set: CustomNoUpgrade + name: clusterimagepolicies.config.openshift.io +spec: + group: config.openshift.io + names: + kind: ClusterImagePolicy + listKind: ClusterImagePolicyList + plural: clusterimagepolicies + singular: clusterimagepolicy + scope: Cluster + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: "ClusterImagePolicy holds cluster-wide configuration for image + signature verification \n Compatibility level 4: No compatibility is provided, + the API can change at any point for any reason. These capabilities should + not be used by applications needing long term support." + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: spec contains the configuration for the cluster image policy. + properties: + policy: + description: policy contains configuration to allow scopes to be verified, + and defines how images not matching the verification policy will + be treated. + properties: + rootOfTrust: + description: rootOfTrust specifies the root of trust for the policy. + properties: + fulcioCAWithRekor: + description: 'fulcioCAWithRekor defines the root of trust + based on the Fulcio certificate and the Rekor public key. + For more information about Fulcio and Rekor, please refer + to the document at: https://github.com/sigstore/fulcio and + https://github.com/sigstore/rekor' + properties: + fulcioCAData: + description: fulcioCAData contains inline base64-encoded + data for the PEM format fulcio CA. fulcioCAData must + be at most 8192 characters. + format: byte + maxLength: 8192 + type: string + fulcioSubject: + description: fulcioSubject specifies OIDC issuer and the + email of the Fulcio authentication configuration. + properties: + oidcIssuer: + description: 'oidcIssuer contains the expected OIDC + issuer. It will be verified that the Fulcio-issued + certificate contains a (Fulcio-defined) certificate + extension pointing at this OIDC issuer URL. When + Fulcio issues certificates, it includes a value + based on an URL inside the client-provided ID token. + Example: "https://expected.OIDC.issuer/"' + type: string + x-kubernetes-validations: + - message: oidcIssuer must be a valid URL + rule: isURL(self) + signedEmail: + description: 'signedEmail holds the email address + the the Fulcio certificate is issued for. Example: + "expected-signing-user@example.com"' + type: string + x-kubernetes-validations: + - message: invalid email address + rule: self.matches('^\\S+@\\S+$') + required: + - oidcIssuer + - signedEmail + type: object + rekorKeyData: + description: rekorKeyData contains inline base64-encoded + data for the PEM format from the Rekor public key. rekorKeyData + must be at most 8192 characters. + maxLength: 8192 + type: string + required: + - fulcioCAData + - fulcioSubject + - rekorKeyData + type: object + policyType: + description: policyType serves as the union's discriminator. + Users are required to assign a value to this field, choosing + one of the policy types that define the root of trust. "PublicKey" + indicates that the policy relies on a sigstore publicKey + and may optionally use a Rekor verification. "FulcioCAWithRekor" + indicates that the policy is based on the Fulcio certification + and incorporates a Rekor verification. + enum: + - PublicKey + - FulcioCAWithRekor + type: string + publicKey: + description: publicKey defines the root of trust based on + a sigstore public key. + properties: + keyData: + description: keyData contains inline base64-encoded data + for the PEM format public key. KeyData must be at most + 8192 characters. + maxLength: 8192 + type: string + rekorKeyData: + description: rekorKeyData contains inline base64-encoded + data for the PEM format from the Rekor public key. rekorKeyData + must be at most 8192 characters. + maxLength: 8192 + type: string + required: + - keyData + type: object + required: + - policyType + type: object + x-kubernetes-validations: + - message: publicKey is required when policyType is PublicKey, + and forbidden otherwise + rule: 'has(self.policyType) && self.policyType == ''PublicKey'' + ? has(self.publicKey) : !has(self.publicKey)' + - message: fulcioCAWithRekor is required when policyType is FulcioCAWithRekor, + and forbidden otherwise + rule: 'has(self.policyType) && self.policyType == ''FulcioCAWithRekor'' + ? has(self.fulcioCAWithRekor) : !has(self.fulcioCAWithRekor)' + signedIdentity: + description: signedIdentity specifies what image identity the + signature claims about the image. The required matchPolicy field + specifies the approach used in the verification process to verify + the identity in the signature and the actual image identity, + the default matchPolicy is "MatchRepoDigestOrExact". + properties: + exactRepository: + description: exactRepository is required if matchPolicy is + set to "ExactRepository". + properties: + repository: + description: repository is the reference of the image + identity to be matched. The value should be a repository + name (by omitting the tag or digest) in a registry implementing + the "Docker Registry HTTP API V2". For example, docker.io/library/busybox + maxLength: 512 + type: string + x-kubernetes-validations: + - message: invalid repository or prefix in the signedIdentity, + should not include the tag or digest + rule: 'self.matches(''.*:([\\w][\\w.-]{0,127})$'')? + self.matches(''^(localhost:[0-9]+)$''): true' + - message: invalid repository or prefix in the signedIdentity + rule: self.matches('^(((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$') + required: + - repository + type: object + matchPolicy: + description: matchPolicy sets the type of matching to be used. + Valid values are "MatchRepoDigestOrExact", "MatchRepository", + "ExactRepository", "RemapIdentity". When omitted, the default + value is "MatchRepoDigestOrExact". If set matchPolicy to + ExactRepository, then the exactRepository must be specified. + If set matchPolicy to RemapIdentity, then the remapIdentity + must be specified. "MatchRepoDigestOrExact" means that the + identity in the signature must be in the same repository + as the image identity if the image identity is referenced + by a digest. Otherwise, the identity in the signature must + be the same as the image identity. "MatchRepository" means + that the identity in the signature must be in the same repository + as the image identity. "ExactRepository" means that the + identity in the signature must be in the same repository + as a specific identity specified by "repository". "RemapIdentity" + means that the signature must be in the same as the remapped + image identity. Remapped image identity is obtained by replacing + the "prefix" with the specified “signedPrefix” if the the + image identity matches the specified remapPrefix. + enum: + - MatchRepoDigestOrExact + - MatchRepository + - ExactRepository + - RemapIdentity + type: string + remapIdentity: + description: remapIdentity is required if matchPolicy is set + to "RemapIdentity". + properties: + prefix: + description: prefix is the prefix of the image identity + to be matched. If the image identity matches the specified + prefix, that prefix is replaced by the specified “signedPrefix” + (otherwise it is used as unchanged and no remapping + takes place). This useful when verifying signatures + for a mirror of some other repository namespace that + preserves the vendor’s repository structure. The prefix + and signedPrefix values can be either host[:port] values + (matching exactly the same host[:port], string), repository + namespaces, or repositories (i.e. they must not contain + tags/digests), and match as prefixes of the fully expanded + form. For example, docker.io/library/busybox (not busybox) + to specify that single repository, or docker.io/library + (not an empty string) to specify the parent namespace + of docker.io/library/busybox. + maxLength: 512 + type: string + x-kubernetes-validations: + - message: invalid repository or prefix in the signedIdentity, + should not include the tag or digest + rule: 'self.matches(''.*:([\\w][\\w.-]{0,127})$'')? + self.matches(''^(localhost:[0-9]+)$''): true' + - message: invalid repository or prefix in the signedIdentity + rule: self.matches('^(((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$') + signedPrefix: + description: signedPrefix is the prefix of the image identity + to be matched in the signature. The format is the same + as "prefix". The values can be either host[:port] values + (matching exactly the same host[:port], string), repository + namespaces, or repositories (i.e. they must not contain + tags/digests), and match as prefixes of the fully expanded + form. For example, docker.io/library/busybox (not busybox) + to specify that single repository, or docker.io/library + (not an empty string) to specify the parent namespace + of docker.io/library/busybox. + maxLength: 512 + type: string + x-kubernetes-validations: + - message: invalid repository or prefix in the signedIdentity, + should not include the tag or digest + rule: 'self.matches(''.*:([\\w][\\w.-]{0,127})$'')? + self.matches(''^(localhost:[0-9]+)$''): true' + - message: invalid repository or prefix in the signedIdentity + rule: self.matches('^(((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$') + required: + - prefix + - signedPrefix + type: object + required: + - matchPolicy + type: object + x-kubernetes-validations: + - message: exactRepository is required when matchPolicy is ExactRepository, + and forbidden otherwise + rule: '(has(self.matchPolicy) && self.matchPolicy == ''ExactRepository'') + ? has(self.exactRepository) : !has(self.exactRepository)' + - message: remapIdentity is required when matchPolicy is RemapIdentity, + and forbidden otherwise + rule: '(has(self.matchPolicy) && self.matchPolicy == ''RemapIdentity'') + ? has(self.remapIdentity) : !has(self.remapIdentity)' + required: + - rootOfTrust + type: object + scopes: + description: 'scopes defines the list of image identities assigned + to a policy. Each item refers to a scope in a registry implementing + the "Docker Registry HTTP API V2". Scopes matching individual images + are named Docker references in the fully expanded form, either using + a tag or digest. For example, docker.io/library/busybox:latest (not + busybox:latest). More general scopes are prefixes of individual-image + scopes, and specify a repository (by omitting the tag or digest), + a repository namespace, or a registry host (by only specifying the + host name and possibly a port number) or a wildcard expression starting + with `*.`, for matching all subdomains (not including a port number). + Wildcards are only supported for subdomain matching, and may not + be used in the middle of the host, i.e. *.example.com is a valid + case, but example*.*.com is not. Please be aware that the scopes + should not be nested under the repositories of OpenShift Container + Platform images. If configured, the policies for OpenShift Container + Platform repositories will not be in effect. For additional details + about the format, please refer to the document explaining the docker + transport field, which can be found at: https://github.com/containers/image/blob/main/docs/containers-policy.json.5.md#docker' + items: + maxLength: 512 + type: string + x-kubernetes-validations: + - message: invalid image scope format, scope must contain a fully + qualified domain name or 'localhost' + rule: 'size(self.split(''/'')[0].split(''.'')) == 1 ? self.split(''/'')[0].split(''.'')[0].split('':'')[0] + == ''localhost'' : true' + - message: invalid image scope with wildcard, a wildcard can only + be at the start of the domain and is only supported for subdomain + matching, not path matching + rule: 'self.contains(''*'') ? self.matches(''^\\*(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+$'') + : true' + - message: invalid repository namespace or image specification in + the image scope + rule: '!self.contains(''*'') ? self.matches(''^((((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?)(?::([\\w][\\w.-]{0,127}))?(?:@([A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}))?$'') + : true' + maxItems: 256 + type: array + x-kubernetes-list-type: set + required: + - policy + - scopes + type: object + status: + description: status contains the observed state of the resource. + properties: + conditions: + description: conditions provide details on the status of this API + Resource. + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/install/0000_10_config-operator_01_clusterimagepolicy-TechPreviewNoUpgrade.crd.yaml b/install/0000_10_config-operator_01_clusterimagepolicy-TechPreviewNoUpgrade.crd.yaml new file mode 100644 index 0000000000..538c44ace1 --- /dev/null +++ b/install/0000_10_config-operator_01_clusterimagepolicy-TechPreviewNoUpgrade.crd.yaml @@ -0,0 +1,395 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + api-approved.openshift.io: https://github.com/openshift/api/pull/1457 + include.release.openshift.io/ibm-cloud-managed: "true" + include.release.openshift.io/self-managed-high-availability: "true" + include.release.openshift.io/single-node-developer: "true" + release.openshift.io/feature-set: TechPreviewNoUpgrade + name: clusterimagepolicies.config.openshift.io +spec: + group: config.openshift.io + names: + kind: ClusterImagePolicy + listKind: ClusterImagePolicyList + plural: clusterimagepolicies + singular: clusterimagepolicy + scope: Cluster + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: "ClusterImagePolicy holds cluster-wide configuration for image + signature verification \n Compatibility level 4: No compatibility is provided, + the API can change at any point for any reason. These capabilities should + not be used by applications needing long term support." + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: spec contains the configuration for the cluster image policy. + properties: + policy: + description: policy contains configuration to allow scopes to be verified, + and defines how images not matching the verification policy will + be treated. + properties: + rootOfTrust: + description: rootOfTrust specifies the root of trust for the policy. + properties: + fulcioCAWithRekor: + description: 'fulcioCAWithRekor defines the root of trust + based on the Fulcio certificate and the Rekor public key. + For more information about Fulcio and Rekor, please refer + to the document at: https://github.com/sigstore/fulcio and + https://github.com/sigstore/rekor' + properties: + fulcioCAData: + description: fulcioCAData contains inline base64-encoded + data for the PEM format fulcio CA. fulcioCAData must + be at most 8192 characters. + format: byte + maxLength: 8192 + type: string + fulcioSubject: + description: fulcioSubject specifies OIDC issuer and the + email of the Fulcio authentication configuration. + properties: + oidcIssuer: + description: 'oidcIssuer contains the expected OIDC + issuer. It will be verified that the Fulcio-issued + certificate contains a (Fulcio-defined) certificate + extension pointing at this OIDC issuer URL. When + Fulcio issues certificates, it includes a value + based on an URL inside the client-provided ID token. + Example: "https://expected.OIDC.issuer/"' + type: string + x-kubernetes-validations: + - message: oidcIssuer must be a valid URL + rule: isURL(self) + signedEmail: + description: 'signedEmail holds the email address + the the Fulcio certificate is issued for. Example: + "expected-signing-user@example.com"' + type: string + x-kubernetes-validations: + - message: invalid email address + rule: self.matches('^\\S+@\\S+$') + required: + - oidcIssuer + - signedEmail + type: object + rekorKeyData: + description: rekorKeyData contains inline base64-encoded + data for the PEM format from the Rekor public key. rekorKeyData + must be at most 8192 characters. + maxLength: 8192 + type: string + required: + - fulcioCAData + - fulcioSubject + - rekorKeyData + type: object + policyType: + description: policyType serves as the union's discriminator. + Users are required to assign a value to this field, choosing + one of the policy types that define the root of trust. "PublicKey" + indicates that the policy relies on a sigstore publicKey + and may optionally use a Rekor verification. "FulcioCAWithRekor" + indicates that the policy is based on the Fulcio certification + and incorporates a Rekor verification. + enum: + - PublicKey + - FulcioCAWithRekor + type: string + publicKey: + description: publicKey defines the root of trust based on + a sigstore public key. + properties: + keyData: + description: keyData contains inline base64-encoded data + for the PEM format public key. KeyData must be at most + 8192 characters. + maxLength: 8192 + type: string + rekorKeyData: + description: rekorKeyData contains inline base64-encoded + data for the PEM format from the Rekor public key. rekorKeyData + must be at most 8192 characters. + maxLength: 8192 + type: string + required: + - keyData + type: object + required: + - policyType + type: object + x-kubernetes-validations: + - message: publicKey is required when policyType is PublicKey, + and forbidden otherwise + rule: 'has(self.policyType) && self.policyType == ''PublicKey'' + ? has(self.publicKey) : !has(self.publicKey)' + - message: fulcioCAWithRekor is required when policyType is FulcioCAWithRekor, + and forbidden otherwise + rule: 'has(self.policyType) && self.policyType == ''FulcioCAWithRekor'' + ? has(self.fulcioCAWithRekor) : !has(self.fulcioCAWithRekor)' + signedIdentity: + description: signedIdentity specifies what image identity the + signature claims about the image. The required matchPolicy field + specifies the approach used in the verification process to verify + the identity in the signature and the actual image identity, + the default matchPolicy is "MatchRepoDigestOrExact". + properties: + exactRepository: + description: exactRepository is required if matchPolicy is + set to "ExactRepository". + properties: + repository: + description: repository is the reference of the image + identity to be matched. The value should be a repository + name (by omitting the tag or digest) in a registry implementing + the "Docker Registry HTTP API V2". For example, docker.io/library/busybox + maxLength: 512 + type: string + x-kubernetes-validations: + - message: invalid repository or prefix in the signedIdentity, + should not include the tag or digest + rule: 'self.matches(''.*:([\\w][\\w.-]{0,127})$'')? + self.matches(''^(localhost:[0-9]+)$''): true' + - message: invalid repository or prefix in the signedIdentity + rule: self.matches('^(((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$') + required: + - repository + type: object + matchPolicy: + description: matchPolicy sets the type of matching to be used. + Valid values are "MatchRepoDigestOrExact", "MatchRepository", + "ExactRepository", "RemapIdentity". When omitted, the default + value is "MatchRepoDigestOrExact". If set matchPolicy to + ExactRepository, then the exactRepository must be specified. + If set matchPolicy to RemapIdentity, then the remapIdentity + must be specified. "MatchRepoDigestOrExact" means that the + identity in the signature must be in the same repository + as the image identity if the image identity is referenced + by a digest. Otherwise, the identity in the signature must + be the same as the image identity. "MatchRepository" means + that the identity in the signature must be in the same repository + as the image identity. "ExactRepository" means that the + identity in the signature must be in the same repository + as a specific identity specified by "repository". "RemapIdentity" + means that the signature must be in the same as the remapped + image identity. Remapped image identity is obtained by replacing + the "prefix" with the specified “signedPrefix” if the the + image identity matches the specified remapPrefix. + enum: + - MatchRepoDigestOrExact + - MatchRepository + - ExactRepository + - RemapIdentity + type: string + remapIdentity: + description: remapIdentity is required if matchPolicy is set + to "RemapIdentity". + properties: + prefix: + description: prefix is the prefix of the image identity + to be matched. If the image identity matches the specified + prefix, that prefix is replaced by the specified “signedPrefix” + (otherwise it is used as unchanged and no remapping + takes place). This useful when verifying signatures + for a mirror of some other repository namespace that + preserves the vendor’s repository structure. The prefix + and signedPrefix values can be either host[:port] values + (matching exactly the same host[:port], string), repository + namespaces, or repositories (i.e. they must not contain + tags/digests), and match as prefixes of the fully expanded + form. For example, docker.io/library/busybox (not busybox) + to specify that single repository, or docker.io/library + (not an empty string) to specify the parent namespace + of docker.io/library/busybox. + maxLength: 512 + type: string + x-kubernetes-validations: + - message: invalid repository or prefix in the signedIdentity, + should not include the tag or digest + rule: 'self.matches(''.*:([\\w][\\w.-]{0,127})$'')? + self.matches(''^(localhost:[0-9]+)$''): true' + - message: invalid repository or prefix in the signedIdentity + rule: self.matches('^(((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$') + signedPrefix: + description: signedPrefix is the prefix of the image identity + to be matched in the signature. The format is the same + as "prefix". The values can be either host[:port] values + (matching exactly the same host[:port], string), repository + namespaces, or repositories (i.e. they must not contain + tags/digests), and match as prefixes of the fully expanded + form. For example, docker.io/library/busybox (not busybox) + to specify that single repository, or docker.io/library + (not an empty string) to specify the parent namespace + of docker.io/library/busybox. + maxLength: 512 + type: string + x-kubernetes-validations: + - message: invalid repository or prefix in the signedIdentity, + should not include the tag or digest + rule: 'self.matches(''.*:([\\w][\\w.-]{0,127})$'')? + self.matches(''^(localhost:[0-9]+)$''): true' + - message: invalid repository or prefix in the signedIdentity + rule: self.matches('^(((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$') + required: + - prefix + - signedPrefix + type: object + required: + - matchPolicy + type: object + x-kubernetes-validations: + - message: exactRepository is required when matchPolicy is ExactRepository, + and forbidden otherwise + rule: '(has(self.matchPolicy) && self.matchPolicy == ''ExactRepository'') + ? has(self.exactRepository) : !has(self.exactRepository)' + - message: remapIdentity is required when matchPolicy is RemapIdentity, + and forbidden otherwise + rule: '(has(self.matchPolicy) && self.matchPolicy == ''RemapIdentity'') + ? has(self.remapIdentity) : !has(self.remapIdentity)' + required: + - rootOfTrust + type: object + scopes: + description: 'scopes defines the list of image identities assigned + to a policy. Each item refers to a scope in a registry implementing + the "Docker Registry HTTP API V2". Scopes matching individual images + are named Docker references in the fully expanded form, either using + a tag or digest. For example, docker.io/library/busybox:latest (not + busybox:latest). More general scopes are prefixes of individual-image + scopes, and specify a repository (by omitting the tag or digest), + a repository namespace, or a registry host (by only specifying the + host name and possibly a port number) or a wildcard expression starting + with `*.`, for matching all subdomains (not including a port number). + Wildcards are only supported for subdomain matching, and may not + be used in the middle of the host, i.e. *.example.com is a valid + case, but example*.*.com is not. Please be aware that the scopes + should not be nested under the repositories of OpenShift Container + Platform images. If configured, the policies for OpenShift Container + Platform repositories will not be in effect. For additional details + about the format, please refer to the document explaining the docker + transport field, which can be found at: https://github.com/containers/image/blob/main/docs/containers-policy.json.5.md#docker' + items: + maxLength: 512 + type: string + x-kubernetes-validations: + - message: invalid image scope format, scope must contain a fully + qualified domain name or 'localhost' + rule: 'size(self.split(''/'')[0].split(''.'')) == 1 ? self.split(''/'')[0].split(''.'')[0].split('':'')[0] + == ''localhost'' : true' + - message: invalid image scope with wildcard, a wildcard can only + be at the start of the domain and is only supported for subdomain + matching, not path matching + rule: 'self.contains(''*'') ? self.matches(''^\\*(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+$'') + : true' + - message: invalid repository namespace or image specification in + the image scope + rule: '!self.contains(''*'') ? self.matches(''^((((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?)(?::([\\w][\\w.-]{0,127}))?(?:@([A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}))?$'') + : true' + maxItems: 256 + type: array + x-kubernetes-list-type: set + required: + - policy + - scopes + type: object + status: + description: status contains the observed state of the resource. + properties: + conditions: + description: conditions provide details on the status of this API + Resource. + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/install/0000_10_config-operator_01_imagepolicy-CustomNoUpgrade.crd.yaml b/install/0000_10_config-operator_01_imagepolicy-CustomNoUpgrade.crd.yaml new file mode 100644 index 0000000000..0d62e2cdae --- /dev/null +++ b/install/0000_10_config-operator_01_imagepolicy-CustomNoUpgrade.crd.yaml @@ -0,0 +1,395 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + api-approved.openshift.io: https://github.com/openshift/api/pull/1457 + include.release.openshift.io/ibm-cloud-managed: "true" + include.release.openshift.io/self-managed-high-availability: "true" + include.release.openshift.io/single-node-developer: "true" + release.openshift.io/feature-set: CustomNoUpgrade + name: imagepolicies.config.openshift.io +spec: + group: config.openshift.io + names: + kind: ImagePolicy + listKind: ImagePolicyList + plural: imagepolicies + singular: imagepolicy + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: "ImagePolicy holds namespace-wide configuration for image signature + verification \n Compatibility level 4: No compatibility is provided, the + API can change at any point for any reason. These capabilities should not + be used by applications needing long term support." + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: spec holds user settable values for configuration + properties: + policy: + description: policy contains configuration to allow scopes to be verified, + and defines how images not matching the verification policy will + be treated. + properties: + rootOfTrust: + description: rootOfTrust specifies the root of trust for the policy. + properties: + fulcioCAWithRekor: + description: 'fulcioCAWithRekor defines the root of trust + based on the Fulcio certificate and the Rekor public key. + For more information about Fulcio and Rekor, please refer + to the document at: https://github.com/sigstore/fulcio and + https://github.com/sigstore/rekor' + properties: + fulcioCAData: + description: fulcioCAData contains inline base64-encoded + data for the PEM format fulcio CA. fulcioCAData must + be at most 8192 characters. + format: byte + maxLength: 8192 + type: string + fulcioSubject: + description: fulcioSubject specifies OIDC issuer and the + email of the Fulcio authentication configuration. + properties: + oidcIssuer: + description: 'oidcIssuer contains the expected OIDC + issuer. It will be verified that the Fulcio-issued + certificate contains a (Fulcio-defined) certificate + extension pointing at this OIDC issuer URL. When + Fulcio issues certificates, it includes a value + based on an URL inside the client-provided ID token. + Example: "https://expected.OIDC.issuer/"' + type: string + x-kubernetes-validations: + - message: oidcIssuer must be a valid URL + rule: isURL(self) + signedEmail: + description: 'signedEmail holds the email address + the the Fulcio certificate is issued for. Example: + "expected-signing-user@example.com"' + type: string + x-kubernetes-validations: + - message: invalid email address + rule: self.matches('^\\S+@\\S+$') + required: + - oidcIssuer + - signedEmail + type: object + rekorKeyData: + description: rekorKeyData contains inline base64-encoded + data for the PEM format from the Rekor public key. rekorKeyData + must be at most 8192 characters. + maxLength: 8192 + type: string + required: + - fulcioCAData + - fulcioSubject + - rekorKeyData + type: object + policyType: + description: policyType serves as the union's discriminator. + Users are required to assign a value to this field, choosing + one of the policy types that define the root of trust. "PublicKey" + indicates that the policy relies on a sigstore publicKey + and may optionally use a Rekor verification. "FulcioCAWithRekor" + indicates that the policy is based on the Fulcio certification + and incorporates a Rekor verification. + enum: + - PublicKey + - FulcioCAWithRekor + type: string + publicKey: + description: publicKey defines the root of trust based on + a sigstore public key. + properties: + keyData: + description: keyData contains inline base64-encoded data + for the PEM format public key. KeyData must be at most + 8192 characters. + maxLength: 8192 + type: string + rekorKeyData: + description: rekorKeyData contains inline base64-encoded + data for the PEM format from the Rekor public key. rekorKeyData + must be at most 8192 characters. + maxLength: 8192 + type: string + required: + - keyData + type: object + required: + - policyType + type: object + x-kubernetes-validations: + - message: publicKey is required when policyType is PublicKey, + and forbidden otherwise + rule: 'has(self.policyType) && self.policyType == ''PublicKey'' + ? has(self.publicKey) : !has(self.publicKey)' + - message: fulcioCAWithRekor is required when policyType is FulcioCAWithRekor, + and forbidden otherwise + rule: 'has(self.policyType) && self.policyType == ''FulcioCAWithRekor'' + ? has(self.fulcioCAWithRekor) : !has(self.fulcioCAWithRekor)' + signedIdentity: + description: signedIdentity specifies what image identity the + signature claims about the image. The required matchPolicy field + specifies the approach used in the verification process to verify + the identity in the signature and the actual image identity, + the default matchPolicy is "MatchRepoDigestOrExact". + properties: + exactRepository: + description: exactRepository is required if matchPolicy is + set to "ExactRepository". + properties: + repository: + description: repository is the reference of the image + identity to be matched. The value should be a repository + name (by omitting the tag or digest) in a registry implementing + the "Docker Registry HTTP API V2". For example, docker.io/library/busybox + maxLength: 512 + type: string + x-kubernetes-validations: + - message: invalid repository or prefix in the signedIdentity, + should not include the tag or digest + rule: 'self.matches(''.*:([\\w][\\w.-]{0,127})$'')? + self.matches(''^(localhost:[0-9]+)$''): true' + - message: invalid repository or prefix in the signedIdentity + rule: self.matches('^(((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$') + required: + - repository + type: object + matchPolicy: + description: matchPolicy sets the type of matching to be used. + Valid values are "MatchRepoDigestOrExact", "MatchRepository", + "ExactRepository", "RemapIdentity". When omitted, the default + value is "MatchRepoDigestOrExact". If set matchPolicy to + ExactRepository, then the exactRepository must be specified. + If set matchPolicy to RemapIdentity, then the remapIdentity + must be specified. "MatchRepoDigestOrExact" means that the + identity in the signature must be in the same repository + as the image identity if the image identity is referenced + by a digest. Otherwise, the identity in the signature must + be the same as the image identity. "MatchRepository" means + that the identity in the signature must be in the same repository + as the image identity. "ExactRepository" means that the + identity in the signature must be in the same repository + as a specific identity specified by "repository". "RemapIdentity" + means that the signature must be in the same as the remapped + image identity. Remapped image identity is obtained by replacing + the "prefix" with the specified “signedPrefix” if the the + image identity matches the specified remapPrefix. + enum: + - MatchRepoDigestOrExact + - MatchRepository + - ExactRepository + - RemapIdentity + type: string + remapIdentity: + description: remapIdentity is required if matchPolicy is set + to "RemapIdentity". + properties: + prefix: + description: prefix is the prefix of the image identity + to be matched. If the image identity matches the specified + prefix, that prefix is replaced by the specified “signedPrefix” + (otherwise it is used as unchanged and no remapping + takes place). This useful when verifying signatures + for a mirror of some other repository namespace that + preserves the vendor’s repository structure. The prefix + and signedPrefix values can be either host[:port] values + (matching exactly the same host[:port], string), repository + namespaces, or repositories (i.e. they must not contain + tags/digests), and match as prefixes of the fully expanded + form. For example, docker.io/library/busybox (not busybox) + to specify that single repository, or docker.io/library + (not an empty string) to specify the parent namespace + of docker.io/library/busybox. + maxLength: 512 + type: string + x-kubernetes-validations: + - message: invalid repository or prefix in the signedIdentity, + should not include the tag or digest + rule: 'self.matches(''.*:([\\w][\\w.-]{0,127})$'')? + self.matches(''^(localhost:[0-9]+)$''): true' + - message: invalid repository or prefix in the signedIdentity + rule: self.matches('^(((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$') + signedPrefix: + description: signedPrefix is the prefix of the image identity + to be matched in the signature. The format is the same + as "prefix". The values can be either host[:port] values + (matching exactly the same host[:port], string), repository + namespaces, or repositories (i.e. they must not contain + tags/digests), and match as prefixes of the fully expanded + form. For example, docker.io/library/busybox (not busybox) + to specify that single repository, or docker.io/library + (not an empty string) to specify the parent namespace + of docker.io/library/busybox. + maxLength: 512 + type: string + x-kubernetes-validations: + - message: invalid repository or prefix in the signedIdentity, + should not include the tag or digest + rule: 'self.matches(''.*:([\\w][\\w.-]{0,127})$'')? + self.matches(''^(localhost:[0-9]+)$''): true' + - message: invalid repository or prefix in the signedIdentity + rule: self.matches('^(((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$') + required: + - prefix + - signedPrefix + type: object + required: + - matchPolicy + type: object + x-kubernetes-validations: + - message: exactRepository is required when matchPolicy is ExactRepository, + and forbidden otherwise + rule: '(has(self.matchPolicy) && self.matchPolicy == ''ExactRepository'') + ? has(self.exactRepository) : !has(self.exactRepository)' + - message: remapIdentity is required when matchPolicy is RemapIdentity, + and forbidden otherwise + rule: '(has(self.matchPolicy) && self.matchPolicy == ''RemapIdentity'') + ? has(self.remapIdentity) : !has(self.remapIdentity)' + required: + - rootOfTrust + type: object + scopes: + description: 'scopes defines the list of image identities assigned + to a policy. Each item refers to a scope in a registry implementing + the "Docker Registry HTTP API V2". Scopes matching individual images + are named Docker references in the fully expanded form, either using + a tag or digest. For example, docker.io/library/busybox:latest (not + busybox:latest). More general scopes are prefixes of individual-image + scopes, and specify a repository (by omitting the tag or digest), + a repository namespace, or a registry host (by only specifying the + host name and possibly a port number) or a wildcard expression starting + with `*.`, for matching all subdomains (not including a port number). + Wildcards are only supported for subdomain matching, and may not + be used in the middle of the host, i.e. *.example.com is a valid + case, but example*.*.com is not. Please be aware that the scopes + should not be nested under the repositories of OpenShift Container + Platform images. If configured, the policies for OpenShift Container + Platform repositories will not be in effect. For additional details + about the format, please refer to the document explaining the docker + transport field, which can be found at: https://github.com/containers/image/blob/main/docs/containers-policy.json.5.md#docker' + items: + maxLength: 512 + type: string + x-kubernetes-validations: + - message: invalid image scope format, scope must contain a fully + qualified domain name or 'localhost' + rule: 'size(self.split(''/'')[0].split(''.'')) == 1 ? self.split(''/'')[0].split(''.'')[0].split('':'')[0] + == ''localhost'' : true' + - message: invalid image scope with wildcard, a wildcard can only + be at the start of the domain and is only supported for subdomain + matching, not path matching + rule: 'self.contains(''*'') ? self.matches(''^\\*(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+$'') + : true' + - message: invalid repository namespace or image specification in + the image scope + rule: '!self.contains(''*'') ? self.matches(''^((((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?)(?::([\\w][\\w.-]{0,127}))?(?:@([A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}))?$'') + : true' + maxItems: 256 + type: array + x-kubernetes-list-type: set + required: + - policy + - scopes + type: object + status: + description: status contains the observed state of the resource. + properties: + conditions: + description: conditions provide details on the status of this API + Resource. + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/install/0000_10_config-operator_01_imagepolicy-TechPreviewNoUpgrade.crd.yaml b/install/0000_10_config-operator_01_imagepolicy-TechPreviewNoUpgrade.crd.yaml new file mode 100644 index 0000000000..3cb1164875 --- /dev/null +++ b/install/0000_10_config-operator_01_imagepolicy-TechPreviewNoUpgrade.crd.yaml @@ -0,0 +1,395 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + api-approved.openshift.io: https://github.com/openshift/api/pull/1457 + include.release.openshift.io/ibm-cloud-managed: "true" + include.release.openshift.io/self-managed-high-availability: "true" + include.release.openshift.io/single-node-developer: "true" + release.openshift.io/feature-set: TechPreviewNoUpgrade + name: imagepolicies.config.openshift.io +spec: + group: config.openshift.io + names: + kind: ImagePolicy + listKind: ImagePolicyList + plural: imagepolicies + singular: imagepolicy + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: "ImagePolicy holds namespace-wide configuration for image signature + verification \n Compatibility level 4: No compatibility is provided, the + API can change at any point for any reason. These capabilities should not + be used by applications needing long term support." + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: spec holds user settable values for configuration + properties: + policy: + description: policy contains configuration to allow scopes to be verified, + and defines how images not matching the verification policy will + be treated. + properties: + rootOfTrust: + description: rootOfTrust specifies the root of trust for the policy. + properties: + fulcioCAWithRekor: + description: 'fulcioCAWithRekor defines the root of trust + based on the Fulcio certificate and the Rekor public key. + For more information about Fulcio and Rekor, please refer + to the document at: https://github.com/sigstore/fulcio and + https://github.com/sigstore/rekor' + properties: + fulcioCAData: + description: fulcioCAData contains inline base64-encoded + data for the PEM format fulcio CA. fulcioCAData must + be at most 8192 characters. + format: byte + maxLength: 8192 + type: string + fulcioSubject: + description: fulcioSubject specifies OIDC issuer and the + email of the Fulcio authentication configuration. + properties: + oidcIssuer: + description: 'oidcIssuer contains the expected OIDC + issuer. It will be verified that the Fulcio-issued + certificate contains a (Fulcio-defined) certificate + extension pointing at this OIDC issuer URL. When + Fulcio issues certificates, it includes a value + based on an URL inside the client-provided ID token. + Example: "https://expected.OIDC.issuer/"' + type: string + x-kubernetes-validations: + - message: oidcIssuer must be a valid URL + rule: isURL(self) + signedEmail: + description: 'signedEmail holds the email address + the the Fulcio certificate is issued for. Example: + "expected-signing-user@example.com"' + type: string + x-kubernetes-validations: + - message: invalid email address + rule: self.matches('^\\S+@\\S+$') + required: + - oidcIssuer + - signedEmail + type: object + rekorKeyData: + description: rekorKeyData contains inline base64-encoded + data for the PEM format from the Rekor public key. rekorKeyData + must be at most 8192 characters. + maxLength: 8192 + type: string + required: + - fulcioCAData + - fulcioSubject + - rekorKeyData + type: object + policyType: + description: policyType serves as the union's discriminator. + Users are required to assign a value to this field, choosing + one of the policy types that define the root of trust. "PublicKey" + indicates that the policy relies on a sigstore publicKey + and may optionally use a Rekor verification. "FulcioCAWithRekor" + indicates that the policy is based on the Fulcio certification + and incorporates a Rekor verification. + enum: + - PublicKey + - FulcioCAWithRekor + type: string + publicKey: + description: publicKey defines the root of trust based on + a sigstore public key. + properties: + keyData: + description: keyData contains inline base64-encoded data + for the PEM format public key. KeyData must be at most + 8192 characters. + maxLength: 8192 + type: string + rekorKeyData: + description: rekorKeyData contains inline base64-encoded + data for the PEM format from the Rekor public key. rekorKeyData + must be at most 8192 characters. + maxLength: 8192 + type: string + required: + - keyData + type: object + required: + - policyType + type: object + x-kubernetes-validations: + - message: publicKey is required when policyType is PublicKey, + and forbidden otherwise + rule: 'has(self.policyType) && self.policyType == ''PublicKey'' + ? has(self.publicKey) : !has(self.publicKey)' + - message: fulcioCAWithRekor is required when policyType is FulcioCAWithRekor, + and forbidden otherwise + rule: 'has(self.policyType) && self.policyType == ''FulcioCAWithRekor'' + ? has(self.fulcioCAWithRekor) : !has(self.fulcioCAWithRekor)' + signedIdentity: + description: signedIdentity specifies what image identity the + signature claims about the image. The required matchPolicy field + specifies the approach used in the verification process to verify + the identity in the signature and the actual image identity, + the default matchPolicy is "MatchRepoDigestOrExact". + properties: + exactRepository: + description: exactRepository is required if matchPolicy is + set to "ExactRepository". + properties: + repository: + description: repository is the reference of the image + identity to be matched. The value should be a repository + name (by omitting the tag or digest) in a registry implementing + the "Docker Registry HTTP API V2". For example, docker.io/library/busybox + maxLength: 512 + type: string + x-kubernetes-validations: + - message: invalid repository or prefix in the signedIdentity, + should not include the tag or digest + rule: 'self.matches(''.*:([\\w][\\w.-]{0,127})$'')? + self.matches(''^(localhost:[0-9]+)$''): true' + - message: invalid repository or prefix in the signedIdentity + rule: self.matches('^(((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$') + required: + - repository + type: object + matchPolicy: + description: matchPolicy sets the type of matching to be used. + Valid values are "MatchRepoDigestOrExact", "MatchRepository", + "ExactRepository", "RemapIdentity". When omitted, the default + value is "MatchRepoDigestOrExact". If set matchPolicy to + ExactRepository, then the exactRepository must be specified. + If set matchPolicy to RemapIdentity, then the remapIdentity + must be specified. "MatchRepoDigestOrExact" means that the + identity in the signature must be in the same repository + as the image identity if the image identity is referenced + by a digest. Otherwise, the identity in the signature must + be the same as the image identity. "MatchRepository" means + that the identity in the signature must be in the same repository + as the image identity. "ExactRepository" means that the + identity in the signature must be in the same repository + as a specific identity specified by "repository". "RemapIdentity" + means that the signature must be in the same as the remapped + image identity. Remapped image identity is obtained by replacing + the "prefix" with the specified “signedPrefix” if the the + image identity matches the specified remapPrefix. + enum: + - MatchRepoDigestOrExact + - MatchRepository + - ExactRepository + - RemapIdentity + type: string + remapIdentity: + description: remapIdentity is required if matchPolicy is set + to "RemapIdentity". + properties: + prefix: + description: prefix is the prefix of the image identity + to be matched. If the image identity matches the specified + prefix, that prefix is replaced by the specified “signedPrefix” + (otherwise it is used as unchanged and no remapping + takes place). This useful when verifying signatures + for a mirror of some other repository namespace that + preserves the vendor’s repository structure. The prefix + and signedPrefix values can be either host[:port] values + (matching exactly the same host[:port], string), repository + namespaces, or repositories (i.e. they must not contain + tags/digests), and match as prefixes of the fully expanded + form. For example, docker.io/library/busybox (not busybox) + to specify that single repository, or docker.io/library + (not an empty string) to specify the parent namespace + of docker.io/library/busybox. + maxLength: 512 + type: string + x-kubernetes-validations: + - message: invalid repository or prefix in the signedIdentity, + should not include the tag or digest + rule: 'self.matches(''.*:([\\w][\\w.-]{0,127})$'')? + self.matches(''^(localhost:[0-9]+)$''): true' + - message: invalid repository or prefix in the signedIdentity + rule: self.matches('^(((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$') + signedPrefix: + description: signedPrefix is the prefix of the image identity + to be matched in the signature. The format is the same + as "prefix". The values can be either host[:port] values + (matching exactly the same host[:port], string), repository + namespaces, or repositories (i.e. they must not contain + tags/digests), and match as prefixes of the fully expanded + form. For example, docker.io/library/busybox (not busybox) + to specify that single repository, or docker.io/library + (not an empty string) to specify the parent namespace + of docker.io/library/busybox. + maxLength: 512 + type: string + x-kubernetes-validations: + - message: invalid repository or prefix in the signedIdentity, + should not include the tag or digest + rule: 'self.matches(''.*:([\\w][\\w.-]{0,127})$'')? + self.matches(''^(localhost:[0-9]+)$''): true' + - message: invalid repository or prefix in the signedIdentity + rule: self.matches('^(((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$') + required: + - prefix + - signedPrefix + type: object + required: + - matchPolicy + type: object + x-kubernetes-validations: + - message: exactRepository is required when matchPolicy is ExactRepository, + and forbidden otherwise + rule: '(has(self.matchPolicy) && self.matchPolicy == ''ExactRepository'') + ? has(self.exactRepository) : !has(self.exactRepository)' + - message: remapIdentity is required when matchPolicy is RemapIdentity, + and forbidden otherwise + rule: '(has(self.matchPolicy) && self.matchPolicy == ''RemapIdentity'') + ? has(self.remapIdentity) : !has(self.remapIdentity)' + required: + - rootOfTrust + type: object + scopes: + description: 'scopes defines the list of image identities assigned + to a policy. Each item refers to a scope in a registry implementing + the "Docker Registry HTTP API V2". Scopes matching individual images + are named Docker references in the fully expanded form, either using + a tag or digest. For example, docker.io/library/busybox:latest (not + busybox:latest). More general scopes are prefixes of individual-image + scopes, and specify a repository (by omitting the tag or digest), + a repository namespace, or a registry host (by only specifying the + host name and possibly a port number) or a wildcard expression starting + with `*.`, for matching all subdomains (not including a port number). + Wildcards are only supported for subdomain matching, and may not + be used in the middle of the host, i.e. *.example.com is a valid + case, but example*.*.com is not. Please be aware that the scopes + should not be nested under the repositories of OpenShift Container + Platform images. If configured, the policies for OpenShift Container + Platform repositories will not be in effect. For additional details + about the format, please refer to the document explaining the docker + transport field, which can be found at: https://github.com/containers/image/blob/main/docs/containers-policy.json.5.md#docker' + items: + maxLength: 512 + type: string + x-kubernetes-validations: + - message: invalid image scope format, scope must contain a fully + qualified domain name or 'localhost' + rule: 'size(self.split(''/'')[0].split(''.'')) == 1 ? self.split(''/'')[0].split(''.'')[0].split('':'')[0] + == ''localhost'' : true' + - message: invalid image scope with wildcard, a wildcard can only + be at the start of the domain and is only supported for subdomain + matching, not path matching + rule: 'self.contains(''*'') ? self.matches(''^\\*(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+$'') + : true' + - message: invalid repository namespace or image specification in + the image scope + rule: '!self.contains(''*'') ? self.matches(''^((((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+(?::[0-9]+)?)|(localhost(?::[0-9]+)?))(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?)(?::([\\w][\\w.-]{0,127}))?(?:@([A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}))?$'') + : true' + maxItems: 256 + type: array + x-kubernetes-list-type: set + required: + - policy + - scopes + type: object + status: + description: status contains the observed state of the resource. + properties: + conditions: + description: conditions provide details on the status of this API + Resource. + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/install/0000_80_machine-config-operator_00_clusterreader_clusterrole.yaml b/install/0000_80_machine-config-operator_00_clusterreader_clusterrole.yaml index fc831ee40f..93457445ca 100644 --- a/install/0000_80_machine-config-operator_00_clusterreader_clusterrole.yaml +++ b/install/0000_80_machine-config-operator_00_clusterreader_clusterrole.yaml @@ -56,3 +56,13 @@ rules: - get - list - watch + - apiGroups: + - config.openshift.io + resources: + - imagepolicies + - clusterimagepolicies + verbs: + - get + - list + - watch + - update diff --git a/manifests/machineconfigcontroller/clusterrole.yaml b/manifests/machineconfigcontroller/clusterrole.yaml index e4f520a02a..bd0d17975b 100644 --- a/manifests/machineconfigcontroller/clusterrole.yaml +++ b/manifests/machineconfigcontroller/clusterrole.yaml @@ -13,7 +13,7 @@ rules: resources: ["configmaps", "secrets"] verbs: ["*"] - apiGroups: ["config.openshift.io"] - resources: ["images", "clusterversions", "featuregates", "nodes", "nodes/status"] + resources: ["images", "clusterversions", "featuregates", "nodes", "nodes/status", "imagepolicies", "imagepolicies/status", "clusterimagepolicies", "clusterimagepolicies/status"] verbs: ["*"] - apiGroups: ["config.openshift.io"] resources: ["schedulers", "apiservers", "infrastructures", "imagedigestmirrorsets", "imagetagmirrorsets"] diff --git a/pkg/apihelpers/apihelpers.go b/pkg/apihelpers/apihelpers.go index e8f41d2bd1..70caaedaf7 100644 --- a/pkg/apihelpers/apihelpers.go +++ b/pkg/apihelpers/apihelpers.go @@ -99,6 +99,16 @@ func NewKubeletConfigCondition(condType mcfgv1.KubeletConfigStatusConditionType, } } +func NewCondition(condType string, status metav1.ConditionStatus, reason, message string) *metav1.Condition { + return &metav1.Condition{ + Type: condType, + Status: status, + LastTransitionTime: metav1.Now(), + Reason: reason, + Message: message, + } +} + // NewContainerRuntimeConfigCondition returns an instance of a ContainerRuntimeConfigCondition func NewContainerRuntimeConfigCondition(condType mcfgv1.ContainerRuntimeConfigStatusConditionType, status corev1.ConditionStatus, message string) *mcfgv1.ContainerRuntimeConfigCondition { return &mcfgv1.ContainerRuntimeConfigCondition{ diff --git a/pkg/controller/bootstrap/bootstrap.go b/pkg/controller/bootstrap/bootstrap.go index c432cdd949..cc50397ce6 100644 --- a/pkg/controller/bootstrap/bootstrap.go +++ b/pkg/controller/bootstrap/bootstrap.go @@ -19,6 +19,7 @@ import ( "k8s.io/klog/v2" apicfgv1 "github.com/openshift/api/config/v1" + apicfgv1alpha1 "github.com/openshift/api/config/v1alpha1" mcfgv1 "github.com/openshift/api/machineconfiguration/v1" apioperatorsv1alpha1 "github.com/openshift/api/operator/v1alpha1" "github.com/openshift/library-go/pkg/operator/configobserver/featuregates" @@ -72,20 +73,25 @@ func (b *Bootstrap) Run(destDir string) error { mcfgv1.Install(scheme) apioperatorsv1alpha1.Install(scheme) apicfgv1.Install(scheme) + apicfgv1alpha1.Install(scheme) codecFactory := serializer.NewCodecFactory(scheme) - decoder := codecFactory.UniversalDecoder(mcfgv1.GroupVersion, apioperatorsv1alpha1.GroupVersion, apicfgv1.GroupVersion) - - var cconfig *mcfgv1.ControllerConfig - var featureGate *apicfgv1.FeatureGate - var nodeConfig *apicfgv1.Node - var kconfigs []*mcfgv1.KubeletConfig - var pools []*mcfgv1.MachineConfigPool - var configs []*mcfgv1.MachineConfig - var crconfigs []*mcfgv1.ContainerRuntimeConfig - var icspRules []*apioperatorsv1alpha1.ImageContentSourcePolicy - var idmsRules []*apicfgv1.ImageDigestMirrorSet - var itmsRules []*apicfgv1.ImageTagMirrorSet - var imgCfg *apicfgv1.Image + decoder := codecFactory.UniversalDecoder(mcfgv1.GroupVersion, apioperatorsv1alpha1.GroupVersion, apicfgv1.GroupVersion, apicfgv1alpha1.GroupVersion) + + var ( + cconfig *mcfgv1.ControllerConfig + featureGate *apicfgv1.FeatureGate + nodeConfig *apicfgv1.Node + kconfigs []*mcfgv1.KubeletConfig + pools []*mcfgv1.MachineConfigPool + configs []*mcfgv1.MachineConfig + crconfigs []*mcfgv1.ContainerRuntimeConfig + icspRules []*apioperatorsv1alpha1.ImageContentSourcePolicy + idmsRules []*apicfgv1.ImageDigestMirrorSet + itmsRules []*apicfgv1.ImageTagMirrorSet + clusterImagePolicies []*apicfgv1alpha1.ClusterImagePolicy + imagePolicies []*apicfgv1alpha1.ImagePolicy + imgCfg *apicfgv1.Image + ) for _, info := range infos { if info.IsDir() { continue @@ -132,6 +138,10 @@ func (b *Bootstrap) Run(destDir string) error { itmsRules = append(itmsRules, obj) case *apicfgv1.Image: imgCfg = obj + case *apicfgv1alpha1.ClusterImagePolicy: + clusterImagePolicies = append(clusterImagePolicies, obj) + case *apicfgv1alpha1.ImagePolicy: + imagePolicies = append(imagePolicies, obj) case *apicfgv1.FeatureGate: if obj.GetName() == ctrlcommon.ClusterFeatureInstanceName { featureGate = obj @@ -168,7 +178,7 @@ func (b *Bootstrap) Run(destDir string) error { configs = append(configs, iconfigs...) - rconfigs, err := containerruntimeconfig.RunImageBootstrap(b.templatesDir, cconfig, pools, icspRules, idmsRules, itmsRules, imgCfg, fgAccess) + rconfigs, err := containerruntimeconfig.RunImageBootstrap(b.templatesDir, cconfig, pools, icspRules, idmsRules, itmsRules, imgCfg, clusterImagePolicies, imagePolicies, fgAccess) if err != nil { return err } diff --git a/pkg/controller/bootstrap/testdata/bootstrap/featuregate.yaml b/pkg/controller/bootstrap/testdata/bootstrap/featuregate.yaml index 57bcab9d54..eaeda285cd 100644 --- a/pkg/controller/bootstrap/testdata/bootstrap/featuregate.yaml +++ b/pkg/controller/bootstrap/testdata/bootstrap/featuregate.yaml @@ -7,3 +7,5 @@ status: - version: 0.0.1-snapshot enabled: - name: OpenShiftPodSecurityAdmission + disabled: + - name: SigstoreImageVerification diff --git a/pkg/controller/container-runtime-config/container_runtime_config_controller.go b/pkg/controller/container-runtime-config/container_runtime_config_controller.go index b3d82196cd..ef0defa61d 100644 --- a/pkg/controller/container-runtime-config/container_runtime_config_controller.go +++ b/pkg/controller/container-runtime-config/container_runtime_config_controller.go @@ -9,15 +9,22 @@ import ( "time" "github.com/clarketm/json" + signature "github.com/containers/image/v5/signature" ign3types "github.com/coreos/ignition/v2/config/v3_4/types" apicfgv1 "github.com/openshift/api/config/v1" + apicfgv1alpha1 "github.com/openshift/api/config/v1alpha1" apioperatorsv1alpha1 "github.com/openshift/api/operator/v1alpha1" configclientset "github.com/openshift/client-go/config/clientset/versioned" cligoinformersv1 "github.com/openshift/client-go/config/informers/externalversions/config/v1" + cligoinformersv1alpha1 "github.com/openshift/client-go/config/informers/externalversions/config/v1alpha1" cligolistersv1 "github.com/openshift/client-go/config/listers/config/v1" + cligolistersv1alpha1 "github.com/openshift/client-go/config/listers/config/v1alpha1" + operatorinformersv1alpha1 "github.com/openshift/client-go/operator/informers/externalversions/operator/v1alpha1" + operatorlistersv1alpha1 "github.com/openshift/client-go/operator/listers/operator/v1alpha1" "github.com/openshift/library-go/pkg/operator/configobserver/featuregates" + runtimeutils "github.com/openshift/runtime-utils/pkg/registries" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/equality" "k8s.io/apimachinery/pkg/api/errors" @@ -41,6 +48,7 @@ import ( "github.com/openshift/client-go/machineconfiguration/clientset/versioned/scheme" mcfginformersv1 "github.com/openshift/client-go/machineconfiguration/informers/externalversions/machineconfiguration/v1" mcfglistersv1 "github.com/openshift/client-go/machineconfiguration/listers/machineconfiguration/v1" + apihelpers "github.com/openshift/machine-config-operator/pkg/apihelpers" ctrlcommon "github.com/openshift/machine-config-operator/pkg/controller/common" mtmpl "github.com/openshift/machine-config-operator/pkg/controller/template" "github.com/openshift/machine-config-operator/pkg/version" @@ -98,6 +106,12 @@ type Controller struct { itmsLister cligolistersv1.ImageTagMirrorSetLister itmsListerSynced cache.InformerSynced + imagePolicyLister cligolistersv1alpha1.ImagePolicyLister + imagePolicyListerSynced cache.InformerSynced + + clusterImagePolicyLister cligolistersv1alpha1.ClusterImagePolicyLister + clusterImagePolicyListerSynced cache.InformerSynced + mcpLister mcfglistersv1.MachineConfigPoolLister mcpListerSynced cache.InformerSynced @@ -119,6 +133,8 @@ func New( imgInformer cligoinformersv1.ImageInformer, idmsInformer cligoinformersv1.ImageDigestMirrorSetInformer, itmsInformer cligoinformersv1.ImageTagMirrorSetInformer, + imagePolicyInformer cligoinformersv1alpha1.ImagePolicyInformer, + clusterImagePolicyInformer cligoinformersv1alpha1.ClusterImagePolicyInformer, icspInformer operatorinformersv1alpha1.ImageContentSourcePolicyInformer, clusterVersionInformer cligoinformersv1.ClusterVersionInformer, kubeClient clientset.Interface, @@ -169,6 +185,18 @@ func New( DeleteFunc: ctrl.itmsConfDeleted, }) + imagePolicyInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: ctrl.imagePolicyAdded, + UpdateFunc: ctrl.imagePolicyUpdated, + DeleteFunc: ctrl.imagePolicyDeleted, + }) + + clusterImagePolicyInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: ctrl.clusterImagePolicyAdded, + UpdateFunc: ctrl.clusterImagePolicyUpdated, + DeleteFunc: ctrl.clusterImagePolicyDeleted, + }) + ctrl.syncHandler = ctrl.syncContainerRuntimeConfig ctrl.syncImgHandler = ctrl.syncImageConfig ctrl.enqueueContainerRuntimeConfig = ctrl.enqueue @@ -197,6 +225,12 @@ func New( ctrl.clusterVersionLister = clusterVersionInformer.Lister() ctrl.clusterVersionListerSynced = clusterVersionInformer.Informer().HasSynced + ctrl.imagePolicyLister = imagePolicyInformer.Lister() + ctrl.imagePolicyListerSynced = imagePolicyInformer.Informer().HasSynced + + ctrl.clusterImagePolicyLister = clusterImagePolicyInformer.Lister() + ctrl.clusterImagePolicyListerSynced = clusterVersionInformer.Informer().HasSynced + ctrl.featureGateAccess = featureGateAccess return ctrl @@ -207,9 +241,12 @@ func (ctrl *Controller) Run(workers int, stopCh <-chan struct{}) { defer utilruntime.HandleCrash() defer ctrl.queue.ShutDown() defer ctrl.imgQueue.ShutDown() - - if !cache.WaitForCacheSync(stopCh, ctrl.mcpListerSynced, ctrl.mccrListerSynced, ctrl.ccListerSynced, - ctrl.imgListerSynced, ctrl.icspListerSynced, ctrl.idmsListerSynced, ctrl.itmsListerSynced, ctrl.clusterVersionListerSynced) { + listerCaches := []cache.InformerSynced{ctrl.mcpListerSynced, ctrl.mccrListerSynced, ctrl.ccListerSynced, + ctrl.imgListerSynced, ctrl.icspListerSynced, ctrl.idmsListerSynced, ctrl.itmsListerSynced, ctrl.clusterVersionListerSynced} + if ctrl.sigstoreAPIEnabled() { + listerCaches = append(listerCaches, ctrl.imagePolicyListerSynced, ctrl.clusterImagePolicyListerSynced) + } + if !cache.WaitForCacheSync(stopCh, listerCaches...) { return } @@ -284,6 +321,87 @@ func (ctrl *Controller) itmsConfDeleted(_ interface{}) { ctrl.imgQueue.Add("openshift-config") } +func (ctrl *Controller) imagePolicyAdded(_ interface{}) { + if !ctrl.sigstoreAPIEnabled() { + return + } + ctrl.imgQueue.Add("openshift-config") +} + +func (ctrl *Controller) imagePolicyUpdated(_, _ interface{}) { + if !ctrl.sigstoreAPIEnabled() { + return + } + ctrl.imgQueue.Add("openshift-config") +} + +func (ctrl *Controller) imagePolicyDeleted(_ interface{}) { + if !ctrl.sigstoreAPIEnabled() { + return + } + ctrl.imagePoliciesDeleted() +} + +func (ctrl *Controller) clusterImagePolicyAdded(_ interface{}) { + if !ctrl.sigstoreAPIEnabled() { + return + } + ctrl.imgQueue.Add("openshift-config") +} + +func (ctrl *Controller) clusterImagePolicyUpdated(_, _ interface{}) { + if !ctrl.sigstoreAPIEnabled() { + return + } + ctrl.imgQueue.Add("openshift-config") +} + +func (ctrl *Controller) clusterImagePolicyDeleted(_ interface{}) { + if !ctrl.sigstoreAPIEnabled() { + return + } + ctrl.imgQueue.Add("openshift-config") +} + +func (ctrl *Controller) imagePoliciesDeleted() { + clusterImagePolicies, err := ctrl.clusterImagePolicyLister.List(labels.Everything()) + if err != nil && !errors.IsNotFound(err) { + utilruntime.HandleError(fmt.Errorf("error listing clusterimagepolicies: %v", err)) + + } + imagepolicies, err := ctrl.imagePolicyLister.List(labels.Everything()) + if err != nil && !errors.IsNotFound(err) { + utilruntime.HandleError(fmt.Errorf("error listing imagepolicies: %v", err)) + } + if len(imagepolicies) == 0 && len(clusterImagePolicies) == 0 { + ctrl.deleteImagePolicyMachineConfig() + } + ctrl.imgQueue.Add("openshift-config") +} + +func (ctrl *Controller) sigstoreAPIEnabled() bool { + featureGates, err := ctrl.featureGateAccess.CurrentFeatureGates() + if err != nil { + utilruntime.HandleError(fmt.Errorf("error getting current featuregates: %v", err)) + return false + } + klog.Infof("imageverification sigstore FeatureGates: %v", featureGates.Enabled(apicfgv1.FeatureGateSigstoreImageVerification)) + return featureGates.Enabled(apicfgv1.FeatureGateSigstoreImageVerification) +} + +func (ctrl *Controller) deleteImagePolicyMachineConfig() { + pools, err := ctrl.mcpLister.List(labels.Everything()) + if err != nil { + utilruntime.HandleError(fmt.Errorf("error listing machineconfigpools: %v", err)) + } + for _, pool := range pools { + imagePolicyMCName := getManagedKeyNamespacedImagePolicy(pool) + if err = ctrl.client.MachineconfigurationV1().MachineConfigs().Delete(context.TODO(), imagePolicyMCName, metav1.DeleteOptions{}); err != nil && !errors.IsNotFound(err) { + utilruntime.HandleError(fmt.Errorf("couldn't delete image policy machine config %#v: %w", imagePolicyMCName, err)) + } + } +} + func (ctrl *Controller) updateContainerRuntimeConfig(oldObj, newObj interface{}) { oldCtrCfg := oldObj.(*mcfgv1.ContainerRuntimeConfig) newCtrCfg := newObj.(*mcfgv1.ContainerRuntimeConfig) @@ -791,7 +909,24 @@ func (ctrl *Controller) syncImageConfig(key string) error { var ( registriesBlocked, policyBlocked, allowedRegs []string releaseImage string + imagePolicies []*apicfgv1alpha1.ImagePolicy + clusterImagePolicies []*apicfgv1alpha1.ClusterImagePolicy + scopeNamespacePolicies map[string]map[string]signature.PolicyRequirements + clusterScopePolicies map[string]signature.PolicyRequirements ) + if ctrl.sigstoreAPIEnabled() { + // Find all ImagePolicy objects + imagePolicies, err = ctrl.imagePolicyLister.List(labels.Everything()) + if err != nil && errors.IsNotFound(err) { + imagePolicies = []*apicfgv1alpha1.ImagePolicy{} + } + // Find all ClusterImagePolicy objects + clusterImagePolicies, err = ctrl.clusterImagePolicyLister.List(labels.Everything()) + if err != nil && errors.IsNotFound(err) { + clusterImagePolicies = []*apicfgv1alpha1.ClusterImagePolicy{} + } + } + if clusterVersionCfg != nil { // The possibility of releaseImage being "" is very unlikely, will only happen if clusterVersionCfg is nil. If this happens // then there is something very wrong with the cluster and in that situation it would be best to fail here till clusterVersionCfg @@ -804,6 +939,13 @@ func (ctrl *Controller) syncImageConfig(key string) error { } else if err == errParsingReference { return err } + + // Get the valid cluster and namespace imagepolicies, update the skipped error to the status + // skip the scope if the scope is for release image repo or + // skip the scope in the namespaced imagepolicy if there is scope overlap between cluster and namespaced policy + if clusterScopePolicies, scopeNamespacePolicies, err = getValidScopeNamespacePolicies(clusterImagePolicies, imagePolicies, releaseImage, ctrl); err != nil { + return err + } } // Get ControllerConfig @@ -824,63 +966,33 @@ func (ctrl *Controller) syncImageConfig(key string) error { for _, pool := range mcpPools { // To keep track of whether we "actually" got an updated image config applied := true + // To keep track of whether we "actually" got an updated 99--generated-imagepolicies machine config + appliedImagePolicy := true role := pool.Name // Get MachineConfig managedKey, err := getManagedKeyReg(pool, ctrl.client) if err != nil { return err } + managedKeyNamespaceImagePolicy := getManagedKeyNamespacedImagePolicy(pool) if err := retry.RetryOnConflict(updateBackoff, func() error { - registriesIgn, err := registriesConfigIgnition(ctrl.templatesDir, controllerConfig, role, releaseImage, + registriesIgn, namespacedPoliciesIgn, err := registriesConfigIgnition(ctrl.templatesDir, controllerConfig, role, releaseImage, imgcfg.Spec.RegistrySources.InsecureRegistries, registriesBlocked, policyBlocked, allowedRegs, - imgcfg.Spec.RegistrySources.ContainerRuntimeSearchRegistries, icspRules, idmsRules, itmsRules, ctrl.featureGateAccess) + imgcfg.Spec.RegistrySources.ContainerRuntimeSearchRegistries, icspRules, idmsRules, itmsRules, ctrl.featureGateAccess, clusterScopePolicies, scopeNamespacePolicies) if err != nil { return err } - rawRegistriesIgn, err := json.Marshal(registriesIgn) + + applied, err = ctrl.syncIgnitionConfig(managedKey, registriesIgn, pool, ownerReferenceForImageConfig(imgcfg)) if err != nil { - return fmt.Errorf("could not encode registries Ignition config: %w", err) - } - mc, err := ctrl.client.MachineconfigurationV1().MachineConfigs().Get(context.TODO(), managedKey, metav1.GetOptions{}) - if err != nil && !errors.IsNotFound(err) { - return fmt.Errorf("could not find MachineConfig: %w", err) - } - isNotFound := errors.IsNotFound(err) - if !isNotFound && equality.Semantic.DeepEqual(rawRegistriesIgn, mc.Spec.Config.Raw) { - // if the configuration for the registries is equal, we still need to compare - // the generated controller version because during an upgrade we need a new one - mcCtrlVersion := mc.Annotations[ctrlcommon.GeneratedByControllerVersionAnnotationKey] - if mcCtrlVersion == version.Hash { - applied = false - return nil - } + return fmt.Errorf("could not sync registries Ignition config: %w", err) } - if isNotFound { - tempIgnCfg := ctrlcommon.NewIgnConfig() - mc, err = ctrlcommon.MachineConfigFromIgnConfig(role, managedKey, tempIgnCfg) + if namespacedPoliciesIgn != nil { + appliedImagePolicy, err = ctrl.syncIgnitionConfig(managedKeyNamespaceImagePolicy, namespacedPoliciesIgn, pool, ownerReferenceForImageConfig(imgcfg)) if err != nil { - return fmt.Errorf("could not create MachineConfig from new Ignition config: %w", err) + return fmt.Errorf("could not sync imagepolicies Ignition config: %w", err) } } - mc.Spec.Config.Raw = rawRegistriesIgn - mc.ObjectMeta.Annotations = map[string]string{ - ctrlcommon.GeneratedByControllerVersionAnnotationKey: version.Hash, - } - mc.ObjectMeta.OwnerReferences = []metav1.OwnerReference{ - { - APIVersion: apicfgv1.SchemeGroupVersion.String(), - Kind: "Image", - Name: imgcfg.Name, - UID: imgcfg.UID, - }, - } - // Create or Update, on conflict retry - if isNotFound { - _, err = ctrl.client.MachineconfigurationV1().MachineConfigs().Create(context.TODO(), mc, metav1.CreateOptions{}) - } else { - _, err = ctrl.client.MachineconfigurationV1().MachineConfigs().Update(context.TODO(), mc, metav1.UpdateOptions{}) - } - return err }); err != nil { return fmt.Errorf("could not Create/Update MachineConfig: %w", err) @@ -889,51 +1001,123 @@ func (ctrl *Controller) syncImageConfig(key string) error { klog.Infof("Applied ImageConfig cluster on MachineConfigPool %v", pool.Name) ctrlcommon.UpdateStateMetric(ctrlcommon.MCCSubControllerState, "machine-config-controller-container-runtime-config", "Sync Image Config", pool.Name) } + if appliedImagePolicy { + klog.Infof("Applied (cluster)imagepolicies on MachineConfigPool %v", pool.Name) + } } return nil } +func (ctrl *Controller) syncIgnitionConfig(managedKey string, ignFile *ign3types.Config, pool *mcfgv1.MachineConfigPool, ownerRef metav1.OwnerReference) (bool, error) { + appliedCtrlVersion := true + rawRegistriesIgn, err := json.Marshal(ignFile) + if err != nil { + return false, fmt.Errorf("could not encode Ignition config: %w", err) + } + mc, err := ctrl.client.MachineconfigurationV1().MachineConfigs().Get(context.TODO(), managedKey, metav1.GetOptions{}) + if err != nil && !errors.IsNotFound(err) { + return false, fmt.Errorf("could not find MachineConfig: %w", err) + } + isNotFound := errors.IsNotFound(err) + if !isNotFound && equality.Semantic.DeepEqual(rawRegistriesIgn, mc.Spec.Config.Raw) { + // if the configuration for the registries is equal, we still need to compare + // the generated controller version because during an upgrade we need a new one + mcCtrlVersion := mc.Annotations[ctrlcommon.GeneratedByControllerVersionAnnotationKey] + if mcCtrlVersion == version.Hash { + appliedCtrlVersion = false + return appliedCtrlVersion, nil + } + } + if isNotFound { + tempIgnCfg := ctrlcommon.NewIgnConfig() + mc, err = ctrlcommon.MachineConfigFromIgnConfig(pool.Name, managedKey, tempIgnCfg) + if err != nil { + return false, fmt.Errorf("could not create MachineConfig from new Ignition config: %w", err) + } + } + mc.Spec.Config.Raw = rawRegistriesIgn + mc.ObjectMeta.Annotations = map[string]string{ + ctrlcommon.GeneratedByControllerVersionAnnotationKey: version.Hash, + } + mc.ObjectMeta.OwnerReferences = []metav1.OwnerReference{ownerRef} + // Create or Update, on conflict retry + if isNotFound { + _, err = ctrl.client.MachineconfigurationV1().MachineConfigs().Create(context.TODO(), mc, metav1.CreateOptions{}) + } else { + _, err = ctrl.client.MachineconfigurationV1().MachineConfigs().Update(context.TODO(), mc, metav1.UpdateOptions{}) + } + + return appliedCtrlVersion, err +} + func registriesConfigIgnition(templateDir string, controllerConfig *mcfgv1.ControllerConfig, role, releaseImage string, insecureRegs, registriesBlocked, policyBlocked, allowedRegs, searchRegs []string, - icspRules []*apioperatorsv1alpha1.ImageContentSourcePolicy, idmsRules []*apicfgv1.ImageDigestMirrorSet, itmsRules []*apicfgv1.ImageTagMirrorSet, featureGateAccess featuregates.FeatureGateAccess) (*ign3types.Config, error) { + icspRules []*apioperatorsv1alpha1.ImageContentSourcePolicy, idmsRules []*apicfgv1.ImageDigestMirrorSet, itmsRules []*apicfgv1.ImageTagMirrorSet, featureGateAccess featuregates.FeatureGateAccess, + clusterScopePolicies map[string]signature.PolicyRequirements, + scopeNamespacePolicies map[string]map[string]signature.PolicyRequirements) (*ign3types.Config, *ign3types.Config, error) { var ( - registriesTOML []byte - policyJSON []byte + registriesTOML []byte + policyJSON []byte + namespacedJSONBase []byte + namespacedPolicyJSONs map[string][]byte + sigstoreRegistriesConfigYaml []byte + imagePolicyIgn *ign3types.Config + err error ) // Generate the original registries config _, originalRegistriesIgn, originalPolicyIgn, err := generateOriginalContainerRuntimeConfigs(templateDir, controllerConfig, role, featureGateAccess) if err != nil { - return nil, fmt.Errorf("could not generate original ContainerRuntime Configs: %w", err) + return nil, nil, fmt.Errorf("could not generate original ContainerRuntime Configs: %w", err) } if insecureRegs != nil || registriesBlocked != nil || len(icspRules) != 0 || len(idmsRules) != 0 || len(itmsRules) != 0 { if originalRegistriesIgn.Contents.Source == nil { - return nil, fmt.Errorf("original registries config is empty") + return nil, nil, fmt.Errorf("original registries config is empty") } contents, err := ctrlcommon.DecodeIgnitionFileContents(originalRegistriesIgn.Contents.Source, originalRegistriesIgn.Contents.Compression) if err != nil { - return nil, fmt.Errorf("could not decode original registries config: %w", err) + return nil, nil, fmt.Errorf("could not decode original registries config: %w", err) } registriesTOML, err = updateRegistriesConfig(contents, insecureRegs, registriesBlocked, icspRules, idmsRules, itmsRules) if err != nil { - return nil, fmt.Errorf("could not update registries config with new changes: %w", err) + return nil, nil, fmt.Errorf("could not update registries config with new changes: %w", err) } } - if policyBlocked != nil || allowedRegs != nil { + if policyBlocked != nil || allowedRegs != nil || len(scopeNamespacePolicies) > 0 || len(clusterScopePolicies) > 0 { if originalPolicyIgn.Contents.Source == nil { - return nil, fmt.Errorf("original policy json is empty") + return nil, nil, fmt.Errorf("original policy json is empty") } contents, err := ctrlcommon.DecodeIgnitionFileContents(originalPolicyIgn.Contents.Source, originalPolicyIgn.Contents.Compression) if err != nil { - return nil, fmt.Errorf("could not decode original policy json: %w", err) + return nil, nil, fmt.Errorf("could not decode original policy json: %w", err) + } + policyJSON, err = updatePolicyJSON(contents, policyBlocked, allowedRegs, releaseImage, clusterScopePolicies) + if err != nil { + return nil, nil, fmt.Errorf("could not update policy json with new changes: %w", err) } - policyJSON, err = updatePolicyJSON(contents, policyBlocked, allowedRegs, releaseImage) + policies := namespaceScopePolicyRequirements(scopeNamespacePolicies) + + // Merge the cluster override namespacedJSONBase and namespace policy + namespacedJSONBase = append(namespacedJSONBase, policyJSON...) + namespacedPolicyJSONs, err = generateNamespacedPolicyJSON(namespacedJSONBase, policies) + if err != nil { + return nil, nil, fmt.Errorf("could not generate namespace policy JSON from imagepolicy: %w", err) + } + // generates configuration under /etc/containers/registries.d to enable sigstore verification + sigstoreRegistriesConfigYaml, err = generateSigstoreRegistriesdConfig(clusterScopePolicies, scopeNamespacePolicies) if err != nil { - return nil, fmt.Errorf("could not update policy json with new changes: %w", err) + return nil, nil, err } } + + generatedImagePolicyConfigFileList := imagePolicyConfigFileList(namespacedPolicyJSONs, sigstoreRegistriesConfigYaml) + if len(generatedImagePolicyConfigFileList) != 0 { + namespacedPoliciesIgn := createNewIgnition(generatedImagePolicyConfigFileList) + imagePolicyIgn = &namespacedPoliciesIgn + } + generatedConfigFileList := []generatedConfigFile{ {filePath: registriesConfigPath, data: registriesTOML}, {filePath: policyConfigPath, data: policyJSON}, @@ -943,19 +1127,156 @@ func registriesConfigIgnition(templateDir string, controllerConfig *mcfgv1.Contr } registriesIgn := createNewIgnition(generatedConfigFileList) - return ®istriesIgn, nil + return ®istriesIgn, imagePolicyIgn, nil +} + +// getValidScopeNamespacePolicies returns a map[scope][namespace]policyRequirement from ImagePolicy CRs, a map[scope]policyRequirement from ClusterImagePolicy +// not add scope to the map if its release image repo, and syncs to status +func getValidScopeNamespacePolicies(clusterImagePolicies []*apicfgv1alpha1.ClusterImagePolicy, imagePolicies []*apicfgv1alpha1.ImagePolicy, releaseImage string, ctrl *Controller) (map[string]signature.PolicyRequirements, map[string]map[string]signature.PolicyRequirements, error) { + namespacePolices := make(map[string]map[string]signature.PolicyRequirements) + clusterScopePolicies := make(map[string]signature.PolicyRequirements) + + // Get the repository being used by the payload from the releaseImage + ref, err := getPayloadRepo(releaseImage) + if err != nil { + return nil, nil, errParsingReference + } + payloadRepo := ref.Name() + + for _, clusterImagePolicy := range clusterImagePolicies { + sigstoreSignedPolicyItem, err := policyItemFromSpec(clusterImagePolicy.Spec.Policy) + if err != nil { + return nil, nil, err + } + var payloadScopes []string + for _, scope := range clusterImagePolicy.Spec.Scopes { + scopeStr := string(scope) + if runtimeutils.ScopeIsNestedInsideScope(payloadRepo, scopeStr) { + payloadScopes = append(payloadScopes, scopeStr) + continue + } + clusterScopePolicies[scopeStr] = append(clusterScopePolicies[scopeStr], sigstoreSignedPolicyItem) + } + if ctrl != nil && len(payloadScopes) > 0 { + msg := fmt.Sprintf("has conflict scope(s) %q of registry of Openshift payload repository %s, skip the scopes", payloadScopes, payloadRepo) + klog.V(2).Info(msg) + ctrl.syncClusterImagePolicyStatusOnly(clusterImagePolicy.ObjectMeta.Name, apicfgv1alpha1.ImagePolicyPending, reasonConflictScopes, msg, metav1.ConditionTrue) + } + } + + for _, imagePolicy := range imagePolicies { + namespace := imagePolicy.ObjectMeta.Namespace + sigstoreSignedPolicyItem, err := policyItemFromSpec(imagePolicy.Spec.Policy) + if err != nil { + return nil, nil, err + } + var ( + conflictScopes []string + payloadScopes []string + conditionMsg []string + ) + for _, scope := range imagePolicy.Spec.Scopes { + scopeStr := string(scope) + if runtimeutils.ScopeIsNestedInsideScope(payloadRepo, scopeStr) { + payloadScopes = append(payloadScopes, scopeStr) + continue + } + nestedInClusterScope := false + for clusterScope := range clusterScopePolicies { + if runtimeutils.ScopeIsNestedInsideScope(scopeStr, clusterScope) { + conflictScopes = append(conflictScopes, scopeStr) + nestedInClusterScope = true + break + } + } + if nestedInClusterScope { + continue + } + if _, ok := namespacePolices[scopeStr]; !ok { + namespacePolices[scopeStr] = make(map[string]signature.PolicyRequirements) + } + namespacePolices[scopeStr][namespace] = append(namespacePolices[scopeStr][namespace], sigstoreSignedPolicyItem) + } + if ctrl != nil { + if len(conflictScopes) > 0 { + conditionMsg = append(conditionMsg, fmt.Sprintf("has conflict scope(s) %q equal to or nest inside existing clusterimagepolicy, only policy from clusterimagepolicy will be applied", conflictScopes)) + } + if len(payloadScopes) > 0 { + conditionMsg = append(conditionMsg, fmt.Sprintf("has conflict scope(s) %q of registry of Openshift payload repository %s, skip the scopes", payloadScopes, payloadRepo)) + } + if len(conditionMsg) > 0 { + msg := strings.Join(conditionMsg, "; ") + klog.V(2).Info(msg) + ctrl.syncImagePolicyStatusOnly(namespace, imagePolicy.ObjectMeta.Name, apicfgv1alpha1.ImagePolicyPending, reasonConflictScopes, msg, metav1.ConditionTrue) + } + + } + } + return clusterScopePolicies, namespacePolices, nil +} + +func (ctrl *Controller) syncImagePolicyStatusOnly(namespace, imagepolicy, conditionType, reason, msg string, status metav1.ConditionStatus) { + statusUpdateErr := retry.RetryOnConflict(updateBackoff, func() error { + newImagePolicy, getErr := ctrl.configClient.ConfigV1alpha1().ImagePolicies(namespace).Get(context.TODO(), imagepolicy, metav1.GetOptions{}) + if getErr != nil { + return getErr + } + + newCondition := apihelpers.NewCondition(conditionType, status, reason, msg) + if newImagePolicy.GetGeneration() != newCondition.ObservedGeneration { + newCondition.ObservedGeneration = newImagePolicy.GetGeneration() + } + newImagePolicy.Status.Conditions = []metav1.Condition{*newCondition} + _, updateErr := ctrl.configClient.ConfigV1alpha1().ImagePolicies(namespace).UpdateStatus(context.TODO(), newImagePolicy, metav1.UpdateOptions{}) + return updateErr + }) + if statusUpdateErr != nil { + klog.Warningf("error updating imagepolicy status: %v", statusUpdateErr) + } +} + +func (ctrl *Controller) syncClusterImagePolicyStatusOnly(imagepolicy, conditionType, reason, msg string, status metav1.ConditionStatus) { + statusUpdateErr := retry.RetryOnConflict(updateBackoff, func() error { + newClusterImagePolicy, getErr := ctrl.configClient.ConfigV1alpha1().ClusterImagePolicies().Get(context.TODO(), imagepolicy, metav1.GetOptions{}) + if getErr != nil { + return getErr + } + newCondition := apihelpers.NewCondition(conditionType, status, reason, msg) + if newClusterImagePolicy.GetGeneration() != newCondition.ObservedGeneration { + newCondition.ObservedGeneration = newClusterImagePolicy.GetGeneration() + } + newClusterImagePolicy.Status.Conditions = []metav1.Condition{*newCondition} + _, updateErr := ctrl.configClient.ConfigV1alpha1().ClusterImagePolicies().UpdateStatus(context.TODO(), newClusterImagePolicy, metav1.UpdateOptions{}) + return updateErr + }) + if statusUpdateErr != nil { + klog.Warningf("error updating clusterimagepolicy status: %v", statusUpdateErr) + } } // RunImageBootstrap generates MachineConfig objects for mcpPools that would have been generated by syncImageConfig, // except that mcfgv1.Image is not available. func RunImageBootstrap(templateDir string, controllerConfig *mcfgv1.ControllerConfig, mcpPools []*mcfgv1.MachineConfigPool, icspRules []*apioperatorsv1alpha1.ImageContentSourcePolicy, - idmsRules []*apicfgv1.ImageDigestMirrorSet, itmsRules []*apicfgv1.ImageTagMirrorSet, imgCfg *apicfgv1.Image, featureGateAccess featuregates.FeatureGateAccess) ([]*mcfgv1.MachineConfig, error) { + idmsRules []*apicfgv1.ImageDigestMirrorSet, itmsRules []*apicfgv1.ImageTagMirrorSet, imgCfg *apicfgv1.Image, clusterImagePolicies []*apicfgv1alpha1.ClusterImagePolicy, imagePolicies []*apicfgv1alpha1.ImagePolicy, featureGateAccess featuregates.FeatureGateAccess) ([]*mcfgv1.MachineConfig, error) { var ( insecureRegs, registriesBlocked, policyBlocked, allowedRegs, searchRegs []string err error ) + clusterScopePolicies := map[string]signature.PolicyRequirements{} + scopeNamespacePolicies := map[string]map[string]signature.PolicyRequirements{} + featureGates, err := featureGateAccess.CurrentFeatureGates() + if err != nil { + return nil, err + } + sigstoreAPIEnabled := featureGates.Enabled(apicfgv1.FeatureGateSigstoreImageVerification) + if sigstoreAPIEnabled { + if clusterScopePolicies, scopeNamespacePolicies, err = getValidScopeNamespacePolicies(clusterImagePolicies, imagePolicies, controllerConfig.Spec.ReleaseImage, nil); err != nil { + return nil, err + } + } + // Read the search, insecure, blocked, and allowed registries from the cluster-wide Image CR if it is not nil if imgCfg != nil { insecureRegs = imgCfg.Spec.RegistrySources.InsecureRegistries @@ -976,8 +1297,9 @@ func RunImageBootstrap(templateDir string, controllerConfig *mcfgv1.ControllerCo if err != nil { return nil, err } - registriesIgn, err := registriesConfigIgnition(templateDir, controllerConfig, role, controllerConfig.Spec.ReleaseImage, - insecureRegs, registriesBlocked, policyBlocked, allowedRegs, searchRegs, icspRules, idmsRules, itmsRules, featureGateAccess) + managedKeyNamespaceImagePolicy := getManagedKeyNamespacedImagePolicy(pool) + registriesIgn, namespacedPoliciesIgn, err := registriesConfigIgnition(templateDir, controllerConfig, role, controllerConfig.Spec.ReleaseImage, + insecureRegs, registriesBlocked, policyBlocked, allowedRegs, searchRegs, icspRules, idmsRules, itmsRules, featureGateAccess, clusterScopePolicies, scopeNamespacePolicies) if err != nil { return nil, err } @@ -995,6 +1317,13 @@ func RunImageBootstrap(templateDir string, controllerConfig *mcfgv1.ControllerCo }, } res = append(res, mc) + if namespacedPoliciesIgn != nil { + namespacedPoliciesMC, err := ctrlcommon.MachineConfigFromIgnConfig(role, managedKeyNamespaceImagePolicy, namespacedPoliciesIgn) + if err != nil { + return nil, err + } + res = append(res, namespacedPoliciesMC) + } } return res, nil } diff --git a/pkg/controller/container-runtime-config/container_runtime_config_controller_test.go b/pkg/controller/container-runtime-config/container_runtime_config_controller_test.go index 8719ec4130..bfa96dab7c 100644 --- a/pkg/controller/container-runtime-config/container_runtime_config_controller_test.go +++ b/pkg/controller/container-runtime-config/container_runtime_config_controller_test.go @@ -3,7 +3,9 @@ package containerruntimeconfig import ( "context" "fmt" + "path/filepath" "reflect" + "strings" "testing" "time" @@ -28,6 +30,7 @@ import ( ign3types "github.com/coreos/ignition/v2/config/v3_4/types" apicfgv1 "github.com/openshift/api/config/v1" + apicfgv1alpha1 "github.com/openshift/api/config/v1alpha1" mcfgv1 "github.com/openshift/api/machineconfiguration/v1" apioperatorsv1alpha1 "github.com/openshift/api/operator/v1alpha1" fakeconfigv1client "github.com/openshift/client-go/config/clientset/versioned/fake" @@ -62,14 +65,16 @@ type fixture struct { imgClient *fakeconfigv1client.Clientset operatorClient *fakeoperatorclient.Clientset - ccLister []*mcfgv1.ControllerConfig - mcpLister []*mcfgv1.MachineConfigPool - mccrLister []*mcfgv1.ContainerRuntimeConfig - imgLister []*apicfgv1.Image - cvLister []*apicfgv1.ClusterVersion - icspLister []*apioperatorsv1alpha1.ImageContentSourcePolicy - idmsLister []*apicfgv1.ImageDigestMirrorSet - itmsLister []*apicfgv1.ImageTagMirrorSet + ccLister []*mcfgv1.ControllerConfig + mcpLister []*mcfgv1.MachineConfigPool + mccrLister []*mcfgv1.ContainerRuntimeConfig + imgLister []*apicfgv1.Image + cvLister []*apicfgv1.ClusterVersion + icspLister []*apioperatorsv1alpha1.ImageContentSourcePolicy + idmsLister []*apicfgv1.ImageDigestMirrorSet + itmsLister []*apicfgv1.ImageTagMirrorSet + clusterImagePolicyLister []*apicfgv1alpha1.ClusterImagePolicy + imagePolicyLister []*apicfgv1alpha1.ImagePolicy actions []core.Action skipActionsValidation bool @@ -86,7 +91,7 @@ func newFixture(t *testing.T) *fixture { f.t = t f.objects = []runtime.Object{} f.fgAccess = featuregates.NewHardcodedFeatureGateAccess( - []apicfgv1.FeatureGateName{}, + []apicfgv1.FeatureGateName{apicfgv1.FeatureGateSigstoreImageVerification}, []apicfgv1.FeatureGateName{ apicfgv1.FeatureGateExternalCloudProvider, apicfgv1.FeatureGateExternalCloudProviderAzure, @@ -210,6 +215,50 @@ func newClusterVersionConfig(name, desiredImage string) *apicfgv1.ClusterVersion } } +func newImagePolicyWithPublicKey(name, namespace string, scopes []string, keyData string) *apicfgv1alpha1.ImagePolicy { + imgScopes := []apicfgv1alpha1.ImageScope{} + for _, scope := range scopes { + imgScopes = append(imgScopes, apicfgv1alpha1.ImageScope(scope)) + } + return &apicfgv1alpha1.ImagePolicy{ + TypeMeta: metav1.TypeMeta{APIVersion: apicfgv1alpha1.SchemeGroupVersion.String()}, + ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: namespace, UID: types.UID(utilrand.String(5)), Generation: 1}, + Spec: apicfgv1alpha1.ImagePolicySpec{ + Scopes: imgScopes, + Policy: apicfgv1alpha1.Policy{ + RootOfTrust: apicfgv1alpha1.PolicyRootOfTrust{ + PolicyType: apicfgv1alpha1.PublicKeyRootOfTrust, + PublicKey: &apicfgv1alpha1.PublicKey{ + KeyData: keyData, + }, + }, + }, + }, + } +} + +func newClusterImagePolicyWithPublicKey(name string, scopes []string, keyData string) *apicfgv1alpha1.ClusterImagePolicy { + imgScopes := []apicfgv1alpha1.ImageScope{} + for _, scope := range scopes { + imgScopes = append(imgScopes, apicfgv1alpha1.ImageScope(scope)) + } + return &apicfgv1alpha1.ClusterImagePolicy{ + TypeMeta: metav1.TypeMeta{APIVersion: apicfgv1alpha1.SchemeGroupVersion.String()}, + ObjectMeta: metav1.ObjectMeta{Name: name, UID: types.UID(utilrand.String(5)), Generation: 1}, + Spec: apicfgv1alpha1.ClusterImagePolicySpec{ + Scopes: imgScopes, + Policy: apicfgv1alpha1.Policy{ + RootOfTrust: apicfgv1alpha1.PolicyRootOfTrust{ + PolicyType: apicfgv1alpha1.PublicKeyRootOfTrust, + PublicKey: &apicfgv1alpha1.PublicKey{ + KeyData: keyData, + }, + }, + }, + }, + } +} + func (f *fixture) newController() *Controller { f.client = fake.NewSimpleClientset(f.objects...) f.imgClient = fakeconfigv1client.NewSimpleClientset(f.imgObjects...) @@ -225,6 +274,8 @@ func (f *fixture) newController() *Controller { ci.Config().V1().Images(), ci.Config().V1().ImageDigestMirrorSets(), ci.Config().V1().ImageTagMirrorSets(), + ci.Config().V1alpha1().ImagePolicies(), + ci.Config().V1alpha1().ClusterImagePolicies(), oi.Operator().V1alpha1().ImageContentSourcePolicies(), ci.Config().V1().ClusterVersions(), k8sfake.NewSimpleClientset(), f.client, f.imgClient, @@ -238,6 +289,8 @@ func (f *fixture) newController() *Controller { c.icspListerSynced = alwaysReady c.idmsListerSynced = alwaysReady c.itmsListerSynced = alwaysReady + c.clusterImagePolicyListerSynced = alwaysReady + c.imagePolicyListerSynced = alwaysReady c.clusterVersionListerSynced = alwaysReady c.eventRecorder = &record.FakeRecorder{} @@ -274,6 +327,12 @@ func (f *fixture) newController() *Controller { for _, c := range f.itmsLister { ci.Config().V1().ImageTagMirrorSets().Informer().GetIndexer().Add(c) } + for _, c := range f.clusterImagePolicyLister { + ci.Config().V1alpha1().ClusterImagePolicies().Informer().GetIndexer().Add(c) + } + for _, c := range f.imagePolicyLister { + ci.Config().V1alpha1().ImagePolicies().Informer().GetIndexer().Add(c) + } return c } @@ -404,7 +463,7 @@ func (f *fixture) expectUpdateContainerRuntimeConfigRoot(config *mcfgv1.Containe f.actions = append(f.actions, core.NewRootUpdateAction(schema.GroupVersionResource{Version: "v1", Group: "machineconfiguration.openshift.io", Resource: "containerruntimeconfigs"}, config)) } -func (f *fixture) verifyRegistriesConfigAndPolicyJSONContents(t *testing.T, mcName string, imgcfg *apicfgv1.Image, icsp *apioperatorsv1alpha1.ImageContentSourcePolicy, idms *apicfgv1.ImageDigestMirrorSet, itms *apicfgv1.ImageTagMirrorSet, releaseImageReg string, verifyPolicyJSON, verifySearchRegsDropin bool) { +func (f *fixture) verifyRegistriesConfigAndPolicyJSONContents(t *testing.T, mcName string, imgcfg *apicfgv1.Image, icsp *apioperatorsv1alpha1.ImageContentSourcePolicy, idms *apicfgv1.ImageDigestMirrorSet, itms *apicfgv1.ImageTagMirrorSet, imagepolicy *apicfgv1alpha1.ClusterImagePolicy, releaseImageReg string, verifyPolicyJSON, verifySearchRegsDropin, verifyImagePoliciesRegistriesConfig bool) { icsps := []*apioperatorsv1alpha1.ImageContentSourcePolicy{} if icsp != nil { icsps = append(icsps, icsp) @@ -417,12 +476,75 @@ func (f *fixture) verifyRegistriesConfigAndPolicyJSONContents(t *testing.T, mcNa if itms != nil { itmss = append(itmss, itms) } + clusterImagePolicies := []*apicfgv1alpha1.ClusterImagePolicy{} + if imagepolicy != nil { + clusterImagePolicies = append(clusterImagePolicies, imagepolicy) + } updatedMC, err := f.client.MachineconfigurationV1().MachineConfigs().Get(context.TODO(), mcName, metav1.GetOptions{}) require.NoError(t, err) - verifyRegistriesConfigAndPolicyJSONContents(t, updatedMC, mcName, imgcfg, icsps, idmss, itmss, releaseImageReg, verifyPolicyJSON, verifySearchRegsDropin) + verifyRegistriesConfigAndPolicyJSONContents(t, updatedMC, mcName, imgcfg, icsps, idmss, itmss, clusterImagePolicies, releaseImageReg, verifyPolicyJSON, verifySearchRegsDropin, verifyImagePoliciesRegistriesConfig) } -func verifyRegistriesConfigAndPolicyJSONContents(t *testing.T, mc *mcfgv1.MachineConfig, mcName string, imgcfg *apicfgv1.Image, icsps []*apioperatorsv1alpha1.ImageContentSourcePolicy, idmss []*apicfgv1.ImageDigestMirrorSet, itmss []*apicfgv1.ImageTagMirrorSet, releaseImageReg string, verifyPolicyJSON, verifySearchRegsDropin bool) { +func (f *fixture) verifySigstoreRegistriesConfdAndPolicyContents(t *testing.T, imgcfg *apicfgv1.Image, mcName, releaseImageReg string, clusterimagepolicy *apicfgv1alpha1.ClusterImagePolicy, imagepolicy *apicfgv1alpha1.ImagePolicy) { + clusterImagePolicies := []*apicfgv1alpha1.ClusterImagePolicy{} + if clusterimagepolicy != nil { + clusterImagePolicies = append(clusterImagePolicies, clusterimagepolicy) + } + imagePolicies := []*apicfgv1alpha1.ImagePolicy{} + if imagepolicy != nil { + imagePolicies = append(imagePolicies, imagepolicy) + + } + updatedMC, err := f.client.MachineconfigurationV1().MachineConfigs().Get(context.TODO(), mcName, metav1.GetOptions{}) + require.NoError(t, err) + verifySigstoreRegistriesConfdAndPolicyContents(t, imgcfg, updatedMC, clusterImagePolicies, imagePolicies, releaseImageReg) +} + +func verifySigstoreRegistriesConfdAndPolicyContents(t *testing.T, imgcfg *apicfgv1.Image, mc *mcfgv1.MachineConfig, clusterImagePolicies []*apicfgv1alpha1.ClusterImagePolicy, imagePolicies []*apicfgv1alpha1.ImagePolicy, releaseImageReg string) { + _, policyBlocked, allowed, _ := getValidBlockedAndAllowedRegistries(releaseImageReg, &imgcfg.Spec, nil, nil) + clusterScopePolicies, scopeNamespacePolicies, err := getValidScopeNamespacePolicies(clusterImagePolicies, imagePolicies, releaseImageReg, nil) + require.NoError(t, err) + expectedRegistriesConfd, err := generateSigstoreRegistriesdConfig(clusterScopePolicies, scopeNamespacePolicies) + require.NoError(t, err) + ignCfg, err := ctrlcommon.ParseAndConvertConfig(mc.Spec.Config.Raw) + require.NoError(t, err) + var registriesConffile ign3types.File + for _, f := range ignCfg.Storage.Files { + if f.Node.Path == sigstoreRegistriesConfigFilePath { + registriesConffile = f + } + } + + require.Equal(t, sigstoreRegistriesConfigFilePath, registriesConffile.Node.Path) + registriesYaml, err := ctrlcommon.DecodeIgnitionFileContents(registriesConffile.Contents.Source, registriesConffile.Contents.Compression) + require.NoError(t, err) + assert.Equal(t, string(expectedRegistriesConfd), string(registriesYaml)) + + policyJSON, err := updatePolicyJSON(templatePolicyJSON, + policyBlocked, + allowed, releaseImageReg, clusterScopePolicies) + require.NoError(t, err) + policies := namespaceScopePolicyRequirements(scopeNamespacePolicies) + + // Merge the cluster override namespacedJSONBase and namespace policy + namespacedJSONBase := append([]byte{}, policyJSON...) + namespacedPolicyJSONs, err := generateNamespacedPolicyJSON(namespacedJSONBase, policies) + require.NoError(t, err) + require.Len(t, ignCfg.Storage.Files, len(namespacedPolicyJSONs)+1) + for _, policyf := range ignCfg.Storage.Files { + if strings.HasSuffix(policyf.Node.Path, "json") { + actualNamespace := strings.TrimSuffix(filepath.Base(policyf.Node.Path), ".json") + value, ok := namespacedPolicyJSONs[actualNamespace] + require.True(t, ok) + gotpolicyJSON, err := ctrlcommon.DecodeIgnitionFileContents(policyf.Contents.Source, policyf.Contents.Compression) + require.NoError(t, err) + assert.JSONEq(t, string(value), string(gotpolicyJSON), "policy.json for namespace %s", actualNamespace) + } + + } +} + +func verifyRegistriesConfigAndPolicyJSONContents(t *testing.T, mc *mcfgv1.MachineConfig, mcName string, imgcfg *apicfgv1.Image, icsps []*apioperatorsv1alpha1.ImageContentSourcePolicy, idmss []*apicfgv1.ImageDigestMirrorSet, itmss []*apicfgv1.ImageTagMirrorSet, clusterImagePolicies []*apicfgv1alpha1.ClusterImagePolicy, releaseImageReg string, verifyPolicyJSON, verifySearchRegsDropin, verifyImagePoliciesRegistriesConfig bool) { // This is not testing updateRegistriesConfig, which has its own tests; this verifies the created object contains the expected // configuration file. // First get the valid blocked registries to ensure we don't block the registry where the release image is from @@ -453,12 +575,15 @@ func verifyRegistriesConfigAndPolicyJSONContents(t *testing.T, mc *mcfgv1.Machin require.NoError(t, err) assert.Equal(t, string(expectedRegistriesConf), string(registriesConf)) + clusterScopePolicies, _, err := getValidScopeNamespacePolicies(clusterImagePolicies, nil, releaseImageReg, nil) + require.NoError(t, err) + // Validate the policy.json contents if a change is expected from the tests if verifyPolicyJSON { allowed = append(allowed, imgcfg.Spec.RegistrySources.AllowedRegistries...) expectedPolicyJSON, err := updatePolicyJSON(templatePolicyJSON, policyBlocked, - allowed, releaseImageReg) + allowed, releaseImageReg, clusterScopePolicies) require.NoError(t, err) policyfile := ignCfg.Storage.Files[1] if policyfile.Node.Path != policyConfigPath { @@ -482,6 +607,7 @@ func verifyRegistriesConfigAndPolicyJSONContents(t *testing.T, mc *mcfgv1.Machin require.NoError(t, err) assert.Equal(t, string(expectedSearchRegsConf[0].data), string(searchRegsConf)) } + } // The patch bytes to expect when creating/updating a containerruntimeconfig @@ -647,7 +773,7 @@ func TestImageConfigCreate(t *testing.T) { f.run("cluster") for _, mcName := range []string{mcs1.Name, mcs2.Name} { - f.verifyRegistriesConfigAndPolicyJSONContents(t, mcName, imgcfg1, nil, nil, nil, cc.Spec.ReleaseImage, true, true) + f.verifyRegistriesConfigAndPolicyJSONContents(t, mcName, imgcfg1, nil, nil, nil, nil, cc.Spec.ReleaseImage, true, true, false) } }) } @@ -702,7 +828,7 @@ func TestImageConfigUpdate(t *testing.T) { close(stopCh) for _, mcName := range []string{mcs1Update.Name, mcs2Update.Name} { - f.verifyRegistriesConfigAndPolicyJSONContents(t, mcName, imgcfg1, nil, nil, nil, cc.Spec.ReleaseImage, true, true) + f.verifyRegistriesConfigAndPolicyJSONContents(t, mcName, imgcfg1, nil, nil, nil, nil, cc.Spec.ReleaseImage, true, true, false) } // Perform Update @@ -743,7 +869,7 @@ func TestImageConfigUpdate(t *testing.T) { close(stopCh) for _, mcName := range []string{mcs1Update.Name, mcs2Update.Name} { - f.verifyRegistriesConfigAndPolicyJSONContents(t, mcName, imgcfgUpdate, nil, nil, nil, cc.Spec.ReleaseImage, true, true) + f.verifyRegistriesConfigAndPolicyJSONContents(t, mcName, imgcfgUpdate, nil, nil, nil, nil, cc.Spec.ReleaseImage, true, true, false) } }) } @@ -803,7 +929,7 @@ func TestICSPUpdate(t *testing.T) { close(stopCh) for _, mcName := range []string{mcs1Update.Name, mcs2Update.Name} { - f.verifyRegistriesConfigAndPolicyJSONContents(t, mcName, imgcfg1, icsp, nil, nil, cc.Spec.ReleaseImage, false, false) + f.verifyRegistriesConfigAndPolicyJSONContents(t, mcName, imgcfg1, icsp, nil, nil, nil, cc.Spec.ReleaseImage, false, false, false) } // Perform Update @@ -848,7 +974,7 @@ func TestICSPUpdate(t *testing.T) { close(stopCh) for _, mcName := range []string{mcs1Update.Name, mcs2Update.Name} { - f.verifyRegistriesConfigAndPolicyJSONContents(t, mcName, imgcfg1, icspUpdate, nil, nil, cc.Spec.ReleaseImage, false, false) + f.verifyRegistriesConfigAndPolicyJSONContents(t, mcName, imgcfg1, icspUpdate, nil, nil, nil, cc.Spec.ReleaseImage, false, false, false) } }) } @@ -906,7 +1032,7 @@ func TestIDMSUpdate(t *testing.T) { close(stopCh) for _, mcName := range []string{mcs1Update.Name, mcs2Update.Name} { - f.verifyRegistriesConfigAndPolicyJSONContents(t, mcName, imgcfg1, nil, idms, nil, cc.Spec.ReleaseImage, false, false) + f.verifyRegistriesConfigAndPolicyJSONContents(t, mcName, imgcfg1, nil, idms, nil, nil, cc.Spec.ReleaseImage, false, false, false) } // Perform Update @@ -951,7 +1077,7 @@ func TestIDMSUpdate(t *testing.T) { close(stopCh) for _, mcName := range []string{mcs1Update.Name, mcs2Update.Name} { - f.verifyRegistriesConfigAndPolicyJSONContents(t, mcName, imgcfg1, nil, idmsUpdate, nil, cc.Spec.ReleaseImage, false, false) + f.verifyRegistriesConfigAndPolicyJSONContents(t, mcName, imgcfg1, nil, idmsUpdate, nil, nil, cc.Spec.ReleaseImage, false, false, false) } }) } @@ -1009,7 +1135,7 @@ func TestITMSUpdate(t *testing.T) { close(stopCh) for _, mcName := range []string{mcs1Update.Name, mcs2Update.Name} { - f.verifyRegistriesConfigAndPolicyJSONContents(t, mcName, imgcfg1, nil, nil, itms, cc.Spec.ReleaseImage, false, false) + f.verifyRegistriesConfigAndPolicyJSONContents(t, mcName, imgcfg1, nil, nil, itms, nil, cc.Spec.ReleaseImage, false, false, false) } // Perform Update @@ -1054,18 +1180,20 @@ func TestITMSUpdate(t *testing.T) { close(stopCh) for _, mcName := range []string{mcs1Update.Name, mcs2Update.Name} { - f.verifyRegistriesConfigAndPolicyJSONContents(t, mcName, imgcfg1, nil, nil, itmsUpdate, cc.Spec.ReleaseImage, false, false) + f.verifyRegistriesConfigAndPolicyJSONContents(t, mcName, imgcfg1, nil, nil, itmsUpdate, nil, cc.Spec.ReleaseImage, false, false, false) } }) } } func TestRunImageBootstrap(t *testing.T) { + testClusterImagePolicy := testClusterImagePolicyCRs["test-cr0"] for _, platform := range []apicfgv1.PlatformType{apicfgv1.AWSPlatformType, apicfgv1.NonePlatformType, "unrecognized"} { for _, tc := range []struct { - icspRules []*apioperatorsv1alpha1.ImageContentSourcePolicy - idmsRules []*apicfgv1.ImageDigestMirrorSet - itmsRules []*apicfgv1.ImageTagMirrorSet + icspRules []*apioperatorsv1alpha1.ImageContentSourcePolicy + idmsRules []*apicfgv1.ImageDigestMirrorSet + itmsRules []*apicfgv1.ImageTagMirrorSet + clusterImagePolicies []*apicfgv1alpha1.ClusterImagePolicy }{ { idmsRules: []*apicfgv1.ImageDigestMirrorSet{ @@ -1092,6 +1220,11 @@ func TestRunImageBootstrap(t *testing.T) { }), }, }, + { + clusterImagePolicies: []*apicfgv1alpha1.ClusterImagePolicy{ + &testClusterImagePolicy, + }, + }, } { t.Run(string(platform), func(t *testing.T) { @@ -1103,16 +1236,26 @@ func TestRunImageBootstrap(t *testing.T) { // Adding the release-image registry "release-reg.io" to the list of blocked registries to ensure that is it not added to // both registries.conf and policy.json as blocked imgCfg := newImageConfig("cluster", &apicfgv1.RegistrySources{InsecureRegistries: []string{"insecure-reg-1.io", "insecure-reg-2.io"}, BlockedRegistries: []string{"blocked-reg.io", "release-reg.io"}, ContainerRuntimeSearchRegistries: []string{"search-reg.io"}}) + // set FeatureGateSigstoreImageVerification enabled for testing + fgAccess := featuregates.NewHardcodedFeatureGateAccess([]apicfgv1.FeatureGateName{apicfgv1.FeatureGateSigstoreImageVerification}, []apicfgv1.FeatureGateName{}) - fgAccess := featuregates.NewHardcodedFeatureGateAccess([]apicfgv1.FeatureGateName{}, []apicfgv1.FeatureGateName{}) - - mcs, err := RunImageBootstrap("../../../templates", cc, pools, tc.icspRules, tc.idmsRules, tc.itmsRules, imgCfg, fgAccess) + mcs, err := RunImageBootstrap("../../../templates", cc, pools, tc.icspRules, tc.idmsRules, tc.itmsRules, imgCfg, tc.clusterImagePolicies, nil, fgAccess) require.NoError(t, err) - require.Len(t, mcs, len(pools)) + if tc.clusterImagePolicies != nil { + require.Len(t, mcs, len(pools)*2) + } else { + require.Len(t, mcs, len(pools)) + } for i := range pools { keyReg, _ := getManagedKeyReg(pools[i], nil) - verifyRegistriesConfigAndPolicyJSONContents(t, mcs[i], keyReg, imgCfg, tc.icspRules, tc.idmsRules, tc.itmsRules, cc.Spec.ReleaseImage, true, true) + if mcs[i].Name == keyReg { + verifyRegistriesConfigAndPolicyJSONContents(t, mcs[i], keyReg, imgCfg, tc.icspRules, tc.idmsRules, tc.itmsRules, tc.clusterImagePolicies, cc.Spec.ReleaseImage, true, true, false) + } + keyImagePolicy := getManagedKeyNamespacedImagePolicy(pools[i]) + if mcs[i].Name == keyImagePolicy { + verifyRegistriesConfigAndPolicyJSONContents(t, mcs[i], keyReg, imgCfg, tc.icspRules, tc.idmsRules, tc.itmsRules, tc.clusterImagePolicies, cc.Spec.ReleaseImage, false, false, true) + } } }) } @@ -1664,3 +1807,112 @@ func TestCleanUpDuplicatedMC(t *testing.T) { }) } } + +func TestClusterImagePolicyCreate(t *testing.T) { + for _, platform := range []apicfgv1.PlatformType{apicfgv1.AWSPlatformType, apicfgv1.NonePlatformType, "unrecognized"} { + t.Run(string(platform), func(t *testing.T) { + f := newFixture(t) + + cc := newControllerConfig(ctrlcommon.ControllerConfigName, platform) + mcp := helpers.NewMachineConfigPool("master", nil, helpers.MasterSelector, "v0") + mcp2 := helpers.NewMachineConfigPool("worker", nil, helpers.WorkerSelector, "v0") + imgcfg1 := newImageConfig("cluster", &apicfgv1.RegistrySources{InsecureRegistries: []string{"blah.io"}, AllowedRegistries: []string{"allow.io"}, ContainerRuntimeSearchRegistries: []string{"search-reg.io"}}) + + cvcfg1 := newClusterVersionConfig("version", "test.io/myuser/myimage:test") + keyReg1, _ := getManagedKeyReg(mcp, nil) + keyReg2, _ := getManagedKeyReg(mcp2, nil) + clusterimgPolicyKey1 := getManagedKeyNamespacedImagePolicy(mcp) + clusterimgPolicyKey2 := getManagedKeyNamespacedImagePolicy(mcp) + clusterimgPolicyMCs1 := helpers.NewMachineConfig(clusterimgPolicyKey1, map[string]string{"node-role": "master"}, "dummy://", []ign3types.File{{}}) + clusterimgPolicyMCs2 := helpers.NewMachineConfig(clusterimgPolicyKey2, map[string]string{"node-role": "worker"}, "dummy://", []ign3types.File{{}}) + + mcs1 := helpers.NewMachineConfig(keyReg1, map[string]string{"node-role": "master"}, "dummy://", []ign3types.File{{}}) + mcs2 := helpers.NewMachineConfig(keyReg2, map[string]string{"node-role": "worker"}, "dummy://", []ign3types.File{{}}) + clusterimgPolicy := newClusterImagePolicyWithPublicKey("image-policy", []string{"example.com"}, "Zm9vIGJhcg==") + f.ccLister = append(f.ccLister, cc) + f.mcpLister = append(f.mcpLister, mcp) + f.mcpLister = append(f.mcpLister, mcp2) + f.imgLister = append(f.imgLister, imgcfg1) + f.clusterImagePolicyLister = append(f.clusterImagePolicyLister, clusterimgPolicy) + f.cvLister = append(f.cvLister, cvcfg1) + f.imgObjects = append(f.imgObjects, imgcfg1) + + f.expectGetMachineConfigAction(mcs1) + f.expectGetMachineConfigAction(mcs1) + f.expectGetMachineConfigAction(mcs1) + f.expectCreateMachineConfigAction(mcs1) + f.expectGetMachineConfigAction(clusterimgPolicyMCs1) + f.expectCreateMachineConfigAction(clusterimgPolicyMCs1) + + f.expectGetMachineConfigAction(mcs2) + f.expectGetMachineConfigAction(mcs2) + f.expectGetMachineConfigAction(mcs2) + + f.expectCreateMachineConfigAction(mcs2) + f.expectGetMachineConfigAction(clusterimgPolicyMCs2) + f.expectCreateMachineConfigAction(clusterimgPolicyMCs2) + + f.run("") + + for _, mcName := range []string{mcs1.Name, mcs2.Name} { + f.verifyRegistriesConfigAndPolicyJSONContents(t, mcName, imgcfg1, nil, nil, nil, clusterimgPolicy, cc.Spec.ReleaseImage, true, true, false) + } + for _, mcName := range []string{clusterimgPolicyKey1, clusterimgPolicyKey2} { + f.verifySigstoreRegistriesConfdAndPolicyContents(t, imgcfg1, mcName, cc.Spec.ReleaseImage, clusterimgPolicy, nil) + } + }) + } +} + +func TestImagePolicyCreate(t *testing.T) { + for _, platform := range []apicfgv1.PlatformType{apicfgv1.AWSPlatformType, apicfgv1.NonePlatformType, "unrecognized"} { + t.Run(string(platform), func(t *testing.T) { + f := newFixture(t) + + cc := newControllerConfig(ctrlcommon.ControllerConfigName, platform) + mcp := helpers.NewMachineConfigPool("master", nil, helpers.MasterSelector, "v0") + mcp2 := helpers.NewMachineConfigPool("worker", nil, helpers.WorkerSelector, "v0") + imgcfg1 := newImageConfig("cluster", &apicfgv1.RegistrySources{InsecureRegistries: []string{"blah.io"}, AllowedRegistries: []string{"allow.io"}, ContainerRuntimeSearchRegistries: []string{"search-reg.io"}}) + + cvcfg1 := newClusterVersionConfig("version", "test.io/myuser/myimage:test") + keyReg1, _ := getManagedKeyReg(mcp, nil) + keyReg2, _ := getManagedKeyReg(mcp2, nil) + imgPolicyKey1 := getManagedKeyNamespacedImagePolicy(mcp) + imgPolicyKey2 := getManagedKeyNamespacedImagePolicy(mcp) + imgPolicyMCs1 := helpers.NewMachineConfig(imgPolicyKey1, map[string]string{"node-role": "master"}, "dummy://", []ign3types.File{{}}) + imgPolicyMCs2 := helpers.NewMachineConfig(imgPolicyKey2, map[string]string{"node-role": "worker"}, "dummy://", []ign3types.File{{}}) + + mcs1 := helpers.NewMachineConfig(keyReg1, map[string]string{"node-role": "master"}, "dummy://", []ign3types.File{{}}) + mcs2 := helpers.NewMachineConfig(keyReg2, map[string]string{"node-role": "worker"}, "dummy://", []ign3types.File{{}}) + imgPolicy := newImagePolicyWithPublicKey("image-policy", "testnamespace", []string{"example.com", "test.io"}, "Zm9vIGJhcg==") + f.ccLister = append(f.ccLister, cc) + f.mcpLister = append(f.mcpLister, mcp) + f.mcpLister = append(f.mcpLister, mcp2) + f.imgLister = append(f.imgLister, imgcfg1) + f.imagePolicyLister = append(f.imagePolicyLister, imgPolicy) + f.cvLister = append(f.cvLister, cvcfg1) + f.imgObjects = append(f.imgObjects, imgcfg1) + + f.expectGetMachineConfigAction(mcs1) + f.expectGetMachineConfigAction(mcs1) + f.expectGetMachineConfigAction(mcs1) + f.expectCreateMachineConfigAction(mcs1) + f.expectGetMachineConfigAction(imgPolicyMCs1) + f.expectCreateMachineConfigAction(imgPolicyMCs1) + + f.expectGetMachineConfigAction(mcs2) + f.expectGetMachineConfigAction(mcs2) + f.expectGetMachineConfigAction(mcs2) + + f.expectCreateMachineConfigAction(mcs2) + f.expectGetMachineConfigAction(imgPolicyMCs2) + f.expectCreateMachineConfigAction(imgPolicyMCs2) + + f.run("") + + for _, mcName := range []string{imgPolicyKey1, imgPolicyKey2} { + f.verifySigstoreRegistriesConfdAndPolicyContents(t, imgcfg1, mcName, cvcfg1.Status.Desired.Image, nil, imgPolicy) + } + }) + } +} diff --git a/pkg/controller/container-runtime-config/helpers.go b/pkg/controller/container-runtime-config/helpers.go index 1e302fcd44..b4683e20da 100644 --- a/pkg/controller/container-runtime-config/helpers.go +++ b/pkg/controller/container-runtime-config/helpers.go @@ -7,11 +7,14 @@ import ( "errors" "fmt" "os" + "path/filepath" "reflect" "regexp" "strconv" "strings" + b64 "encoding/base64" + "github.com/BurntSushi/toml" "github.com/containers/image/v5/docker/reference" "github.com/containers/image/v5/pkg/sysregistriesv2" @@ -19,7 +22,9 @@ import ( "github.com/containers/image/v5/types" storageconfig "github.com/containers/storage/pkg/config" ign3types "github.com/coreos/ignition/v2/config/v3_4/types" + "github.com/ghodss/yaml" apicfgv1 "github.com/openshift/api/config/v1" + apicfgv1alpha1 "github.com/openshift/api/config/v1alpha1" apioperatorsv1alpha1 "github.com/openshift/api/operator/v1alpha1" "github.com/openshift/runtime-utils/pkg/registries" runtimeutils "github.com/openshift/runtime-utils/pkg/registries" @@ -48,14 +53,18 @@ const ( crioDropInFilePathPidsLimit = "/etc/crio/crio.conf.d/01-ctrcfg-pidsLimit" crioDropInFilePathLogSizeMax = "/etc/crio/crio.conf.d/01-ctrcfg-logSizeMax" CRIODropInFilePathDefaultRuntime = "/etc/crio/crio.conf.d/01-ctrcfg-defaultRuntime" + imagepolicyType = "sigstoreSigned" + sigstoreRegistriesConfigFilePath = "/etc/containers/registries.d/sigstore-registries.yaml" ) var ( - errParsingReference = errors.New("error parsing reference of release image") // sourceRegex and mirrorRegex pattern should stay the same with https://github.com/openshift/api/blob/ef62af078a9387e739abd99ec1d80e9129bb5475/config/v1/types_image_digest_mirror_set.go // Validation the source and mirror format for IDMS/ITMS already exists in the CRD. We need to keep this regex validation for ICSP - sourceRegex = regexp.MustCompile(`^\*(?:\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+$|^((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:(?:\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+)?(?::[0-9]+)?)(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$`) - mirrorRegex = regexp.MustCompile(`^((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:(?:\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+)?(?::[0-9]+)?)(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$`) + sourceRegex = regexp.MustCompile(`^\*(?:\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+$|^((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:(?:\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+)?(?::[0-9]+)?)(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$`) + mirrorRegex = regexp.MustCompile(`^((?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:(?:\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+)?(?::[0-9]+)?)(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$`) + errParsingReference = errors.New("error parsing reference of release image") + namespacedPolicyFilePathFormat = filepath.FromSlash("/etc/crio/policies/%s.json") + reasonConflictScopes = "ConflictScopes" ) // TOML-friendly explicit tables used for conversions. @@ -116,6 +125,13 @@ type tomlConfigCRIODefaultRuntime struct { } `toml:"crio"` } +type DockerConfig struct { + UseSigstoreAttachments bool `json:"use-sigstore-attachments,omitempty"` +} +type RegistriesConfig struct { + Docker map[string]DockerConfig `json:"docker,omitempty"` +} + // generatedConfigFile is a struct that holds the filepath and data of the various configs // Using a struct array ensures that the order of the ignition files always stay the same // ensuring that double MCs are not created due to a change in the order @@ -130,6 +146,9 @@ type updateConfigFunc func(data []byte, internal *mcfgv1.ContainerRuntimeConfigu // new data in the form of a byte array. The function returns the ignition config with the // updated data. func createNewIgnition(configs []generatedConfigFile) ign3types.Config { + if len(configs) == 0 { + return ign3types.Config{} + } tempIgnConfig := ctrlcommon.NewIgnConfig() // Create ignitions for _, ignConf := range configs { @@ -329,6 +348,10 @@ func getManagedKeyReg(pool *mcfgv1.MachineConfigPool, client mcfgclientset.Inter return ctrlcommon.GetManagedKey(pool, client, "99", "registries", getManagedKeyRegDeprecated(pool)) } +func getManagedKeyNamespacedImagePolicy(pool *mcfgv1.MachineConfigPool) string { + return fmt.Sprintf("99-%s-generated-imagepolicies", pool.Name) +} + func wrapErrorWithCondition(err error, args ...interface{}) mcfgv1.ContainerRuntimeConfigCondition { var condition *mcfgv1.ContainerRuntimeConfigCondition if err != nil { @@ -480,7 +503,7 @@ func updateRegistriesConfig(data []byte, internalInsecure, internalBlocked []str // It also returns an error if both allowed and blocked registries are set // WARNING: This can not safely edit policy files with arbitrary complexity, especially files which include signedBy // requirements. It expects the input policy to be generated by templates in this project. -func updatePolicyJSON(data []byte, internalBlocked, internalAllowed []string, releaseImage string) ([]byte, error) { +func updatePolicyJSON(data []byte, internalBlocked, internalAllowed []string, releaseImage string, clusterScopePolicies map[string]signature.PolicyRequirements) ([]byte, error) { if len(internalAllowed) != 0 && len(internalBlocked) != 0 { payloadRepo, err := getPayloadRepo(releaseImage) if err != nil { @@ -492,10 +515,14 @@ func updatePolicyJSON(data []byte, internalBlocked, internalAllowed []string, re return nil, fmt.Errorf("invalid images config: only one of AllowedRegistries or BlockedRegistries may be specified") } } + + if err := validateClusterImagePolicyWithAllowedBlockedRegistries(clusterScopePolicies, internalAllowed, internalBlocked); err != nil { + return nil, err + } // Return original data if neither allowed or blocked registries are configured // Note: this is just for testing, the controller does not call this function till // either allowed or blocked registries are configured - if internalAllowed == nil && internalBlocked == nil { + if internalAllowed == nil && internalBlocked == nil && len(clusterScopePolicies) == 0 { return data, nil } @@ -530,18 +557,21 @@ func updatePolicyJSON(data []byte, internalBlocked, internalAllowed []string, re } } } - - policyObj.Transports["docker"] = transportScopes - // The “atomic” policy is the same as the “docker” policy, but “atomic” does not support three or more - // scope segments, so filter those scopes out. - policyObj.Transports["atomic"] = make(signature.PolicyTransportScopes) - for reg, config := range transportScopes { - if strings.Count(reg, "/") >= 3 { - continue + if len(transportScopes) > 0 { + policyObj.Transports["docker"] = transportScopes + // The “atomic” policy is the same as the “docker” policy, but “atomic” does not support three or more + // scope segments, so filter those scopes out. + policyObj.Transports["atomic"] = make(signature.PolicyTransportScopes) + for reg, config := range transportScopes { + if strings.Count(reg, "/") >= 3 { + continue + } + policyObj.Transports["atomic"][reg] = config } - policyObj.Transports["atomic"][reg] = config } + updatePolicyObjWithScopeRequirements(policyObj, clusterScopePolicies) + policyJSON, err := json.Marshal(policyObj) if err != nil { return nil, err @@ -812,3 +842,210 @@ func convertICSPToIDMS(icsp *apioperatorsv1alpha1.ImageContentSourcePolicy) *api }, } } + +func ownerReferenceForImageConfig(imageConfig *apicfgv1.Image) metav1.OwnerReference { + return metav1.OwnerReference{ + APIVersion: apicfgv1.SchemeGroupVersion.String(), + Kind: "Image", + Name: imageConfig.Name, + UID: imageConfig.UID, + } +} + +func ownerReferenceForImagePolicy(imagePolicies []*apicfgv1alpha1.ImagePolicy, clusterImagePolicies []*apicfgv1alpha1.ClusterImagePolicy) metav1.OwnerReference { + if len(imagePolicies) > 0 { + return metav1.OwnerReference{ + APIVersion: apicfgv1alpha1.SchemeGroupVersion.String(), + Kind: "ImagePolicy", + Name: imagePolicies[0].Name, + UID: imagePolicies[0].UID, + } + } + if len(clusterImagePolicies) > 0 { + return metav1.OwnerReference{ + APIVersion: apicfgv1alpha1.SchemeGroupVersion.String(), + Kind: "ClusterImagePolicy", + Name: clusterImagePolicies[0].Name, + UID: clusterImagePolicies[0].UID, + } + } + return metav1.OwnerReference{} +} + +func policyItemFromSpec(policy apicfgv1alpha1.Policy) (signature.PolicyRequirement, error) { + var ( + sigstorePolicyRequirement signature.PolicyRequirement + signedIdentity signature.PolicyReferenceMatch + signedOptions []signature.PRSigstoreSignedOption + err error + ) + switch policy.RootOfTrust.PolicyType { + case apicfgv1alpha1.PublicKeyRootOfTrust: + keyDataDec, err := b64.StdEncoding.DecodeString(policy.RootOfTrust.PublicKey.KeyData) + if err != nil { + return nil, fmt.Errorf("failed to decode public key: %v", err) + } + signedOptions = append(signedOptions, signature.PRSigstoreSignedWithKeyData(keyDataDec)) + if policy.RootOfTrust.PublicKey.RekorKeyData != "" { + rekorKeyDataDec, err := b64.StdEncoding.DecodeString(policy.RootOfTrust.PublicKey.RekorKeyData) + if err != nil { + return nil, fmt.Errorf("failed to decode rekorKeyData: %v", err) + } + signedOptions = append(signedOptions, signature.PRSigstoreSignedWithRekorPublicKeyData(rekorKeyDataDec)) + } + case apicfgv1alpha1.FulcioCAWithRekorRootOfTrust: + fulcioOptions := []signature.PRSigstoreSignedFulcioOption{} + fulcioOptions = append(fulcioOptions, signature.PRSigstoreSignedFulcioWithCAData(policy.RootOfTrust.FulcioCAWithRekor.FulcioCAData), + signature.PRSigstoreSignedFulcioWithOIDCIssuer(policy.RootOfTrust.FulcioCAWithRekor.FulcioSubject.OIDCIssuer), + signature.PRSigstoreSignedFulcioWithSubjectEmail(policy.RootOfTrust.FulcioCAWithRekor.FulcioSubject.SignedEmail)) + + prSigstoreSignedFulcio, err := signature.NewPRSigstoreSignedFulcio(fulcioOptions...) + if err != nil { + return nil, err + } + signedOptions = append(signedOptions, signature.PRSigstoreSignedWithFulcio(prSigstoreSignedFulcio)) + + rekorKeyDataDec, err := b64.StdEncoding.DecodeString(policy.RootOfTrust.FulcioCAWithRekor.RekorKeyData) + if err != nil { + return nil, fmt.Errorf("failed to decode rekorKeyData: %v", err) + } + signedOptions = append(signedOptions, signature.PRSigstoreSignedWithRekorPublicKeyData(rekorKeyDataDec)) + } + + switch policy.SignedIdentity.MatchPolicy { + case apicfgv1alpha1.IdentityMatchPolicyRemapIdentity: + identity, err := signature.NewPRMRemapIdentity(string(policy.SignedIdentity.PolicyMatchRemapIdentity.Prefix), string(policy.SignedIdentity.PolicyMatchRemapIdentity.SignedPrefix)) + if err != nil { + return nil, fmt.Errorf("error getting signedIdentity for %s: %v", apicfgv1alpha1.IdentityMatchPolicyRemapIdentity, err) + } + signedIdentity = identity + case apicfgv1alpha1.IdentityMatchPolicyExactRepository: + identity, err := signature.NewPRMExactRepository(string(policy.SignedIdentity.PolicyMatchExactRepository.Repository)) + if err != nil { + return nil, fmt.Errorf("error getting signedIdentity for %s: %v", apicfgv1alpha1.IdentityMatchPolicyExactRepository, err) + } + signedIdentity = identity + case apicfgv1alpha1.IdentityMatchPolicyMatchRepository: + signedIdentity = signature.NewPRMMatchRepository() + case apicfgv1alpha1.IdentityMatchPolicyMatchRepoDigestOrExact, "": + signedIdentity = signature.NewPRMMatchRepoDigestOrExact() + default: + return nil, fmt.Errorf("unknown signedIdentity match policy: %s", policy.SignedIdentity.MatchPolicy) + } + + signedOptions = append(signedOptions, signature.PRSigstoreSignedWithSignedIdentity(signedIdentity)) + + if sigstorePolicyRequirement, err = signature.NewPRSigstoreSigned(signedOptions...); err != nil { + return nil, err + } + + return sigstorePolicyRequirement, nil +} + +func validateClusterImagePolicyWithAllowedBlockedRegistries(clusterScopePolicies map[string]signature.PolicyRequirements, allowedRegs, policyBlocked []string) error { + for _, reg := range allowedRegs { + if _, ok := clusterScopePolicies[reg]; ok { + return fmt.Errorf("allowedRegistries and clusterimagePolicy configured for the same scope %s ", reg) + } + } + + for _, reg := range policyBlocked { + if _, ok := clusterScopePolicies[reg]; ok { + return fmt.Errorf("blockedRegistries and clusterimagePolicy configured for the same scope %s ", reg) + } + } + return nil +} + +func generateSigstoreRegistriesdConfig(clusterScopePolicies map[string]signature.PolicyRequirements, + scopeNamespacePolicies map[string]map[string]signature.PolicyRequirements) ([]byte, error) { + if len(clusterScopePolicies) == 0 && len(scopeNamespacePolicies) == 0 { + return nil, nil + } + + dockerConfig := make(map[string]DockerConfig) + sigstoreAttachment := DockerConfig{ + UseSigstoreAttachments: true, + } + for scope := range clusterScopePolicies { + dockerConfig[scope] = sigstoreAttachment + } + for scope := range scopeNamespacePolicies { + dockerConfig[scope] = sigstoreAttachment + } + + registriesConfig := &RegistriesConfig{} + registriesConfig.Docker = dockerConfig + data, err := yaml.Marshal(registriesConfig) + if err != nil { + return nil, fmt.Errorf("error marshalling regisres configuration for sigstore (cluster)imagepolicies: %w", err) + } + return data, nil +} + +// updatePolicyObjWithScopeRequirements adds signature requirements to docker transport of the non nil policyObj +func updatePolicyObjWithScopeRequirements(policyObj *signature.Policy, requirements map[string]signature.PolicyRequirements) { + if policyObj.Transports == nil { + policyObj.Transports = make(map[string]signature.PolicyTransportScopes) + } + if _, ok := policyObj.Transports["docker"]; !ok { + policyObj.Transports["docker"] = make(map[string]signature.PolicyRequirements) + } + for scope, requirement := range requirements { + policyObj.Transports["docker"][scope] = append(policyObj.Transports["docker"][scope], requirement...) + } +} + +// namespacePolicyRequirements collects imagepolicies into map[namespace]map[scope]policyRequirements +func namespaceScopePolicyRequirements(scopeNamespacePolicies map[string]map[string]signature.PolicyRequirements) map[string]map[string]signature.PolicyRequirements { + policies := make(map[string]map[string]signature.PolicyRequirements) + for scope, namespacePolicies := range scopeNamespacePolicies { + for namespace, requirements := range namespacePolicies { + if policies[namespace] == nil { + policies[namespace] = make(map[string]signature.PolicyRequirements) + } + policies[namespace][scope] = append(policies[namespace][scope], requirements...) + } + } + return policies +} + +func generateNamespacedPolicyJSON(namespaceJSONBase []byte, policies map[string]map[string]signature.PolicyRequirements) (map[string][]byte, error) { + namespaceJSONs := make(map[string][]byte) + decoder := json.NewDecoder(bytes.NewBuffer(namespaceJSONBase)) + + for namespace, requirements := range policies { + + policyObj := &signature.Policy{} + + err := decoder.Decode(policyObj) + if err != nil { + return nil, fmt.Errorf("error decoding policy json for namespaced policies: %w", err) + } + updatePolicyObjWithScopeRequirements(policyObj, requirements) + data, err := json.Marshal(policyObj) + if err != nil { + return nil, fmt.Errorf("error marshalling policy json for namespaced policies: %w", err) + } + namespaceJSONs[namespace] = data + } + return namespaceJSONs, nil +} + +func imagePolicyConfigFileList(namespaceJSONs map[string][]byte, sigstoreRegistriesConfigYaml []byte) []generatedConfigFile { + var namespacedPolicyConfigFileList []generatedConfigFile + for namespace, data := range namespaceJSONs { + namespacedPolicyFilePath := fmt.Sprintf(namespacedPolicyFilePathFormat, namespace) + namespacedPolicyConfigFileList = append(namespacedPolicyConfigFileList, generatedConfigFile{ + filePath: namespacedPolicyFilePath, + data: data, + }) + } + if len(sigstoreRegistriesConfigYaml) > 0 { + namespacedPolicyConfigFileList = append(namespacedPolicyConfigFileList, generatedConfigFile{ + filePath: sigstoreRegistriesConfigFilePath, + data: sigstoreRegistriesConfigYaml, + }) + } + return namespacedPolicyConfigFileList +} diff --git a/pkg/controller/container-runtime-config/helpers_test.go b/pkg/controller/container-runtime-config/helpers_test.go index 50434c03a5..ff08490d84 100644 --- a/pkg/controller/container-runtime-config/helpers_test.go +++ b/pkg/controller/container-runtime-config/helpers_test.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "errors" + "fmt" "os" "reflect" "testing" @@ -14,6 +15,7 @@ import ( "github.com/containers/image/v5/types" storageconfig "github.com/containers/storage/pkg/config" apicfgv1 "github.com/openshift/api/config/v1" + apicfgv1alpha1 "github.com/openshift/api/config/v1alpha1" mcfgv1 "github.com/openshift/api/machineconfiguration/v1" apioperatorsv1alpha1 "github.com/openshift/api/operator/v1alpha1" "github.com/stretchr/testify/assert" @@ -463,7 +465,86 @@ func TestUpdateRegistriesConfig(t *testing.T) { } } +var testImagePolicyCRs = map[string]apicfgv1alpha1.ImagePolicy{ + "test-cr2": { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cr2", + Namespace: "testnamespace", + }, + Spec: apicfgv1alpha1.ImagePolicySpec{ + Scopes: []apicfgv1alpha1.ImageScope{"test0.com", "test2.com"}, + Policy: apicfgv1alpha1.Policy{ + RootOfTrust: apicfgv1alpha1.PolicyRootOfTrust{ + PolicyType: apicfgv1alpha1.PublicKeyRootOfTrust, + PublicKey: &apicfgv1alpha1.PublicKey{ + KeyData: "dGVzdC1rZXktZGF0YQ==", + }, + }, + }, + }, + }, +} + +var testClusterImagePolicyCRs = map[string]apicfgv1alpha1.ClusterImagePolicy{ + "test-cr0": { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cr0", + }, + Spec: apicfgv1alpha1.ClusterImagePolicySpec{ + Scopes: []apicfgv1alpha1.ImageScope{"test0.com"}, + Policy: apicfgv1alpha1.Policy{ + RootOfTrust: apicfgv1alpha1.PolicyRootOfTrust{ + PolicyType: apicfgv1alpha1.FulcioCAWithRekorRootOfTrust, + FulcioCAWithRekor: &apicfgv1alpha1.FulcioCAWithRekor{ + FulcioCAData: []byte("dGVzdC1jYS1kYXRhLWRhdGE="), + RekorKeyData: "dGVzdC1yZWtvci1rZXktZGF0YQ==", + FulcioSubject: apicfgv1alpha1.PolicyFulcioSubject{ + OIDCIssuer: "https://OIDC.example.com", + SignedEmail: "test-user@example.com", + }, + }, + }, + SignedIdentity: apicfgv1alpha1.PolicyIdentity{ + MatchPolicy: apicfgv1alpha1.IdentityMatchPolicyRemapIdentity, + PolicyMatchRemapIdentity: &apicfgv1alpha1.PolicyMatchRemapIdentity{ + Prefix: "test-remap-prefix", + SignedPrefix: "test-remap-signed-prefix", + }, + }, + }, + }, + }, + "test-cr1": { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cr1", + }, + Spec: apicfgv1alpha1.ClusterImagePolicySpec{ + Scopes: []apicfgv1alpha1.ImageScope{"test0.com", "test1.com"}, + Policy: apicfgv1alpha1.Policy{ + RootOfTrust: apicfgv1alpha1.PolicyRootOfTrust{ + PolicyType: apicfgv1alpha1.PublicKeyRootOfTrust, + PublicKey: &apicfgv1alpha1.PublicKey{ + KeyData: "dGVzdC1rZXktZGF0YQ==", + RekorKeyData: "dGVzdC1yZWtvci1rZXktZGF0YQ==", + }, + }, + SignedIdentity: apicfgv1alpha1.PolicyIdentity{ + MatchPolicy: apicfgv1alpha1.IdentityMatchPolicyRemapIdentity, + PolicyMatchRemapIdentity: &apicfgv1alpha1.PolicyMatchRemapIdentity{ + Prefix: "test-remap-prefix", + SignedPrefix: "test-remap-signed-prefix", + }, + }, + }, + }, + }, +} + func TestUpdatePolicyJSON(t *testing.T) { + testClusterImagePolicyCR := testClusterImagePolicyCRs["test-cr0"] + expectSigRequirement, policyerr := policyItemFromSpec(testClusterImagePolicyCR.Spec.Policy) + require.NoError(t, policyerr) + templateConfig := signature.Policy{ Default: signature.PolicyRequirements{signature.NewPRInsecureAcceptAnything()}, Transports: map[string]signature.PolicyTransportScopes{ @@ -480,6 +561,7 @@ func TestUpdatePolicyJSON(t *testing.T) { tests := []struct { name string allowed, blocked []string + imagepolicy *apicfgv1alpha1.ClusterImagePolicy errorExpected bool want signature.Policy }{ @@ -626,10 +708,41 @@ func TestUpdatePolicyJSON(t *testing.T) { }, errorExpected: false, }, + { + name: "update global policy with ImagePolicy CR", + allowed: []string{"allow.io"}, + imagepolicy: &testClusterImagePolicyCR, + want: signature.Policy{ + Default: signature.PolicyRequirements{signature.NewPRReject()}, + Transports: map[string]signature.PolicyTransportScopes{ + "atomic": map[string]signature.PolicyRequirements{ + "allow.io": {signature.NewPRInsecureAcceptAnything()}, + }, + "docker": map[string]signature.PolicyRequirements{ + "allow.io": {signature.NewPRInsecureAcceptAnything()}, + "test0.com": {expectSigRequirement}, + }, + "docker-daemon": map[string]signature.PolicyRequirements{ + "": {signature.NewPRInsecureAcceptAnything()}, + }, + }, + }, + errorExpected: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := updatePolicyJSON(templateBytes, tt.blocked, tt.allowed, "release-reg.io/image/release") + + var ( + clusterImagePolicies map[string]signature.PolicyRequirements + policyerr error + ) + + if tt.imagepolicy != nil { + clusterImagePolicies, _, policyerr = getValidScopeNamespacePolicies([]*apicfgv1alpha1.ClusterImagePolicy{tt.imagepolicy}, []*apicfgv1alpha1.ImagePolicy{}, "release-reg.io/image/release", nil) + require.NoError(t, policyerr) + } + got, err := updatePolicyJSON(templateBytes, tt.blocked, tt.allowed, "release-reg.io/image/release", clusterImagePolicies) if err == nil && tt.errorExpected { t.Errorf("updatePolicyJSON() error = %v", err) return @@ -1105,3 +1218,259 @@ func TestUpdateStorageConfig(t *testing.T) { } } } + +func TestGetValidScopeNamespacePolicies(t *testing.T) { + type testcase struct { + name string + clusterImagePolicyCRs []*apicfgv1alpha1.ClusterImagePolicy + imagePolicyCRs []*apicfgv1alpha1.ImagePolicy + releaseImg string + expectedScopePolicies map[string]signature.PolicyRequirements + expectedScopeNamespace map[string]map[string]signature.PolicyRequirements + errorExpected bool + } + + testCR0 := testClusterImagePolicyCRs["test-cr0"] + testCR1 := testClusterImagePolicyCRs["test-cr1"] + testCR2 := testImagePolicyCRs["test-cr2"] + policyreq0, err := policyItemFromSpec(testCR0.Spec.Policy) + require.NoError(t, err) + policyreq1, err := policyItemFromSpec(testCR1.Spec.Policy) + require.NoError(t, err) + policyreq2, err := policyItemFromSpec(testCR2.Spec.Policy) + require.NoError(t, err) + + tests := []testcase{ + { + name: "cluster and namespace CRs contains the same scope", + imagePolicyCRs: []*apicfgv1alpha1.ImagePolicy{&testCR2}, + clusterImagePolicyCRs: []*apicfgv1alpha1.ClusterImagePolicy{&testCR0, &testCR1}, + releaseImg: "release-reg.io/image/release", + expectedScopeNamespace: map[string]map[string]signature.PolicyRequirements{ + "test2.com": { + testCR2.ObjectMeta.Namespace: {policyreq2}, + }, + }, + expectedScopePolicies: map[string]signature.PolicyRequirements{ + "test0.com": {policyreq0, policyreq1}, + "test1.com": {policyreq1}, + }, + errorExpected: false, + }, + { + name: "clusterimagepolicy CRs has scope release image scope nested in", + imagePolicyCRs: []*apicfgv1alpha1.ImagePolicy{&testCR2}, + clusterImagePolicyCRs: []*apicfgv1alpha1.ClusterImagePolicy{&testCR0, &testCR1}, + releaseImg: "test1.com/image/release", + expectedScopeNamespace: map[string]map[string]signature.PolicyRequirements{ + "test2.com": { + testCR2.ObjectMeta.Namespace: {policyreq2}, + }, + }, + expectedScopePolicies: map[string]signature.PolicyRequirements{ + "test0.com": {policyreq0, policyreq1}, + }, + errorExpected: false, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + gotScopePolicies, gotScopeNamespacePolicies, err := getValidScopeNamespacePolicies(test.clusterImagePolicyCRs, test.imagePolicyCRs, test.releaseImg, nil) + fmt.Println(err) + require.Equal(t, test.errorExpected, err != nil) + if !test.errorExpected { + require.Equal(t, test.expectedScopeNamespace, gotScopeNamespacePolicies) + require.Equal(t, test.expectedScopePolicies, gotScopePolicies) + } + }) + } +} + +func TestGenerateNamespacedPolicyJSON(t *testing.T) { + // Test empty namespacePolicies does not generate any policy + namespacePoliciesJsons, err := generateNamespacedPolicyJSON([]byte{}, make(map[string]map[string]signature.PolicyRequirements)) + require.NoError(t, err) + require.Equal(t, 0, len(namespacePoliciesJsons)) + + testImagePolicyCR0 := testClusterImagePolicyCRs["test-cr0"] + testImagePolicyCR1 := testClusterImagePolicyCRs["test-cr1"] + testImagePolicyCR2 := testImagePolicyCRs["test-cr2"] + + expectClusterPolicy := []byte(` + { + "default": [ + { + "type": "insecureAcceptAnything" + } + ], + "transports": { + "docker": { + "test0.com": [ + { + "type": "sigstoreSigned", + "fulcio": { + "caData": "dGVzdC1jYS1kYXRhLWRhdGE=", + "oidcIssuer": "https://OIDC.example.com", + "subjectEmail": "test-user@example.com" + }, + "rekorPublicKeyData": "dGVzdC1yZWtvci1rZXktZGF0YQ==", + "signedIdentity": { + "type": "remapIdentity", + "prefix": "test-remap-prefix", + "signedPrefix": "test-remap-signed-prefix" + } + }, + { + "type": "sigstoreSigned", + "keyData": "dGVzdC1rZXktZGF0YQ==", + "rekorPublicKeyData": "dGVzdC1yZWtvci1rZXktZGF0YQ==", + "signedIdentity": { + "type": "remapIdentity", + "prefix": "test-remap-prefix", + "signedPrefix": "test-remap-signed-prefix" + } + } + ], + "test1.com": [ + { + "type": "sigstoreSigned", + "keyData": "dGVzdC1rZXktZGF0YQ==", + "rekorPublicKeyData": "dGVzdC1yZWtvci1rZXktZGF0YQ==", + "signedIdentity": { + "type": "remapIdentity", + "prefix": "test-remap-prefix", + "signedPrefix": "test-remap-signed-prefix" + } + } + ] + }, + "docker-daemon": { + "": [ + { + "type": "insecureAcceptAnything" + } + ] + } + } + } + `) + expectnamespacedPolicy := []byte(` + { + "default": [ + { + "type": "insecureAcceptAnything" + } + ], + "transports": { + "docker": { + "test0.com": [ + { + "type": "sigstoreSigned", + "fulcio": { + "caData": "dGVzdC1jYS1kYXRhLWRhdGE=", + "oidcIssuer": "https://OIDC.example.com", + "subjectEmail": "test-user@example.com" + }, + "rekorPublicKeyData": "dGVzdC1yZWtvci1rZXktZGF0YQ==", + "signedIdentity": { + "type": "remapIdentity", + "prefix": "test-remap-prefix", + "signedPrefix": "test-remap-signed-prefix" + } + }, + { + "type": "sigstoreSigned", + "keyData": "dGVzdC1rZXktZGF0YQ==", + "rekorPublicKeyData": "dGVzdC1yZWtvci1rZXktZGF0YQ==", + "signedIdentity": { + "type": "remapIdentity", + "prefix": "test-remap-prefix", + "signedPrefix": "test-remap-signed-prefix" + } + } + ], + "test1.com": [ + { + "type": "sigstoreSigned", + "keyData": "dGVzdC1rZXktZGF0YQ==", + "rekorPublicKeyData": "dGVzdC1yZWtvci1rZXktZGF0YQ==", + "signedIdentity": { + "type": "remapIdentity", + "prefix": "test-remap-prefix", + "signedPrefix": "test-remap-signed-prefix" + } + } + ], + "test2.com": [ + { + "type": "sigstoreSigned", + "keyData": "dGVzdC1rZXktZGF0YQ==", + "signedIdentity": { + "type": "matchRepoDigestOrExact" + } + } + ] + }, + "docker-daemon": { + "": [ + { + "type": "insecureAcceptAnything" + } + ] + } + } + } +`) + + expectRet := map[string][]byte{ + testImagePolicyCR2.ObjectMeta.Namespace: expectnamespacedPolicy, + } + + clusterScopePolicies, scopeNamespacePolicies, err := getValidScopeNamespacePolicies([]*apicfgv1alpha1.ClusterImagePolicy{&testImagePolicyCR0, &testImagePolicyCR1}, []*apicfgv1alpha1.ImagePolicy{&testImagePolicyCR2}, "release-reg.io/image/release", nil) + require.NoError(t, err) + policies := namespaceScopePolicyRequirements(scopeNamespacePolicies) + + templateConfig := signature.Policy{ + Default: signature.PolicyRequirements{signature.NewPRInsecureAcceptAnything()}, + Transports: map[string]signature.PolicyTransportScopes{ + "docker-daemon": map[string]signature.PolicyRequirements{ + "": {signature.NewPRInsecureAcceptAnything()}, + }, + }, + } + buf := bytes.Buffer{} + err = json.NewEncoder(&buf).Encode(templateConfig) + require.NoError(t, err) + templateBytes := buf.Bytes() + + baseData, err := updatePolicyJSON(templateBytes, []string{}, []string{}, "release-reg.io/image/release", clusterScopePolicies) + require.NoError(t, err) + require.JSONEq(t, string(expectClusterPolicy), string(baseData)) + got, err := generateNamespacedPolicyJSON(baseData, policies) + require.NoError(t, err) + for namespace, v := range got { + require.JSONEq(t, string(expectRet[namespace]), string(v)) + } +} + +func TestGenerateSigstoreRegistriesConfig(t *testing.T) { + testImagePolicyCR0 := testClusterImagePolicyCRs["test-cr0"] + testImagePolicyCR1 := testClusterImagePolicyCRs["test-cr1"] + testImagePolicyCR2 := testImagePolicyCRs["test-cr2"] + + expectSigstoreRegistriesConfig := []byte( + `docker: + test0.com: + use-sigstore-attachments: true + test1.com: + use-sigstore-attachments: true + test2.com: + use-sigstore-attachments: true +`) + + clusterScopePolicies, scopeNamespacePolicies, err := getValidScopeNamespacePolicies([]*apicfgv1alpha1.ClusterImagePolicy{&testImagePolicyCR0, &testImagePolicyCR1}, []*apicfgv1alpha1.ImagePolicy{&testImagePolicyCR2}, "release-reg.io/image/release", nil) + require.NoError(t, err) + got, err := generateSigstoreRegistriesdConfig(clusterScopePolicies, scopeNamespacePolicies) + require.NoError(t, err) + require.Equal(t, string(expectSigstoreRegistriesConfig), string(got)) +} diff --git a/pkg/daemon/constants/constants.go b/pkg/daemon/constants/constants.go index b77c1339de..aabea9707d 100644 --- a/pkg/daemon/constants/constants.go +++ b/pkg/daemon/constants/constants.go @@ -93,6 +93,12 @@ const ( // changes to registries.conf will cause a crio reload and require extra logic about whether to drain ContainerRegistryConfPath = "/etc/containers/registries.conf" + // changes to registries.d will cause a crio reload + SigstoreRegistriesConfigDir = "/etc/containers/registries.d" + + // changes to /etc/crio/policies will cause a crio reload + CrioPoliciesDir = "/etc/crio/policies" + // SSH Keys for user "core" will only be written at /home/core/.ssh CoreUserSSHPath = "/home/" + CoreUserName + "/.ssh" diff --git a/pkg/daemon/update.go b/pkg/daemon/update.go index 38433d608f..c8ae2baeeb 100644 --- a/pkg/daemon/update.go +++ b/pkg/daemon/update.go @@ -427,6 +427,10 @@ func calculatePostConfigChangeActionFromFileDiffs(diffFileSet []string) (actions filesPostConfigChangeActionRestartCrio := []string{ "/etc/pki/ca-trust/source/anchors/openshift-config-user-ca-bundle.crt", } + dirsPostConfigChangeActionReloadCrio := []string{ + constants.CrioPoliciesDir, + constants.SigstoreRegistriesConfigDir, + } actions = []string{postConfigChangeActionNone} for _, path := range diffFileSet { @@ -436,6 +440,8 @@ func calculatePostConfigChangeActionFromFileDiffs(diffFileSet []string) (actions actions = []string{postConfigChangeActionReloadCrio} } else if ctrlcommon.InSlice(path, filesPostConfigChangeActionRestartCrio) { actions = []string{postConfigChangeActionRestartCrio} + } else if ctrlcommon.InSlice(filepath.Dir(path), dirsPostConfigChangeActionReloadCrio) { + actions = []string{postConfigChangeActionReloadCrio} } else { actions = []string{postConfigChangeActionReboot} return diff --git a/test/e2e-bootstrap/bootstrap_test.go b/test/e2e-bootstrap/bootstrap_test.go index 11795b97fb..0cb9f2ca65 100644 --- a/test/e2e-bootstrap/bootstrap_test.go +++ b/test/e2e-bootstrap/bootstrap_test.go @@ -16,6 +16,7 @@ import ( "github.com/stretchr/testify/require" configv1 "github.com/openshift/api/config/v1" + configv1alpha1 "github.com/openshift/api/config/v1alpha1" mcfgv1 "github.com/openshift/api/machineconfiguration/v1" apioperatorsv1alpha1 "github.com/openshift/api/operator/v1alpha1" featuregatescontroller "github.com/openshift/cluster-config-operator/pkg/operator/featuregates" @@ -65,6 +66,7 @@ func TestE2EBootstrap(t *testing.T) { testEnv := framework.NewTestEnv(t) configv1.Install(scheme.Scheme) + configv1alpha1.Install(scheme.Scheme) mcfgv1.Install(scheme.Scheme) apioperatorsv1alpha1.Install(scheme.Scheme) @@ -486,6 +488,8 @@ func createControllers(ctx *ctrlcommon.ControllerContext) []ctrlcommon.Controlle ctx.ConfigInformerFactory.Config().V1().Images(), ctx.ConfigInformerFactory.Config().V1().ImageDigestMirrorSets(), ctx.ConfigInformerFactory.Config().V1().ImageTagMirrorSets(), + ctx.ConfigInformerFactory.Config().V1alpha1().ImagePolicies(), + ctx.ConfigInformerFactory.Config().V1alpha1().ClusterImagePolicies(), ctx.OperatorInformerFactory.Operator().V1alpha1().ImageContentSourcePolicies(), ctx.ConfigInformerFactory.Config().V1().ClusterVersions(), ctx.ClientBuilder.KubeClientOrDie("container-runtime-config-controller"), @@ -615,7 +619,7 @@ func loadBaseTestManifests(t *testing.T) []runtime.Object { func loadRawManifests(t *testing.T, rawObjs [][]byte) []runtime.Object { codecFactory := serializer.NewCodecFactory(scheme.Scheme) - decoder := codecFactory.UniversalDecoder(corev1GroupVersion, mcfgv1.GroupVersion, apioperatorsv1alpha1.GroupVersion, configv1.GroupVersion) + decoder := codecFactory.UniversalDecoder(corev1GroupVersion, mcfgv1.GroupVersion, apioperatorsv1alpha1.GroupVersion, configv1alpha1.GroupVersion, configv1.GroupVersion) objs := []runtime.Object{} for _, raw := range rawObjs { diff --git a/vendor/github.com/openshift/api/config/v1/0000_10_config-operator_01_network-Default.crd.yaml b/vendor/github.com/openshift/api/config/v1/0000_10_config-operator_01_network-Default.crd.yaml index 303fc63019..d71799f595 100644 --- a/vendor/github.com/openshift/api/config/v1/0000_10_config-operator_01_network-Default.crd.yaml +++ b/vendor/github.com/openshift/api/config/v1/0000_10_config-operator_01_network-Default.crd.yaml @@ -147,6 +147,81 @@ spec: clusterNetworkMTU: description: ClusterNetworkMTU is the MTU for inter-pod networking. type: integer + conditions: + description: 'conditions represents the observations of a network.config + current state. Known .status.conditions.type are: "NetworkTypeMigrationInProgress", + "NetworkTypeMigrationMTUReady", "NetworkTypeMigrationTargetCNIAvailable", + "NetworkTypeMigrationTargetCNIInUse" and "NetworkTypeMigrationOriginalCNIPurged"' + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map migration: description: Migration contains the cluster network migration configuration. properties: diff --git a/vendor/github.com/openshift/api/config/v1/stable.network.testsuite.yaml b/vendor/github.com/openshift/api/config/v1/stable.network.testsuite.yaml index 7922d44812..c85d122a65 100644 --- a/vendor/github.com/openshift/api/config/v1/stable.network.testsuite.yaml +++ b/vendor/github.com/openshift/api/config/v1/stable.network.testsuite.yaml @@ -12,7 +12,7 @@ tests: apiVersion: config.openshift.io/v1 kind: Network spec: {} - - name: Should not be able to set status conditions + - name: Should be able to set status conditions initial: | apiVersion: config.openshift.io/v1 kind: Network @@ -28,4 +28,10 @@ tests: apiVersion: config.openshift.io/v1 kind: Network spec: {} - status: {} + status: + conditions: + - type: NetworkTypeMigrationInProgress + status: "False" + reason: "Reason" + message: "Message" + lastTransitionTime: "2023-10-25T12:00:00Z" diff --git a/vendor/github.com/openshift/api/config/v1/types_network.go b/vendor/github.com/openshift/api/config/v1/types_network.go index 3d345b2d60..794f3db7b7 100644 --- a/vendor/github.com/openshift/api/config/v1/types_network.go +++ b/vendor/github.com/openshift/api/config/v1/types_network.go @@ -95,7 +95,6 @@ type NetworkStatus struct { // +patchStrategy=merge // +listType=map // +listMapKey=type - // +openshift:enable:FeatureSets=CustomNoUpgrade;TechPreviewNoUpgrade Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` } diff --git a/vendor/github.com/openshift/api/config/v1alpha1/0000_10_config-operator_01_clusterimagepolicy-CustomNoUpgrade.crd.yaml b/vendor/github.com/openshift/api/config/v1alpha1/0000_10_config-operator_01_clusterimagepolicy-CustomNoUpgrade.crd.yaml index 607b85698d..79c1767755 100644 --- a/vendor/github.com/openshift/api/config/v1alpha1/0000_10_config-operator_01_clusterimagepolicy-CustomNoUpgrade.crd.yaml +++ b/vendor/github.com/openshift/api/config/v1alpha1/0000_10_config-operator_01_clusterimagepolicy-CustomNoUpgrade.crd.yaml @@ -59,6 +59,7 @@ spec: description: fulcioCAData contains inline base64-encoded data for the PEM format fulcio CA. fulcioCAData must be at most 8192 characters. + format: byte maxLength: 8192 type: string fulcioSubject: diff --git a/vendor/github.com/openshift/api/config/v1alpha1/0000_10_config-operator_01_clusterimagepolicy-TechPreviewNoUpgrade.crd.yaml b/vendor/github.com/openshift/api/config/v1alpha1/0000_10_config-operator_01_clusterimagepolicy-TechPreviewNoUpgrade.crd.yaml index c5129d7b4a..538c44ace1 100644 --- a/vendor/github.com/openshift/api/config/v1alpha1/0000_10_config-operator_01_clusterimagepolicy-TechPreviewNoUpgrade.crd.yaml +++ b/vendor/github.com/openshift/api/config/v1alpha1/0000_10_config-operator_01_clusterimagepolicy-TechPreviewNoUpgrade.crd.yaml @@ -59,6 +59,7 @@ spec: description: fulcioCAData contains inline base64-encoded data for the PEM format fulcio CA. fulcioCAData must be at most 8192 characters. + format: byte maxLength: 8192 type: string fulcioSubject: diff --git a/vendor/github.com/openshift/api/config/v1alpha1/0000_10_config-operator_01_imagepolicy-CustomNoUpgrade.crd.yaml b/vendor/github.com/openshift/api/config/v1alpha1/0000_10_config-operator_01_imagepolicy-CustomNoUpgrade.crd.yaml index a94542da44..0d62e2cdae 100644 --- a/vendor/github.com/openshift/api/config/v1alpha1/0000_10_config-operator_01_imagepolicy-CustomNoUpgrade.crd.yaml +++ b/vendor/github.com/openshift/api/config/v1alpha1/0000_10_config-operator_01_imagepolicy-CustomNoUpgrade.crd.yaml @@ -59,6 +59,7 @@ spec: description: fulcioCAData contains inline base64-encoded data for the PEM format fulcio CA. fulcioCAData must be at most 8192 characters. + format: byte maxLength: 8192 type: string fulcioSubject: diff --git a/vendor/github.com/openshift/api/config/v1alpha1/0000_10_config-operator_01_imagepolicy-TechPreviewNoUpgrade.crd.yaml b/vendor/github.com/openshift/api/config/v1alpha1/0000_10_config-operator_01_imagepolicy-TechPreviewNoUpgrade.crd.yaml index 11f72b1724..3cb1164875 100644 --- a/vendor/github.com/openshift/api/config/v1alpha1/0000_10_config-operator_01_imagepolicy-TechPreviewNoUpgrade.crd.yaml +++ b/vendor/github.com/openshift/api/config/v1alpha1/0000_10_config-operator_01_imagepolicy-TechPreviewNoUpgrade.crd.yaml @@ -59,6 +59,7 @@ spec: description: fulcioCAData contains inline base64-encoded data for the PEM format fulcio CA. fulcioCAData must be at most 8192 characters. + format: byte maxLength: 8192 type: string fulcioSubject: diff --git a/vendor/github.com/openshift/api/config/v1alpha1/types_image_policy.go b/vendor/github.com/openshift/api/config/v1alpha1/types_image_policy.go index b93f17c5da..e9bc278636 100644 --- a/vendor/github.com/openshift/api/config/v1alpha1/types_image_policy.go +++ b/vendor/github.com/openshift/api/config/v1alpha1/types_image_policy.go @@ -110,7 +110,7 @@ type FulcioCAWithRekor struct { // fulcioCAData must be at most 8192 characters. // +kubebuilder:validation:Required // +kubebuilder:validation:MaxLength=8192 - FulcioCAData string `json:"fulcioCAData"` + FulcioCAData []byte `json:"fulcioCAData"` // rekorKeyData contains inline base64-encoded data for the PEM format from the Rekor public key. // rekorKeyData must be at most 8192 characters. // +kubebuilder:validation:Required diff --git a/vendor/github.com/openshift/api/config/v1alpha1/zz_generated.deepcopy.go b/vendor/github.com/openshift/api/config/v1alpha1/zz_generated.deepcopy.go index 21b08cf333..2060517364 100644 --- a/vendor/github.com/openshift/api/config/v1alpha1/zz_generated.deepcopy.go +++ b/vendor/github.com/openshift/api/config/v1alpha1/zz_generated.deepcopy.go @@ -230,6 +230,11 @@ func (in *EtcdBackupSpec) DeepCopy() *EtcdBackupSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FulcioCAWithRekor) DeepCopyInto(out *FulcioCAWithRekor) { *out = *in + if in.FulcioCAData != nil { + in, out := &in.FulcioCAData, &out.FulcioCAData + *out = make([]byte, len(*in)) + copy(*out, *in) + } out.FulcioSubject = in.FulcioSubject return } @@ -568,7 +573,7 @@ func (in *PolicyRootOfTrust) DeepCopyInto(out *PolicyRootOfTrust) { if in.FulcioCAWithRekor != nil { in, out := &in.FulcioCAWithRekor, &out.FulcioCAWithRekor *out = new(FulcioCAWithRekor) - **out = **in + (*in).DeepCopyInto(*out) } return } diff --git a/vendor/github.com/openshift/api/operator/v1/0000_90_cluster_csi_driver_01_config.crd.yaml b/vendor/github.com/openshift/api/operator/v1/0000_90_cluster_csi_driver_01_config.crd.yaml index 6911ce89c8..52f6f37ea5 100644 --- a/vendor/github.com/openshift/api/operator/v1/0000_90_cluster_csi_driver_01_config.crd.yaml +++ b/vendor/github.com/openshift/api/operator/v1/0000_90_cluster_csi_driver_01_config.crd.yaml @@ -54,6 +54,7 @@ spec: - vpc.block.csi.ibm.io - powervs.csi.ibm.com - secrets-store.csi.k8s.io + - smb.csi.k8s.io type: string type: object spec: diff --git a/vendor/github.com/openshift/api/operator/v1/0000_90_cluster_csi_driver_01_config.crd.yaml-patch b/vendor/github.com/openshift/api/operator/v1/0000_90_cluster_csi_driver_01_config.crd.yaml-patch index 2a02f97f2e..ce0db8be98 100644 --- a/vendor/github.com/openshift/api/operator/v1/0000_90_cluster_csi_driver_01_config.crd.yaml-patch +++ b/vendor/github.com/openshift/api/operator/v1/0000_90_cluster_csi_driver_01_config.crd.yaml-patch @@ -20,3 +20,4 @@ - vpc.block.csi.ibm.io - powervs.csi.ibm.com - secrets-store.csi.k8s.io + - smb.csi.k8s.io diff --git a/vendor/github.com/openshift/api/operator/v1/types_csi_cluster_driver.go b/vendor/github.com/openshift/api/operator/v1/types_csi_cluster_driver.go index 8e9853b06f..00a36015e3 100644 --- a/vendor/github.com/openshift/api/operator/v1/types_csi_cluster_driver.go +++ b/vendor/github.com/openshift/api/operator/v1/types_csi_cluster_driver.go @@ -84,6 +84,7 @@ const ( IBMVPCBlockCSIDriver CSIDriverName = "vpc.block.csi.ibm.io" IBMPowerVSBlockCSIDriver CSIDriverName = "powervs.csi.ibm.com" SecretsStoreCSIDriver CSIDriverName = "secrets-store.csi.k8s.io" + SambaCSIDriver CSIDriverName = "smb.csi.k8s.io" ) // ClusterCSIDriverSpec is the desired behavior of CSI driver operator diff --git a/vendor/github.com/openshift/client-go/config/applyconfigurations/config/v1alpha1/fulciocawithrekor.go b/vendor/github.com/openshift/client-go/config/applyconfigurations/config/v1alpha1/fulciocawithrekor.go index 6fe09c0eb4..5469397374 100644 --- a/vendor/github.com/openshift/client-go/config/applyconfigurations/config/v1alpha1/fulciocawithrekor.go +++ b/vendor/github.com/openshift/client-go/config/applyconfigurations/config/v1alpha1/fulciocawithrekor.go @@ -5,7 +5,7 @@ package v1alpha1 // FulcioCAWithRekorApplyConfiguration represents an declarative configuration of the FulcioCAWithRekor type for use // with apply. type FulcioCAWithRekorApplyConfiguration struct { - FulcioCAData *string `json:"fulcioCAData,omitempty"` + FulcioCAData []byte `json:"fulcioCAData,omitempty"` RekorKeyData *string `json:"rekorKeyData,omitempty"` FulcioSubject *PolicyFulcioSubjectApplyConfiguration `json:"fulcioSubject,omitempty"` } @@ -16,11 +16,13 @@ func FulcioCAWithRekor() *FulcioCAWithRekorApplyConfiguration { return &FulcioCAWithRekorApplyConfiguration{} } -// WithFulcioCAData sets the FulcioCAData field in the declarative configuration to the given value -// and returns the receiver, so that objects can be built by chaining "With" function invocations. -// If called multiple times, the FulcioCAData field is set to the value of the last call. -func (b *FulcioCAWithRekorApplyConfiguration) WithFulcioCAData(value string) *FulcioCAWithRekorApplyConfiguration { - b.FulcioCAData = &value +// WithFulcioCAData adds the given value to the FulcioCAData field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the FulcioCAData field. +func (b *FulcioCAWithRekorApplyConfiguration) WithFulcioCAData(values ...byte) *FulcioCAWithRekorApplyConfiguration { + for i := range values { + b.FulcioCAData = append(b.FulcioCAData, values[i]) + } return b } diff --git a/vendor/github.com/openshift/client-go/config/applyconfigurations/internal/internal.go b/vendor/github.com/openshift/client-go/config/applyconfigurations/internal/internal.go index 328e4df340..da3007f6b5 100644 --- a/vendor/github.com/openshift/client-go/config/applyconfigurations/internal/internal.go +++ b/vendor/github.com/openshift/client-go/config/applyconfigurations/internal/internal.go @@ -3619,7 +3619,6 @@ var schemaYAML = typed.YAMLObject(`types: - name: fulcioCAData type: scalar: string - default: "" - name: fulcioSubject type: namedType: com.github.openshift.api.config.v1alpha1.PolicyFulcioSubject diff --git a/vendor/github.com/openshift/client-go/machine/applyconfigurations/internal/internal.go b/vendor/github.com/openshift/client-go/machine/applyconfigurations/internal/internal.go index cdbebac7c3..edbc7323bf 100644 --- a/vendor/github.com/openshift/client-go/machine/applyconfigurations/internal/internal.go +++ b/vendor/github.com/openshift/client-go/machine/applyconfigurations/internal/internal.go @@ -238,7 +238,9 @@ var schemaYAML = typed.YAMLObject(`types: list: elementType: namedType: com.github.openshift.api.machine.v1.VSphereFailureDomain - elementRelationship: atomic + elementRelationship: associative + keys: + - name unions: - discriminator: platform fields: diff --git a/vendor/github.com/openshift/client-go/operator/applyconfigurations/internal/internal.go b/vendor/github.com/openshift/client-go/operator/applyconfigurations/internal/internal.go index 067f820eda..900c47c231 100644 --- a/vendor/github.com/openshift/client-go/operator/applyconfigurations/internal/internal.go +++ b/vendor/github.com/openshift/client-go/operator/applyconfigurations/internal/internal.go @@ -1398,16 +1398,10 @@ var schemaYAML = typed.YAMLObject(`types: elementRelationship: atomic - name: com.github.openshift.api.operator.v1.IPsecConfig map: - elementType: - scalar: untyped - list: - elementType: - namedType: __untyped_atomic_ - elementRelationship: atomic - map: - elementType: - namedType: __untyped_deduced_ - elementRelationship: separable + fields: + - name: mode + type: + scalar: string - name: com.github.openshift.api.operator.v1.IPv4GatewayConfig map: fields: @@ -2506,6 +2500,8 @@ var schemaYAML = typed.YAMLObject(`types: - name: ipsecConfig type: namedType: com.github.openshift.api.operator.v1.IPsecConfig + default: + mode: Disabled - name: mtu type: scalar: numeric diff --git a/vendor/github.com/openshift/client-go/operator/applyconfigurations/operator/v1/ipsecconfig.go b/vendor/github.com/openshift/client-go/operator/applyconfigurations/operator/v1/ipsecconfig.go new file mode 100644 index 0000000000..864010dffc --- /dev/null +++ b/vendor/github.com/openshift/client-go/operator/applyconfigurations/operator/v1/ipsecconfig.go @@ -0,0 +1,27 @@ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1 + +import ( + v1 "github.com/openshift/api/operator/v1" +) + +// IPsecConfigApplyConfiguration represents an declarative configuration of the IPsecConfig type for use +// with apply. +type IPsecConfigApplyConfiguration struct { + Mode *v1.IPsecMode `json:"mode,omitempty"` +} + +// IPsecConfigApplyConfiguration constructs an declarative configuration of the IPsecConfig type for use with +// apply. +func IPsecConfig() *IPsecConfigApplyConfiguration { + return &IPsecConfigApplyConfiguration{} +} + +// WithMode sets the Mode field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Mode field is set to the value of the last call. +func (b *IPsecConfigApplyConfiguration) WithMode(value v1.IPsecMode) *IPsecConfigApplyConfiguration { + b.Mode = &value + return b +} diff --git a/vendor/github.com/openshift/client-go/operator/applyconfigurations/operator/v1/ovnkubernetesconfig.go b/vendor/github.com/openshift/client-go/operator/applyconfigurations/operator/v1/ovnkubernetesconfig.go index 6b5f7a4781..61701f52c6 100644 --- a/vendor/github.com/openshift/client-go/operator/applyconfigurations/operator/v1/ovnkubernetesconfig.go +++ b/vendor/github.com/openshift/client-go/operator/applyconfigurations/operator/v1/ovnkubernetesconfig.go @@ -2,17 +2,13 @@ package v1 -import ( - operatorv1 "github.com/openshift/api/operator/v1" -) - // OVNKubernetesConfigApplyConfiguration represents an declarative configuration of the OVNKubernetesConfig type for use // with apply. type OVNKubernetesConfigApplyConfiguration struct { MTU *uint32 `json:"mtu,omitempty"` GenevePort *uint32 `json:"genevePort,omitempty"` HybridOverlayConfig *HybridOverlayConfigApplyConfiguration `json:"hybridOverlayConfig,omitempty"` - IPsecConfig *operatorv1.IPsecConfig `json:"ipsecConfig,omitempty"` + IPsecConfig *IPsecConfigApplyConfiguration `json:"ipsecConfig,omitempty"` PolicyAuditConfig *PolicyAuditConfigApplyConfiguration `json:"policyAuditConfig,omitempty"` GatewayConfig *GatewayConfigApplyConfiguration `json:"gatewayConfig,omitempty"` V4InternalSubnet *string `json:"v4InternalSubnet,omitempty"` @@ -53,8 +49,8 @@ func (b *OVNKubernetesConfigApplyConfiguration) WithHybridOverlayConfig(value *H // WithIPsecConfig sets the IPsecConfig field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the IPsecConfig field is set to the value of the last call. -func (b *OVNKubernetesConfigApplyConfiguration) WithIPsecConfig(value operatorv1.IPsecConfig) *OVNKubernetesConfigApplyConfiguration { - b.IPsecConfig = &value +func (b *OVNKubernetesConfigApplyConfiguration) WithIPsecConfig(value *IPsecConfigApplyConfiguration) *OVNKubernetesConfigApplyConfiguration { + b.IPsecConfig = value return b } diff --git a/vendor/k8s.io/code-generator/generate-groups.sh b/vendor/k8s.io/code-generator/generate-groups.sh old mode 100755 new mode 100644 diff --git a/vendor/k8s.io/code-generator/generate-internal-groups.sh b/vendor/k8s.io/code-generator/generate-internal-groups.sh old mode 100755 new mode 100644 diff --git a/vendor/modules.txt b/vendor/modules.txt index d8d80ed7b4..80fe9319a5 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -815,7 +815,7 @@ github.com/opencontainers/runc/libcontainer/user # github.com/opencontainers/runtime-spec v1.1.0 ## explicit github.com/opencontainers/runtime-spec/specs-go -# github.com/openshift/api v0.0.0-20240205144533-7162acc29bb6 +# github.com/openshift/api v0.0.0-20240124164020-e2ce40831f2e => github.com/QiWang19/api v0.0.0-20240210054700-a95bb144f44f ## explicit; go 1.21 github.com/openshift/api github.com/openshift/api/apiserver @@ -888,7 +888,7 @@ github.com/openshift/api/template github.com/openshift/api/template/v1 github.com/openshift/api/user github.com/openshift/api/user/v1 -# github.com/openshift/client-go v0.0.0-20240104132419-223261fd8630 +# github.com/openshift/client-go v0.0.0-20240104132419-223261fd8630 => github.com/QiWang19/client-go v0.0.0-20240210061104-d13d84b73765 ## explicit; go 1.21 github.com/openshift/client-go/build/applyconfigurations/build/v1 github.com/openshift/client-go/build/applyconfigurations/internal @@ -2288,3 +2288,5 @@ sigs.k8s.io/structured-merge-diff/v4/value sigs.k8s.io/yaml sigs.k8s.io/yaml/goyaml.v2 # k8s.io/kube-openapi => github.com/openshift/kube-openapi v0.0.0-20230816122517-ffc8f001abb0 +# github.com/openshift/api => github.com/QiWang19/api v0.0.0-20240210054700-a95bb144f44f +# github.com/openshift/client-go => github.com/QiWang19/client-go v0.0.0-20240210061104-d13d84b73765