From b215e396a513881da960ca1731bf8ad36355466f Mon Sep 17 00:00:00 2001 From: openshift-app-platform-shift-bot <267347085+openshift-app-platform-shift-bot@users.noreply.github.com> Date: Fri, 8 May 2026 12:46:39 +0000 Subject: [PATCH 1/3] CM-830: Add TrustManager API types for trust-manager operand management Introduces the TrustManager CRD (trustmanagers.operator.openshift.io/v1alpha1) as specified in the enhancement proposal (EP-1914). This cluster-scoped singleton resource enables day-2 installation and configuration of the trust-manager operand. Key additions: - TrustManager types with spec fields: logLevel, logFormat, trustNamespace, secretTargets, filterExpiredCertificates, defaultCAPackage, resources, affinity, tolerations, nodeSelector - SecretTargetsConfig with Custom/Disabled policy and authorizedSecrets - DefaultCAPackageConfig for OpenShift trusted CA bundle integration - FeatureTrustManager feature gate (Alpha, default disabled) - CRD manifest, deepcopy, client/lister/informer generation - Integration tests for CRD validation (status defaults, singleton, scope, immutability) - YAML test suite covering create/update validation scenarios Co-Authored-By: Claude Opus 4.6 --- api/operator/v1alpha1/features.go | 13 +- .../trustmanager.testsuite.yaml | 697 +++++++++ api/operator/v1alpha1/trustmanager_types.go | 267 ++++ .../v1alpha1/trustmanager_types_test.go | 177 +++ .../v1alpha1/zz_generated.deepcopy.go | 193 +++ .../operator.openshift.io_trustmanagers.yaml | 1327 +++++++++++++++++ .../applyconfigurations/internal/internal.go | 10 + .../v1alpha1/defaultcapackageconfig.go | 27 + .../operator/v1alpha1/secrettargetsconfig.go | 38 + .../operator/v1alpha1/trustmanager.go | 246 +++ .../operator/v1alpha1/trustmanagerconfig.go | 117 ++ .../v1alpha1/trustmanagercontrollerconfig.go | 44 + .../operator/v1alpha1/trustmanagerspec.go | 32 + .../operator/v1alpha1/trustmanagerstatus.go | 78 + pkg/operator/applyconfigurations/utils.go | 14 + .../v1alpha1/fake/fake_operator_client.go | 4 + .../v1alpha1/fake/fake_trustmanager.go | 37 + .../operator/v1alpha1/generated_expansion.go | 2 + .../operator/v1alpha1/operator_client.go | 5 + .../typed/operator/v1alpha1/trustmanager.go | 58 + .../informers/externalversions/generic.go | 2 + .../operator/v1alpha1/interface.go | 7 + .../operator/v1alpha1/trustmanager.go | 85 ++ .../operator/v1alpha1/expansion_generated.go | 4 + .../listers/operator/v1alpha1/trustmanager.go | 32 + 25 files changed, 3515 insertions(+), 1 deletion(-) create mode 100644 api/operator/v1alpha1/tests/trustmanagers.operator.openshift.io/trustmanager.testsuite.yaml create mode 100644 api/operator/v1alpha1/trustmanager_types.go create mode 100644 api/operator/v1alpha1/trustmanager_types_test.go create mode 100644 config/crd/bases/operator.openshift.io_trustmanagers.yaml create mode 100644 pkg/operator/applyconfigurations/operator/v1alpha1/defaultcapackageconfig.go create mode 100644 pkg/operator/applyconfigurations/operator/v1alpha1/secrettargetsconfig.go create mode 100644 pkg/operator/applyconfigurations/operator/v1alpha1/trustmanager.go create mode 100644 pkg/operator/applyconfigurations/operator/v1alpha1/trustmanagerconfig.go create mode 100644 pkg/operator/applyconfigurations/operator/v1alpha1/trustmanagercontrollerconfig.go create mode 100644 pkg/operator/applyconfigurations/operator/v1alpha1/trustmanagerspec.go create mode 100644 pkg/operator/applyconfigurations/operator/v1alpha1/trustmanagerstatus.go create mode 100644 pkg/operator/clientset/versioned/typed/operator/v1alpha1/fake/fake_trustmanager.go create mode 100644 pkg/operator/clientset/versioned/typed/operator/v1alpha1/trustmanager.go create mode 100644 pkg/operator/informers/externalversions/operator/v1alpha1/trustmanager.go create mode 100644 pkg/operator/listers/operator/v1alpha1/trustmanager.go diff --git a/api/operator/v1alpha1/features.go b/api/operator/v1alpha1/features.go index d28e35132..5e74f661f 100644 --- a/api/operator/v1alpha1/features.go +++ b/api/operator/v1alpha1/features.go @@ -13,8 +13,19 @@ var ( // For more details, // https://github.com/openshift/enhancements/blob/master/enhancements/cert-manager/istio-csr-controller.md FeatureIstioCSR featuregate.Feature = "IstioCSR" + + // FeatureTrustManager enables the controller for trustmanagers.operator.openshift.io resource, + // which extends cert-manager-operator to deploy and manage the trust-manager operand. + // trust-manager provides a way to manage trust bundles in Kubernetes and OpenShift + // clusters by combining trusted certificate sources into bundles that applications + // can trust directly. + // + // For more details, + // https://github.com/openshift/enhancements/blob/master/enhancements/cert-manager/trust-manager-controller.md + FeatureTrustManager featuregate.Feature = "TrustManager" ) var OperatorFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{ - FeatureIstioCSR: {Default: true, PreRelease: featuregate.GA}, + FeatureIstioCSR: {Default: true, PreRelease: featuregate.GA}, + FeatureTrustManager: {Default: false, PreRelease: featuregate.Alpha}, } diff --git a/api/operator/v1alpha1/tests/trustmanagers.operator.openshift.io/trustmanager.testsuite.yaml b/api/operator/v1alpha1/tests/trustmanagers.operator.openshift.io/trustmanager.testsuite.yaml new file mode 100644 index 000000000..924905bd1 --- /dev/null +++ b/api/operator/v1alpha1/tests/trustmanagers.operator.openshift.io/trustmanager.testsuite.yaml @@ -0,0 +1,697 @@ +apiVersion: apiextensions.k8s.io/v1 +name: TrustManager +crdName: trustmanagers.operator.openshift.io +tests: + onCreate: + - name: Should be able to create a minimal TrustManager with only required fields + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: {} + expected: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + logLevel: 1 + logFormat: "text" + trustNamespace: "cert-manager" + filterExpiredCertificates: "Disabled" + secretTargets: + policy: "Disabled" + defaultCAPackage: + policy: "Disabled" + + - name: Should reject TrustManager with name other than cluster + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: not-cluster + spec: + trustManagerConfig: {} + expectedError: "TrustManager is a singleton, .metadata.name must be 'cluster'" + + - name: Should accept logFormat text + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + logFormat: "text" + expected: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + logFormat: "text" + + - name: Should accept logFormat json + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + logFormat: "json" + expected: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + logFormat: "json" + + - name: Should reject invalid logFormat value + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + logFormat: "xml" + expectedError: "Unsupported value" + + - name: Should accept logLevel at minimum boundary (1) + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + logLevel: 1 + expected: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + logLevel: 1 + + - name: Should accept logLevel at maximum boundary (5) + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + logLevel: 5 + expected: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + logLevel: 5 + + - name: Should reject logLevel below minimum (0) + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + logLevel: 0 + expectedError: "should be greater than or equal to 1" + + - name: Should reject logLevel above maximum (6) + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + logLevel: 6 + expectedError: "should be less than or equal to 5" + + - name: Should accept valid trustNamespace + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + trustNamespace: "my-trust-ns" + expected: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + trustNamespace: "my-trust-ns" + + - name: Should reject trustNamespace exceeding maxLength + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + trustNamespace: "this-is-a-very-long-namespace-name-that-exceeds-the-maximum-length-allowed" + expectedError: "should be at most 63" + + - name: Should accept filterExpiredCertificates Enabled + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + filterExpiredCertificates: "Enabled" + expected: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + filterExpiredCertificates: "Enabled" + + - name: Should accept filterExpiredCertificates Disabled + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + filterExpiredCertificates: "Disabled" + expected: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + filterExpiredCertificates: "Disabled" + + - name: Should reject invalid filterExpiredCertificates value + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + filterExpiredCertificates: "Invalid" + expectedError: "Unsupported value" + + - name: Should accept secretTargets policy Disabled + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + secretTargets: + policy: "Disabled" + expected: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + secretTargets: + policy: "Disabled" + + - name: Should accept secretTargets policy Custom with authorizedSecrets + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + secretTargets: + policy: "Custom" + authorizedSecrets: + - "my-secret" + expected: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + secretTargets: + policy: "Custom" + authorizedSecrets: + - "my-secret" + + - name: Should reject invalid secretTargets policy + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + secretTargets: + policy: "Invalid" + expectedError: "Unsupported value" + + - name: Should reject secretTargets policy Custom without authorizedSecrets + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + secretTargets: + policy: "Custom" + expectedError: "authorizedSecrets must not be empty when policy is Custom" + + - name: Should reject secretTargets policy Custom with empty authorizedSecrets + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + secretTargets: + policy: "Custom" + authorizedSecrets: [] + expectedError: "authorizedSecrets must not be empty when policy is Custom" + + - name: Should reject authorizedSecrets when policy is not Custom + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + secretTargets: + policy: "Disabled" + authorizedSecrets: + - "my-secret" + expectedError: "authorizedSecrets must be empty when policy is not Custom" + + - name: Should accept defaultCAPackage policy Enabled + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + defaultCAPackage: + policy: "Enabled" + expected: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + defaultCAPackage: + policy: "Enabled" + + - name: Should accept defaultCAPackage policy Disabled + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + defaultCAPackage: + policy: "Disabled" + expected: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + defaultCAPackage: + policy: "Disabled" + + - name: Should reject invalid defaultCAPackage policy value + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + defaultCAPackage: + policy: "Invalid" + expectedError: "Unsupported value" + + - name: Should accept tolerations + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + tolerations: + - key: "example-key" + operator: "Exists" + effect: "NoSchedule" + expected: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + tolerations: + - key: "example-key" + operator: "Exists" + effect: "NoSchedule" + + - name: Should accept nodeSelector + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + nodeSelector: + kubernetes.io/os: linux + expected: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + nodeSelector: + kubernetes.io/os: linux + + - name: Should accept controllerConfig with labels and annotations + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: {} + controllerConfig: + labels: + custom-label: value + annotations: + custom-annotation: value + expected: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: {} + controllerConfig: + labels: + custom-label: value + annotations: + custom-annotation: value + + - name: Should accept full configuration + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + logLevel: 3 + logFormat: "json" + trustNamespace: "custom-trust-ns" + filterExpiredCertificates: "Enabled" + secretTargets: + policy: "Custom" + authorizedSecrets: + - "secret-a" + - "secret-b" + defaultCAPackage: + policy: "Enabled" + nodeSelector: + kubernetes.io/os: linux + controllerConfig: + labels: + env: prod + annotations: + note: test + expected: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + logLevel: 3 + logFormat: "json" + trustNamespace: "custom-trust-ns" + filterExpiredCertificates: "Enabled" + secretTargets: + policy: "Custom" + authorizedSecrets: + - "secret-a" + - "secret-b" + defaultCAPackage: + policy: "Enabled" + nodeSelector: + kubernetes.io/os: linux + controllerConfig: + labels: + env: prod + annotations: + note: test + + - name: Should accept secretTargets with multiple authorizedSecrets + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + secretTargets: + policy: "Custom" + authorizedSecrets: + - "secret-one" + - "secret-two" + - "secret-three" + expected: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + secretTargets: + policy: "Custom" + authorizedSecrets: + - "secret-one" + - "secret-two" + - "secret-three" + + onUpdate: + - name: Should not allow changing immutable trustNamespace + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + trustNamespace: "original-ns" + updated: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + trustNamespace: "changed-ns" + expectedError: "trustNamespace is immutable once set" + + - name: Should allow updating logLevel + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + logLevel: 1 + updated: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + logLevel: 3 + expected: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + logLevel: 3 + + - name: Should allow updating logFormat + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + logFormat: "text" + updated: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + logFormat: "json" + expected: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + logFormat: "json" + + - name: Should allow updating filterExpiredCertificates + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + filterExpiredCertificates: "Disabled" + updated: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + filterExpiredCertificates: "Enabled" + expected: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + filterExpiredCertificates: "Enabled" + + - name: Should allow updating secretTargets from Disabled to Custom + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + secretTargets: + policy: "Disabled" + updated: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + secretTargets: + policy: "Custom" + authorizedSecrets: + - "new-secret" + expected: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + secretTargets: + policy: "Custom" + authorizedSecrets: + - "new-secret" + + - name: Should allow updating defaultCAPackage policy + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + defaultCAPackage: + policy: "Disabled" + updated: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + defaultCAPackage: + policy: "Enabled" + expected: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + defaultCAPackage: + policy: "Enabled" + + - name: Should reject updating secretTargets to Custom without authorizedSecrets + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + secretTargets: + policy: "Disabled" + updated: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + secretTargets: + policy: "Custom" + expectedError: "authorizedSecrets must not be empty when policy is Custom" diff --git a/api/operator/v1alpha1/trustmanager_types.go b/api/operator/v1alpha1/trustmanager_types.go new file mode 100644 index 000000000..a44e06ace --- /dev/null +++ b/api/operator/v1alpha1/trustmanager_types.go @@ -0,0 +1,267 @@ +package v1alpha1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:object:root=true + +// TrustManagerList is a list of TrustManager objects. +type TrustManagerList struct { + metav1.TypeMeta `json:",inline"` + + // metadata is the standard list's metadata. + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + metav1.ListMeta `json:"metadata"` + Items []TrustManager `json:"items"` +} + +// +genclient +// +genclient:nonNamespaced +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:path=trustmanagers,scope=Cluster,categories={cert-manager-operator} +// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].status" +// +kubebuilder:printcolumn:name="Message",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].message" +// +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp" +// +kubebuilder:metadata:labels={"app.kubernetes.io/name=trustmanager", "app.kubernetes.io/part-of=cert-manager-operator"} + +// TrustManager describes the configuration and information about the managed trust-manager deployment. +// The name must be `cluster` to make TrustManager a singleton, allowing only one instance per cluster. +// +// When a TrustManager is created, trust-manager is deployed in the cert-manager namespace. +// +// +kubebuilder:validation:XValidation:rule="self.metadata.name == 'cluster'",message="TrustManager is a singleton, .metadata.name must be 'cluster'" +// +operator-sdk:csv:customresourcedefinitions:displayName="TrustManager" +type TrustManager struct { + metav1.TypeMeta `json:",inline"` + + // metadata is the standard object's metadata. + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + metav1.ObjectMeta `json:"metadata,omitempty"` + + // spec is the specification of the desired behavior of the TrustManager. + // +kubebuilder:validation:Required + // +required + Spec TrustManagerSpec `json:"spec"` + + // status is the most recently observed status of the TrustManager. + // +kubebuilder:validation:Optional + // +optional + Status TrustManagerStatus `json:"status,omitempty"` +} + +// TrustManagerSpec defines the desired state of TrustManager. +// Note: trust-manager operand is always deployed in the cert-manager namespace. +type TrustManagerSpec struct { + // trustManagerConfig configures the trust-manager operand's behavior. + // +kubebuilder:validation:Required + // +required + TrustManagerConfig TrustManagerConfig `json:"trustManagerConfig"` + + // controllerConfig configures the operator's behavior for resource creation. + // +kubebuilder:validation:Optional + // +optional + ControllerConfig TrustManagerControllerConfig `json:"controllerConfig,omitempty"` +} + +// TrustManagerConfig configures the trust-manager operand's behavior. +type TrustManagerConfig struct { + // logLevel configures the verbosity of trust-manager logging. + // Follows Kubernetes logging guidelines: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/logging.md#what-method-to-use + // +kubebuilder:default:=1 + // +kubebuilder:validation:Minimum:=1 + // +kubebuilder:validation:Maximum:=5 + // +kubebuilder:validation:Optional + // +optional + LogLevel int32 `json:"logLevel,omitempty"` + + // logFormat specifies the output format for trust-manager logging. + // Supported formats are "text" and "json". + // +kubebuilder:validation:Enum:="text";"json" + // +kubebuilder:default:="text" + // +kubebuilder:validation:Optional + // +optional + LogFormat string `json:"logFormat,omitempty"` + + // trustNamespace is the namespace where trust-manager looks for trust sources + // (ConfigMaps and Secrets containing CA certificates). + // Defaults to "cert-manager" if not specified. + // This field is immutable once set. + // This field can have a maximum of 63 characters. + // +kubebuilder:default:="cert-manager" + // +kubebuilder:validation:MinLength:=1 + // +kubebuilder:validation:MaxLength:=63 + // +kubebuilder:validation:XValidation:rule="oldSelf == '' || self == oldSelf",message="trustNamespace is immutable once set" + // +kubebuilder:validation:Optional + // +optional + TrustNamespace string `json:"trustNamespace,omitempty"` + + // secretTargets configures whether trust-manager can write trust bundles to Secrets. + // +kubebuilder:validation:Optional + // +optional + SecretTargets SecretTargetsConfig `json:"secretTargets,omitempty"` + + // filterExpiredCertificates controls whether trust-manager filters out + // expired certificates from trust bundles before distributing them. + // When set to "Enabled", expired certificates are removed from bundles. + // When set to "Disabled", expired certificates are included (default behavior). + // +kubebuilder:default:="Disabled" + // +kubebuilder:validation:Optional + // +optional + FilterExpiredCertificates FilterExpiredCertificatesPolicy `json:"filterExpiredCertificates,omitempty"` + + // defaultCAPackage configures the default CA package for trust-manager. + // When enabled, the operator will use OpenShift's trusted CA bundle injection mechanism. + // +kubebuilder:validation:Optional + // +optional + DefaultCAPackage DefaultCAPackageConfig `json:"defaultCAPackage,omitempty"` + + // resources defines the compute resource requirements for the trust-manager pod. + // ref: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + // +kubebuilder:validation:Optional + // +optional + Resources corev1.ResourceRequirements `json:"resources,omitempty"` + + // affinity defines scheduling constraints for the trust-manager pod. + // ref: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/ + // +kubebuilder:validation:Optional + // +optional + Affinity *corev1.Affinity `json:"affinity,omitempty"` + + // tolerations allows the trust-manager pod to be scheduled on tainted nodes. + // This field can have a maximum of 50 entries. + // ref: https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/ + // +listType=atomic + // +kubebuilder:validation:MinItems:=0 + // +kubebuilder:validation:MaxItems:=50 + // +kubebuilder:validation:Optional + // +optional + Tolerations []corev1.Toleration `json:"tolerations,omitempty"` + + // nodeSelector restricts which nodes the trust-manager pod can be scheduled on. + // This field can have a maximum of 50 entries. + // ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/ + // +mapType=atomic + // +kubebuilder:validation:MinProperties:=0 + // +kubebuilder:validation:MaxProperties:=50 + // +kubebuilder:validation:Optional + // +optional + NodeSelector map[string]string `json:"nodeSelector,omitempty"` +} + +// SecretTargetsConfig configures whether and how trust-manager can write +// trust bundles to Secrets. +// +// +kubebuilder:validation:XValidation:rule="self.policy != 'Custom' || (has(self.authorizedSecrets) && size(self.authorizedSecrets) > 0)",message="authorizedSecrets must not be empty when policy is Custom" +// +kubebuilder:validation:XValidation:rule="self.policy == 'Custom' || !has(self.authorizedSecrets) || size(self.authorizedSecrets) == 0",message="authorizedSecrets must be empty when policy is not Custom" +type SecretTargetsConfig struct { + // policy controls whether and how trust-manager can write trust bundles to Secrets. + // Allowed values are "Disabled" or "Custom". + // "Disabled" means trust-manager cannot write trust bundles to Secrets (default behavior). + // "Custom" grants trust-manager permission to create and update only the secrets listed in authorizedSecrets. + // +kubebuilder:default:="Disabled" + // +kubebuilder:validation:Optional + // +optional + Policy SecretTargetsPolicy `json:"policy,omitempty"` + + // authorizedSecrets is a list of specific secret names that trust-manager + // is authorized to create and update. This field is only valid when policy is "Custom". + // +listType=set + // +kubebuilder:validation:MinItems:=0 + // +kubebuilder:validation:items:MinLength:=1 + // +kubebuilder:validation:Optional + // +optional + AuthorizedSecrets []string `json:"authorizedSecrets,omitempty"` +} + +// DefaultCAPackageConfig configures the default CA package feature for trust-manager. +type DefaultCAPackageConfig struct { + // policy controls whether the default CA package feature is enabled. + // When set to "Enabled", the operator will inject OpenShift's trusted CA bundle + // into trust-manager, enabling the "useDefaultCAs: true" source in Bundle resources. + // When set to "Disabled", no default CA package is configured and Bundles cannot use useDefaultCAs (default behavior). + // +kubebuilder:default:="Disabled" + // +kubebuilder:validation:Optional + // +optional + Policy DefaultCAPackagePolicy `json:"policy,omitempty"` +} + +// TrustManagerControllerConfig configures the operator's behavior for +// creating trust-manager resources. +type TrustManagerControllerConfig struct { + // labels to apply to all resources created for the trust-manager deployment. + // +mapType=granular + // +kubebuilder:validation:MinProperties:=0 + // +kubebuilder:validation:Optional + // +optional + Labels map[string]string `json:"labels,omitempty"` + + // annotations to apply to all resources created for the trust-manager deployment. + // +mapType=granular + // +kubebuilder:validation:MinProperties:=0 + // +kubebuilder:validation:Optional + // +optional + Annotations map[string]string `json:"annotations,omitempty"` +} + +// FilterExpiredCertificatesPolicy defines the policy for filtering expired certificates. +// +kubebuilder:validation:Enum:=Enabled;Disabled +type FilterExpiredCertificatesPolicy string + +const ( + // FilterExpiredCertificatesPolicyEnabled filters out expired certificates from bundles. + FilterExpiredCertificatesPolicyEnabled FilterExpiredCertificatesPolicy = "Enabled" + // FilterExpiredCertificatesPolicyDisabled includes expired certificates in bundles. + FilterExpiredCertificatesPolicyDisabled FilterExpiredCertificatesPolicy = "Disabled" +) + +// SecretTargetsPolicy defines the policy for writing trust bundles to Secrets. +// +kubebuilder:validation:Enum:=Disabled;Custom +type SecretTargetsPolicy string + +const ( + // SecretTargetsPolicyDisabled means trust-manager cannot write trust bundles to Secrets. + SecretTargetsPolicyDisabled SecretTargetsPolicy = "Disabled" + // SecretTargetsPolicyCustom grants trust-manager permission to write to specific secrets only. + SecretTargetsPolicyCustom SecretTargetsPolicy = "Custom" +) + +// DefaultCAPackagePolicy defines the policy for the default CA package feature. +// +kubebuilder:validation:Enum:=Enabled;Disabled +type DefaultCAPackagePolicy string + +const ( + // DefaultCAPackagePolicyEnabled enables the default CA package feature. + DefaultCAPackagePolicyEnabled DefaultCAPackagePolicy = "Enabled" + // DefaultCAPackagePolicyDisabled disables the default CA package feature. + DefaultCAPackagePolicyDisabled DefaultCAPackagePolicy = "Disabled" +) + +// TrustManagerStatus defines the observed state of TrustManager. +type TrustManagerStatus struct { + // conditions holds information about the current state of the trust-manager deployment. + ConditionalStatus `json:",inline,omitempty"` + + // trustManagerImage is the container image (name:tag) used for trust-manager. + TrustManagerImage string `json:"trustManagerImage,omitempty"` + + // trustNamespace is the namespace where trust-manager looks for trust sources. + TrustNamespace string `json:"trustNamespace,omitempty"` + + // secretTargetsPolicy indicates the current secret targets policy. + SecretTargetsPolicy SecretTargetsPolicy `json:"secretTargetsPolicy,omitempty"` + + // defaultCAPackagePolicy indicates the current default CA package policy. + DefaultCAPackagePolicy DefaultCAPackagePolicy `json:"defaultCAPackagePolicy,omitempty"` + + // filterExpiredCertificatesPolicy indicates the current policy for filtering expired certificates. + FilterExpiredCertificatesPolicy FilterExpiredCertificatesPolicy `json:"filterExpiredCertificatesPolicy,omitempty"` +} + +func init() { + SchemeBuilder.Register(&TrustManager{}, &TrustManagerList{}) +} diff --git a/api/operator/v1alpha1/trustmanager_types_test.go b/api/operator/v1alpha1/trustmanager_types_test.go new file mode 100644 index 000000000..ae7314a95 --- /dev/null +++ b/api/operator/v1alpha1/trustmanager_types_test.go @@ -0,0 +1,177 @@ +package v1alpha1 + +import ( + "os" + "path" + "testing" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + + "sigs.k8s.io/yaml" +) + +const ( + trustmanagerCRDFile = "operator.openshift.io_trustmanagers.yaml" + trustmanagerCRDFilePath = "../../../config/crd/bases" +) + +// TestTrustManagerStatusDefault verifies that the trustmanager CR status does not have default value +// The admission code is expecting that the trustmanager status +// field will not have a default value. +// It allows separating between clean installation and the roll-back to the previous version of the cluster +func TestTrustManagerStatusDefault(t *testing.T) { + filepath := path.Join(trustmanagerCRDFilePath, trustmanagerCRDFile) + trustmanagerCRDBytes, err := os.ReadFile(filepath) + if err != nil { + t.Fatalf("failed to read trustmanager CRD file %q: %v", filepath, err) + } + + var trustmanagerCRD map[string]interface{} + if err := yaml.Unmarshal(trustmanagerCRDBytes, &trustmanagerCRD); err != nil { + t.Fatalf("failed to unmarshal trustmanager CRD: %v", err) + } + trustmanagerCRDSpec := trustmanagerCRD["spec"].(map[string]interface{}) + trustmanagerCRDVersions := trustmanagerCRDSpec["versions"].([]interface{}) + for _, v := range trustmanagerCRDVersions { + trustmanagerCRDVersion := v.(map[string]interface{}) + status, exists, err := unstructured.NestedMap(trustmanagerCRDVersion, "schema", "openAPIV3Schema", "properties", "status") + if err != nil { + t.Fatalf("failed to get nested map: %v", err) + } + + if !exists { + t.Fatalf("one of fields does not exist under the CRD") + } + + if _, ok := status["default"]; ok { + t.Fatalf("expected no default for the trustmanager CRD status") + } + } +} + +// TestTrustManagerCRDSingleton verifies that the trustmanager CRD has the singleton validation rule +func TestTrustManagerCRDSingleton(t *testing.T) { + filepath := path.Join(trustmanagerCRDFilePath, trustmanagerCRDFile) + trustmanagerCRDBytes, err := os.ReadFile(filepath) + if err != nil { + t.Fatalf("failed to read trustmanager CRD file %q: %v", filepath, err) + } + + var trustmanagerCRD map[string]interface{} + if err := yaml.Unmarshal(trustmanagerCRDBytes, &trustmanagerCRD); err != nil { + t.Fatalf("failed to unmarshal trustmanager CRD: %v", err) + } + trustmanagerCRDSpec := trustmanagerCRD["spec"].(map[string]interface{}) + trustmanagerCRDVersions := trustmanagerCRDSpec["versions"].([]interface{}) + for _, v := range trustmanagerCRDVersions { + trustmanagerCRDVersion := v.(map[string]interface{}) + schema, exists, err := unstructured.NestedMap(trustmanagerCRDVersion, "schema", "openAPIV3Schema") + if err != nil { + t.Fatalf("failed to get nested map: %v", err) + } + if !exists { + t.Fatalf("openAPIV3Schema does not exist under the CRD version") + } + + // Check for x-kubernetes-validations rules containing the singleton rule + validations, exists, err := unstructured.NestedSlice(schema, "x-kubernetes-validations") + if err != nil { + t.Fatalf("failed to get x-kubernetes-validations: %v", err) + } + if !exists { + t.Fatalf("x-kubernetes-validations does not exist on TrustManager CRD schema") + } + + found := false + for _, v := range validations { + rule, ok := v.(map[string]interface{}) + if !ok { + continue + } + if ruleStr, ok := rule["rule"].(string); ok { + if ruleStr == "self.metadata.name == 'cluster'" { + found = true + break + } + } + } + if !found { + t.Fatalf("expected singleton validation rule 'self.metadata.name == cluster' not found") + } + } +} + +// TestTrustManagerCRDScope verifies that the trustmanager CRD is cluster-scoped +func TestTrustManagerCRDScope(t *testing.T) { + filepath := path.Join(trustmanagerCRDFilePath, trustmanagerCRDFile) + trustmanagerCRDBytes, err := os.ReadFile(filepath) + if err != nil { + t.Fatalf("failed to read trustmanager CRD file %q: %v", filepath, err) + } + + var trustmanagerCRD map[string]interface{} + if err := yaml.Unmarshal(trustmanagerCRDBytes, &trustmanagerCRD); err != nil { + t.Fatalf("failed to unmarshal trustmanager CRD: %v", err) + } + trustmanagerCRDSpec := trustmanagerCRD["spec"].(map[string]interface{}) + scope, ok := trustmanagerCRDSpec["scope"].(string) + if !ok { + t.Fatalf("scope field not found in CRD spec") + } + if scope != "Cluster" { + t.Fatalf("expected CRD scope to be 'Cluster', got %q", scope) + } +} + +// TestTrustManagerCRDTrustNamespaceImmutability verifies that trustNamespace has an immutability XValidation rule +func TestTrustManagerCRDTrustNamespaceImmutability(t *testing.T) { + filepath := path.Join(trustmanagerCRDFilePath, trustmanagerCRDFile) + trustmanagerCRDBytes, err := os.ReadFile(filepath) + if err != nil { + t.Fatalf("failed to read trustmanager CRD file %q: %v", filepath, err) + } + + var trustmanagerCRD map[string]interface{} + if err := yaml.Unmarshal(trustmanagerCRDBytes, &trustmanagerCRD); err != nil { + t.Fatalf("failed to unmarshal trustmanager CRD: %v", err) + } + trustmanagerCRDSpec := trustmanagerCRD["spec"].(map[string]interface{}) + trustmanagerCRDVersions := trustmanagerCRDSpec["versions"].([]interface{}) + for _, v := range trustmanagerCRDVersions { + trustmanagerCRDVersion := v.(map[string]interface{}) + trustNamespace, exists, err := unstructured.NestedMap(trustmanagerCRDVersion, + "schema", "openAPIV3Schema", "properties", "spec", "properties", + "trustManagerConfig", "properties", "trustNamespace") + if err != nil { + t.Fatalf("failed to get trustNamespace: %v", err) + } + if !exists { + t.Fatalf("trustNamespace field does not exist in the CRD") + } + + validations, exists, err := unstructured.NestedSlice(trustNamespace, "x-kubernetes-validations") + if err != nil { + t.Fatalf("failed to get x-kubernetes-validations on trustNamespace: %v", err) + } + if !exists { + t.Fatalf("trustNamespace does not have x-kubernetes-validations") + } + + found := false + for _, val := range validations { + rule, ok := val.(map[string]interface{}) + if !ok { + continue + } + if msg, ok := rule["message"].(string); ok { + if msg == "trustNamespace is immutable once set" { + found = true + break + } + } + } + if !found { + t.Fatalf("expected immutability validation rule for trustNamespace not found") + } + } +} diff --git a/api/operator/v1alpha1/zz_generated.deepcopy.go b/api/operator/v1alpha1/zz_generated.deepcopy.go index d878e25ce..eddbd4337 100644 --- a/api/operator/v1alpha1/zz_generated.deepcopy.go +++ b/api/operator/v1alpha1/zz_generated.deepcopy.go @@ -278,6 +278,21 @@ func (in *ControllerConfig) DeepCopy() *ControllerConfig { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DefaultCAPackageConfig) DeepCopyInto(out *DefaultCAPackageConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DefaultCAPackageConfig. +func (in *DefaultCAPackageConfig) DeepCopy() *DefaultCAPackageConfig { + if in == nil { + return nil + } + out := new(DefaultCAPackageConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DeploymentConfig) DeepCopyInto(out *DeploymentConfig) { *out = *in @@ -535,6 +550,26 @@ func (in *NetworkPolicy) DeepCopy() *NetworkPolicy { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecretTargetsConfig) DeepCopyInto(out *SecretTargetsConfig) { + *out = *in + if in.AuthorizedSecrets != nil { + in, out := &in.AuthorizedSecrets, &out.AuthorizedSecrets + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretTargetsConfig. +func (in *SecretTargetsConfig) DeepCopy() *SecretTargetsConfig { + if in == nil { + return nil + } + out := new(SecretTargetsConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServerConfig) DeepCopyInto(out *ServerConfig) { *out = *in @@ -550,6 +585,164 @@ func (in *ServerConfig) DeepCopy() *ServerConfig { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TrustManager) DeepCopyInto(out *TrustManager) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TrustManager. +func (in *TrustManager) DeepCopy() *TrustManager { + if in == nil { + return nil + } + out := new(TrustManager) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TrustManager) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TrustManagerConfig) DeepCopyInto(out *TrustManagerConfig) { + *out = *in + in.SecretTargets.DeepCopyInto(&out.SecretTargets) + out.DefaultCAPackage = in.DefaultCAPackage + in.Resources.DeepCopyInto(&out.Resources) + if in.Affinity != nil { + in, out := &in.Affinity, &out.Affinity + *out = new(v1.Affinity) + (*in).DeepCopyInto(*out) + } + if in.Tolerations != nil { + in, out := &in.Tolerations, &out.Tolerations + *out = make([]v1.Toleration, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.NodeSelector != nil { + in, out := &in.NodeSelector, &out.NodeSelector + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TrustManagerConfig. +func (in *TrustManagerConfig) DeepCopy() *TrustManagerConfig { + if in == nil { + return nil + } + out := new(TrustManagerConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TrustManagerControllerConfig) DeepCopyInto(out *TrustManagerControllerConfig) { + *out = *in + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TrustManagerControllerConfig. +func (in *TrustManagerControllerConfig) DeepCopy() *TrustManagerControllerConfig { + if in == nil { + return nil + } + out := new(TrustManagerControllerConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TrustManagerList) DeepCopyInto(out *TrustManagerList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]TrustManager, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TrustManagerList. +func (in *TrustManagerList) DeepCopy() *TrustManagerList { + if in == nil { + return nil + } + out := new(TrustManagerList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *TrustManagerList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TrustManagerSpec) DeepCopyInto(out *TrustManagerSpec) { + *out = *in + in.TrustManagerConfig.DeepCopyInto(&out.TrustManagerConfig) + in.ControllerConfig.DeepCopyInto(&out.ControllerConfig) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TrustManagerSpec. +func (in *TrustManagerSpec) DeepCopy() *TrustManagerSpec { + if in == nil { + return nil + } + out := new(TrustManagerSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TrustManagerStatus) DeepCopyInto(out *TrustManagerStatus) { + *out = *in + in.ConditionalStatus.DeepCopyInto(&out.ConditionalStatus) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TrustManagerStatus. +func (in *TrustManagerStatus) DeepCopy() *TrustManagerStatus { + if in == nil { + return nil + } + out := new(TrustManagerStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *UnsupportedConfigOverrides) DeepCopyInto(out *UnsupportedConfigOverrides) { *out = *in diff --git a/config/crd/bases/operator.openshift.io_trustmanagers.yaml b/config/crd/bases/operator.openshift.io_trustmanagers.yaml new file mode 100644 index 000000000..ce0f61ea7 --- /dev/null +++ b/config/crd/bases/operator.openshift.io_trustmanagers.yaml @@ -0,0 +1,1327 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.15.0 + labels: + app.kubernetes.io/name: trustmanager + app.kubernetes.io/part-of: cert-manager-operator + name: trustmanagers.operator.openshift.io +spec: + group: operator.openshift.io + names: + categories: + - cert-manager-operator + kind: TrustManager + listKind: TrustManagerList + plural: trustmanagers + singular: trustmanager + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=='Ready')].status + name: Ready + type: string + - jsonPath: .status.conditions[?(@.type=='Ready')].message + name: Message + type: string + - jsonPath: .metadata.creationTimestamp + name: AGE + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + TrustManager describes the configuration and information about the managed trust-manager deployment. + The name must be `cluster` to make TrustManager a singleton, allowing only one instance per cluster. + + When a TrustManager is created, trust-manager is deployed in the cert-manager namespace. + 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 is the specification of the desired behavior of the + TrustManager. + properties: + controllerConfig: + description: controllerConfig configures the operator's behavior for + resource creation. + properties: + annotations: + additionalProperties: + type: string + description: annotations to apply to all resources created for + the trust-manager deployment. + minProperties: 0 + type: object + x-kubernetes-map-type: granular + labels: + additionalProperties: + type: string + description: labels to apply to all resources created for the + trust-manager deployment. + minProperties: 0 + type: object + x-kubernetes-map-type: granular + type: object + trustManagerConfig: + description: trustManagerConfig configures the trust-manager operand's + behavior. + properties: + affinity: + description: |- + affinity defines scheduling constraints for the trust-manager pod. + ref: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/ + properties: + nodeAffinity: + description: Describes node affinity scheduling rules for + the pod. + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node matches the corresponding matchExpressions; the + node(s) with the highest sum are the most preferred. + items: + description: |- + An empty preferred scheduling term matches all objects with implicit weight 0 + (i.e. it's a no-op). A null preferred scheduling term matches no objects (i.e. is also a no-op). + properties: + preference: + description: A node selector term, associated with + the corresponding weight. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + weight: + description: Weight associated with matching the + corresponding nodeSelectorTerm, in the range 1-100. + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to an update), the system + may or may not try to eventually evict the pod from its node. + properties: + nodeSelectorTerms: + description: Required. A list of node selector terms. + The terms are ORed. + items: + description: |- + A null or empty node selector term matches no objects. The requirements of + them are ANDed. + The TopologySelectorTerm type implements a subset of the NodeSelectorTerm. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: |- + A node selector requirement is a selector that contains values, a key, and an operator + that relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: |- + Represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: |- + An array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. If the operator is Gt or Lt, the values + array must have a single element, which will be interpreted as an integer. + This array is replaced during a strategic merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + type: object + x-kubernetes-map-type: atomic + type: array + x-kubernetes-list-type: atomic + required: + - nodeSelectorTerms + type: object + x-kubernetes-map-type: atomic + type: object + podAffinity: + description: Describes pod affinity scheduling rules (e.g. + co-locate this pod in the same node, zone, etc. as some + other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the + node(s) with the highest sum are the most preferred. + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred + node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both matchLabelKeys and labelSelector. + Also, matchLabelKeys cannot be set when labelSelector isn't set. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both mismatchLabelKeys and labelSelector. + Also, mismatchLabelKeys cannot be set when labelSelector isn't set. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to a pod label update), the + system may or may not try to eventually evict the pod from its node. + When there are multiple elements, the lists of nodes corresponding to each + podAffinityTerm are intersected, i.e. all terms must be satisfied. + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should be + co-located (affinity) or not co-located (anti-affinity) with, + where co-located is defined as running on a node whose value of + the label with key matches that of any node on which + a pod of the set of pods is running + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both matchLabelKeys and labelSelector. + Also, matchLabelKeys cannot be set when labelSelector isn't set. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both mismatchLabelKeys and labelSelector. + Also, mismatchLabelKeys cannot be set when labelSelector isn't set. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + podAntiAffinity: + description: Describes pod anti-affinity scheduling rules + (e.g. avoid putting this pod in the same node, zone, etc. + as some other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: |- + The scheduler will prefer to schedule pods to nodes that satisfy + the anti-affinity expressions specified by this field, but it may choose + a node that violates one or more of the expressions. The node that is + most preferred is the one with the greatest sum of weights, i.e. + for each node that meets all of the scheduling requirements (resource + request, requiredDuringScheduling anti-affinity expressions, etc.), + compute a sum by iterating through the elements of this field and adding + "weight" to the sum if the node has pods which matches the corresponding podAffinityTerm; the + node(s) with the highest sum are the most preferred. + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred + node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both matchLabelKeys and labelSelector. + Also, matchLabelKeys cannot be set when labelSelector isn't set. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both mismatchLabelKeys and labelSelector. + Also, mismatchLabelKeys cannot be set when labelSelector isn't set. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: |- + weight associated with matching the corresponding podAffinityTerm, + in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + x-kubernetes-list-type: atomic + requiredDuringSchedulingIgnoredDuringExecution: + description: |- + If the anti-affinity requirements specified by this field are not met at + scheduling time, the pod will not be scheduled onto the node. + If the anti-affinity requirements specified by this field cease to be met + at some point during pod execution (e.g. due to a pod label update), the + system may or may not try to eventually evict the pod from its node. + When there are multiple elements, the lists of nodes corresponding to each + podAffinityTerm are intersected, i.e. all terms must be satisfied. + items: + description: |- + Defines a set of pods (namely those matching the labelSelector + relative to the given namespace(s)) that this pod should be + co-located (affinity) or not co-located (anti-affinity) with, + where co-located is defined as running on a node whose value of + the label with key matches that of any node on which + a pod of the set of pods is running + properties: + labelSelector: + description: |- + A label query over a set of resources, in this case pods. + If it's null, this PodAffinityTerm matches with no Pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + matchLabelKeys: + description: |- + MatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `labelSelector` as `key in (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both matchLabelKeys and labelSelector. + Also, matchLabelKeys cannot be set when labelSelector isn't set. + items: + type: string + type: array + x-kubernetes-list-type: atomic + mismatchLabelKeys: + description: |- + MismatchLabelKeys is a set of pod label keys to select which pods will + be taken into consideration. The keys are used to lookup values from the + incoming pod labels, those key-value labels are merged with `labelSelector` as `key notin (value)` + to select the group of existing pods which pods will be taken into consideration + for the incoming pod's pod (anti) affinity. Keys that don't exist in the incoming + pod labels will be ignored. The default value is empty. + The same key is forbidden to exist in both mismatchLabelKeys and labelSelector. + Also, mismatchLabelKeys cannot be set when labelSelector isn't set. + items: + type: string + type: array + x-kubernetes-list-type: atomic + namespaceSelector: + description: |- + A label query over the set of namespaces that the term applies to. + The term is applied to the union of the namespaces selected by this field + and the ones listed in the namespaces field. + null selector and null or empty namespaces list means "this pod's namespace". + An empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: |- + A label selector requirement is a selector that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that + the selector applies to. + type: string + operator: + description: |- + operator represents a key's relationship to a set of values. + Valid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: |- + values is an array of string values. If the operator is In or NotIn, + the values array must be non-empty. If the operator is Exists or DoesNotExist, + the values array must be empty. This array is replaced during a strategic + merge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: |- + matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: |- + namespaces specifies a static list of namespace names that the term applies to. + The term is applied to the union of the namespaces listed in this field + and the ones selected by namespaceSelector. + null or empty namespaces list and null namespaceSelector means "this pod's namespace". + items: + type: string + type: array + x-kubernetes-list-type: atomic + topologyKey: + description: |- + This pod should be co-located (affinity) or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where co-located is defined as running on a node + whose value of the label with key topologyKey matches that of any node on which any of the + selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + x-kubernetes-list-type: atomic + type: object + type: object + defaultCAPackage: + description: |- + defaultCAPackage configures the default CA package for trust-manager. + When enabled, the operator will use OpenShift's trusted CA bundle injection mechanism. + properties: + policy: + default: Disabled + description: |- + policy controls whether the default CA package feature is enabled. + When set to "Enabled", the operator will inject OpenShift's trusted CA bundle + into trust-manager, enabling the "useDefaultCAs: true" source in Bundle resources. + When set to "Disabled", no default CA package is configured and Bundles cannot use useDefaultCAs (default behavior). + enum: + - Enabled + - Disabled + type: string + type: object + filterExpiredCertificates: + default: Disabled + description: |- + filterExpiredCertificates controls whether trust-manager filters out + expired certificates from trust bundles before distributing them. + When set to "Enabled", expired certificates are removed from bundles. + When set to "Disabled", expired certificates are included (default behavior). + enum: + - Enabled + - Disabled + type: string + logFormat: + default: text + description: |- + logFormat specifies the output format for trust-manager logging. + Supported formats are "text" and "json". + enum: + - text + - json + type: string + logLevel: + default: 1 + description: |- + logLevel configures the verbosity of trust-manager logging. + Follows Kubernetes logging guidelines: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/logging.md#what-method-to-use + format: int32 + maximum: 5 + minimum: 1 + type: integer + nodeSelector: + additionalProperties: + type: string + description: |- + nodeSelector restricts which nodes the trust-manager pod can be scheduled on. + This field can have a maximum of 50 entries. + ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/ + maxProperties: 50 + minProperties: 0 + type: object + x-kubernetes-map-type: atomic + resources: + description: |- + resources defines the compute resource requirements for the trust-manager pod. + ref: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + properties: + claims: + description: |- + Claims lists the names of resources, defined in spec.resourceClaims, + that are used by this container. + + This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. + + This field is immutable. It can only be set for containers. + items: + description: ResourceClaim references one entry in PodSpec.ResourceClaims. + properties: + name: + description: |- + Name must match the name of one entry in pod.spec.resourceClaims of + the Pod where this field is used. It makes that resource available + inside a container. + type: string + request: + description: |- + Request is the name chosen for a request in the referenced claim. + If empty, everything from the claim is made available, otherwise + only the result of this request. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Limits describes the maximum amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Requests describes the minimum amount of compute resources required. + If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, + otherwise to an implementation-defined value. Requests cannot exceed Limits. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + type: object + type: object + secretTargets: + description: secretTargets configures whether trust-manager can + write trust bundles to Secrets. + properties: + authorizedSecrets: + description: |- + authorizedSecrets is a list of specific secret names that trust-manager + is authorized to create and update. This field is only valid when policy is "Custom". + items: + minLength: 1 + type: string + minItems: 0 + type: array + x-kubernetes-list-type: set + policy: + default: Disabled + description: |- + policy controls whether and how trust-manager can write trust bundles to Secrets. + Allowed values are "Disabled" or "Custom". + "Disabled" means trust-manager cannot write trust bundles to Secrets (default behavior). + "Custom" grants trust-manager permission to create and update only the secrets listed in authorizedSecrets. + enum: + - Disabled + - Custom + type: string + type: object + x-kubernetes-validations: + - message: authorizedSecrets must not be empty when policy is + Custom + rule: self.policy != 'Custom' || (has(self.authorizedSecrets) + && size(self.authorizedSecrets) > 0) + - message: authorizedSecrets must be empty when policy is not + Custom + rule: self.policy == 'Custom' || !has(self.authorizedSecrets) + || size(self.authorizedSecrets) == 0 + tolerations: + description: |- + tolerations allows the trust-manager pod to be scheduled on tainted nodes. + This field can have a maximum of 50 entries. + ref: https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/ + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple using the matching operator . + properties: + effect: + description: |- + Effect indicates the taint effect to match. Empty means match all taint effects. + When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + type: string + key: + description: |- + Key is the taint key that the toleration applies to. Empty means match all taint keys. + If the key is empty, operator must be Exists; this combination means to match all values and all keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. Defaults to Equal. + Exists is equivalent to wildcard for value, so that a pod can + tolerate all taints of a particular category. + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, + it is not set, which means tolerate the taint forever (do not evict). Zero and + negative values will be treated as 0 (evict immediately) by the system. + format: int64 + type: integer + value: + description: |- + Value is the taint value the toleration matches to. + If the operator is Exists, the value should be empty, otherwise just a regular string. + type: string + type: object + maxItems: 50 + minItems: 0 + type: array + x-kubernetes-list-type: atomic + trustNamespace: + default: cert-manager + description: |- + trustNamespace is the namespace where trust-manager looks for trust sources + (ConfigMaps and Secrets containing CA certificates). + Defaults to "cert-manager" if not specified. + This field is immutable once set. + This field can have a maximum of 63 characters. + maxLength: 63 + minLength: 1 + type: string + x-kubernetes-validations: + - message: trustNamespace is immutable once set + rule: oldSelf == '' || self == oldSelf + type: object + required: + - trustManagerConfig + type: object + status: + description: status is the most recently observed status of the TrustManager. + properties: + conditions: + description: conditions holds information about the current state + of the istio-csr agent deployment. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + 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. + 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 + defaultCAPackagePolicy: + description: defaultCAPackagePolicy indicates the current default + CA package policy. + enum: + - Enabled + - Disabled + type: string + filterExpiredCertificatesPolicy: + description: filterExpiredCertificatesPolicy indicates the current + policy for filtering expired certificates. + enum: + - Enabled + - Disabled + type: string + secretTargetsPolicy: + description: secretTargetsPolicy indicates the current secret targets + policy. + enum: + - Disabled + - Custom + type: string + trustManagerImage: + description: trustManagerImage is the container image (name:tag) used + for trust-manager. + type: string + trustNamespace: + description: trustNamespace is the namespace where trust-manager looks + for trust sources. + type: string + type: object + required: + - spec + type: object + x-kubernetes-validations: + - message: TrustManager is a singleton, .metadata.name must be 'cluster' + rule: self.metadata.name == 'cluster' + served: true + storage: true + subresources: + status: {} diff --git a/pkg/operator/applyconfigurations/internal/internal.go b/pkg/operator/applyconfigurations/internal/internal.go index ab48e360d..cf84b3f02 100644 --- a/pkg/operator/applyconfigurations/internal/internal.go +++ b/pkg/operator/applyconfigurations/internal/internal.go @@ -43,6 +43,16 @@ var schemaYAML = typed.YAMLObject(`types: elementType: namedType: __untyped_deduced_ elementRelationship: separable +- name: com.github.openshift.cert-manager-operator.api.operator.v1alpha1.TrustManager + scalar: untyped + list: + elementType: + namedType: __untyped_atomic_ + elementRelationship: atomic + map: + elementType: + namedType: __untyped_deduced_ + elementRelationship: separable - name: __untyped_atomic_ scalar: untyped list: diff --git a/pkg/operator/applyconfigurations/operator/v1alpha1/defaultcapackageconfig.go b/pkg/operator/applyconfigurations/operator/v1alpha1/defaultcapackageconfig.go new file mode 100644 index 000000000..a45123bee --- /dev/null +++ b/pkg/operator/applyconfigurations/operator/v1alpha1/defaultcapackageconfig.go @@ -0,0 +1,27 @@ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + operatorv1alpha1 "github.com/openshift/cert-manager-operator/api/operator/v1alpha1" +) + +// DefaultCAPackageConfigApplyConfiguration represents a declarative configuration of the DefaultCAPackageConfig type for use +// with apply. +type DefaultCAPackageConfigApplyConfiguration struct { + Policy *operatorv1alpha1.DefaultCAPackagePolicy `json:"policy,omitempty"` +} + +// DefaultCAPackageConfigApplyConfiguration constructs a declarative configuration of the DefaultCAPackageConfig type for use with +// apply. +func DefaultCAPackageConfig() *DefaultCAPackageConfigApplyConfiguration { + return &DefaultCAPackageConfigApplyConfiguration{} +} + +// WithPolicy sets the Policy 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 Policy field is set to the value of the last call. +func (b *DefaultCAPackageConfigApplyConfiguration) WithPolicy(value operatorv1alpha1.DefaultCAPackagePolicy) *DefaultCAPackageConfigApplyConfiguration { + b.Policy = &value + return b +} diff --git a/pkg/operator/applyconfigurations/operator/v1alpha1/secrettargetsconfig.go b/pkg/operator/applyconfigurations/operator/v1alpha1/secrettargetsconfig.go new file mode 100644 index 000000000..c272e6bd7 --- /dev/null +++ b/pkg/operator/applyconfigurations/operator/v1alpha1/secrettargetsconfig.go @@ -0,0 +1,38 @@ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + operatorv1alpha1 "github.com/openshift/cert-manager-operator/api/operator/v1alpha1" +) + +// SecretTargetsConfigApplyConfiguration represents a declarative configuration of the SecretTargetsConfig type for use +// with apply. +type SecretTargetsConfigApplyConfiguration struct { + Policy *operatorv1alpha1.SecretTargetsPolicy `json:"policy,omitempty"` + AuthorizedSecrets []string `json:"authorizedSecrets,omitempty"` +} + +// SecretTargetsConfigApplyConfiguration constructs a declarative configuration of the SecretTargetsConfig type for use with +// apply. +func SecretTargetsConfig() *SecretTargetsConfigApplyConfiguration { + return &SecretTargetsConfigApplyConfiguration{} +} + +// WithPolicy sets the Policy 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 Policy field is set to the value of the last call. +func (b *SecretTargetsConfigApplyConfiguration) WithPolicy(value operatorv1alpha1.SecretTargetsPolicy) *SecretTargetsConfigApplyConfiguration { + b.Policy = &value + return b +} + +// WithAuthorizedSecrets adds the given value to the AuthorizedSecrets 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 AuthorizedSecrets field. +func (b *SecretTargetsConfigApplyConfiguration) WithAuthorizedSecrets(values ...string) *SecretTargetsConfigApplyConfiguration { + for i := range values { + b.AuthorizedSecrets = append(b.AuthorizedSecrets, values[i]) + } + return b +} diff --git a/pkg/operator/applyconfigurations/operator/v1alpha1/trustmanager.go b/pkg/operator/applyconfigurations/operator/v1alpha1/trustmanager.go new file mode 100644 index 000000000..2ab528196 --- /dev/null +++ b/pkg/operator/applyconfigurations/operator/v1alpha1/trustmanager.go @@ -0,0 +1,246 @@ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + operatorv1alpha1 "github.com/openshift/cert-manager-operator/api/operator/v1alpha1" + internal "github.com/openshift/cert-manager-operator/pkg/operator/applyconfigurations/internal" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + managedfields "k8s.io/apimachinery/pkg/util/managedfields" + v1 "k8s.io/client-go/applyconfigurations/meta/v1" +) + +// TrustManagerApplyConfiguration represents a declarative configuration of the TrustManager type for use +// with apply. +type TrustManagerApplyConfiguration struct { + v1.TypeMetaApplyConfiguration `json:",inline"` + *v1.ObjectMetaApplyConfiguration `json:"metadata,omitempty"` + Spec *TrustManagerSpecApplyConfiguration `json:"spec,omitempty"` + Status *TrustManagerStatusApplyConfiguration `json:"status,omitempty"` +} + +// TrustManager constructs a declarative configuration of the TrustManager type for use with +// apply. +func TrustManager(name string) *TrustManagerApplyConfiguration { + b := &TrustManagerApplyConfiguration{} + b.WithName(name) + b.WithKind("TrustManager") + b.WithAPIVersion("operator.openshift.io/v1alpha1") + return b +} + +// ExtractTrustManager extracts the applied configuration owned by fieldManager from +// trustManager. If no managedFields are found in trustManager for fieldManager, a +// TrustManagerApplyConfiguration is returned with only the Name, Namespace (if applicable), +// APIVersion and Kind populated. It is possible that no managed fields were found for because other +// field managers have taken ownership of all the fields previously owned by fieldManager, or because +// the fieldManager never owned fields any fields. +// trustManager must be a unmodified TrustManager API object that was retrieved from the Kubernetes API. +// ExtractTrustManager provides a way to perform a extract/modify-in-place/apply workflow. +// Note that an extracted apply configuration will contain fewer fields than what the fieldManager previously +// applied if another fieldManager has updated or force applied any of the previously applied fields. +// Experimental! +func ExtractTrustManager(trustManager *operatorv1alpha1.TrustManager, fieldManager string) (*TrustManagerApplyConfiguration, error) { + return extractTrustManager(trustManager, fieldManager, "") +} + +// ExtractTrustManagerStatus is the same as ExtractTrustManager except +// that it extracts the status subresource applied configuration. +// Experimental! +func ExtractTrustManagerStatus(trustManager *operatorv1alpha1.TrustManager, fieldManager string) (*TrustManagerApplyConfiguration, error) { + return extractTrustManager(trustManager, fieldManager, "status") +} + +func extractTrustManager(trustManager *operatorv1alpha1.TrustManager, fieldManager string, subresource string) (*TrustManagerApplyConfiguration, error) { + b := &TrustManagerApplyConfiguration{} + err := managedfields.ExtractInto(trustManager, internal.Parser().Type("com.github.openshift.cert-manager-operator.api.operator.v1alpha1.TrustManager"), fieldManager, b, subresource) + if err != nil { + return nil, err + } + b.WithName(trustManager.Name) + + b.WithKind("TrustManager") + b.WithAPIVersion("operator.openshift.io/v1alpha1") + return b, nil +} + +// WithKind sets the Kind 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 Kind field is set to the value of the last call. +func (b *TrustManagerApplyConfiguration) WithKind(value string) *TrustManagerApplyConfiguration { + b.TypeMetaApplyConfiguration.Kind = &value + return b +} + +// WithAPIVersion sets the APIVersion 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 APIVersion field is set to the value of the last call. +func (b *TrustManagerApplyConfiguration) WithAPIVersion(value string) *TrustManagerApplyConfiguration { + b.TypeMetaApplyConfiguration.APIVersion = &value + return b +} + +// WithName sets the Name 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 Name field is set to the value of the last call. +func (b *TrustManagerApplyConfiguration) WithName(value string) *TrustManagerApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.Name = &value + return b +} + +// WithGenerateName sets the GenerateName 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 GenerateName field is set to the value of the last call. +func (b *TrustManagerApplyConfiguration) WithGenerateName(value string) *TrustManagerApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.GenerateName = &value + return b +} + +// WithNamespace sets the Namespace 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 Namespace field is set to the value of the last call. +func (b *TrustManagerApplyConfiguration) WithNamespace(value string) *TrustManagerApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.Namespace = &value + return b +} + +// WithUID sets the UID 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 UID field is set to the value of the last call. +func (b *TrustManagerApplyConfiguration) WithUID(value types.UID) *TrustManagerApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.UID = &value + return b +} + +// WithResourceVersion sets the ResourceVersion 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 ResourceVersion field is set to the value of the last call. +func (b *TrustManagerApplyConfiguration) WithResourceVersion(value string) *TrustManagerApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.ResourceVersion = &value + return b +} + +// WithGeneration sets the Generation 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 Generation field is set to the value of the last call. +func (b *TrustManagerApplyConfiguration) WithGeneration(value int64) *TrustManagerApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.Generation = &value + return b +} + +// WithCreationTimestamp sets the CreationTimestamp 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 CreationTimestamp field is set to the value of the last call. +func (b *TrustManagerApplyConfiguration) WithCreationTimestamp(value metav1.Time) *TrustManagerApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.CreationTimestamp = &value + return b +} + +// WithDeletionTimestamp sets the DeletionTimestamp 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 DeletionTimestamp field is set to the value of the last call. +func (b *TrustManagerApplyConfiguration) WithDeletionTimestamp(value metav1.Time) *TrustManagerApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.DeletionTimestamp = &value + return b +} + +// WithDeletionGracePeriodSeconds sets the DeletionGracePeriodSeconds 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 DeletionGracePeriodSeconds field is set to the value of the last call. +func (b *TrustManagerApplyConfiguration) WithDeletionGracePeriodSeconds(value int64) *TrustManagerApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.DeletionGracePeriodSeconds = &value + return b +} + +// WithLabels puts the entries into the Labels field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Labels field, +// overwriting an existing map entries in Labels field with the same key. +func (b *TrustManagerApplyConfiguration) WithLabels(entries map[string]string) *TrustManagerApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + if b.ObjectMetaApplyConfiguration.Labels == nil && len(entries) > 0 { + b.ObjectMetaApplyConfiguration.Labels = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.ObjectMetaApplyConfiguration.Labels[k] = v + } + return b +} + +// WithAnnotations puts the entries into the Annotations field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Annotations field, +// overwriting an existing map entries in Annotations field with the same key. +func (b *TrustManagerApplyConfiguration) WithAnnotations(entries map[string]string) *TrustManagerApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + if b.ObjectMetaApplyConfiguration.Annotations == nil && len(entries) > 0 { + b.ObjectMetaApplyConfiguration.Annotations = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.ObjectMetaApplyConfiguration.Annotations[k] = v + } + return b +} + +// WithOwnerReferences adds the given value to the OwnerReferences 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 OwnerReferences field. +func (b *TrustManagerApplyConfiguration) WithOwnerReferences(values ...*v1.OwnerReferenceApplyConfiguration) *TrustManagerApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + for i := range values { + if values[i] == nil { + panic("nil value passed to WithOwnerReferences") + } + b.ObjectMetaApplyConfiguration.OwnerReferences = append(b.ObjectMetaApplyConfiguration.OwnerReferences, *values[i]) + } + return b +} + +// WithFinalizers adds the given value to the Finalizers 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 Finalizers field. +func (b *TrustManagerApplyConfiguration) WithFinalizers(values ...string) *TrustManagerApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + for i := range values { + b.ObjectMetaApplyConfiguration.Finalizers = append(b.ObjectMetaApplyConfiguration.Finalizers, values[i]) + } + return b +} + +func (b *TrustManagerApplyConfiguration) ensureObjectMetaApplyConfigurationExists() { + if b.ObjectMetaApplyConfiguration == nil { + b.ObjectMetaApplyConfiguration = &v1.ObjectMetaApplyConfiguration{} + } +} + +// WithSpec sets the Spec 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 Spec field is set to the value of the last call. +func (b *TrustManagerApplyConfiguration) WithSpec(value *TrustManagerSpecApplyConfiguration) *TrustManagerApplyConfiguration { + b.Spec = value + return b +} + +// WithStatus sets the Status 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 Status field is set to the value of the last call. +func (b *TrustManagerApplyConfiguration) WithStatus(value *TrustManagerStatusApplyConfiguration) *TrustManagerApplyConfiguration { + b.Status = value + return b +} + +// GetName retrieves the value of the Name field in the declarative configuration. +func (b *TrustManagerApplyConfiguration) GetName() *string { + b.ensureObjectMetaApplyConfigurationExists() + return b.ObjectMetaApplyConfiguration.Name +} diff --git a/pkg/operator/applyconfigurations/operator/v1alpha1/trustmanagerconfig.go b/pkg/operator/applyconfigurations/operator/v1alpha1/trustmanagerconfig.go new file mode 100644 index 000000000..9f1e59efa --- /dev/null +++ b/pkg/operator/applyconfigurations/operator/v1alpha1/trustmanagerconfig.go @@ -0,0 +1,117 @@ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + operatorv1alpha1 "github.com/openshift/cert-manager-operator/api/operator/v1alpha1" + v1 "k8s.io/api/core/v1" +) + +// TrustManagerConfigApplyConfiguration represents a declarative configuration of the TrustManagerConfig type for use +// with apply. +type TrustManagerConfigApplyConfiguration struct { + LogLevel *int32 `json:"logLevel,omitempty"` + LogFormat *string `json:"logFormat,omitempty"` + TrustNamespace *string `json:"trustNamespace,omitempty"` + SecretTargets *SecretTargetsConfigApplyConfiguration `json:"secretTargets,omitempty"` + FilterExpiredCertificates *operatorv1alpha1.FilterExpiredCertificatesPolicy `json:"filterExpiredCertificates,omitempty"` + DefaultCAPackage *DefaultCAPackageConfigApplyConfiguration `json:"defaultCAPackage,omitempty"` + Resources *v1.ResourceRequirements `json:"resources,omitempty"` + Affinity *v1.Affinity `json:"affinity,omitempty"` + Tolerations []v1.Toleration `json:"tolerations,omitempty"` + NodeSelector map[string]string `json:"nodeSelector,omitempty"` +} + +// TrustManagerConfigApplyConfiguration constructs a declarative configuration of the TrustManagerConfig type for use with +// apply. +func TrustManagerConfig() *TrustManagerConfigApplyConfiguration { + return &TrustManagerConfigApplyConfiguration{} +} + +// WithLogLevel sets the LogLevel 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 LogLevel field is set to the value of the last call. +func (b *TrustManagerConfigApplyConfiguration) WithLogLevel(value int32) *TrustManagerConfigApplyConfiguration { + b.LogLevel = &value + return b +} + +// WithLogFormat sets the LogFormat 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 LogFormat field is set to the value of the last call. +func (b *TrustManagerConfigApplyConfiguration) WithLogFormat(value string) *TrustManagerConfigApplyConfiguration { + b.LogFormat = &value + return b +} + +// WithTrustNamespace sets the TrustNamespace 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 TrustNamespace field is set to the value of the last call. +func (b *TrustManagerConfigApplyConfiguration) WithTrustNamespace(value string) *TrustManagerConfigApplyConfiguration { + b.TrustNamespace = &value + return b +} + +// WithSecretTargets sets the SecretTargets 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 SecretTargets field is set to the value of the last call. +func (b *TrustManagerConfigApplyConfiguration) WithSecretTargets(value *SecretTargetsConfigApplyConfiguration) *TrustManagerConfigApplyConfiguration { + b.SecretTargets = value + return b +} + +// WithFilterExpiredCertificates sets the FilterExpiredCertificates 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 FilterExpiredCertificates field is set to the value of the last call. +func (b *TrustManagerConfigApplyConfiguration) WithFilterExpiredCertificates(value operatorv1alpha1.FilterExpiredCertificatesPolicy) *TrustManagerConfigApplyConfiguration { + b.FilterExpiredCertificates = &value + return b +} + +// WithDefaultCAPackage sets the DefaultCAPackage 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 DefaultCAPackage field is set to the value of the last call. +func (b *TrustManagerConfigApplyConfiguration) WithDefaultCAPackage(value *DefaultCAPackageConfigApplyConfiguration) *TrustManagerConfigApplyConfiguration { + b.DefaultCAPackage = value + return b +} + +// WithResources sets the Resources 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 Resources field is set to the value of the last call. +func (b *TrustManagerConfigApplyConfiguration) WithResources(value v1.ResourceRequirements) *TrustManagerConfigApplyConfiguration { + b.Resources = &value + return b +} + +// WithAffinity sets the Affinity 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 Affinity field is set to the value of the last call. +func (b *TrustManagerConfigApplyConfiguration) WithAffinity(value v1.Affinity) *TrustManagerConfigApplyConfiguration { + b.Affinity = &value + return b +} + +// WithTolerations adds the given value to the Tolerations 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 Tolerations field. +func (b *TrustManagerConfigApplyConfiguration) WithTolerations(values ...v1.Toleration) *TrustManagerConfigApplyConfiguration { + for i := range values { + b.Tolerations = append(b.Tolerations, values[i]) + } + return b +} + +// WithNodeSelector puts the entries into the NodeSelector field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the NodeSelector field, +// overwriting an existing map entries in NodeSelector field with the same key. +func (b *TrustManagerConfigApplyConfiguration) WithNodeSelector(entries map[string]string) *TrustManagerConfigApplyConfiguration { + if b.NodeSelector == nil && len(entries) > 0 { + b.NodeSelector = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.NodeSelector[k] = v + } + return b +} diff --git a/pkg/operator/applyconfigurations/operator/v1alpha1/trustmanagercontrollerconfig.go b/pkg/operator/applyconfigurations/operator/v1alpha1/trustmanagercontrollerconfig.go new file mode 100644 index 000000000..031986672 --- /dev/null +++ b/pkg/operator/applyconfigurations/operator/v1alpha1/trustmanagercontrollerconfig.go @@ -0,0 +1,44 @@ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +// TrustManagerControllerConfigApplyConfiguration represents a declarative configuration of the TrustManagerControllerConfig type for use +// with apply. +type TrustManagerControllerConfigApplyConfiguration struct { + Labels map[string]string `json:"labels,omitempty"` + Annotations map[string]string `json:"annotations,omitempty"` +} + +// TrustManagerControllerConfigApplyConfiguration constructs a declarative configuration of the TrustManagerControllerConfig type for use with +// apply. +func TrustManagerControllerConfig() *TrustManagerControllerConfigApplyConfiguration { + return &TrustManagerControllerConfigApplyConfiguration{} +} + +// WithLabels puts the entries into the Labels field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Labels field, +// overwriting an existing map entries in Labels field with the same key. +func (b *TrustManagerControllerConfigApplyConfiguration) WithLabels(entries map[string]string) *TrustManagerControllerConfigApplyConfiguration { + if b.Labels == nil && len(entries) > 0 { + b.Labels = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.Labels[k] = v + } + return b +} + +// WithAnnotations puts the entries into the Annotations field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Annotations field, +// overwriting an existing map entries in Annotations field with the same key. +func (b *TrustManagerControllerConfigApplyConfiguration) WithAnnotations(entries map[string]string) *TrustManagerControllerConfigApplyConfiguration { + if b.Annotations == nil && len(entries) > 0 { + b.Annotations = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.Annotations[k] = v + } + return b +} diff --git a/pkg/operator/applyconfigurations/operator/v1alpha1/trustmanagerspec.go b/pkg/operator/applyconfigurations/operator/v1alpha1/trustmanagerspec.go new file mode 100644 index 000000000..7f8659cda --- /dev/null +++ b/pkg/operator/applyconfigurations/operator/v1alpha1/trustmanagerspec.go @@ -0,0 +1,32 @@ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +// TrustManagerSpecApplyConfiguration represents a declarative configuration of the TrustManagerSpec type for use +// with apply. +type TrustManagerSpecApplyConfiguration struct { + TrustManagerConfig *TrustManagerConfigApplyConfiguration `json:"trustManagerConfig,omitempty"` + ControllerConfig *TrustManagerControllerConfigApplyConfiguration `json:"controllerConfig,omitempty"` +} + +// TrustManagerSpecApplyConfiguration constructs a declarative configuration of the TrustManagerSpec type for use with +// apply. +func TrustManagerSpec() *TrustManagerSpecApplyConfiguration { + return &TrustManagerSpecApplyConfiguration{} +} + +// WithTrustManagerConfig sets the TrustManagerConfig 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 TrustManagerConfig field is set to the value of the last call. +func (b *TrustManagerSpecApplyConfiguration) WithTrustManagerConfig(value *TrustManagerConfigApplyConfiguration) *TrustManagerSpecApplyConfiguration { + b.TrustManagerConfig = value + return b +} + +// WithControllerConfig sets the ControllerConfig 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 ControllerConfig field is set to the value of the last call. +func (b *TrustManagerSpecApplyConfiguration) WithControllerConfig(value *TrustManagerControllerConfigApplyConfiguration) *TrustManagerSpecApplyConfiguration { + b.ControllerConfig = value + return b +} diff --git a/pkg/operator/applyconfigurations/operator/v1alpha1/trustmanagerstatus.go b/pkg/operator/applyconfigurations/operator/v1alpha1/trustmanagerstatus.go new file mode 100644 index 000000000..fce632a82 --- /dev/null +++ b/pkg/operator/applyconfigurations/operator/v1alpha1/trustmanagerstatus.go @@ -0,0 +1,78 @@ +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + operatorv1alpha1 "github.com/openshift/cert-manager-operator/api/operator/v1alpha1" + v1 "k8s.io/client-go/applyconfigurations/meta/v1" +) + +// TrustManagerStatusApplyConfiguration represents a declarative configuration of the TrustManagerStatus type for use +// with apply. +type TrustManagerStatusApplyConfiguration struct { + ConditionalStatusApplyConfiguration `json:",omitempty,inline"` + TrustManagerImage *string `json:"trustManagerImage,omitempty"` + TrustNamespace *string `json:"trustNamespace,omitempty"` + SecretTargetsPolicy *operatorv1alpha1.SecretTargetsPolicy `json:"secretTargetsPolicy,omitempty"` + DefaultCAPackagePolicy *operatorv1alpha1.DefaultCAPackagePolicy `json:"defaultCAPackagePolicy,omitempty"` + FilterExpiredCertificatesPolicy *operatorv1alpha1.FilterExpiredCertificatesPolicy `json:"filterExpiredCertificatesPolicy,omitempty"` +} + +// TrustManagerStatusApplyConfiguration constructs a declarative configuration of the TrustManagerStatus type for use with +// apply. +func TrustManagerStatus() *TrustManagerStatusApplyConfiguration { + return &TrustManagerStatusApplyConfiguration{} +} + +// WithConditions adds the given value to the Conditions 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 Conditions field. +func (b *TrustManagerStatusApplyConfiguration) WithConditions(values ...*v1.ConditionApplyConfiguration) *TrustManagerStatusApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithConditions") + } + b.ConditionalStatusApplyConfiguration.Conditions = append(b.ConditionalStatusApplyConfiguration.Conditions, *values[i]) + } + return b +} + +// WithTrustManagerImage sets the TrustManagerImage 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 TrustManagerImage field is set to the value of the last call. +func (b *TrustManagerStatusApplyConfiguration) WithTrustManagerImage(value string) *TrustManagerStatusApplyConfiguration { + b.TrustManagerImage = &value + return b +} + +// WithTrustNamespace sets the TrustNamespace 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 TrustNamespace field is set to the value of the last call. +func (b *TrustManagerStatusApplyConfiguration) WithTrustNamespace(value string) *TrustManagerStatusApplyConfiguration { + b.TrustNamespace = &value + return b +} + +// WithSecretTargetsPolicy sets the SecretTargetsPolicy 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 SecretTargetsPolicy field is set to the value of the last call. +func (b *TrustManagerStatusApplyConfiguration) WithSecretTargetsPolicy(value operatorv1alpha1.SecretTargetsPolicy) *TrustManagerStatusApplyConfiguration { + b.SecretTargetsPolicy = &value + return b +} + +// WithDefaultCAPackagePolicy sets the DefaultCAPackagePolicy 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 DefaultCAPackagePolicy field is set to the value of the last call. +func (b *TrustManagerStatusApplyConfiguration) WithDefaultCAPackagePolicy(value operatorv1alpha1.DefaultCAPackagePolicy) *TrustManagerStatusApplyConfiguration { + b.DefaultCAPackagePolicy = &value + return b +} + +// WithFilterExpiredCertificatesPolicy sets the FilterExpiredCertificatesPolicy 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 FilterExpiredCertificatesPolicy field is set to the value of the last call. +func (b *TrustManagerStatusApplyConfiguration) WithFilterExpiredCertificatesPolicy(value operatorv1alpha1.FilterExpiredCertificatesPolicy) *TrustManagerStatusApplyConfiguration { + b.FilterExpiredCertificatesPolicy = &value + return b +} diff --git a/pkg/operator/applyconfigurations/utils.go b/pkg/operator/applyconfigurations/utils.go index 11e43885f..50f42a187 100644 --- a/pkg/operator/applyconfigurations/utils.go +++ b/pkg/operator/applyconfigurations/utils.go @@ -34,6 +34,8 @@ func ForKind(kind schema.GroupVersionKind) interface{} { return &operatorv1alpha1.ConfigMapReferenceApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("ControllerConfig"): return &operatorv1alpha1.ControllerConfigApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("DefaultCAPackageConfig"): + return &operatorv1alpha1.DefaultCAPackageConfigApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("DeploymentConfig"): return &operatorv1alpha1.DeploymentConfigApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("IstioConfig"): @@ -50,8 +52,20 @@ func ForKind(kind schema.GroupVersionKind) interface{} { return &operatorv1alpha1.IstiodTLSConfigApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("NetworkPolicy"): return &operatorv1alpha1.NetworkPolicyApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("SecretTargetsConfig"): + return &operatorv1alpha1.SecretTargetsConfigApplyConfiguration{} case v1alpha1.SchemeGroupVersion.WithKind("ServerConfig"): return &operatorv1alpha1.ServerConfigApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("TrustManager"): + return &operatorv1alpha1.TrustManagerApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("TrustManagerConfig"): + return &operatorv1alpha1.TrustManagerConfigApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("TrustManagerControllerConfig"): + return &operatorv1alpha1.TrustManagerControllerConfigApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("TrustManagerSpec"): + return &operatorv1alpha1.TrustManagerSpecApplyConfiguration{} + case v1alpha1.SchemeGroupVersion.WithKind("TrustManagerStatus"): + return &operatorv1alpha1.TrustManagerStatusApplyConfiguration{} } return nil diff --git a/pkg/operator/clientset/versioned/typed/operator/v1alpha1/fake/fake_operator_client.go b/pkg/operator/clientset/versioned/typed/operator/v1alpha1/fake/fake_operator_client.go index cc50d82f9..aaca26cb7 100644 --- a/pkg/operator/clientset/versioned/typed/operator/v1alpha1/fake/fake_operator_client.go +++ b/pkg/operator/clientset/versioned/typed/operator/v1alpha1/fake/fake_operator_client.go @@ -20,6 +20,10 @@ func (c *FakeOperatorV1alpha1) IstioCSRs(namespace string) v1alpha1.IstioCSRInte return newFakeIstioCSRs(c, namespace) } +func (c *FakeOperatorV1alpha1) TrustManagers() v1alpha1.TrustManagerInterface { + return newFakeTrustManagers(c) +} + // RESTClient returns a RESTClient that is used to communicate // with API server by this client implementation. func (c *FakeOperatorV1alpha1) RESTClient() rest.Interface { diff --git a/pkg/operator/clientset/versioned/typed/operator/v1alpha1/fake/fake_trustmanager.go b/pkg/operator/clientset/versioned/typed/operator/v1alpha1/fake/fake_trustmanager.go new file mode 100644 index 000000000..ce9077b56 --- /dev/null +++ b/pkg/operator/clientset/versioned/typed/operator/v1alpha1/fake/fake_trustmanager.go @@ -0,0 +1,37 @@ +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + v1alpha1 "github.com/openshift/cert-manager-operator/api/operator/v1alpha1" + operatorv1alpha1 "github.com/openshift/cert-manager-operator/pkg/operator/applyconfigurations/operator/v1alpha1" + typedoperatorv1alpha1 "github.com/openshift/cert-manager-operator/pkg/operator/clientset/versioned/typed/operator/v1alpha1" + gentype "k8s.io/client-go/gentype" +) + +// fakeTrustManagers implements TrustManagerInterface +type fakeTrustManagers struct { + *gentype.FakeClientWithListAndApply[*v1alpha1.TrustManager, *v1alpha1.TrustManagerList, *operatorv1alpha1.TrustManagerApplyConfiguration] + Fake *FakeOperatorV1alpha1 +} + +func newFakeTrustManagers(fake *FakeOperatorV1alpha1) typedoperatorv1alpha1.TrustManagerInterface { + return &fakeTrustManagers{ + gentype.NewFakeClientWithListAndApply[*v1alpha1.TrustManager, *v1alpha1.TrustManagerList, *operatorv1alpha1.TrustManagerApplyConfiguration]( + fake.Fake, + "", + v1alpha1.SchemeGroupVersion.WithResource("trustmanagers"), + v1alpha1.SchemeGroupVersion.WithKind("TrustManager"), + func() *v1alpha1.TrustManager { return &v1alpha1.TrustManager{} }, + func() *v1alpha1.TrustManagerList { return &v1alpha1.TrustManagerList{} }, + func(dst, src *v1alpha1.TrustManagerList) { dst.ListMeta = src.ListMeta }, + func(list *v1alpha1.TrustManagerList) []*v1alpha1.TrustManager { + return gentype.ToPointerSlice(list.Items) + }, + func(list *v1alpha1.TrustManagerList, items []*v1alpha1.TrustManager) { + list.Items = gentype.FromPointerSlice(items) + }, + ), + fake, + } +} diff --git a/pkg/operator/clientset/versioned/typed/operator/v1alpha1/generated_expansion.go b/pkg/operator/clientset/versioned/typed/operator/v1alpha1/generated_expansion.go index 56f852de5..df39e06da 100644 --- a/pkg/operator/clientset/versioned/typed/operator/v1alpha1/generated_expansion.go +++ b/pkg/operator/clientset/versioned/typed/operator/v1alpha1/generated_expansion.go @@ -5,3 +5,5 @@ package v1alpha1 type CertManagerExpansion interface{} type IstioCSRExpansion interface{} + +type TrustManagerExpansion interface{} diff --git a/pkg/operator/clientset/versioned/typed/operator/v1alpha1/operator_client.go b/pkg/operator/clientset/versioned/typed/operator/v1alpha1/operator_client.go index 67d7b0aee..9eabd32fe 100644 --- a/pkg/operator/clientset/versioned/typed/operator/v1alpha1/operator_client.go +++ b/pkg/operator/clientset/versioned/typed/operator/v1alpha1/operator_client.go @@ -14,6 +14,7 @@ type OperatorV1alpha1Interface interface { RESTClient() rest.Interface CertManagersGetter IstioCSRsGetter + TrustManagersGetter } // OperatorV1alpha1Client is used to interact with features provided by the operator.openshift.io group. @@ -29,6 +30,10 @@ func (c *OperatorV1alpha1Client) IstioCSRs(namespace string) IstioCSRInterface { return newIstioCSRs(c, namespace) } +func (c *OperatorV1alpha1Client) TrustManagers() TrustManagerInterface { + return newTrustManagers(c) +} + // NewForConfig creates a new OperatorV1alpha1Client for the given config. // NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), // where httpClient was generated with rest.HTTPClientFor(c). diff --git a/pkg/operator/clientset/versioned/typed/operator/v1alpha1/trustmanager.go b/pkg/operator/clientset/versioned/typed/operator/v1alpha1/trustmanager.go new file mode 100644 index 000000000..cea6d2742 --- /dev/null +++ b/pkg/operator/clientset/versioned/typed/operator/v1alpha1/trustmanager.go @@ -0,0 +1,58 @@ +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + context "context" + + operatorv1alpha1 "github.com/openshift/cert-manager-operator/api/operator/v1alpha1" + applyconfigurationsoperatorv1alpha1 "github.com/openshift/cert-manager-operator/pkg/operator/applyconfigurations/operator/v1alpha1" + scheme "github.com/openshift/cert-manager-operator/pkg/operator/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + gentype "k8s.io/client-go/gentype" +) + +// TrustManagersGetter has a method to return a TrustManagerInterface. +// A group's client should implement this interface. +type TrustManagersGetter interface { + TrustManagers() TrustManagerInterface +} + +// TrustManagerInterface has methods to work with TrustManager resources. +type TrustManagerInterface interface { + Create(ctx context.Context, trustManager *operatorv1alpha1.TrustManager, opts v1.CreateOptions) (*operatorv1alpha1.TrustManager, error) + Update(ctx context.Context, trustManager *operatorv1alpha1.TrustManager, opts v1.UpdateOptions) (*operatorv1alpha1.TrustManager, error) + // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). + UpdateStatus(ctx context.Context, trustManager *operatorv1alpha1.TrustManager, opts v1.UpdateOptions) (*operatorv1alpha1.TrustManager, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*operatorv1alpha1.TrustManager, error) + List(ctx context.Context, opts v1.ListOptions) (*operatorv1alpha1.TrustManagerList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *operatorv1alpha1.TrustManager, err error) + Apply(ctx context.Context, trustManager *applyconfigurationsoperatorv1alpha1.TrustManagerApplyConfiguration, opts v1.ApplyOptions) (result *operatorv1alpha1.TrustManager, err error) + // Add a +genclient:noStatus comment above the type to avoid generating ApplyStatus(). + ApplyStatus(ctx context.Context, trustManager *applyconfigurationsoperatorv1alpha1.TrustManagerApplyConfiguration, opts v1.ApplyOptions) (result *operatorv1alpha1.TrustManager, err error) + TrustManagerExpansion +} + +// trustManagers implements TrustManagerInterface +type trustManagers struct { + *gentype.ClientWithListAndApply[*operatorv1alpha1.TrustManager, *operatorv1alpha1.TrustManagerList, *applyconfigurationsoperatorv1alpha1.TrustManagerApplyConfiguration] +} + +// newTrustManagers returns a TrustManagers +func newTrustManagers(c *OperatorV1alpha1Client) *trustManagers { + return &trustManagers{ + gentype.NewClientWithListAndApply[*operatorv1alpha1.TrustManager, *operatorv1alpha1.TrustManagerList, *applyconfigurationsoperatorv1alpha1.TrustManagerApplyConfiguration]( + "trustmanagers", + c.RESTClient(), + scheme.ParameterCodec, + "", + func() *operatorv1alpha1.TrustManager { return &operatorv1alpha1.TrustManager{} }, + func() *operatorv1alpha1.TrustManagerList { return &operatorv1alpha1.TrustManagerList{} }, + ), + } +} diff --git a/pkg/operator/informers/externalversions/generic.go b/pkg/operator/informers/externalversions/generic.go index 0c542fe66..7dc954ca9 100644 --- a/pkg/operator/informers/externalversions/generic.go +++ b/pkg/operator/informers/externalversions/generic.go @@ -41,6 +41,8 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource return &genericInformer{resource: resource.GroupResource(), informer: f.Operator().V1alpha1().CertManagers().Informer()}, nil case v1alpha1.SchemeGroupVersion.WithResource("istiocsrs"): return &genericInformer{resource: resource.GroupResource(), informer: f.Operator().V1alpha1().IstioCSRs().Informer()}, nil + case v1alpha1.SchemeGroupVersion.WithResource("trustmanagers"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Operator().V1alpha1().TrustManagers().Informer()}, nil } diff --git a/pkg/operator/informers/externalversions/operator/v1alpha1/interface.go b/pkg/operator/informers/externalversions/operator/v1alpha1/interface.go index 5eb8c8ede..422750840 100644 --- a/pkg/operator/informers/externalversions/operator/v1alpha1/interface.go +++ b/pkg/operator/informers/externalversions/operator/v1alpha1/interface.go @@ -12,6 +12,8 @@ type Interface interface { CertManagers() CertManagerInformer // IstioCSRs returns a IstioCSRInformer. IstioCSRs() IstioCSRInformer + // TrustManagers returns a TrustManagerInformer. + TrustManagers() TrustManagerInformer } type version struct { @@ -34,3 +36,8 @@ func (v *version) CertManagers() CertManagerInformer { func (v *version) IstioCSRs() IstioCSRInformer { return &istioCSRInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } + +// TrustManagers returns a TrustManagerInformer. +func (v *version) TrustManagers() TrustManagerInformer { + return &trustManagerInformer{factory: v.factory, tweakListOptions: v.tweakListOptions} +} diff --git a/pkg/operator/informers/externalversions/operator/v1alpha1/trustmanager.go b/pkg/operator/informers/externalversions/operator/v1alpha1/trustmanager.go new file mode 100644 index 000000000..cdb0943ba --- /dev/null +++ b/pkg/operator/informers/externalversions/operator/v1alpha1/trustmanager.go @@ -0,0 +1,85 @@ +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + context "context" + time "time" + + apioperatorv1alpha1 "github.com/openshift/cert-manager-operator/api/operator/v1alpha1" + versioned "github.com/openshift/cert-manager-operator/pkg/operator/clientset/versioned" + internalinterfaces "github.com/openshift/cert-manager-operator/pkg/operator/informers/externalversions/internalinterfaces" + operatorv1alpha1 "github.com/openshift/cert-manager-operator/pkg/operator/listers/operator/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// TrustManagerInformer provides access to a shared informer and lister for +// TrustManagers. +type TrustManagerInformer interface { + Informer() cache.SharedIndexInformer + Lister() operatorv1alpha1.TrustManagerLister +} + +type trustManagerInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// NewTrustManagerInformer constructs a new informer for TrustManager type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewTrustManagerInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredTrustManagerInformer(client, resyncPeriod, indexers, nil) +} + +// NewFilteredTrustManagerInformer constructs a new informer for TrustManager type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredTrustManagerInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.OperatorV1alpha1().TrustManagers().List(context.Background(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.OperatorV1alpha1().TrustManagers().Watch(context.Background(), options) + }, + ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.OperatorV1alpha1().TrustManagers().List(ctx, options) + }, + WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.OperatorV1alpha1().TrustManagers().Watch(ctx, options) + }, + }, + &apioperatorv1alpha1.TrustManager{}, + resyncPeriod, + indexers, + ) +} + +func (f *trustManagerInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredTrustManagerInformer(client, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *trustManagerInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&apioperatorv1alpha1.TrustManager{}, f.defaultInformer) +} + +func (f *trustManagerInformer) Lister() operatorv1alpha1.TrustManagerLister { + return operatorv1alpha1.NewTrustManagerLister(f.Informer().GetIndexer()) +} diff --git a/pkg/operator/listers/operator/v1alpha1/expansion_generated.go b/pkg/operator/listers/operator/v1alpha1/expansion_generated.go index c91ed34e9..1692896d0 100644 --- a/pkg/operator/listers/operator/v1alpha1/expansion_generated.go +++ b/pkg/operator/listers/operator/v1alpha1/expansion_generated.go @@ -13,3 +13,7 @@ type IstioCSRListerExpansion interface{} // IstioCSRNamespaceListerExpansion allows custom methods to be added to // IstioCSRNamespaceLister. type IstioCSRNamespaceListerExpansion interface{} + +// TrustManagerListerExpansion allows custom methods to be added to +// TrustManagerLister. +type TrustManagerListerExpansion interface{} diff --git a/pkg/operator/listers/operator/v1alpha1/trustmanager.go b/pkg/operator/listers/operator/v1alpha1/trustmanager.go new file mode 100644 index 000000000..96293ae92 --- /dev/null +++ b/pkg/operator/listers/operator/v1alpha1/trustmanager.go @@ -0,0 +1,32 @@ +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + operatorv1alpha1 "github.com/openshift/cert-manager-operator/api/operator/v1alpha1" + labels "k8s.io/apimachinery/pkg/labels" + listers "k8s.io/client-go/listers" + cache "k8s.io/client-go/tools/cache" +) + +// TrustManagerLister helps list TrustManagers. +// All objects returned here must be treated as read-only. +type TrustManagerLister interface { + // List lists all TrustManagers in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*operatorv1alpha1.TrustManager, err error) + // Get retrieves the TrustManager from the index for a given name. + // Objects returned here must be treated as read-only. + Get(name string) (*operatorv1alpha1.TrustManager, error) + TrustManagerListerExpansion +} + +// trustManagerLister implements the TrustManagerLister interface. +type trustManagerLister struct { + listers.ResourceIndexer[*operatorv1alpha1.TrustManager] +} + +// NewTrustManagerLister returns a new TrustManagerLister. +func NewTrustManagerLister(indexer cache.Indexer) TrustManagerLister { + return &trustManagerLister{listers.New[*operatorv1alpha1.TrustManager](indexer, operatorv1alpha1.Resource("trustmanager"))} +} From 5ed70e7f9d098d9cea69a3e45ace2961a7c305c4 Mon Sep 17 00:00:00 2001 From: openshift-app-platform-shift-bot <267347085+openshift-app-platform-shift-bot@users.noreply.github.com> Date: Fri, 8 May 2026 12:59:51 +0000 Subject: [PATCH 2/3] CM-830: Add TrustManager controller for trust-manager operand management Implements the controller-runtime based reconciler for the TrustManager CRD. The controller watches trustmanagers.operator.openshift.io resources and reconciles the trust-manager operand deployment in the cert-manager namespace, including ServiceAccount, RBAC, Certificates, Services, DefaultCAPackage ConfigMap, and Deployment resources. The controller follows the same patterns as the IstioCSR controller: - Feature-gated via FeatureTrustManager (Alpha, default disabled) - Separate controller-runtime manager with its own cache builder - Non-destructive cleanup on CR deletion per EP non-goals - Dynamic RBAC for SecretTargets policy - DefaultCAPackage integration with CNO trusted CA bundle injection Co-Authored-By: Claude Opus 4.6 --- pkg/controller/trustmanager/client.go | 114 +++ pkg/controller/trustmanager/constants.go | 101 +++ pkg/controller/trustmanager/controller.go | 310 +++++++ .../trustmanager/install_trustmanager.go | 758 ++++++++++++++++++ pkg/controller/trustmanager/utils.go | 234 ++++++ pkg/operator/setup_manager.go | 38 +- pkg/operator/starter.go | 25 +- 7 files changed, 1574 insertions(+), 6 deletions(-) create mode 100644 pkg/controller/trustmanager/client.go create mode 100644 pkg/controller/trustmanager/constants.go create mode 100644 pkg/controller/trustmanager/controller.go create mode 100644 pkg/controller/trustmanager/install_trustmanager.go create mode 100644 pkg/controller/trustmanager/utils.go diff --git a/pkg/controller/trustmanager/client.go b/pkg/controller/trustmanager/client.go new file mode 100644 index 000000000..5428527cc --- /dev/null +++ b/pkg/controller/trustmanager/client.go @@ -0,0 +1,114 @@ +package trustmanager + +import ( + "context" + "fmt" + "reflect" + + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/client-go/util/retry" + + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/manager" +) + +type ctrlClientImpl struct { + client.Client +} + +//go:generate go run github.com/maxbrunsfeld/counterfeiter/v6 -generate +//counterfeiter:generate -o fakes . ctrlClient +type ctrlClient interface { + Get(context.Context, client.ObjectKey, client.Object) error + List(context.Context, client.ObjectList, ...client.ListOption) error + StatusUpdate(context.Context, client.Object, ...client.SubResourceUpdateOption) error + Update(context.Context, client.Object, ...client.UpdateOption) error + UpdateWithRetry(context.Context, client.Object, ...client.UpdateOption) error + Create(context.Context, client.Object, ...client.CreateOption) error + Delete(context.Context, client.Object, ...client.DeleteOption) error + Patch(context.Context, client.Object, client.Patch, ...client.PatchOption) error + Exists(context.Context, client.ObjectKey, client.Object) (bool, error) +} + +func NewClient(m manager.Manager) (ctrlClient, error) { + // Use the manager's client directly instead of creating a custom client. + // The manager's client uses the manager's cache, which ensures the reconciler + // reads from the same cache that the controller's watches use, preventing + // cache mismatch issues. + return &ctrlClientImpl{ + Client: m.GetClient(), + }, nil +} + +func (c *ctrlClientImpl) Get( + ctx context.Context, key client.ObjectKey, obj client.Object, +) error { + return c.Client.Get(ctx, key, obj) +} + +func (c *ctrlClientImpl) List( + ctx context.Context, list client.ObjectList, opts ...client.ListOption, +) error { + return c.Client.List(ctx, list, opts...) +} + +func (c *ctrlClientImpl) Create( + ctx context.Context, obj client.Object, opts ...client.CreateOption, +) error { + return c.Client.Create(ctx, obj, opts...) +} + +func (c *ctrlClientImpl) Delete( + ctx context.Context, obj client.Object, opts ...client.DeleteOption, +) error { + return c.Client.Delete(ctx, obj, opts...) +} + +func (c *ctrlClientImpl) Update( + ctx context.Context, obj client.Object, opts ...client.UpdateOption, +) error { + return c.Client.Update(ctx, obj, opts...) +} + +func (c *ctrlClientImpl) UpdateWithRetry( + ctx context.Context, obj client.Object, opts ...client.UpdateOption, +) error { + key := client.ObjectKeyFromObject(obj) + if err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + current := reflect.New(reflect.TypeOf(obj).Elem()).Interface().(client.Object) + if err := c.Client.Get(ctx, key, current); err != nil { + return fmt.Errorf("failed to fetch latest %q for update: %w", key, err) + } + obj.SetResourceVersion(current.GetResourceVersion()) + if err := c.Client.Update(ctx, obj, opts...); err != nil { + return fmt.Errorf("failed to update %q resource: %w", key, err) + } + return nil + }); err != nil { + return err + } + + return nil +} + +func (c *ctrlClientImpl) StatusUpdate( + ctx context.Context, obj client.Object, opts ...client.SubResourceUpdateOption, +) error { + return c.Client.Status().Update(ctx, obj, opts...) +} + +func (c *ctrlClientImpl) Patch( + ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption, +) error { + return c.Client.Patch(ctx, obj, patch, opts...) +} + +func (c *ctrlClientImpl) Exists(ctx context.Context, key client.ObjectKey, obj client.Object) (bool, error) { + if err := c.Client.Get(ctx, key, obj); err != nil { + if errors.IsNotFound(err) { + return false, nil + } + return false, err + } + return true, nil +} diff --git a/pkg/controller/trustmanager/constants.go b/pkg/controller/trustmanager/constants.go new file mode 100644 index 000000000..b7c5271e1 --- /dev/null +++ b/pkg/controller/trustmanager/constants.go @@ -0,0 +1,101 @@ +package trustmanager + +import ( + "os" + "time" +) + +const ( + // trustManagerCommonName is the name commonly used for naming resources. + trustManagerCommonName = "cert-manager-trust-manager" + + // ControllerName is the name of the controller used in logs and events. + ControllerName = trustManagerCommonName + "-controller" + + // controllerProcessedAnnotation is the annotation added to trustmanager resource once after + // successful reconciliation by the controller. + controllerProcessedAnnotation = "operator.openshift.io/trust-manager-processed" + + // finalizer name for trustmanagers.operator.openshift.io resource. + finalizer = "trustmanagers.operator.openshift.io/" + ControllerName + + // defaultRequeueTime is the default reconcile requeue time. + defaultRequeueTime = time.Second * 30 + + // trustManagerObjectName is the name of the trust-manager resource created by user. + // trust-manager CRD enforces name to be `cluster`. + trustManagerObjectName = "cluster" + + // trustManagerContainerName is the name of the container created for trust-manager. + trustManagerContainerName = "trust-manager" + + // trustManagerImageNameEnvVarName is the environment variable key name + // containing the image name of the trust-manager as value. + trustManagerImageNameEnvVarName = "RELATED_IMAGE_CERT_MANAGER_TRUST_MANAGER" + + // trustManagerImageVersionEnvVarName is the environment variable key name + // containing the image version of the trust-manager as value. + trustManagerImageVersionEnvVarName = "TRUST_MANAGER_OPERAND_IMAGE_VERSION" + + // operandNamespace is the namespace where trust-manager is deployed. + operandNamespace = "cert-manager" + + // operatorNamespace is the namespace where the cert-manager operator is deployed. + operatorNamespace = "cert-manager-operator" + + // defaultCAPackageConfigMapName is the name of the ConfigMap created by the controller + // containing the formatted CA package for trust-manager. + defaultCAPackageConfigMapName = "trust-manager-default-ca-package" + + // trustedCABundleConfigMapName is the name of the ConfigMap in the operator namespace + // that receives the injected CA bundle from CNO. + trustedCABundleConfigMapName = "cert-manager-operator-trusted-ca-bundle" + + // caPackageJSONName is the name of the JSON package file within the ConfigMap. + caPackageJSONName = "cert-manager-package-openshift.json" + + // caPackageMountPath is the path where the default CA package is mounted in the trust-manager container. + caPackageMountPath = "/packages" + + // defaultCAPackageHashAnnotation is the annotation on the Deployment storing the hash + // of the default CA package content, used to trigger rolling restarts when the package changes. + defaultCAPackageHashAnnotation = "operator.openshift.io/default-ca-package-hash" + + // webhookPort is the port used by the trust-manager webhook. + webhookPort int32 = 6443 + + // metricsPort is the port used by the trust-manager metrics endpoint. + metricsPort int32 = 9402 + + // readinessProbePort is the port used for the trust-manager readiness probe. + readinessProbePort int32 = 6060 +) + +var ( + controllerDefaultResourceLabels = map[string]string{ + "app": trustManagerCommonName, + "app.kubernetes.io/name": trustManagerCommonName, + "app.kubernetes.io/instance": trustManagerCommonName, + "app.kubernetes.io/version": os.Getenv(trustManagerImageVersionEnvVarName), + "app.kubernetes.io/managed-by": "cert-manager-operator", + "app.kubernetes.io/part-of": "cert-manager-operator", + } +) + +// asset names are the files present in the root bindata/ dir. Which are then loaded +// and made available by the pkg/operator/assets package. +const ( + deploymentAssetName = "trust-manager/trust-manager-deployment.yaml" + clusterRoleAssetName = "trust-manager/trust-manager-clusterrole.yaml" + clusterRoleBindingAssetName = "trust-manager/trust-manager-clusterrolebinding.yaml" + roleAssetName = "trust-manager/trust-manager-role.yaml" + roleLeasesAssetName = "trust-manager/trust-manager-leaderelection-role.yaml" + roleBindingAssetName = "trust-manager/trust-manager-rolebinding.yaml" + roleBindingLeasesAssetName = "trust-manager/trust-manager-leaderelection-rolebinding.yaml" + serviceAssetName = "trust-manager/trust-manager-service.yaml" + metricsServiceAssetName = "trust-manager/trust-manager-metrics-service.yaml" + serviceAccountAssetName = "trust-manager/trust-manager-serviceaccount.yaml" + certificateAssetName = "trust-manager/trust-manager-certificate.yaml" + issuerAssetName = "trust-manager/trust-manager-issuer.yaml" + webhookAssetName = "trust-manager/trust-manager-validatingwebhookconfiguration.yaml" +) diff --git a/pkg/controller/trustmanager/controller.go b/pkg/controller/trustmanager/controller.go new file mode 100644 index 000000000..0b1b6dd55 --- /dev/null +++ b/pkg/controller/trustmanager/controller.go @@ -0,0 +1,310 @@ +package trustmanager + +import ( + "context" + "fmt" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/selection" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/record" + + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/cache" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/go-logr/logr" + + certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" + + v1alpha1 "github.com/openshift/cert-manager-operator/api/operator/v1alpha1" +) + +var ( + // requestEnqueueLabelKey is the label key name used for filtering reconcile + // events to include only the resources created by the controller. + requestEnqueueLabelKey = "app" + + // requestEnqueueLabelValue is the label value used for filtering reconcile + // events to include only the resources created by the controller. + requestEnqueueLabelValue = trustManagerCommonName +) + +// Reconciler reconciles a TrustManager object +type Reconciler struct { + ctrlClient + + ctx context.Context + eventRecorder record.EventRecorder + log logr.Logger + scheme *runtime.Scheme +} + +// +kubebuilder:rbac:groups=operator.openshift.io,resources=trustmanagers,verbs=get;list;watch;update;patch +// +kubebuilder:rbac:groups=operator.openshift.io,resources=trustmanagers/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=operator.openshift.io,resources=trustmanagers/finalizers,verbs=update +// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=core,resources=serviceaccounts,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=core,resources=services,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=core,resources=configmaps,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=core,resources=namespaces,verbs=get;list;watch +// +kubebuilder:rbac:groups=core,resources=events,verbs=create;patch +// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=clusterroles;clusterrolebindings,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=roles;rolebindings,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=cert-manager.io,resources=certificates;issuers,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=admissionregistration.k8s.io,resources=validatingwebhookconfigurations,verbs=get;list;watch;create;update;patch;delete + +// NewCacheBuilder returns a cache builder function configured with label selectors +// for managed resources. This function is used by the manager to create its cache +// to ensure the reconciler reads from the same cache that the controller's watches use. +func NewCacheBuilder(config *rest.Config, opts cache.Options) (cache.Cache, error) { + managedResourceLabelReq, err := labels.NewRequirement(requestEnqueueLabelKey, selection.Equals, []string{requestEnqueueLabelValue}) + if err != nil { + return nil, fmt.Errorf("invalid cache label requirement for %q: %w", requestEnqueueLabelKey, err) + } + managedResourceLabelReqSelector := labels.NewSelector().Add(*managedResourceLabelReq) + + // Configure cache with label selectors for managed resources + opts.ByObject = map[client.Object]cache.ByObject{ + // Explicitly include TrustManager to ensure the cache properly watches and syncs all TrustManager objects + &v1alpha1.TrustManager{}: {}, + // Resources managed by controller (with label selectors) + &certmanagerv1.Certificate{}: { + Label: managedResourceLabelReqSelector, + }, + &certmanagerv1.Issuer{}: { + Label: managedResourceLabelReqSelector, + }, + &appsv1.Deployment{}: { + Label: managedResourceLabelReqSelector, + }, + &rbacv1.ClusterRole{}: { + Label: managedResourceLabelReqSelector, + }, + &rbacv1.ClusterRoleBinding{}: { + Label: managedResourceLabelReqSelector, + }, + &rbacv1.Role{}: { + Label: managedResourceLabelReqSelector, + }, + &rbacv1.RoleBinding{}: { + Label: managedResourceLabelReqSelector, + }, + &corev1.Service{}: { + Label: managedResourceLabelReqSelector, + }, + &corev1.ServiceAccount{}: { + Label: managedResourceLabelReqSelector, + }, + // ConfigMaps in operator and operand namespaces (for DefaultCAPackage) + &corev1.ConfigMap{}: {}, + } + + return cache.New(config, opts) +} + +// New returns a new Reconciler instance. +func New(mgr ctrl.Manager) (*Reconciler, error) { + c, err := NewClient(mgr) + if err != nil { + return nil, err + } + return &Reconciler{ + ctrlClient: c, + ctx: context.Background(), + eventRecorder: mgr.GetEventRecorderFor(ControllerName), + log: ctrl.Log.WithName(ControllerName), + scheme: mgr.GetScheme(), + }, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { + mapFunc := func(ctx context.Context, obj client.Object) []reconcile.Request { + r.log.V(4).Info("received reconcile event", "object", fmt.Sprintf("%T", obj), "name", obj.GetName(), "namespace", obj.GetNamespace()) + + objLabels := obj.GetLabels() + if objLabels != nil && objLabels[requestEnqueueLabelKey] == requestEnqueueLabelValue { + return []reconcile.Request{ + { + NamespacedName: types.NamespacedName{ + Name: trustManagerObjectName, + }, + }, + } + } + + r.log.V(4).Info("object not of interest, ignoring reconcile event", "object", fmt.Sprintf("%T", obj), "name", obj.GetName(), "namespace", obj.GetNamespace()) + return []reconcile.Request{} + } + + // mapConfigMapFunc maps ConfigMap changes to TrustManager reconcile requests. + // This watches the trusted CA bundle ConfigMap in operator namespace and the + // default CA package ConfigMap in operand namespace. + mapConfigMapFunc := func(ctx context.Context, obj client.Object) []reconcile.Request { + name := obj.GetName() + ns := obj.GetNamespace() + if (name == trustedCABundleConfigMapName && ns == operatorNamespace) || + (name == defaultCAPackageConfigMapName && ns == operandNamespace) { + return []reconcile.Request{ + { + NamespacedName: types.NamespacedName{ + Name: trustManagerObjectName, + }, + }, + } + } + + // Also reconcile on managed ConfigMaps + objLabels := obj.GetLabels() + if objLabels != nil && objLabels[requestEnqueueLabelKey] == requestEnqueueLabelValue { + return []reconcile.Request{ + { + NamespacedName: types.NamespacedName{ + Name: trustManagerObjectName, + }, + }, + } + } + + return []reconcile.Request{} + } + + // predicate function to ignore events for objects not managed by controller. + controllerManagedResources := predicate.NewPredicateFuncs(func(object client.Object) bool { + return object.GetLabels() != nil && object.GetLabels()[requestEnqueueLabelKey] == requestEnqueueLabelValue + }) + + withIgnoreStatusUpdatePredicates := builder.WithPredicates(predicate.GenerationChangedPredicate{}, controllerManagedResources) + controllerManagedResourcePredicates := builder.WithPredicates(controllerManagedResources) + + // ConfigMap predicate: watch managed ConfigMaps and the CA bundle ConfigMaps + configMapPredicate := predicate.NewPredicateFuncs(func(object client.Object) bool { + name := object.GetName() + ns := object.GetNamespace() + if (name == trustedCABundleConfigMapName && ns == operatorNamespace) || + (name == defaultCAPackageConfigMapName && ns == operandNamespace) { + return true + } + return object.GetLabels() != nil && object.GetLabels()[requestEnqueueLabelKey] == requestEnqueueLabelValue + }) + configMapPredicates := builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}, configMapPredicate) + + return ctrl.NewControllerManagedBy(mgr). + For(&v1alpha1.TrustManager{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). + Named(ControllerName). + Watches(&certmanagerv1.Certificate{}, handler.EnqueueRequestsFromMapFunc(mapFunc), withIgnoreStatusUpdatePredicates). + Watches(&certmanagerv1.Issuer{}, handler.EnqueueRequestsFromMapFunc(mapFunc), controllerManagedResourcePredicates). + Watches(&appsv1.Deployment{}, handler.EnqueueRequestsFromMapFunc(mapFunc), withIgnoreStatusUpdatePredicates). + Watches(&rbacv1.ClusterRole{}, handler.EnqueueRequestsFromMapFunc(mapFunc), controllerManagedResourcePredicates). + Watches(&rbacv1.ClusterRoleBinding{}, handler.EnqueueRequestsFromMapFunc(mapFunc), controllerManagedResourcePredicates). + Watches(&rbacv1.Role{}, handler.EnqueueRequestsFromMapFunc(mapFunc), controllerManagedResourcePredicates). + Watches(&rbacv1.RoleBinding{}, handler.EnqueueRequestsFromMapFunc(mapFunc), controllerManagedResourcePredicates). + Watches(&corev1.Service{}, handler.EnqueueRequestsFromMapFunc(mapFunc), controllerManagedResourcePredicates). + Watches(&corev1.ServiceAccount{}, handler.EnqueueRequestsFromMapFunc(mapFunc), controllerManagedResourcePredicates). + Watches(&corev1.ConfigMap{}, handler.EnqueueRequestsFromMapFunc(mapConfigMapFunc), configMapPredicates). + Complete(r) +} + +// Reconcile function to compare the state specified by the TrustManager object against the actual cluster state, +// and to make the cluster state reflect the state specified by the user. +func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + r.log.V(1).Info("reconciling", "request", req) + + // Fetch the trustmanagers.operator.openshift.io CR + trustManager := &v1alpha1.TrustManager{} + if err := r.Get(ctx, req.NamespacedName, trustManager); err != nil { + if errors.IsNotFound(err) { + // NotFound errors, since they can't be fixed by an immediate + // requeue (have to wait for a new notification), and can be processed + // on deleted requests. + r.log.V(1).Info("trustmanagers.operator.openshift.io object not found, skipping reconciliation", "request", req) + return ctrl.Result{}, nil + } + return ctrl.Result{}, fmt.Errorf("failed to fetch trustmanagers.operator.openshift.io %q during reconciliation: %w", req.NamespacedName, err) + } + + if !trustManager.DeletionTimestamp.IsZero() { + r.log.V(1).Info("trustmanagers.operator.openshift.io is marked for deletion", "name", req.NamespacedName) + + if err := r.cleanUp(trustManager); err != nil { + return ctrl.Result{}, fmt.Errorf("clean up failed for %q trustmanagers.operator.openshift.io instance deletion: %w", req.NamespacedName, err) + } + + if err := r.removeFinalizer(ctx, trustManager, finalizer); err != nil { + return ctrl.Result{}, err + } + + r.log.V(1).Info("removed finalizer, cleanup complete", "request", req.NamespacedName) + return ctrl.Result{}, nil + } + + // Set finalizers on the trustmanagers.operator.openshift.io resource + if err := r.addFinalizer(ctx, trustManager); err != nil { + return ctrl.Result{}, fmt.Errorf("failed to update %q trustmanagers.operator.openshift.io with finalizers: %w", req.NamespacedName, err) + } + + return r.processReconcileRequest(trustManager, req.NamespacedName) +} + +func (r *Reconciler) processReconcileRequest(trustManager *v1alpha1.TrustManager, req types.NamespacedName) (ctrl.Result, error) { + if err := r.reconcileTrustManagerDeployment(trustManager); err != nil { + r.log.Error(err, "failed to reconcile TrustManager deployment", "request", req) + + // Set both conditions atomically before updating status + degradedChanged := trustManager.Status.SetCondition(v1alpha1.Degraded, metav1.ConditionTrue, v1alpha1.ReasonFailed, fmt.Sprintf("reconciliation failed: %v", err)) + readyChanged := trustManager.Status.SetCondition(v1alpha1.Ready, metav1.ConditionFalse, v1alpha1.ReasonFailed, "") + + if degradedChanged || readyChanged { + r.log.V(2).Info("updating trustmanager conditions on error", + "name", trustManager.GetName(), + "degradedChanged", degradedChanged, + "readyChanged", readyChanged, + "error", err) + if updateErr := r.updateCondition(trustManager, err); updateErr != nil { + return ctrl.Result{}, updateErr + } + } + return ctrl.Result{RequeueAfter: defaultRequeueTime}, nil + } + + // Update status with observed state + r.updateObservedStatus(trustManager) + + // Set both conditions atomically before updating status on success + degradedChanged := trustManager.Status.SetCondition(v1alpha1.Degraded, metav1.ConditionFalse, v1alpha1.ReasonReady, "") + readyChanged := trustManager.Status.SetCondition(v1alpha1.Ready, metav1.ConditionTrue, v1alpha1.ReasonReady, "reconciliation successful") + + if degradedChanged || readyChanged { + r.log.V(2).Info("updating trustmanager conditions on successful reconciliation", + "name", trustManager.GetName(), + "degradedChanged", degradedChanged, + "readyChanged", readyChanged) + if updateErr := r.updateCondition(trustManager, nil); updateErr != nil { + return ctrl.Result{}, updateErr + } + } + return ctrl.Result{}, nil +} + +// cleanUp handles deletion of trustmanagers.operator.openshift.io gracefully. +// Per the EP Non-Goals: deleting the CR only stops reconciliation, it does not +// remove the trust-manager deployment or its associated resources. +func (r *Reconciler) cleanUp(trustManager *v1alpha1.TrustManager) error { + r.eventRecorder.Eventf(trustManager, corev1.EventTypeWarning, "RemoveDeployment", + "trustmanager %s marked for deletion, reconciliation of trust-manager resources will stop. "+ + "Resources created for trust-manager deployment will not be removed automatically.", + trustManager.GetName()) + return nil +} diff --git a/pkg/controller/trustmanager/install_trustmanager.go b/pkg/controller/trustmanager/install_trustmanager.go new file mode 100644 index 000000000..dd9e92d1e --- /dev/null +++ b/pkg/controller/trustmanager/install_trustmanager.go @@ -0,0 +1,758 @@ +package trustmanager + +import ( + "crypto/sha256" + "encoding/json" + "fmt" + "os" + "strings" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" + + certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" + certmanagermetav1 "github.com/cert-manager/cert-manager/pkg/apis/meta/v1" + + "github.com/openshift/cert-manager-operator/api/operator/v1alpha1" +) + +func (r *Reconciler) reconcileTrustManagerDeployment(trustManager *v1alpha1.TrustManager) error { + if err := validateTrustManagerConfig(trustManager); err != nil { + return fmt.Errorf("%s configuration validation failed: %w", trustManager.GetName(), err) + } + + // Merge user-provided labels with controller's own default labels. + resourceLabels := make(map[string]string) + if len(trustManager.Spec.ControllerConfig.Labels) != 0 { + for k, v := range trustManager.Spec.ControllerConfig.Labels { + resourceLabels[k] = v + } + } + for k, v := range controllerDefaultResourceLabels { + resourceLabels[k] = v + } + + // Validate trust namespace exists + trustNamespace := trustManager.Spec.TrustManagerConfig.TrustNamespace + if trustNamespace == "" { + trustNamespace = operandNamespace + } + if err := r.validateNamespaceExists(trustNamespace); err != nil { + return fmt.Errorf("trust namespace %q does not exist: %w", trustNamespace, err) + } + + // Step 1: Create ServiceAccount + if err := r.reconcileServiceAccount(trustManager, resourceLabels); err != nil { + r.log.Error(err, "failed to reconcile ServiceAccount") + return err + } + + // Step 2: Create RBAC resources + if err := r.reconcileRBAC(trustManager, resourceLabels, trustNamespace); err != nil { + r.log.Error(err, "failed to reconcile RBAC resources") + return err + } + + // Step 3: Create Certificate and Issuer for webhook TLS + if err := r.reconcileCertificates(trustManager, resourceLabels); err != nil { + r.log.Error(err, "failed to reconcile Certificate resources") + return err + } + + // Step 4: Create Services + if err := r.reconcileServices(trustManager, resourceLabels); err != nil { + r.log.Error(err, "failed to reconcile Service resources") + return err + } + + // Step 5: Handle DefaultCAPackage ConfigMap if enabled + var caPackageHash string + if trustManager.Spec.TrustManagerConfig.DefaultCAPackage.Policy == v1alpha1.DefaultCAPackagePolicyEnabled { + hash, err := r.reconcileDefaultCAPackage(trustManager, resourceLabels) + if err != nil { + r.log.Error(err, "failed to reconcile DefaultCAPackage") + return err + } + caPackageHash = hash + } + + // Step 6: Create Deployment + if err := r.reconcileDeployment(trustManager, resourceLabels, trustNamespace, caPackageHash); err != nil { + r.log.Error(err, "failed to reconcile Deployment") + return err + } + + r.log.V(4).Info("finished reconciliation of trustmanager", "name", trustManager.GetName()) + return nil +} + +func (r *Reconciler) validateNamespaceExists(namespace string) error { + ns := &corev1.Namespace{} + if err := r.Get(r.ctx, types.NamespacedName{Name: namespace}, ns); err != nil { + if errors.IsNotFound(err) { + return fmt.Errorf("namespace %q does not exist, please create it before creating the TrustManager CR", namespace) + } + return fmt.Errorf("failed to check namespace %q: %w", namespace, err) + } + return nil +} + +func validateTrustManagerConfig(trustManager *v1alpha1.TrustManager) error { + if trustManager.Spec.TrustManagerConfig.SecretTargets.Policy == v1alpha1.SecretTargetsPolicyCustom { + if len(trustManager.Spec.TrustManagerConfig.SecretTargets.AuthorizedSecrets) == 0 { + return fmt.Errorf("spec.trustManagerConfig.secretTargets.authorizedSecrets must not be empty when policy is Custom") + } + } + return nil +} + +func (r *Reconciler) reconcileServiceAccount(trustManager *v1alpha1.TrustManager, resourceLabels map[string]string) error { + desired := &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: trustManagerContainerName, + Namespace: operandNamespace, + Labels: resourceLabels, + }, + } + + existing := &corev1.ServiceAccount{} + exists, err := r.Exists(r.ctx, types.NamespacedName{Name: desired.Name, Namespace: desired.Namespace}, existing) + if err != nil { + return fmt.Errorf("failed to check ServiceAccount existence: %w", err) + } + + if !exists { + r.log.Info("creating ServiceAccount", "name", desired.Name, "namespace", desired.Namespace) + if err := r.Create(r.ctx, desired); err != nil { + return fmt.Errorf("failed to create ServiceAccount: %w", err) + } + r.eventRecorder.Eventf(trustManager, corev1.EventTypeNormal, "Created", "Created ServiceAccount %s/%s", desired.Namespace, desired.Name) + return nil + } + + if objectMetadataModified(desired, existing) { + existing.Labels = desired.Labels + r.log.Info("updating ServiceAccount", "name", desired.Name, "namespace", desired.Namespace) + if err := r.Update(r.ctx, existing); err != nil { + return fmt.Errorf("failed to update ServiceAccount: %w", err) + } + } + + return nil +} + +func (r *Reconciler) reconcileRBAC(trustManager *v1alpha1.TrustManager, resourceLabels map[string]string, trustNamespace string) error { + // ClusterRole with dynamic rules based on secretTargets + clusterRoleRules := []rbacv1.PolicyRule{ + { + APIGroups: []string{"trust.cert-manager.io"}, + Resources: []string{"bundles"}, + Verbs: []string{"get", "list", "watch"}, + }, + { + APIGroups: []string{"trust.cert-manager.io"}, + Resources: []string{"bundles/finalizers"}, + Verbs: []string{"update"}, + }, + { + APIGroups: []string{"trust.cert-manager.io"}, + Resources: []string{"bundles/status"}, + Verbs: []string{"patch"}, + }, + { + APIGroups: []string{""}, + Resources: []string{"configmaps"}, + Verbs: []string{"get", "list", "create", "update", "patch", "watch", "delete"}, + }, + { + APIGroups: []string{""}, + Resources: []string{"namespaces"}, + Verbs: []string{"get", "list", "watch"}, + }, + { + APIGroups: []string{""}, + Resources: []string{"events"}, + Verbs: []string{"create", "patch"}, + }, + { + APIGroups: []string{""}, + Resources: []string{"secrets"}, + Verbs: []string{"get", "list", "watch"}, + }, + } + + // Add secret write rules when secretTargets policy is Custom + if trustManager.Spec.TrustManagerConfig.SecretTargets.Policy == v1alpha1.SecretTargetsPolicyCustom { + clusterRoleRules = append(clusterRoleRules, rbacv1.PolicyRule{ + APIGroups: []string{""}, + Resources: []string{"secrets"}, + ResourceNames: trustManager.Spec.TrustManagerConfig.SecretTargets.AuthorizedSecrets, + Verbs: []string{"create", "update", "patch", "delete"}, + }) + } + + desiredClusterRole := &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: trustManagerContainerName, + Labels: resourceLabels, + }, + Rules: clusterRoleRules, + } + + if err := r.createOrUpdateClusterRole(desiredClusterRole, trustManager); err != nil { + return fmt.Errorf("failed to reconcile ClusterRole: %w", err) + } + + // ClusterRoleBinding + desiredCRB := &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: trustManagerContainerName, + Labels: resourceLabels, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "ClusterRole", + Name: trustManagerContainerName, + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + Name: trustManagerContainerName, + Namespace: operandNamespace, + }, + }, + } + + if err := r.createOrUpdateClusterRoleBinding(desiredCRB, trustManager); err != nil { + return fmt.Errorf("failed to reconcile ClusterRoleBinding: %w", err) + } + + // Role in trust namespace for secret access + desiredTrustRole := &rbacv1.Role{ + ObjectMeta: metav1.ObjectMeta{ + Name: trustManagerContainerName, + Namespace: trustNamespace, + Labels: resourceLabels, + }, + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"secrets"}, + Verbs: []string{"get", "list", "watch"}, + }, + }, + } + + if err := r.createOrUpdateRole(desiredTrustRole, trustManager); err != nil { + return fmt.Errorf("failed to reconcile Role in trust namespace: %w", err) + } + + // RoleBinding in trust namespace + desiredTrustRB := &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: trustManagerContainerName, + Namespace: trustNamespace, + Labels: resourceLabels, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "Role", + Name: trustManagerContainerName, + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + Name: trustManagerContainerName, + Namespace: operandNamespace, + }, + }, + } + + if err := r.createOrUpdateRoleBinding(desiredTrustRB, trustManager); err != nil { + return fmt.Errorf("failed to reconcile RoleBinding in trust namespace: %w", err) + } + + // Leader election Role in operand namespace + desiredLeaderRole := &rbacv1.Role{ + ObjectMeta: metav1.ObjectMeta{ + Name: trustManagerContainerName + ":leaderelection", + Namespace: operandNamespace, + Labels: resourceLabels, + }, + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{"coordination.k8s.io"}, + Resources: []string{"leases"}, + Verbs: []string{"get", "create", "update", "watch", "list"}, + }, + }, + } + + if err := r.createOrUpdateRole(desiredLeaderRole, trustManager); err != nil { + return fmt.Errorf("failed to reconcile leader election Role: %w", err) + } + + // Leader election RoleBinding in operand namespace + desiredLeaderRB := &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: trustManagerContainerName + ":leaderelection", + Namespace: operandNamespace, + Labels: resourceLabels, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "Role", + Name: trustManagerContainerName + ":leaderelection", + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + Name: trustManagerContainerName, + Namespace: operandNamespace, + }, + }, + } + + if err := r.createOrUpdateRoleBinding(desiredLeaderRB, trustManager); err != nil { + return fmt.Errorf("failed to reconcile leader election RoleBinding: %w", err) + } + + return nil +} + +func (r *Reconciler) reconcileCertificates(trustManager *v1alpha1.TrustManager, resourceLabels map[string]string) error { + // Issuer for webhook TLS + desiredIssuer := &certmanagerv1.Issuer{ + ObjectMeta: metav1.ObjectMeta{ + Name: trustManagerContainerName, + Namespace: operandNamespace, + Labels: resourceLabels, + }, + Spec: certmanagerv1.IssuerSpec{ + IssuerConfig: certmanagerv1.IssuerConfig{ + SelfSigned: &certmanagerv1.SelfSignedIssuer{}, + }, + }, + } + + existingIssuer := &certmanagerv1.Issuer{} + exists, err := r.Exists(r.ctx, types.NamespacedName{Name: desiredIssuer.Name, Namespace: desiredIssuer.Namespace}, existingIssuer) + if err != nil { + return fmt.Errorf("failed to check Issuer existence: %w", err) + } + if !exists { + r.log.Info("creating Issuer", "name", desiredIssuer.Name) + if err := r.Create(r.ctx, desiredIssuer); err != nil { + return fmt.Errorf("failed to create Issuer: %w", err) + } + r.eventRecorder.Eventf(trustManager, corev1.EventTypeNormal, "Created", "Created Issuer %s/%s", desiredIssuer.Namespace, desiredIssuer.Name) + } + + // Certificate for webhook TLS + desiredCert := &certmanagerv1.Certificate{ + ObjectMeta: metav1.ObjectMeta{ + Name: trustManagerContainerName, + Namespace: operandNamespace, + Labels: resourceLabels, + }, + Spec: certmanagerv1.CertificateSpec{ + CommonName: fmt.Sprintf("%s.%s.svc", trustManagerContainerName, operandNamespace), + DNSNames: []string{ + fmt.Sprintf("%s.%s.svc", trustManagerContainerName, operandNamespace), + }, + SecretName: trustManagerContainerName + "-tls", + RevisionHistoryLimit: int32Ptr(1), + IssuerRef: certmanagermetav1.ObjectReference{ + Name: trustManagerContainerName, + Kind: "Issuer", + }, + }, + } + + existingCert := &certmanagerv1.Certificate{} + exists, err = r.Exists(r.ctx, types.NamespacedName{Name: desiredCert.Name, Namespace: desiredCert.Namespace}, existingCert) + if err != nil { + return fmt.Errorf("failed to check Certificate existence: %w", err) + } + if !exists { + r.log.Info("creating Certificate", "name", desiredCert.Name) + if err := r.Create(r.ctx, desiredCert); err != nil { + return fmt.Errorf("failed to create Certificate: %w", err) + } + r.eventRecorder.Eventf(trustManager, corev1.EventTypeNormal, "Created", "Created Certificate %s/%s", desiredCert.Namespace, desiredCert.Name) + } + + return nil +} + +func (r *Reconciler) reconcileServices(trustManager *v1alpha1.TrustManager, resourceLabels map[string]string) error { + // Webhook service + desiredService := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: trustManagerContainerName, + Namespace: operandNamespace, + Labels: resourceLabels, + }, + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeClusterIP, + Selector: map[string]string{ + "app": trustManagerCommonName, + }, + Ports: []corev1.ServicePort{ + { + Name: "webhook", + Port: 443, + TargetPort: intstr.FromInt32(webhookPort), + Protocol: corev1.ProtocolTCP, + }, + }, + }, + } + + existing := &corev1.Service{} + exists, err := r.Exists(r.ctx, types.NamespacedName{Name: desiredService.Name, Namespace: desiredService.Namespace}, existing) + if err != nil { + return fmt.Errorf("failed to check Service existence: %w", err) + } + if !exists { + r.log.Info("creating Service", "name", desiredService.Name) + if err := r.Create(r.ctx, desiredService); err != nil { + return fmt.Errorf("failed to create Service: %w", err) + } + r.eventRecorder.Eventf(trustManager, corev1.EventTypeNormal, "Created", "Created Service %s/%s", desiredService.Namespace, desiredService.Name) + } + + // Metrics service + desiredMetricsService := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: trustManagerContainerName + "-metrics", + Namespace: operandNamespace, + Labels: resourceLabels, + }, + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeClusterIP, + Selector: map[string]string{ + "app": trustManagerCommonName, + }, + Ports: []corev1.ServicePort{ + { + Name: "metrics", + Port: metricsPort, + TargetPort: intstr.FromInt32(metricsPort), + Protocol: corev1.ProtocolTCP, + }, + }, + }, + } + + existingMetrics := &corev1.Service{} + exists, err = r.Exists(r.ctx, types.NamespacedName{Name: desiredMetricsService.Name, Namespace: desiredMetricsService.Namespace}, existingMetrics) + if err != nil { + return fmt.Errorf("failed to check Metrics Service existence: %w", err) + } + if !exists { + r.log.Info("creating Metrics Service", "name", desiredMetricsService.Name) + if err := r.Create(r.ctx, desiredMetricsService); err != nil { + return fmt.Errorf("failed to create Metrics Service: %w", err) + } + r.eventRecorder.Eventf(trustManager, corev1.EventTypeNormal, "Created", "Created Service %s/%s", desiredMetricsService.Namespace, desiredMetricsService.Name) + } + + return nil +} + +func (r *Reconciler) reconcileDefaultCAPackage(trustManager *v1alpha1.TrustManager, resourceLabels map[string]string) (string, error) { + // Read the injected CA bundle from operator namespace + caConfigMap := &corev1.ConfigMap{} + if err := r.Get(r.ctx, types.NamespacedName{Name: trustedCABundleConfigMapName, Namespace: operatorNamespace}, caConfigMap); err != nil { + if errors.IsNotFound(err) { + return "", fmt.Errorf("trusted CA bundle ConfigMap %s/%s not found, waiting for CNO injection", operatorNamespace, trustedCABundleConfigMapName) + } + return "", fmt.Errorf("failed to get trusted CA bundle ConfigMap: %w", err) + } + + caBundle, ok := caConfigMap.Data["ca-bundle.crt"] + if !ok || caBundle == "" { + return "", fmt.Errorf("trusted CA bundle ConfigMap %s/%s does not contain ca-bundle.crt or is empty, waiting for CNO injection", operatorNamespace, trustedCABundleConfigMapName) + } + + // Format into trust-manager expected JSON format + packageJSON := map[string]string{ + "name": "cert-manager-package-openshift", + "bundle": caBundle, + "version": caConfigMap.ResourceVersion, + } + packageBytes, err := json.Marshal(packageJSON) + if err != nil { + return "", fmt.Errorf("failed to marshal CA package JSON: %w", err) + } + + // Compute hash of the package for deployment annotation + hash := fmt.Sprintf("%x", sha256.Sum256(packageBytes)) + + // Create or update the package ConfigMap in operand namespace + desiredCM := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: defaultCAPackageConfigMapName, + Namespace: operandNamespace, + Labels: resourceLabels, + }, + Data: map[string]string{ + caPackageJSONName: string(packageBytes), + }, + } + + existingCM := &corev1.ConfigMap{} + exists, err := r.Exists(r.ctx, types.NamespacedName{Name: desiredCM.Name, Namespace: desiredCM.Namespace}, existingCM) + if err != nil { + return "", fmt.Errorf("failed to check DefaultCAPackage ConfigMap existence: %w", err) + } + + if !exists { + r.log.Info("creating DefaultCAPackage ConfigMap", "name", desiredCM.Name, "namespace", desiredCM.Namespace) + if err := r.Create(r.ctx, desiredCM); err != nil { + return "", fmt.Errorf("failed to create DefaultCAPackage ConfigMap: %w", err) + } + r.eventRecorder.Eventf(trustManager, corev1.EventTypeNormal, "Created", "Created DefaultCAPackage ConfigMap %s/%s", desiredCM.Namespace, desiredCM.Name) + } else if configMapDataModified(desiredCM, existingCM) { + existingCM.Data = desiredCM.Data + existingCM.Labels = desiredCM.Labels + r.log.Info("updating DefaultCAPackage ConfigMap", "name", desiredCM.Name, "namespace", desiredCM.Namespace) + if err := r.Update(r.ctx, existingCM); err != nil { + return "", fmt.Errorf("failed to update DefaultCAPackage ConfigMap: %w", err) + } + r.eventRecorder.Eventf(trustManager, corev1.EventTypeNormal, "Updated", "Updated DefaultCAPackage ConfigMap %s/%s", desiredCM.Namespace, desiredCM.Name) + } + + return hash, nil +} + +func (r *Reconciler) reconcileDeployment(trustManager *v1alpha1.TrustManager, resourceLabels map[string]string, trustNamespace string, caPackageHash string) error { + image := os.Getenv(trustManagerImageNameEnvVarName) + if image == "" { + return fmt.Errorf("environment variable %s is not set", trustManagerImageNameEnvVarName) + } + + // Build container args based on spec + args := buildContainerArgs(trustManager, trustNamespace) + + // Build volumes and volume mounts + volumes := []corev1.Volume{ + { + Name: "tls", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: trustManagerContainerName + "-tls", + }, + }, + }, + } + + volumeMounts := []corev1.VolumeMount{ + { + Name: "tls", + MountPath: "/tls", + ReadOnly: true, + }, + } + + // Add default CA package volume if enabled + if trustManager.Spec.TrustManagerConfig.DefaultCAPackage.Policy == v1alpha1.DefaultCAPackagePolicyEnabled { + volumes = append(volumes, corev1.Volume{ + Name: "default-ca-package", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: defaultCAPackageConfigMapName, + }, + }, + }, + }) + volumeMounts = append(volumeMounts, corev1.VolumeMount{ + Name: "default-ca-package", + MountPath: caPackageMountPath, + ReadOnly: true, + }) + } + + // Build resource requirements + resourceRequirements := trustManager.Spec.TrustManagerConfig.Resources + if resourceRequirements.Requests == nil && resourceRequirements.Limits == nil { + // Set sensible defaults if none provided + resourceRequirements = corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("50m"), + corev1.ResourceMemory: resource.MustParse("64Mi"), + }, + } + } + + replicas := int32(1) + podTemplateAnnotations := make(map[string]string) + if caPackageHash != "" { + podTemplateAnnotations[defaultCAPackageHashAnnotation] = caPackageHash + } + + desired := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: trustManagerContainerName, + Namespace: operandNamespace, + Labels: resourceLabels, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: &replicas, + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": trustManagerCommonName, + }, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: resourceLabels, + Annotations: podTemplateAnnotations, + }, + Spec: corev1.PodSpec{ + ServiceAccountName: trustManagerContainerName, + NodeSelector: trustManager.Spec.TrustManagerConfig.NodeSelector, + Tolerations: trustManager.Spec.TrustManagerConfig.Tolerations, + Affinity: trustManager.Spec.TrustManagerConfig.Affinity, + Containers: []corev1.Container{ + { + Name: trustManagerContainerName, + Image: image, + ImagePullPolicy: corev1.PullIfNotPresent, + Args: args, + Ports: []corev1.ContainerPort{ + { + ContainerPort: webhookPort, + }, + { + ContainerPort: metricsPort, + }, + }, + ReadinessProbe: &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{ + Port: intstr.FromInt32(readinessProbePort), + Path: "/readyz", + }, + }, + InitialDelaySeconds: 5, + PeriodSeconds: 10, + }, + Resources: resourceRequirements, + VolumeMounts: volumeMounts, + }, + }, + Volumes: volumes, + }, + }, + }, + } + + existingDeploy := &appsv1.Deployment{} + exists, err := r.Exists(r.ctx, types.NamespacedName{Name: desired.Name, Namespace: desired.Namespace}, existingDeploy) + if err != nil { + return fmt.Errorf("failed to check Deployment existence: %w", err) + } + + if !exists { + r.log.Info("creating Deployment", "name", desired.Name, "namespace", desired.Namespace) + if err := r.Create(r.ctx, desired); err != nil { + return fmt.Errorf("failed to create Deployment: %w", err) + } + r.eventRecorder.Eventf(trustManager, corev1.EventTypeNormal, "Created", "Created Deployment %s/%s", desired.Namespace, desired.Name) + return nil + } + + if deploymentNeedsUpdate(desired, existingDeploy) { + existingDeploy.Spec = desired.Spec + existingDeploy.Labels = desired.Labels + r.log.Info("updating Deployment", "name", desired.Name, "namespace", desired.Namespace) + if err := r.Update(r.ctx, existingDeploy); err != nil { + return fmt.Errorf("failed to update Deployment: %w", err) + } + r.eventRecorder.Eventf(trustManager, corev1.EventTypeNormal, "Updated", "Updated Deployment %s/%s", desired.Namespace, desired.Name) + } + + return nil +} + +func buildContainerArgs(trustManager *v1alpha1.TrustManager, trustNamespace string) []string { + args := []string{ + fmt.Sprintf("--log-format=%s", trustManager.Spec.TrustManagerConfig.LogFormat), + fmt.Sprintf("--log-level=%d", trustManager.Spec.TrustManagerConfig.LogLevel), + fmt.Sprintf("--metrics-port=%d", metricsPort), + fmt.Sprintf("--readiness-probe-port=%d", readinessProbePort), + "--readiness-probe-path=/readyz", + fmt.Sprintf("--trust-namespace=%s", trustNamespace), + "--webhook-host=0.0.0.0", + fmt.Sprintf("--webhook-port=%d", webhookPort), + "--webhook-certificate-dir=/tls", + } + + if trustManager.Spec.TrustManagerConfig.SecretTargets.Policy == v1alpha1.SecretTargetsPolicyCustom { + args = append(args, "--secret-targets-enabled=true") + } + + if trustManager.Spec.TrustManagerConfig.DefaultCAPackage.Policy == v1alpha1.DefaultCAPackagePolicyEnabled { + args = append(args, fmt.Sprintf("--default-package-location=%s/%s", caPackageMountPath, caPackageJSONName)) + } + + if trustManager.Spec.TrustManagerConfig.FilterExpiredCertificates == v1alpha1.FilterExpiredCertificatesPolicyEnabled { + args = append(args, "--filter-expired-certificates=true") + } + + return args +} + +func deploymentNeedsUpdate(desired, existing *appsv1.Deployment) bool { + if len(desired.Spec.Template.Spec.Containers) == 0 || len(existing.Spec.Template.Spec.Containers) == 0 { + return true + } + + desiredContainer := desired.Spec.Template.Spec.Containers[0] + existingContainer := existing.Spec.Template.Spec.Containers[0] + + if desiredContainer.Image != existingContainer.Image { + return true + } + + if !stringSlicesEqual(desiredContainer.Args, existingContainer.Args) { + return true + } + + // Check pod template annotations (for CA package hash) + desiredAnnotations := desired.Spec.Template.Annotations + existingAnnotations := existing.Spec.Template.Annotations + if desiredAnnotations == nil { + desiredAnnotations = map[string]string{} + } + if existingAnnotations == nil { + existingAnnotations = map[string]string{} + } + if desiredAnnotations[defaultCAPackageHashAnnotation] != existingAnnotations[defaultCAPackageHashAnnotation] { + return true + } + + return false +} + +func stringSlicesEqual(a, b []string) bool { + if len(a) != len(b) { + return false + } + aStr := strings.Join(a, ",") + bStr := strings.Join(b, ",") + return aStr == bStr +} + +func int32Ptr(val int32) *int32 { + return &val +} diff --git a/pkg/controller/trustmanager/utils.go b/pkg/controller/trustmanager/utils.go new file mode 100644 index 000000000..16cfacfc7 --- /dev/null +++ b/pkg/controller/trustmanager/utils.go @@ -0,0 +1,234 @@ +package trustmanager + +import ( + "context" + "fmt" + "os" + "reflect" + + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + utilerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/client-go/util/retry" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + "github.com/openshift/cert-manager-operator/api/operator/v1alpha1" +) + +// updateStatus is for updating the status subresource of trustmanagers.operator.openshift.io. +func (r *Reconciler) updateStatus(ctx context.Context, changed *v1alpha1.TrustManager) error { + namespacedName := client.ObjectKeyFromObject(changed) + if err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + r.log.V(4).Info("updating trustmanagers.operator.openshift.io status", "request", namespacedName) + current := &v1alpha1.TrustManager{} + if err := r.Get(ctx, namespacedName, current); err != nil { + return fmt.Errorf("failed to fetch trustmanagers.operator.openshift.io %q for status update: %w", namespacedName, err) + } + changed.Status.DeepCopyInto(¤t.Status) + + if err := r.StatusUpdate(ctx, current); err != nil { + return fmt.Errorf("failed to update trustmanagers.operator.openshift.io %q status: %w", namespacedName, err) + } + + return nil + }); err != nil { + return err + } + + return nil +} + +// addFinalizer adds finalizer to trustmanagers.operator.openshift.io resource. +func (r *Reconciler) addFinalizer(ctx context.Context, trustManager *v1alpha1.TrustManager) error { + namespacedName := client.ObjectKeyFromObject(trustManager) + if !controllerutil.ContainsFinalizer(trustManager, finalizer) { + if !controllerutil.AddFinalizer(trustManager, finalizer) { + return fmt.Errorf("failed to create %q trustmanagers.operator.openshift.io object with finalizers added", namespacedName) + } + + // update trustmanagers.operator.openshift.io on adding finalizer. + if err := r.UpdateWithRetry(ctx, trustManager); err != nil { + return fmt.Errorf("failed to add finalizers on %q trustmanagers.operator.openshift.io with %w", namespacedName, err) + } + + updated := &v1alpha1.TrustManager{} + if err := r.Get(ctx, namespacedName, updated); err != nil { + return fmt.Errorf("failed to fetch trustmanagers.operator.openshift.io %q after updating finalizers: %w", namespacedName, err) + } + updated.DeepCopyInto(trustManager) + return nil + } + return nil +} + +// removeFinalizer removes finalizers added to trustmanagers.operator.openshift.io resource. +func (r *Reconciler) removeFinalizer(ctx context.Context, trustManager *v1alpha1.TrustManager, finalizer string) error { + namespacedName := client.ObjectKeyFromObject(trustManager) + if controllerutil.ContainsFinalizer(trustManager, finalizer) { + if !controllerutil.RemoveFinalizer(trustManager, finalizer) { + return fmt.Errorf("failed to create %q trustmanagers.operator.openshift.io object with finalizers removed", namespacedName) + } + + if err := r.UpdateWithRetry(ctx, trustManager); err != nil { + return fmt.Errorf("failed to remove finalizers on %q trustmanagers.operator.openshift.io with %w", namespacedName, err) + } + return nil + } + + return nil +} + +// updateCondition updates the status of the trustmanagers.operator.openshift.io resource. +func (r *Reconciler) updateCondition(trustManager *v1alpha1.TrustManager, prependErr error) error { + if err := r.updateStatus(r.ctx, trustManager); err != nil { + errUpdate := fmt.Errorf("failed to update %s status: %w", trustManager.GetName(), err) + if prependErr != nil { + return utilerrors.NewAggregate([]error{prependErr, errUpdate}) + } + return errUpdate + } + return prependErr +} + +// updateObservedStatus updates the observed state fields in the TrustManager status. +func (r *Reconciler) updateObservedStatus(trustManager *v1alpha1.TrustManager) { + trustManager.Status.TrustManagerImage = os.Getenv(trustManagerImageNameEnvVarName) + trustManager.Status.TrustNamespace = trustManager.Spec.TrustManagerConfig.TrustNamespace + if trustManager.Status.TrustNamespace == "" { + trustManager.Status.TrustNamespace = operandNamespace + } + trustManager.Status.SecretTargetsPolicy = trustManager.Spec.TrustManagerConfig.SecretTargets.Policy + trustManager.Status.DefaultCAPackagePolicy = trustManager.Spec.TrustManagerConfig.DefaultCAPackage.Policy + trustManager.Status.FilterExpiredCertificatesPolicy = trustManager.Spec.TrustManagerConfig.FilterExpiredCertificates +} + +func objectMetadataModified(desired, fetched client.Object) bool { + return !reflect.DeepEqual(desired.GetLabels(), fetched.GetLabels()) +} + +func configMapDataModified(desired, fetched *corev1.ConfigMap) bool { + return !reflect.DeepEqual(desired.Data, fetched.Data) +} + +func (r *Reconciler) createOrUpdateClusterRole(desired *rbacv1.ClusterRole, trustManager *v1alpha1.TrustManager) error { + existing := &rbacv1.ClusterRole{} + exists, err := r.Exists(r.ctx, client.ObjectKeyFromObject(desired), existing) + if err != nil { + return fmt.Errorf("failed to check ClusterRole %q existence: %w", desired.Name, err) + } + + if !exists { + r.log.Info("creating ClusterRole", "name", desired.Name) + if err := r.Create(r.ctx, desired); err != nil { + return fmt.Errorf("failed to create ClusterRole %q: %w", desired.Name, err) + } + r.eventRecorder.Eventf(trustManager, corev1.EventTypeNormal, "Created", "Created ClusterRole %s", desired.Name) + return nil + } + + if !reflect.DeepEqual(existing.Rules, desired.Rules) || objectMetadataModified(desired, existing) { + existing.Rules = desired.Rules + existing.Labels = desired.Labels + r.log.Info("updating ClusterRole", "name", desired.Name) + if err := r.Update(r.ctx, existing); err != nil { + return fmt.Errorf("failed to update ClusterRole %q: %w", desired.Name, err) + } + r.eventRecorder.Eventf(trustManager, corev1.EventTypeNormal, "Updated", "Updated ClusterRole %s", desired.Name) + } + + return nil +} + +func (r *Reconciler) createOrUpdateClusterRoleBinding(desired *rbacv1.ClusterRoleBinding, trustManager *v1alpha1.TrustManager) error { + existing := &rbacv1.ClusterRoleBinding{} + exists, err := r.Exists(r.ctx, client.ObjectKeyFromObject(desired), existing) + if err != nil { + return fmt.Errorf("failed to check ClusterRoleBinding %q existence: %w", desired.Name, err) + } + + if !exists { + r.log.Info("creating ClusterRoleBinding", "name", desired.Name) + if err := r.Create(r.ctx, desired); err != nil { + return fmt.Errorf("failed to create ClusterRoleBinding %q: %w", desired.Name, err) + } + r.eventRecorder.Eventf(trustManager, corev1.EventTypeNormal, "Created", "Created ClusterRoleBinding %s", desired.Name) + return nil + } + + if !reflect.DeepEqual(existing.RoleRef, desired.RoleRef) || + !reflect.DeepEqual(existing.Subjects, desired.Subjects) || + objectMetadataModified(desired, existing) { + existing.RoleRef = desired.RoleRef + existing.Subjects = desired.Subjects + existing.Labels = desired.Labels + r.log.Info("updating ClusterRoleBinding", "name", desired.Name) + if err := r.Update(r.ctx, existing); err != nil { + return fmt.Errorf("failed to update ClusterRoleBinding %q: %w", desired.Name, err) + } + r.eventRecorder.Eventf(trustManager, corev1.EventTypeNormal, "Updated", "Updated ClusterRoleBinding %s", desired.Name) + } + + return nil +} + +func (r *Reconciler) createOrUpdateRole(desired *rbacv1.Role, trustManager *v1alpha1.TrustManager) error { + existing := &rbacv1.Role{} + exists, err := r.Exists(r.ctx, client.ObjectKeyFromObject(desired), existing) + if err != nil { + return fmt.Errorf("failed to check Role %s/%s existence: %w", desired.Namespace, desired.Name, err) + } + + if !exists { + r.log.Info("creating Role", "name", desired.Name, "namespace", desired.Namespace) + if err := r.Create(r.ctx, desired); err != nil { + return fmt.Errorf("failed to create Role %s/%s: %w", desired.Namespace, desired.Name, err) + } + r.eventRecorder.Eventf(trustManager, corev1.EventTypeNormal, "Created", "Created Role %s/%s", desired.Namespace, desired.Name) + return nil + } + + if !reflect.DeepEqual(existing.Rules, desired.Rules) || objectMetadataModified(desired, existing) { + existing.Rules = desired.Rules + existing.Labels = desired.Labels + r.log.Info("updating Role", "name", desired.Name, "namespace", desired.Namespace) + if err := r.Update(r.ctx, existing); err != nil { + return fmt.Errorf("failed to update Role %s/%s: %w", desired.Namespace, desired.Name, err) + } + r.eventRecorder.Eventf(trustManager, corev1.EventTypeNormal, "Updated", "Updated Role %s/%s", desired.Namespace, desired.Name) + } + + return nil +} + +func (r *Reconciler) createOrUpdateRoleBinding(desired *rbacv1.RoleBinding, trustManager *v1alpha1.TrustManager) error { + existing := &rbacv1.RoleBinding{} + exists, err := r.Exists(r.ctx, client.ObjectKeyFromObject(desired), existing) + if err != nil { + return fmt.Errorf("failed to check RoleBinding %s/%s existence: %w", desired.Namespace, desired.Name, err) + } + + if !exists { + r.log.Info("creating RoleBinding", "name", desired.Name, "namespace", desired.Namespace) + if err := r.Create(r.ctx, desired); err != nil { + return fmt.Errorf("failed to create RoleBinding %s/%s: %w", desired.Namespace, desired.Name, err) + } + r.eventRecorder.Eventf(trustManager, corev1.EventTypeNormal, "Created", "Created RoleBinding %s/%s", desired.Namespace, desired.Name) + return nil + } + + if !reflect.DeepEqual(existing.RoleRef, desired.RoleRef) || + !reflect.DeepEqual(existing.Subjects, desired.Subjects) || + objectMetadataModified(desired, existing) { + existing.RoleRef = desired.RoleRef + existing.Subjects = desired.Subjects + existing.Labels = desired.Labels + r.log.Info("updating RoleBinding", "name", desired.Name, "namespace", desired.Namespace) + if err := r.Update(r.ctx, existing); err != nil { + return fmt.Errorf("failed to update RoleBinding %s/%s: %w", desired.Namespace, desired.Name, err) + } + r.eventRecorder.Eventf(trustManager, corev1.EventTypeNormal, "Updated", "Updated RoleBinding %s/%s", desired.Namespace, desired.Name) + } + + return nil +} diff --git a/pkg/operator/setup_manager.go b/pkg/operator/setup_manager.go index ba8c49602..4c9566851 100644 --- a/pkg/operator/setup_manager.go +++ b/pkg/operator/setup_manager.go @@ -19,8 +19,11 @@ import ( certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + v1alpha1 "github.com/openshift/cert-manager-operator/api/operator/v1alpha1" "github.com/openshift/cert-manager-operator/pkg/controller/istiocsr" + "github.com/openshift/cert-manager-operator/pkg/controller/trustmanager" "github.com/openshift/cert-manager-operator/pkg/version" ) @@ -42,7 +45,7 @@ func init() { // +kubebuilder:scaffold:scheme } -// Manager holds the manager resource for the istio-csr controller +// Manager holds the manager resource for a controller-runtime based controller. type Manager struct { manager manager.Manager } @@ -81,3 +84,36 @@ func (mgr *Manager) Start(ctx context.Context) error { mgr.manager.GetEventRecorderFor("cert-manager-istio-csr-controller").Event(&v1alpha1.IstioCSR{}, corev1.EventTypeNormal, "ControllerStarted", "controller is starting") return mgr.manager.Start(ctx) } + +// NewTrustManagerControllerManager creates a new manager for the trust-manager controller. +func NewTrustManagerControllerManager() (*Manager, error) { + setupLog.Info("setting up operator manager", "controller", trustmanager.ControllerName) + setupLog.Info("controller", "version", version.Get()) + + mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ + Scheme: scheme, + NewCache: trustmanager.NewCacheBuilder, + Logger: ctrl.Log.WithName("trustmanager-operator-manager"), + // Disable health and metrics endpoints to avoid port conflicts + // with the IstioCSR controller manager. + HealthProbeBindAddress: "0", + Metrics: metricsserver.Options{ + BindAddress: "0", + }, + }) + if err != nil { + return nil, fmt.Errorf("failed to create trust-manager manager: %w", err) + } + + r, err := trustmanager.New(mgr) + if err != nil { + return nil, fmt.Errorf("failed to create %s reconciler object: %w", trustmanager.ControllerName, err) + } + if err := r.SetupWithManager(mgr); err != nil { + return nil, fmt.Errorf("failed to create %s controller: %w", trustmanager.ControllerName, err) + } + + return &Manager{ + manager: mgr, + }, nil +} diff --git a/pkg/operator/starter.go b/pkg/operator/starter.go index dad5dfe8f..db3a3e445 100644 --- a/pkg/operator/starter.go +++ b/pkg/operator/starter.go @@ -7,8 +7,7 @@ import ( apiextensionsclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" "k8s.io/client-go/kubernetes" - - ctrl "sigs.k8s.io/controller-runtime" + "k8s.io/klog/v2" configv1 "github.com/openshift/api/config/v1" configv1client "github.com/openshift/client-go/config/clientset/versioned" @@ -145,11 +144,27 @@ func RunOperator(ctx context.Context, cc *controllercmd.ControllerContext) error if features.DefaultFeatureGate.Enabled(v1alpha1.FeatureIstioCSR) { manager, err := NewControllerManager() if err != nil { - return fmt.Errorf("failed to create controller manager: %w", err) + return fmt.Errorf("failed to create istiocsr controller manager: %w", err) } - if err := manager.Start(ctrl.SetupSignalHandler()); err != nil { - return fmt.Errorf("failed to start istiocsr controller: %w", err) + go func() { + if err := manager.Start(ctx); err != nil { + klog.Errorf("failed to start istiocsr controller: %v", err) + } + }() + } + + // enable controller-runtime and trust-manager controller + // only when "TrustManager" feature is turned on from --addon-features + if features.DefaultFeatureGate.Enabled(v1alpha1.FeatureTrustManager) { + manager, err := NewTrustManagerControllerManager() + if err != nil { + return fmt.Errorf("failed to create trustmanager controller manager: %w", err) } + go func() { + if err := manager.Start(ctx); err != nil { + klog.Errorf("failed to start trustmanager controller: %v", err) + } + }() } <-ctx.Done() From 9bb579c41c368b8a1a3cad23ca17db7ddae73f6c Mon Sep 17 00:00:00 2001 From: openshift-app-platform-shift-bot <267347085+openshift-app-platform-shift-bot@users.noreply.github.com> Date: Fri, 8 May 2026 13:06:16 +0000 Subject: [PATCH 3/3] CM-830: Add E2E tests for TrustManager controller Adds end-to-end tests for the TrustManager controller covering: - Basic trust-manager deployment with default configuration - Status field verification (Ready/Degraded conditions, observed state) - SecretTargets Custom policy with dynamic RBAC verification - DefaultCAPackage ConfigMap creation when policy is Enabled - Non-destructive cleanup on TrustManager CR deletion Tests follow the existing IstioCSR E2E test patterns using Ginkgo v2, polling helpers, and the dynamic resource loader. Test data YAML manifests are provided in testdata/trustmanager/. Co-Authored-By: Claude Opus 4.6 --- .../trustmanager/trustmanager_cr.yaml | 14 + ...ustmanager_cr_with_default_ca_package.yaml | 14 + .../trustmanager_cr_with_secret_targets.yaml | 17 + test/e2e/trustmanager_test.go | 402 ++++++++++++++++++ 4 files changed, 447 insertions(+) create mode 100644 test/e2e/testdata/trustmanager/trustmanager_cr.yaml create mode 100644 test/e2e/testdata/trustmanager/trustmanager_cr_with_default_ca_package.yaml create mode 100644 test/e2e/testdata/trustmanager/trustmanager_cr_with_secret_targets.yaml create mode 100644 test/e2e/trustmanager_test.go diff --git a/test/e2e/testdata/trustmanager/trustmanager_cr.yaml b/test/e2e/testdata/trustmanager/trustmanager_cr.yaml new file mode 100644 index 000000000..4097370dd --- /dev/null +++ b/test/e2e/testdata/trustmanager/trustmanager_cr.yaml @@ -0,0 +1,14 @@ +apiVersion: operator.openshift.io/v1alpha1 +kind: TrustManager +metadata: + name: cluster +spec: + trustManagerConfig: + logLevel: 1 + logFormat: text + trustNamespace: cert-manager + filterExpiredCertificates: Disabled + defaultCAPackage: + policy: Disabled + secretTargets: + policy: Disabled diff --git a/test/e2e/testdata/trustmanager/trustmanager_cr_with_default_ca_package.yaml b/test/e2e/testdata/trustmanager/trustmanager_cr_with_default_ca_package.yaml new file mode 100644 index 000000000..7c7a25cee --- /dev/null +++ b/test/e2e/testdata/trustmanager/trustmanager_cr_with_default_ca_package.yaml @@ -0,0 +1,14 @@ +apiVersion: operator.openshift.io/v1alpha1 +kind: TrustManager +metadata: + name: cluster +spec: + trustManagerConfig: + logLevel: 1 + logFormat: text + trustNamespace: cert-manager + filterExpiredCertificates: Disabled + defaultCAPackage: + policy: Enabled + secretTargets: + policy: Disabled diff --git a/test/e2e/testdata/trustmanager/trustmanager_cr_with_secret_targets.yaml b/test/e2e/testdata/trustmanager/trustmanager_cr_with_secret_targets.yaml new file mode 100644 index 000000000..e50e44ac8 --- /dev/null +++ b/test/e2e/testdata/trustmanager/trustmanager_cr_with_secret_targets.yaml @@ -0,0 +1,17 @@ +apiVersion: operator.openshift.io/v1alpha1 +kind: TrustManager +metadata: + name: cluster +spec: + trustManagerConfig: + logLevel: 1 + logFormat: text + trustNamespace: cert-manager + filterExpiredCertificates: Disabled + defaultCAPackage: + policy: Disabled + secretTargets: + policy: Custom + authorizedSecrets: + - my-trust-bundle-secret + - another-trust-bundle-secret diff --git a/test/e2e/trustmanager_test.go b/test/e2e/trustmanager_test.go new file mode 100644 index 000000000..e6f754537 --- /dev/null +++ b/test/e2e/trustmanager_test.go @@ -0,0 +1,402 @@ +//go:build e2e +// +build e2e + +package e2e + +import ( + "context" + "fmt" + "path/filepath" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/kubernetes" + + "github.com/openshift/cert-manager-operator/api/operator/v1alpha1" + "github.com/openshift/cert-manager-operator/test/library" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +const ( + trustManagerDeploymentName = "cert-manager-trust-manager" + trustManagerServiceAccount = "trust-manager" + trustManagerClusterRoleName = "trust-manager" + trustManagerDefaultCAPackageCM = "trust-manager-default-ca-package" +) + +var trustmanagerSchema = schema.GroupVersionResource{ + Group: "operator.openshift.io", + Version: "v1alpha1", + Resource: "trustmanagers", +} + +var _ = Describe("TrustManager", Ordered, Label("Feature:TrustManager"), func() { + ctx := context.TODO() + var clientset *kubernetes.Clientset + + // waitForTrustManagerReady polls the TrustManager CR until it reaches Ready state + waitForTrustManagerReady := func() v1alpha1.TrustManagerStatus { + By("poll till trust-manager deployment is available") + err := pollTillDeploymentAvailable(ctx, clientset, operandNamespace, trustManagerDeploymentName) + Expect(err).Should(BeNil()) + + By("poll till trustmanager object is available and ready") + status, err := pollTillTrustManagerAvailable(ctx, loader, "cluster") + Expect(err).Should(BeNil()) + + return status + } + + BeforeAll(func() { + var err error + clientset, err = kubernetes.NewForConfig(cfg) + Expect(err).Should(BeNil()) + + By("increase operator log verbosity") + err = patchSubscriptionWithEnvVars(ctx, loader, map[string]string{ + "OPERATOR_LOG_LEVEL": "5", + }) + Expect(err).NotTo(HaveOccurred()) + }) + + BeforeEach(func() { + By("waiting for operator status to become available") + err := VerifyHealthyOperatorConditions(certmanageroperatorclient.OperatorV1alpha1()) + Expect(err).NotTo(HaveOccurred(), "Operator is expected to be available") + }) + + Context("basic trust-manager deployment", func() { + It("should deploy trust-manager with default configuration", func() { + By("creating trustmanager.operator.openshift.io resource") + loader.CreateFromFile(testassets.ReadFile, filepath.Join("testdata", "trustmanager", "trustmanager_cr.yaml"), "") + defer func() { + By("deleting trustmanager.operator.openshift.io resource") + loader.DeleteFromFile(testassets.ReadFile, filepath.Join("testdata", "trustmanager", "trustmanager_cr.yaml"), "") + // cleanup cluster-scoped RBAC resources + cleanupTrustManagerRBAC(ctx, clientset) + }() + + status := waitForTrustManagerReady() + + By("verifying trust-manager status fields are populated") + Expect(status.TrustManagerImage).NotTo(BeEmpty(), "TrustManagerImage should be populated") + Expect(status.TrustNamespace).To(Equal("cert-manager"), "TrustNamespace should be cert-manager") + Expect(string(status.SecretTargetsPolicy)).To(Equal("Disabled"), "SecretTargetsPolicy should be Disabled") + Expect(string(status.DefaultCAPackagePolicy)).To(Equal("Disabled"), "DefaultCAPackagePolicy should be Disabled") + Expect(string(status.FilterExpiredCertificatesPolicy)).To(Equal("Disabled"), "FilterExpiredCertificatesPolicy should be Disabled") + + By("verifying Ready condition is True") + readyCondition := meta.FindStatusCondition(status.Conditions, v1alpha1.Ready) + Expect(readyCondition).NotTo(BeNil(), "Ready condition should exist") + Expect(readyCondition.Status).To(Equal(metav1.ConditionTrue), "Ready condition should be True") + + By("verifying Degraded condition is False") + degradedCondition := meta.FindStatusCondition(status.Conditions, v1alpha1.Degraded) + Expect(degradedCondition).NotTo(BeNil(), "Degraded condition should exist") + Expect(degradedCondition.Status).To(Equal(metav1.ConditionFalse), "Degraded condition should be False") + + By("verifying ServiceAccount is created") + err := pollTillServiceAccountAvailable(ctx, clientset, operandNamespace, trustManagerContainerName) + Expect(err).Should(BeNil(), "ServiceAccount should be created") + + By("verifying ClusterRole is created") + _, err = clientset.RbacV1().ClusterRoles().Get(ctx, trustManagerClusterRoleName, metav1.GetOptions{}) + Expect(err).Should(BeNil(), "ClusterRole should be created") + + By("verifying ClusterRoleBinding is created") + _, err = clientset.RbacV1().ClusterRoleBindings().Get(ctx, trustManagerClusterRoleName, metav1.GetOptions{}) + Expect(err).Should(BeNil(), "ClusterRoleBinding should be created") + + By("verifying webhook Service is created") + _, err = clientset.CoreV1().Services(operandNamespace).Get(ctx, trustManagerDeploymentName, metav1.GetOptions{}) + Expect(err).Should(BeNil(), "webhook Service should be created") + + By("verifying metrics Service is created") + _, err = clientset.CoreV1().Services(operandNamespace).Get(ctx, trustManagerDeploymentName+"-metrics", metav1.GetOptions{}) + Expect(err).Should(BeNil(), "metrics Service should be created") + + By("verifying Certificate is created for webhook TLS") + cert, err := certmanagerClient.CertmanagerV1().Certificates(operandNamespace).Get(ctx, trustManagerDeploymentName, metav1.GetOptions{}) + Expect(err).Should(BeNil(), "Certificate should be created") + Expect(cert.Spec.SecretName).To(Equal(trustManagerDeploymentName + "-tls"), "Certificate should reference the TLS secret") + + By("verifying Issuer is created for webhook TLS") + _, err = certmanagerClient.CertmanagerV1().Issuers(operandNamespace).Get(ctx, trustManagerDeploymentName, metav1.GetOptions{}) + Expect(err).Should(BeNil(), "Issuer should be created") + + By("verifying DefaultCAPackage ConfigMap is NOT created when policy is Disabled") + err = pollTillConfigMapRemains(ctx, clientset, operandNamespace, trustManagerDefaultCAPackageCM, lowTimeout) + Expect(err).Should(BeNil(), "DefaultCAPackage ConfigMap should not exist when policy is Disabled") + + By("verifying trust-manager deployment container args") + deployment, err := clientset.AppsV1().Deployments(operandNamespace).Get(ctx, trustManagerDeploymentName, metav1.GetOptions{}) + Expect(err).Should(BeNil()) + Expect(len(deployment.Spec.Template.Spec.Containers)).To(BeNumerically(">", 0)) + container := deployment.Spec.Template.Spec.Containers[0] + Expect(container.Args).To(ContainElement("--trust-namespace=cert-manager")) + Expect(container.Args).To(ContainElement("--log-format=text")) + Expect(container.Args).To(ContainElement("--log-level=1")) + }) + }) + + Context("trust-manager with SecretTargets configuration", func() { + It("should configure RBAC rules for authorized secrets", func() { + By("creating trustmanager.operator.openshift.io resource with SecretTargets Custom policy") + loader.CreateFromFile(testassets.ReadFile, filepath.Join("testdata", "trustmanager", "trustmanager_cr_with_secret_targets.yaml"), "") + defer func() { + By("deleting trustmanager.operator.openshift.io resource") + loader.DeleteFromFile(testassets.ReadFile, filepath.Join("testdata", "trustmanager", "trustmanager_cr_with_secret_targets.yaml"), "") + cleanupTrustManagerRBAC(ctx, clientset) + }() + + waitForTrustManagerReady() + + By("verifying ClusterRole has secret write rules for authorized secrets") + err := wait.PollUntilContextTimeout(ctx, fastPollInterval, lowTimeout, true, func(context.Context) (bool, error) { + clusterRole, err := clientset.RbacV1().ClusterRoles().Get(ctx, trustManagerClusterRoleName, metav1.GetOptions{}) + if err != nil { + if apierrors.IsNotFound(err) { + return false, nil + } + return false, err + } + + // Check for the secret write rule with authorized secret names + for _, rule := range clusterRole.Rules { + if containsString(rule.Resources, "secrets") && + containsString(rule.Verbs, "create") && + containsString(rule.Verbs, "update") && + containsString(rule.ResourceNames, "my-trust-bundle-secret") && + containsString(rule.ResourceNames, "another-trust-bundle-secret") { + return true, nil + } + } + return false, nil + }) + Expect(err).Should(BeNil(), "ClusterRole should have authorized secret write rules") + + By("verifying trust-manager deployment container args include secret-targets-enabled") + deployment, err := clientset.AppsV1().Deployments(operandNamespace).Get(ctx, trustManagerDeploymentName, metav1.GetOptions{}) + Expect(err).Should(BeNil()) + container := deployment.Spec.Template.Spec.Containers[0] + Expect(container.Args).To(ContainElement("--secret-targets-enabled=true")) + }) + }) + + Context("trust-manager with DefaultCAPackage configuration", func() { + It("should create DefaultCAPackage ConfigMap when policy is Enabled", func() { + By("creating trustmanager.operator.openshift.io resource with DefaultCAPackage Enabled") + loader.CreateFromFile(testassets.ReadFile, filepath.Join("testdata", "trustmanager", "trustmanager_cr_with_default_ca_package.yaml"), "") + defer func() { + By("deleting trustmanager.operator.openshift.io resource") + loader.DeleteFromFile(testassets.ReadFile, filepath.Join("testdata", "trustmanager", "trustmanager_cr_with_default_ca_package.yaml"), "") + cleanupTrustManagerRBAC(ctx, clientset) + }() + + // Note: This test may fail if the operator namespace does not have the + // cert-manager-operator-trusted-ca-bundle ConfigMap injected by CNO. + // In that case, the controller will set Degraded=True with a message + // about the missing CA bundle. + By("poll till trust-manager deployment is available or status becomes degraded") + status, err := pollTillTrustManagerAvailable(ctx, loader, "cluster") + if err != nil { + // If DefaultCAPackage failed due to missing CA bundle, verify the error is expected + By("checking if failure is due to missing CA bundle ConfigMap (expected in test environments)") + degradedStatus, statusErr := getTrustManagerStatus(ctx, loader, "cluster") + if statusErr == nil { + degradedCondition := meta.FindStatusCondition(degradedStatus.Conditions, v1alpha1.Degraded) + if degradedCondition != nil && degradedCondition.Status == metav1.ConditionTrue { + Skip("DefaultCAPackage test skipped: CA bundle ConfigMap not available in test environment") + } + } + Expect(err).Should(BeNil(), "TrustManager should become available") + } + + By("verifying DefaultCAPackage ConfigMap is created") + err = pollTillConfigMapAvailable(ctx, clientset, operandNamespace, trustManagerDefaultCAPackageCM) + Expect(err).Should(BeNil(), "DefaultCAPackage ConfigMap should be created") + + By("verifying DefaultCAPackage ConfigMap has expected data key") + cm, err := clientset.CoreV1().ConfigMaps(operandNamespace).Get(ctx, trustManagerDefaultCAPackageCM, metav1.GetOptions{}) + Expect(err).Should(BeNil()) + Expect(cm.Data).To(HaveKey("cert-manager-package-openshift.json"), "ConfigMap should have CA package JSON") + + By("verifying trust-manager deployment has default-package-location arg") + deployment, err := clientset.AppsV1().Deployments(operandNamespace).Get(ctx, trustManagerDeploymentName, metav1.GetOptions{}) + Expect(err).Should(BeNil()) + container := deployment.Spec.Template.Spec.Containers[0] + Expect(container.Args).To(ContainElement(ContainSubstring("--default-package-location="))) + + By("verifying deployment has CA package volume mount") + found := false + for _, vm := range container.VolumeMounts { + if vm.Name == "default-ca-package" { + found = true + break + } + } + Expect(found).To(BeTrue(), "Container should have default-ca-package volume mount") + + _ = status + }) + }) + + Context("trust-manager CR deletion", func() { + It("should stop reconciliation without removing operand resources", func() { + By("creating trustmanager.operator.openshift.io resource") + loader.CreateFromFile(testassets.ReadFile, filepath.Join("testdata", "trustmanager", "trustmanager_cr.yaml"), "") + + waitForTrustManagerReady() + + By("deleting trustmanager.operator.openshift.io resource") + loader.DeleteFromFile(testassets.ReadFile, filepath.Join("testdata", "trustmanager", "trustmanager_cr.yaml"), "") + + By("verifying TrustManager CR is deleted") + trustmanagerClient := loader.DynamicClient.Resource(trustmanagerSchema) + err := wait.PollUntilContextTimeout(ctx, fastPollInterval, lowTimeout, true, func(context.Context) (bool, error) { + _, err := trustmanagerClient.Get(ctx, "cluster", metav1.GetOptions{}) + if apierrors.IsNotFound(err) { + return true, nil + } + return false, err + }) + Expect(err).Should(BeNil(), "TrustManager CR should be deleted") + + By("verifying trust-manager deployment still exists (non-destructive cleanup)") + _, err = clientset.AppsV1().Deployments(operandNamespace).Get(ctx, trustManagerDeploymentName, metav1.GetOptions{}) + Expect(err).Should(BeNil(), "Deployment should still exist after CR deletion") + + By("verifying ServiceAccount still exists") + _, err = clientset.CoreV1().ServiceAccounts(operandNamespace).Get(ctx, trustManagerContainerName, metav1.GetOptions{}) + Expect(err).Should(BeNil(), "ServiceAccount should still exist after CR deletion") + + defer func() { + // Final cleanup: remove leftover resources manually since cleanup is non-destructive + cleanupTrustManagerRBAC(ctx, clientset) + cleanupTrustManagerResources(ctx, clientset) + }() + }) + }) +}) + +// trustManagerContainerName is the expected name of the trust-manager container and ServiceAccount +const trustManagerContainerName = "trust-manager" + +// pollTillTrustManagerAvailable polls the TrustManager CR and returns its status +// once it reaches Ready=True, otherwise returns a timeout error. +func pollTillTrustManagerAvailable(ctx context.Context, loader library.DynamicResourceLoader, trustManagerName string) (v1alpha1.TrustManagerStatus, error) { + var trustManagerStatus v1alpha1.TrustManagerStatus + trustmanagerClient := loader.DynamicClient.Resource(trustmanagerSchema) + err := wait.PollUntilContextTimeout(ctx, slowPollInterval, highTimeout, true, func(context.Context) (bool, error) { + customResource, err := trustmanagerClient.Get(ctx, trustManagerName, metav1.GetOptions{}) + if err != nil { + if apierrors.IsNotFound(err) { + return false, nil + } + return false, err + } + + status, found, err := unstructured.NestedMap(customResource.Object, "status") + if err != nil { + return false, fmt.Errorf("failed to extract status from TrustManager: %w", err) + } + if !found { + return false, nil + } + + err = runtime.DefaultUnstructuredConverter.FromUnstructured(status, &trustManagerStatus) + if err != nil { + return false, fmt.Errorf("failed to convert status to TrustManagerStatus: %w", err) + } + + // Check ready condition + readyCondition := meta.FindStatusCondition(trustManagerStatus.Conditions, v1alpha1.Ready) + if readyCondition == nil { + return false, nil + } + + // Check for degraded condition + degradedCondition := meta.FindStatusCondition(trustManagerStatus.Conditions, v1alpha1.Degraded) + if degradedCondition != nil && degradedCondition.Status == metav1.ConditionTrue { + return false, fmt.Errorf("TrustManager is degraded: %s", degradedCondition.Message) + } + + return readyCondition.Status == metav1.ConditionTrue, nil + }) + + return trustManagerStatus, err +} + +// getTrustManagerStatus fetches the current status of the TrustManager CR without waiting for readiness. +func getTrustManagerStatus(ctx context.Context, loader library.DynamicResourceLoader, trustManagerName string) (v1alpha1.TrustManagerStatus, error) { + var trustManagerStatus v1alpha1.TrustManagerStatus + trustmanagerClient := loader.DynamicClient.Resource(trustmanagerSchema) + + customResource, err := trustmanagerClient.Get(ctx, trustManagerName, metav1.GetOptions{}) + if err != nil { + return trustManagerStatus, err + } + + status, found, err := unstructured.NestedMap(customResource.Object, "status") + if err != nil { + return trustManagerStatus, fmt.Errorf("failed to extract status from TrustManager: %w", err) + } + if !found { + return trustManagerStatus, fmt.Errorf("status not found in TrustManager CR") + } + + err = runtime.DefaultUnstructuredConverter.FromUnstructured(status, &trustManagerStatus) + if err != nil { + return trustManagerStatus, fmt.Errorf("failed to convert status to TrustManagerStatus: %w", err) + } + + return trustManagerStatus, nil +} + +// containsString checks if a string slice contains a given string. +func containsString(slice []string, s string) bool { + for _, item := range slice { + if item == s { + return true + } + } + return false +} + +// cleanupTrustManagerRBAC removes cluster-scoped RBAC resources created by the trust-manager controller. +func cleanupTrustManagerRBAC(ctx context.Context, clientset *kubernetes.Clientset) { + clientset.RbacV1().ClusterRoles().DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{ + LabelSelector: "app=cert-manager-trust-manager", + }) + clientset.RbacV1().ClusterRoleBindings().DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{ + LabelSelector: "app=cert-manager-trust-manager", + }) +} + +// cleanupTrustManagerResources removes namespaced resources created by the trust-manager controller. +// This is used for manual cleanup since the TrustManager controller does not remove resources on CR deletion. +func cleanupTrustManagerResources(ctx context.Context, clientset *kubernetes.Clientset) { + // Delete deployment + clientset.AppsV1().Deployments(operandNamespace).Delete(ctx, trustManagerDeploymentName, metav1.DeleteOptions{}) + // Delete services + clientset.CoreV1().Services(operandNamespace).Delete(ctx, trustManagerDeploymentName, metav1.DeleteOptions{}) + clientset.CoreV1().Services(operandNamespace).Delete(ctx, trustManagerDeploymentName+"-metrics", metav1.DeleteOptions{}) + // Delete service account + clientset.CoreV1().ServiceAccounts(operandNamespace).Delete(ctx, trustManagerContainerName, metav1.DeleteOptions{}) + // Delete ConfigMap + clientset.CoreV1().ConfigMaps(operandNamespace).Delete(ctx, trustManagerDefaultCAPackageCM, metav1.DeleteOptions{}) + // Delete roles and rolebindings in operand namespace + clientset.RbacV1().Roles(operandNamespace).DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{ + LabelSelector: "app=cert-manager-trust-manager", + }) + clientset.RbacV1().RoleBindings(operandNamespace).DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{ + LabelSelector: "app=cert-manager-trust-manager", + }) +}