diff --git a/api/operator/v1alpha1/features.go b/api/operator/v1alpha1/features.go index d28e35132..5c3bb5624 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" + + // TrustManager 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, taking a list of trusted certificate sources and combining them into a + // bundle which 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..ca8e523e8 --- /dev/null +++ b/api/operator/v1alpha1/tests/trustmanagers.operator.openshift.io/trustmanager.testsuite.yaml @@ -0,0 +1,677 @@ +apiVersion: apiextensions.k8s.io/v1 +name: "TrustManager" +crdName: trustmanagers.operator.openshift.io +tests: + onCreate: + - name: Should be able to create a minimal TrustManager with required fields only + 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 + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + logFormat: "yaml" + expectedError: "Unsupported value" + + - name: Should accept logLevel 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 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: "spec.trustManagerConfig.logLevel" + + - name: Should reject logLevel above maximum (6) + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + logLevel: 6 + expectedError: "spec.trustManagerConfig.logLevel" + + - 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 of 63 + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + trustNamespace: "this-namespace-name-is-way-too-long-and-exceeds-the-maximum-length-of-63-characters" + expectedError: "spec.trustManagerConfig.trustNamespace" + + - 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 with 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 with policy Custom and authorizedSecrets + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + secretTargets: + policy: "Custom" + authorizedSecrets: + - "my-trust-bundle" + expected: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + secretTargets: + policy: "Custom" + authorizedSecrets: + - "my-trust-bundle" + + - name: Should reject secretTargets with policy Custom and 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 secretTargets with policy Custom and no 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 with policy Disabled and authorizedSecrets set + 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 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 accept defaultCAPackage with 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 with 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 + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + defaultCAPackage: + policy: "Invalid" + expectedError: "Unsupported value" + + - name: Should accept controllerConfig with labels and annotations + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: {} + controllerConfig: + labels: + my-label: my-value + annotations: + my-annotation: my-value + expected: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: {} + controllerConfig: + labels: + my-label: my-value + annotations: + my-annotation: my-value + + - name: Should accept tolerations + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + tolerations: + - key: "node-role.kubernetes.io/master" + operator: "Exists" + effect: "NoSchedule" + expected: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + tolerations: + - key: "node-role.kubernetes.io/master" + 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 full configuration with all fields + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + logLevel: 3 + logFormat: "json" + trustNamespace: "my-trust-ns" + filterExpiredCertificates: "Enabled" + secretTargets: + policy: "Custom" + authorizedSecrets: + - "bundle-secret-1" + - "bundle-secret-2" + defaultCAPackage: + policy: "Enabled" + nodeSelector: + kubernetes.io/os: linux + tolerations: + - key: "node-role.kubernetes.io/master" + operator: "Exists" + effect: "NoSchedule" + controllerConfig: + labels: + team: security + annotations: + description: trust-manager deployment + expected: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + logLevel: 3 + logFormat: "json" + trustNamespace: "my-trust-ns" + filterExpiredCertificates: "Enabled" + secretTargets: + policy: "Custom" + authorizedSecrets: + - "bundle-secret-1" + - "bundle-secret-2" + defaultCAPackage: + policy: "Enabled" + nodeSelector: + kubernetes.io/os: linux + tolerations: + - key: "node-role.kubernetes.io/master" + operator: "Exists" + effect: "NoSchedule" + controllerConfig: + labels: + team: security + annotations: + description: trust-manager deployment + + onUpdate: + - name: Should not allow changing immutable trustNamespace + initial: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + trustNamespace: "cert-manager" + updated: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + trustNamespace: "different-namespace" + 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: + - "my-secret" + expected: | + apiVersion: operator.openshift.io/v1alpha1 + kind: TrustManager + metadata: + name: cluster + spec: + trustManagerConfig: + secretTargets: + policy: "Custom" + authorizedSecrets: + - "my-secret" + + - name: Should allow updating defaultCAPackage from Disabled to Enabled + 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 update of 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..e05598447 --- /dev/null +++ b/api/operator/v1alpha1/trustmanager_types.go @@ -0,0 +1,268 @@ +package v1alpha1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func init() { + SchemeBuilder.Register(&TrustManager{}, &TrustManagerList{}) +} + +// +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"` +} diff --git a/api/operator/v1alpha1/trustmanager_types_test.go b/api/operator/v1alpha1/trustmanager_types_test.go new file mode 100644 index 000000000..4765da957 --- /dev/null +++ b/api/operator/v1alpha1/trustmanager_types_test.go @@ -0,0 +1,265 @@ +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 expects 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") + } + } +} + +// TestTrustManagerSingletonValidation verifies that the CRD has singleton validation rule. +func TestTrustManagerSingletonValidation(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") + } + + 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("expected x-kubernetes-validations for singleton enforcement") + } + + found := false + for _, val := range validations { + v, ok := val.(map[string]interface{}) + if !ok { + continue + } + rule, _ := v["rule"].(string) + if rule == "self.metadata.name == 'cluster'" { + found = true + break + } + } + + if !found { + t.Fatalf("expected singleton validation rule 'self.metadata.name == cluster' not found") + } + } +} + +// TestTrustManagerSpecRequired verifies that the spec field is required in the CRD. +func TestTrustManagerSpecRequired(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") + } + + required, exists, err := unstructured.NestedStringSlice(schema, "required") + if err != nil { + t.Fatalf("failed to get required fields: %v", err) + } + + if !exists { + t.Fatalf("expected required fields at root level") + } + + found := false + for _, r := range required { + if r == "spec" { + found = true + break + } + } + + if !found { + t.Fatalf("expected 'spec' to be in the required fields list") + } + } +} + +// TestTrustManagerTrustNamespaceImmutability verifies that the trustNamespace field has immutability validation. +func TestTrustManagerTrustNamespaceImmutability(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 schema") + } + + validations, exists, err := unstructured.NestedSlice(trustNamespace, "x-kubernetes-validations") + if err != nil { + t.Fatalf("failed to get x-kubernetes-validations: %v", err) + } + + if !exists { + t.Fatalf("expected x-kubernetes-validations for trustNamespace immutability") + } + + found := false + for _, val := range validations { + v, ok := val.(map[string]interface{}) + if !ok { + continue + } + message, _ := v["message"].(string) + if message == "trustNamespace is immutable once set" { + found = true + break + } + } + + if !found { + t.Fatalf("expected immutability validation rule for trustNamespace not found") + } + } +} + +// TestTrustManagerSecretTargetsValidation verifies that the secretTargets field has cross-field validation. +func TestTrustManagerSecretTargetsValidation(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{}) + secretTargets, exists, err := unstructured.NestedMap(trustManagerCRDVersion, + "schema", "openAPIV3Schema", "properties", "spec", "properties", + "trustManagerConfig", "properties", "secretTargets") + if err != nil { + t.Fatalf("failed to get secretTargets: %v", err) + } + + if !exists { + t.Fatalf("secretTargets field does not exist in schema") + } + + validations, exists, err := unstructured.NestedSlice(secretTargets, "x-kubernetes-validations") + if err != nil { + t.Fatalf("failed to get x-kubernetes-validations: %v", err) + } + + if !exists { + t.Fatalf("expected x-kubernetes-validations for secretTargets") + } + + expectedMessages := map[string]bool{ + "authorizedSecrets must not be empty when policy is Custom": false, + "authorizedSecrets must be empty when policy is not Custom": false, + } + + for _, val := range validations { + v, ok := val.(map[string]interface{}) + if !ok { + continue + } + message, _ := v["message"].(string) + if _, exists := expectedMessages[message]; exists { + expectedMessages[message] = true + } + } + + for msg, found := range expectedMessages { + if !found { + t.Errorf("expected validation rule with message %q not found", msg) + } + } + } +} 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/bindata/trust-manager/trust-manager-clusterrole.yaml b/bindata/trust-manager/trust-manager-clusterrole.yaml new file mode 100644 index 000000000..efb12cdef --- /dev/null +++ b/bindata/trust-manager/trust-manager-clusterrole.yaml @@ -0,0 +1,52 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: trust-manager + labels: + app: trust-manager + app.kubernetes.io/name: trust-manager + app.kubernetes.io/instance: trust-manager + app.kubernetes.io/managed-by: cert-manager-operator + app.kubernetes.io/part-of: cert-manager-operator +rules: + - apiGroups: + - trust.cert-manager.io + resources: + - bundles + verbs: + - get + - list + - watch + - apiGroups: + - trust.cert-manager.io + resources: + - bundles/status + verbs: + - patch + - apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - create + - update + - patch + - watch + - delete + - apiGroups: + - "" + resources: + - namespaces + verbs: + - get + - list + - watch + - apiGroups: + - "" + resources: + - events + verbs: + - create + - patch diff --git a/bindata/trust-manager/trust-manager-clusterrolebinding.yaml b/bindata/trust-manager/trust-manager-clusterrolebinding.yaml new file mode 100644 index 000000000..604cbe446 --- /dev/null +++ b/bindata/trust-manager/trust-manager-clusterrolebinding.yaml @@ -0,0 +1,18 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: trust-manager + labels: + app: trust-manager + app.kubernetes.io/name: trust-manager + app.kubernetes.io/instance: trust-manager + app.kubernetes.io/managed-by: cert-manager-operator + app.kubernetes.io/part-of: cert-manager-operator +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: trust-manager +subjects: + - kind: ServiceAccount + name: trust-manager + namespace: cert-manager diff --git a/bindata/trust-manager/trust-manager-deployment.yaml b/bindata/trust-manager/trust-manager-deployment.yaml new file mode 100644 index 000000000..e51b3f720 --- /dev/null +++ b/bindata/trust-manager/trust-manager-deployment.yaml @@ -0,0 +1,49 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: trust-manager + namespace: cert-manager + labels: + app: trust-manager + app.kubernetes.io/name: trust-manager + app.kubernetes.io/instance: trust-manager + app.kubernetes.io/managed-by: cert-manager-operator + app.kubernetes.io/part-of: cert-manager-operator +spec: + replicas: 1 + selector: + matchLabels: + app: trust-manager + template: + metadata: + labels: + app: trust-manager + spec: + serviceAccountName: trust-manager + containers: + - name: trust-manager + image: trust-manager:latest + imagePullPolicy: IfNotPresent + args: [] + ports: + - containerPort: 6443 + name: webhook + protocol: TCP + - containerPort: 9402 + name: metrics + protocol: TCP + readinessProbe: + httpGet: + path: /readyz + port: 6060 + initialDelaySeconds: 3 + periodSeconds: 7 + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsNonRoot: true + securityContext: + runAsNonRoot: true diff --git a/bindata/trust-manager/trust-manager-leases-role.yaml b/bindata/trust-manager/trust-manager-leases-role.yaml new file mode 100644 index 000000000..85ac23416 --- /dev/null +++ b/bindata/trust-manager/trust-manager-leases-role.yaml @@ -0,0 +1,22 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: trust-manager:leases + namespace: cert-manager + labels: + app: trust-manager + app.kubernetes.io/name: trust-manager + app.kubernetes.io/instance: trust-manager + app.kubernetes.io/managed-by: cert-manager-operator + app.kubernetes.io/part-of: cert-manager-operator +rules: + - apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - create + - update + - watch + - list diff --git a/bindata/trust-manager/trust-manager-leases-rolebinding.yaml b/bindata/trust-manager/trust-manager-leases-rolebinding.yaml new file mode 100644 index 000000000..f844908f3 --- /dev/null +++ b/bindata/trust-manager/trust-manager-leases-rolebinding.yaml @@ -0,0 +1,19 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: trust-manager:leases + namespace: cert-manager + labels: + app: trust-manager + app.kubernetes.io/name: trust-manager + app.kubernetes.io/instance: trust-manager + app.kubernetes.io/managed-by: cert-manager-operator + app.kubernetes.io/part-of: cert-manager-operator +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: trust-manager:leases +subjects: + - kind: ServiceAccount + name: trust-manager + namespace: cert-manager diff --git a/bindata/trust-manager/trust-manager-metrics-service.yaml b/bindata/trust-manager/trust-manager-metrics-service.yaml new file mode 100644 index 000000000..8dd8ac653 --- /dev/null +++ b/bindata/trust-manager/trust-manager-metrics-service.yaml @@ -0,0 +1,20 @@ +apiVersion: v1 +kind: Service +metadata: + name: trust-manager-metrics + namespace: cert-manager + labels: + app: trust-manager + app.kubernetes.io/name: trust-manager + app.kubernetes.io/instance: trust-manager + app.kubernetes.io/managed-by: cert-manager-operator + app.kubernetes.io/part-of: cert-manager-operator +spec: + type: ClusterIP + selector: + app: trust-manager + ports: + - name: metrics + port: 9402 + targetPort: metrics + protocol: TCP diff --git a/bindata/trust-manager/trust-manager-secret-targets-role.yaml b/bindata/trust-manager/trust-manager-secret-targets-role.yaml new file mode 100644 index 000000000..ac34b913d --- /dev/null +++ b/bindata/trust-manager/trust-manager-secret-targets-role.yaml @@ -0,0 +1,23 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: trust-manager:secret-targets + labels: + app: trust-manager + app.kubernetes.io/name: trust-manager + app.kubernetes.io/instance: trust-manager + app.kubernetes.io/managed-by: cert-manager-operator + app.kubernetes.io/part-of: cert-manager-operator +rules: + - apiGroups: + - "" + resources: + - secrets + verbs: + - get + - list + - create + - update + - patch + - watch + - delete diff --git a/bindata/trust-manager/trust-manager-secret-targets-rolebinding.yaml b/bindata/trust-manager/trust-manager-secret-targets-rolebinding.yaml new file mode 100644 index 000000000..049555e22 --- /dev/null +++ b/bindata/trust-manager/trust-manager-secret-targets-rolebinding.yaml @@ -0,0 +1,18 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: trust-manager:secret-targets + labels: + app: trust-manager + app.kubernetes.io/name: trust-manager + app.kubernetes.io/instance: trust-manager + app.kubernetes.io/managed-by: cert-manager-operator + app.kubernetes.io/part-of: cert-manager-operator +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: trust-manager:secret-targets +subjects: + - kind: ServiceAccount + name: trust-manager + namespace: cert-manager diff --git a/bindata/trust-manager/trust-manager-serviceaccount.yaml b/bindata/trust-manager/trust-manager-serviceaccount.yaml new file mode 100644 index 000000000..42473bfe9 --- /dev/null +++ b/bindata/trust-manager/trust-manager-serviceaccount.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: trust-manager + namespace: cert-manager + labels: + app: trust-manager + app.kubernetes.io/name: trust-manager + app.kubernetes.io/instance: trust-manager + app.kubernetes.io/managed-by: cert-manager-operator + app.kubernetes.io/part-of: cert-manager-operator 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..211f1539a --- /dev/null +++ b/config/crd/bases/operator.openshift.io_trustmanagers.yaml @@ -0,0 +1,1328 @@ +--- +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/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..502956434 --- /dev/null +++ b/pkg/controller/trustmanager/constants.go @@ -0,0 +1,75 @@ +package trustmanager + +import ( + "os" + "time" +) + +const ( + // trustManagerCommonName is the name commonly used for naming resources. + trustManagerCommonName = "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" + + // controllerProcessingRejectedAnnotation is the annotation added to trustmanager resource when multiple + // instances of trustmanager resource is created. + controllerProcessingRejectedAnnotation = "operator.openshift.io/trust-manager-reject-multiple-instance" + + // 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. + // TrustManager CRD enforces name to be `cluster`. + trustManagerObjectName = "cluster" + + // trustManagerContainerName is the name of the container created for trust-manager. + trustManagerContainerName = trustManagerCommonName + + // trustManagerImageNameEnvVarName is the environment variable key name + // containing the image name of the trust-manager as value. + trustManagerImageNameEnvVarName = "RELATED_IMAGE_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" + + // trustManagerDefaultNamespace is the default namespace where trust-manager + // looks for trust sources and where it is deployed. + trustManagerDefaultNamespace = "cert-manager" + + // roleBindingSubjectKind is the kind of subject in a RoleBinding or ClusterRoleBinding. + roleBindingSubjectKind = "ServiceAccount" +) + +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 ( + clusterRoleAssetName = "trust-manager/trust-manager-clusterrole.yaml" + clusterRoleBindingAssetName = "trust-manager/trust-manager-clusterrolebinding.yaml" + deploymentAssetName = "trust-manager/trust-manager-deployment.yaml" + roleLeasesAssetName = "trust-manager/trust-manager-leases-role.yaml" + roleBindingLeasesAssetName = "trust-manager/trust-manager-leases-rolebinding.yaml" + metricsServiceAssetName = "trust-manager/trust-manager-metrics-service.yaml" + serviceAccountAssetName = "trust-manager/trust-manager-serviceaccount.yaml" + secretTargetsRoleAssetName = "trust-manager/trust-manager-secret-targets-role.yaml" + secretTargetsRoleBindingName = "trust-manager/trust-manager-secret-targets-rolebinding.yaml" +) diff --git a/pkg/controller/trustmanager/controller.go b/pkg/controller/trustmanager/controller.go new file mode 100644 index 000000000..7185a7819 --- /dev/null +++ b/pkg/controller/trustmanager/controller.go @@ -0,0 +1,299 @@ +package trustmanager + +import ( + "context" + "fmt" + "reflect" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "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" + + 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 = "trust-manager" +) + +// 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 + +// 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) + &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, + }, + } + + 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 { + if objLabels[requestEnqueueLabelKey] == requestEnqueueLabelValue { + return []reconcile.Request{ + { + NamespacedName: types.NamespacedName{ + // TrustManager is cluster-scoped, so only Name is needed. + 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{} + } + + // 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) + + return ctrl.NewControllerManagedBy(mgr). + For(&v1alpha1.TrustManager{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). + Named(ControllerName). + 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). + 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 + tm := &v1alpha1.TrustManager{} + if err := r.Get(ctx, req.NamespacedName, tm); 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 !tm.DeletionTimestamp.IsZero() { + r.log.V(1).Info("trustmanagers.operator.openshift.io is marked for deletion", "name", req.NamespacedName) + + if requeue, err := r.cleanUp(tm); err != nil { + return ctrl.Result{}, fmt.Errorf("clean up failed for %q trustmanagers.operator.openshift.io instance deletion: %w", req.NamespacedName, err) + } else if requeue { + return ctrl.Result{RequeueAfter: defaultRequeueTime}, nil + } + + if err := r.removeFinalizer(ctx, tm, 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, tm); err != nil { + return ctrl.Result{}, fmt.Errorf("failed to update %q trustmanagers.operator.openshift.io with finalizers: %w", req.NamespacedName, err) + } + + return r.processReconcileRequest(tm, req.NamespacedName) +} + +func (r *Reconciler) processReconcileRequest(tm *v1alpha1.TrustManager, req types.NamespacedName) (ctrl.Result, error) { + trustManagerCreateRecon := false + if !containsProcessedAnnotation(tm) && reflect.DeepEqual(tm.Status, v1alpha1.TrustManagerStatus{}) { + r.log.V(1).Info("starting reconciliation of newly created trustmanager", "name", tm.GetName()) + trustManagerCreateRecon = true + } + + var errUpdate error + if err := r.reconcileTrustManagerDeployment(tm, trustManagerCreateRecon); err != nil { + r.log.Error(err, "failed to reconcile TrustManager deployment", "request", req) + if IsIrrecoverableError(err) { + // Set both conditions atomically before updating status + degradedChanged := tm.Status.SetCondition(v1alpha1.Degraded, metav1.ConditionTrue, v1alpha1.ReasonFailed, fmt.Sprintf("reconciliation failed with irrecoverable error not retrying: %v", err)) + readyChanged := tm.Status.SetCondition(v1alpha1.Ready, metav1.ConditionFalse, v1alpha1.ReasonReady, "") + + if degradedChanged || readyChanged { + r.log.V(2).Info("updating trustmanager conditions on irrecoverable error", + "name", tm.GetName(), + "degradedChanged", degradedChanged, + "readyChanged", readyChanged, + "error", err) + errUpdate = r.updateCondition(tm, nil) + } + return ctrl.Result{}, errUpdate + } + // Set both conditions atomically before updating status + degradedChanged := tm.Status.SetCondition(v1alpha1.Degraded, metav1.ConditionFalse, v1alpha1.ReasonReady, "") + readyChanged := tm.Status.SetCondition(v1alpha1.Ready, metav1.ConditionFalse, v1alpha1.ReasonInProgress, fmt.Sprintf("reconciliation failed, retrying: %v", err)) + + if degradedChanged || readyChanged { + r.log.V(2).Info("updating trustmanager conditions on recoverable error", + "name", tm.GetName(), + "degradedChanged", degradedChanged, + "readyChanged", readyChanged, + "error", err) + errUpdate = r.updateCondition(tm, err) + } + // For recoverable errors, either requeue manually or return error, not both + // If status update failed, return the update error; otherwise return the original error + if errUpdate != nil { + return ctrl.Result{}, errUpdate + } + return ctrl.Result{RequeueAfter: defaultRequeueTime}, nil + } + + // Set both conditions atomically before updating status on success + degradedChanged := tm.Status.SetCondition(v1alpha1.Degraded, metav1.ConditionFalse, v1alpha1.ReasonReady, "") + readyChanged := tm.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", tm.GetName(), + "degradedChanged", degradedChanged, + "readyChanged", readyChanged) + errUpdate = r.updateCondition(tm, nil) + } + return ctrl.Result{}, errUpdate +} + +// cleanUp handles deletion of trustmanagers.operator.openshift.io gracefully. +func (r *Reconciler) cleanUp(tm *v1alpha1.TrustManager) (bool, error) { + r.eventRecorder.Eventf(tm, corev1.EventTypeWarning, "RemoveDeployment", "%s trustmanager marked for deletion, removing all resources created for trust-manager deployment", tm.GetName()) + + // Clean up cluster-scoped RBAC resources since they won't be garbage collected. + if err := r.cleanUpClusterScopedResources(tm); err != nil { + r.log.Error(err, "failed to clean up cluster-scoped resources") + return true, err + } + + return false, nil +} + +// cleanUpClusterScopedResources removes ClusterRoles and ClusterRoleBindings created by the controller. +func (r *Reconciler) cleanUpClusterScopedResources(tm *v1alpha1.TrustManager) error { + // Delete ClusterRoleBindings with matching labels + clusterRoleBindingList := &rbacv1.ClusterRoleBindingList{} + if err := r.List(r.ctx, clusterRoleBindingList, client.MatchingLabels(controllerDefaultResourceLabels)); err != nil { + return fmt.Errorf("failed to list clusterrolebinding resources for cleanup: %w", err) + } + for i := range clusterRoleBindingList.Items { + if err := r.Delete(r.ctx, &clusterRoleBindingList.Items[i]); err != nil && !errors.IsNotFound(err) { + return fmt.Errorf("failed to delete clusterrolebinding %s: %w", clusterRoleBindingList.Items[i].GetName(), err) + } + r.log.V(1).Info("deleted clusterrolebinding during cleanup", "name", clusterRoleBindingList.Items[i].GetName()) + } + + // Delete ClusterRoles with matching labels + clusterRoleList := &rbacv1.ClusterRoleList{} + if err := r.List(r.ctx, clusterRoleList, client.MatchingLabels(controllerDefaultResourceLabels)); err != nil { + return fmt.Errorf("failed to list clusterrole resources for cleanup: %w", err) + } + for i := range clusterRoleList.Items { + if err := r.Delete(r.ctx, &clusterRoleList.Items[i]); err != nil && !errors.IsNotFound(err) { + return fmt.Errorf("failed to delete clusterrole %s: %w", clusterRoleList.Items[i].GetName(), err) + } + r.log.V(1).Info("deleted clusterrole during cleanup", "name", clusterRoleList.Items[i].GetName()) + } + + return nil +} diff --git a/pkg/controller/trustmanager/deployments.go b/pkg/controller/trustmanager/deployments.go new file mode 100644 index 000000000..457ea353f --- /dev/null +++ b/pkg/controller/trustmanager/deployments.go @@ -0,0 +1,310 @@ +package trustmanager + +import ( + "fmt" + "os" + "reflect" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/validation/field" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/openshift/cert-manager-operator/api/operator/v1alpha1" + "github.com/openshift/cert-manager-operator/pkg/operator/assets" +) + +const ( + defaultCAPackageVolumeMountPath = "/var/run/configmaps/default-package" + defaultCAPackageConfigMapName = "trust-manager-default-package" + defaultCAPackageKeyName = "ca-certificates.crt" +) + +func (r *Reconciler) createOrApplyDeployments(tm *v1alpha1.TrustManager, trustNamespace string, resourceLabels map[string]string, trustManagerCreateRecon bool) error { + desired, err := r.getDeploymentObject(tm, trustNamespace, resourceLabels) + if err != nil { + return fmt.Errorf("failed to generate deployment resource for creation in %s: %w", trustNamespace, err) + } + + deploymentName := fmt.Sprintf("%s/%s", desired.GetNamespace(), desired.GetName()) + r.log.V(4).Info("reconciling deployment resource", "name", deploymentName) + fetched := &appsv1.Deployment{} + exist, err := r.Exists(r.ctx, client.ObjectKeyFromObject(desired), fetched) + if err != nil { + return FromClientError(err, "failed to check %s deployment resource already exists", deploymentName) + } + + if exist && trustManagerCreateRecon { + r.eventRecorder.Eventf(tm, corev1.EventTypeWarning, "ResourceAlreadyExists", "%s deployment resource already exists, maybe from previous installation", deploymentName) + } + if exist && hasObjectChanged(desired, fetched) { + r.log.V(1).Info("deployment has been modified, updating to desired state", "name", deploymentName) + if err := r.UpdateWithRetry(r.ctx, desired); err != nil { + return FromClientError(err, "failed to update %s deployment resource", deploymentName) + } + r.eventRecorder.Eventf(tm, corev1.EventTypeNormal, "Reconciled", "deployment resource %s reconciled back to desired state", deploymentName) + } else { + r.log.V(4).Info("deployment resource already exists and is in expected state", "name", deploymentName) + } + if !exist { + if err := r.Create(r.ctx, desired); err != nil { + return FromClientError(err, "failed to create %s deployment resource", deploymentName) + } + r.eventRecorder.Eventf(tm, corev1.EventTypeNormal, "Reconciled", "deployment resource %s created", deploymentName) + } + + if err := r.updateImageInStatus(tm, desired); err != nil { + return FromClientError(err, "failed to update %s trustmanager status with image info", tm.GetName()) + } + return nil +} + +func (r *Reconciler) getDeploymentObject(tm *v1alpha1.TrustManager, trustNamespace string, resourceLabels map[string]string) (*appsv1.Deployment, error) { + deployment := decodeDeploymentObjBytes(assets.MustAsset(deploymentAssetName)) + + updateNamespace(deployment, trustNamespace) + updateResourceLabels(deployment, resourceLabels) + updatePodTemplateLabels(deployment, resourceLabels) + + updateArgList(deployment, tm) + + if err := updateResourceRequirement(deployment, tm); err != nil { + return nil, fmt.Errorf("failed to update resource requirements: %w", err) + } + if err := updatePodTolerations(deployment, tm); err != nil { + return nil, fmt.Errorf("failed to update pod tolerations: %w", err) + } + if err := updateNodeSelector(deployment, tm); err != nil { + return nil, fmt.Errorf("failed to update node selector: %w", err) + } + if tm.Spec.TrustManagerConfig.Affinity != nil { + deployment.Spec.Template.Spec.Affinity = tm.Spec.TrustManagerConfig.Affinity + } + if err := r.updateImage(deployment); err != nil { + return nil, NewIrrecoverableError(err, "failed to update image for %s", tm.GetName()) + } + + // Handle defaultCAPackage volume mount if enabled + if tm.Spec.TrustManagerConfig.DefaultCAPackage.Policy == v1alpha1.DefaultCAPackagePolicyEnabled { + updateVolumeWithDefaultCAPackage(deployment) + } + + return deployment, nil +} + +func (r *Reconciler) updateImage(deployment *appsv1.Deployment) error { + image := os.Getenv(trustManagerImageNameEnvVarName) + if image == "" { + return fmt.Errorf("%s environment variable with trust-manager image not set", trustManagerImageNameEnvVarName) + } + for i, container := range deployment.Spec.Template.Spec.Containers { + if container.Name == trustManagerContainerName { + deployment.Spec.Template.Spec.Containers[i].Image = image + } + } + return nil +} + +func (r *Reconciler) updateImageInStatus(tm *v1alpha1.TrustManager, deployment *appsv1.Deployment) error { + for _, container := range deployment.Spec.Template.Spec.Containers { + if container.Name == trustManagerContainerName { + if tm.Status.TrustManagerImage == container.Image { + return nil + } + tm.Status.TrustManagerImage = container.Image + } + } + return r.updateStatus(r.ctx, tm) +} + +func updatePodTemplateLabels(deployment *appsv1.Deployment, resourceLabels map[string]string) { + deployment.Spec.Template.Labels = resourceLabels +} + +func updateArgList(deployment *appsv1.Deployment, tm *v1alpha1.TrustManager) { + tmConfig := tm.Spec.TrustManagerConfig + + trustNamespace := tmConfig.TrustNamespace + if trustNamespace == "" { + trustNamespace = trustManagerDefaultNamespace + } + + args := []string{ + fmt.Sprintf("--log-level=%d", tmConfig.LogLevel), + fmt.Sprintf("--log-format=%s", tmConfig.LogFormat), + fmt.Sprintf("--trust-namespace=%s", trustNamespace), + "--metrics-port=9402", + "--readiness-probe-port=6060", + "--readiness-probe-path=/readyz", + fmt.Sprintf("--filter-expired-certs=%t", tmConfig.FilterExpiredCertificates == v1alpha1.FilterExpiredCertificatesPolicyEnabled), + } + + // Add secret targets configuration + if tmConfig.SecretTargets.Policy == v1alpha1.SecretTargetsPolicyCustom { + args = append(args, "--secret-targets-enabled=true") + } + + // Add default CA package location if enabled + if tmConfig.DefaultCAPackage.Policy == v1alpha1.DefaultCAPackagePolicyEnabled { + args = append(args, fmt.Sprintf("--default-package-location=%s/%s", defaultCAPackageVolumeMountPath, defaultCAPackageKeyName)) + } + + for i, container := range deployment.Spec.Template.Spec.Containers { + if container.Name == trustManagerContainerName { + deployment.Spec.Template.Spec.Containers[i].Args = args + } + } +} + +func updateResourceRequirement(deployment *appsv1.Deployment, tm *v1alpha1.TrustManager) error { + if reflect.ValueOf(tm.Spec.TrustManagerConfig.Resources).IsZero() { + return nil + } + if err := validateResourceRequirements(tm.Spec.TrustManagerConfig.Resources, + field.NewPath("spec", "trustManagerConfig")); err != nil { + return err + } + for i := range deployment.Spec.Template.Spec.Containers { + deployment.Spec.Template.Spec.Containers[i].Resources = tm.Spec.TrustManagerConfig.Resources + } + return nil +} + +func updatePodTolerations(deployment *appsv1.Deployment, tm *v1alpha1.TrustManager) error { + if tm.Spec.TrustManagerConfig.Tolerations == nil { + return nil + } + if err := validateTolerationsConfig(tm.Spec.TrustManagerConfig.Tolerations, + field.NewPath("spec", "trustManagerConfig")); err != nil { + return err + } + deployment.Spec.Template.Spec.Tolerations = tm.Spec.TrustManagerConfig.Tolerations + return nil +} + +func updateNodeSelector(deployment *appsv1.Deployment, tm *v1alpha1.TrustManager) error { + if tm.Spec.TrustManagerConfig.NodeSelector == nil { + return nil + } + if err := validateNodeSelectorConfig(tm.Spec.TrustManagerConfig.NodeSelector, + field.NewPath("spec", "trustManagerConfig")); err != nil { + return err + } + deployment.Spec.Template.Spec.NodeSelector = tm.Spec.TrustManagerConfig.NodeSelector + return nil +} + +// updateVolumeWithDefaultCAPackage adds the default CA package ConfigMap volume and mount +// to the trust-manager deployment. The ConfigMap is injected with the OpenShift trusted CA +// bundle via the config.openshift.io/inject-trusted-cabundle annotation. +func updateVolumeWithDefaultCAPackage(deployment *appsv1.Deployment) { + const ( + caVolumeName = "default-package" + ) + var ( + defaultMode = int32(420) + ) + + desiredVolumeMount := corev1.VolumeMount{ + Name: caVolumeName, + MountPath: defaultCAPackageVolumeMountPath, + ReadOnly: true, + } + + desiredVolume := corev1.Volume{ + Name: caVolumeName, + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: defaultCAPackageConfigMapName, + }, + DefaultMode: &defaultMode, + }, + }, + } + + // Update or append volume mount in the trust-manager container + for i, container := range deployment.Spec.Template.Spec.Containers { + if container.Name == trustManagerContainerName { + volumeMountExists := false + for j, vm := range container.VolumeMounts { + if vm.Name == caVolumeName { + deployment.Spec.Template.Spec.Containers[i].VolumeMounts[j] = desiredVolumeMount + volumeMountExists = true + break + } + } + if !volumeMountExists { + deployment.Spec.Template.Spec.Containers[i].VolumeMounts = append( + deployment.Spec.Template.Spec.Containers[i].VolumeMounts, + desiredVolumeMount, + ) + } + break + } + } + + // Update or append volume in the deployment + volumeExists := false + for i, vol := range deployment.Spec.Template.Spec.Volumes { + if vol.Name == caVolumeName { + deployment.Spec.Template.Spec.Volumes[i] = desiredVolume + volumeExists = true + break + } + } + if !volumeExists { + deployment.Spec.Template.Spec.Volumes = append( + deployment.Spec.Template.Spec.Volumes, + desiredVolume, + ) + } +} + +// createOrApplyDefaultCAPackageConfigMap creates or updates the ConfigMap used for the default +// CA package with the OpenShift trusted CA bundle injection annotation. +func (r *Reconciler) createOrApplyDefaultCAPackageConfigMap(tm *v1alpha1.TrustManager, trustNamespace string, resourceLabels map[string]string) error { + configmapKey := client.ObjectKey{ + Name: defaultCAPackageConfigMapName, + Namespace: trustNamespace, + } + + desired := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: configmapKey.Name, + Namespace: configmapKey.Namespace, + Labels: resourceLabels, + Annotations: map[string]string{ + // This annotation triggers the Cluster Network Operator (CNO) + // to inject the trusted CA bundle into this ConfigMap. + "config.openshift.io/inject-trusted-cabundle": "true", + }, + }, + } + + fetched := &corev1.ConfigMap{} + exist, err := r.Exists(r.ctx, configmapKey, fetched) + if err != nil { + return FromClientError(err, "failed to check if default CA package configmap exists") + } + + if exist && hasObjectChanged(desired, fetched) { + r.log.V(1).Info("default CA package configmap needs update", "name", configmapKey) + if err := r.UpdateWithRetry(r.ctx, desired); err != nil { + return FromClientError(err, "failed to update %s configmap resource", configmapKey) + } + r.eventRecorder.Eventf(tm, corev1.EventTypeNormal, "Reconciled", "configmap resource %s reconciled back to desired state", configmapKey) + } else { + r.log.V(4).Info("default CA package configmap already exists and is in expected state", "name", configmapKey) + } + + if !exist { + if err := r.Create(r.ctx, desired); err != nil { + return FromClientError(err, "failed to create %s configmap resource", configmapKey) + } + r.eventRecorder.Eventf(tm, corev1.EventTypeNormal, "Reconciled", "configmap resource %s created", configmapKey) + } + + return nil +} diff --git a/pkg/controller/trustmanager/errors.go b/pkg/controller/trustmanager/errors.go new file mode 100644 index 000000000..87b3801bb --- /dev/null +++ b/pkg/controller/trustmanager/errors.go @@ -0,0 +1,87 @@ +package trustmanager + +import ( + "errors" + "fmt" + + apierrors "k8s.io/apimachinery/pkg/api/errors" +) + +type ErrorReason string + +const ( + IrrecoverableError ErrorReason = "IrrecoverableError" + + RetryRequiredError ErrorReason = "RetryRequiredError" +) + +type ReconcileError struct { + Reason ErrorReason `json:"reason,omitempty"` + Message string `json:"message,omitempty"` + Err error `json:"error,omitempty"` +} + +var _ error = &ReconcileError{} + +func NewIrrecoverableError(err error, message string, args ...any) *ReconcileError { + if err == nil { + return nil + } + return &ReconcileError{ + Reason: IrrecoverableError, + Message: fmt.Sprintf(message, args...), + Err: err, + } +} + +func NewRetryRequiredError(err error, message string, args ...any) *ReconcileError { + if err == nil { + return nil + } + return &ReconcileError{ + Reason: RetryRequiredError, + Message: fmt.Sprintf(message, args...), + Err: err, + } +} + +func FromClientError(err error, message string, args ...any) *ReconcileError { + if err == nil { + return nil + } + if apierrors.IsUnauthorized(err) || apierrors.IsForbidden(err) || apierrors.IsInvalid(err) || + apierrors.IsBadRequest(err) || apierrors.IsServiceUnavailable(err) { + return NewIrrecoverableError(err, message, args...) + } + + return NewRetryRequiredError(err, message, args...) +} + +func FromError(err error, message string, args ...any) *ReconcileError { + if err == nil { + return nil + } + if IsIrrecoverableError(err) { + return NewIrrecoverableError(err, message, args...) + } + return NewRetryRequiredError(err, message, args...) +} + +func IsIrrecoverableError(err error) bool { + if rerr, ok := err.(*ReconcileError); ok || errors.As(err, &rerr) { + return rerr.Reason == IrrecoverableError + } + return false +} + +func IsRetryRequiredError(err error) bool { + if rerr, ok := err.(*ReconcileError); ok || errors.As(err, &rerr) { + return rerr.Reason == RetryRequiredError + } + return false +} + +// ReconcileError implements the error interface. +func (e *ReconcileError) Error() string { + return fmt.Sprintf("%s: %s", e.Message, e.Err) +} diff --git a/pkg/controller/trustmanager/install_trustmanager.go b/pkg/controller/trustmanager/install_trustmanager.go new file mode 100644 index 000000000..5325df7c2 --- /dev/null +++ b/pkg/controller/trustmanager/install_trustmanager.go @@ -0,0 +1,73 @@ +package trustmanager + +import ( + "fmt" + + "github.com/openshift/cert-manager-operator/api/operator/v1alpha1" +) + +func (r *Reconciler) reconcileTrustManagerDeployment(tm *v1alpha1.TrustManager, trustManagerCreateRecon bool) error { + // if user has set custom labels to be added to all resources created by the controller + // merge it with the controller's own default labels. + resourceLabels := make(map[string]string) + if len(tm.Spec.ControllerConfig.Labels) != 0 { + for k, v := range tm.Spec.ControllerConfig.Labels { + resourceLabels[k] = v + } + } + for k, v := range controllerDefaultResourceLabels { + resourceLabels[k] = v + } + + // Determine the target namespace for trust-manager deployment. + // trust-manager is always deployed in the cert-manager namespace. + trustNamespace := tm.Spec.TrustManagerConfig.TrustNamespace + if trustNamespace == "" { + trustNamespace = trustManagerDefaultNamespace + } + + if err := r.createOrApplyServiceAccounts(tm, trustNamespace, resourceLabels, trustManagerCreateRecon); err != nil { + r.log.Error(err, "failed to reconcile serviceaccount resource") + return err + } + + if err := r.createOrApplyRBACResource(tm, trustNamespace, resourceLabels, trustManagerCreateRecon); err != nil { + r.log.Error(err, "failed to reconcile rbac resources") + return err + } + + if err := r.createOrApplyServices(tm, trustNamespace, resourceLabels, trustManagerCreateRecon); err != nil { + r.log.Error(err, "failed to reconcile service resource") + return err + } + + if err := r.createOrApplyDeployments(tm, trustNamespace, resourceLabels, trustManagerCreateRecon); err != nil { + r.log.Error(err, "failed to reconcile deployment resource") + return err + } + + if addProcessedAnnotation(tm) { + if err := r.UpdateWithRetry(r.ctx, tm); err != nil { + return fmt.Errorf("failed to update processed annotation to %s: %w", tm.GetName(), err) + } + } + + // Update status with observed configuration + r.updateStatusFields(tm) + + r.log.V(4).Info("finished reconciliation of trustmanager", "name", tm.GetName()) + return nil +} + +// updateStatusFields updates the status fields with the observed configuration. +func (r *Reconciler) updateStatusFields(tm *v1alpha1.TrustManager) { + trustNamespace := tm.Spec.TrustManagerConfig.TrustNamespace + if trustNamespace == "" { + trustNamespace = trustManagerDefaultNamespace + } + + tm.Status.TrustNamespace = trustNamespace + tm.Status.SecretTargetsPolicy = tm.Spec.TrustManagerConfig.SecretTargets.Policy + tm.Status.DefaultCAPackagePolicy = tm.Spec.TrustManagerConfig.DefaultCAPackage.Policy + tm.Status.FilterExpiredCertificatesPolicy = tm.Spec.TrustManagerConfig.FilterExpiredCertificates +} diff --git a/pkg/controller/trustmanager/rbacs.go b/pkg/controller/trustmanager/rbacs.go new file mode 100644 index 000000000..7ebd7a337 --- /dev/null +++ b/pkg/controller/trustmanager/rbacs.go @@ -0,0 +1,306 @@ +package trustmanager + +import ( + "fmt" + + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/openshift/cert-manager-operator/api/operator/v1alpha1" + "github.com/openshift/cert-manager-operator/pkg/operator/assets" +) + +func (r *Reconciler) createOrApplyRBACResource(tm *v1alpha1.TrustManager, trustNamespace string, resourceLabels map[string]string, trustManagerCreateRecon bool) error { + serviceAccount := decodeServiceAccountObjBytes(assets.MustAsset(serviceAccountAssetName)).GetName() + + if err := r.createOrApplyClusterRoles(tm, resourceLabels, trustManagerCreateRecon); err != nil { + r.log.Error(err, "failed to reconcile clusterrole resource") + return err + } + + if err := r.createOrApplyClusterRoleBindings(tm, serviceAccount, trustNamespace, resourceLabels, trustManagerCreateRecon); err != nil { + r.log.Error(err, "failed to reconcile clusterrolebinding resource") + return err + } + + if err := r.createOrApplyRoleForLeases(tm, trustNamespace, resourceLabels, trustManagerCreateRecon); err != nil { + r.log.Error(err, "failed to reconcile role for leases resource") + return err + } + + if err := r.createOrApplyRoleBindingForLeases(tm, serviceAccount, trustNamespace, resourceLabels, trustManagerCreateRecon); err != nil { + r.log.Error(err, "failed to reconcile rolebinding for leases resource") + return err + } + + // Handle SecretTargets RBAC: create or clean up secret targets ClusterRole/ClusterRoleBinding + if tm.Spec.TrustManagerConfig.SecretTargets.Policy == v1alpha1.SecretTargetsPolicyCustom { + if err := r.createOrApplySecretTargetsRBAC(tm, serviceAccount, trustNamespace, resourceLabels, trustManagerCreateRecon); err != nil { + r.log.Error(err, "failed to reconcile secret targets rbac resources") + return err + } + } else { + if err := r.cleanUpSecretTargetsRBAC(tm); err != nil { + r.log.Error(err, "failed to clean up secret targets rbac resources") + return err + } + } + + return nil +} + +func (r *Reconciler) createOrApplyClusterRoles(tm *v1alpha1.TrustManager, resourceLabels map[string]string, trustManagerCreateRecon bool) error { + desired := r.getClusterRoleObject(resourceLabels) + + roleName := desired.GetName() + r.log.V(4).Info("reconciling clusterrole resource", "name", roleName) + fetched := &rbacv1.ClusterRole{} + exist, err := r.Exists(r.ctx, client.ObjectKeyFromObject(desired), fetched) + if err != nil { + return FromClientError(err, "failed to check %s clusterrole resource already exists", roleName) + } + + if exist && trustManagerCreateRecon { + r.eventRecorder.Eventf(tm, corev1.EventTypeWarning, "ResourceAlreadyExists", "%s clusterrole resource already exists, maybe from previous installation", roleName) + } + if exist && hasObjectChanged(desired, fetched) { + r.log.V(1).Info("clusterrole has been modified, updating to desired state", "name", roleName) + if err := r.UpdateWithRetry(r.ctx, desired); err != nil { + return FromClientError(err, "failed to update %s clusterrole resource", roleName) + } + r.eventRecorder.Eventf(tm, corev1.EventTypeNormal, "Reconciled", "clusterrole resource %s reconciled back to desired state", roleName) + } else { + r.log.V(4).Info("clusterrole resource already exists and is in expected state", "name", roleName) + } + if !exist { + if err := r.Create(r.ctx, desired); err != nil { + return FromClientError(err, "failed to create %s clusterrole resource", roleName) + } + r.eventRecorder.Eventf(tm, corev1.EventTypeNormal, "Reconciled", "clusterrole resource %s created", roleName) + } + + return nil +} + +func (r *Reconciler) getClusterRoleObject(resourceLabels map[string]string) *rbacv1.ClusterRole { + clusterRole := decodeClusterRoleObjBytes(assets.MustAsset(clusterRoleAssetName)) + updateResourceLabels(clusterRole, resourceLabels) + return clusterRole +} + +func (r *Reconciler) createOrApplyClusterRoleBindings(tm *v1alpha1.TrustManager, serviceAccount, trustNamespace string, resourceLabels map[string]string, trustManagerCreateRecon bool) error { + desired := r.getClusterRoleBindingObject(serviceAccount, trustNamespace, resourceLabels) + + roleBindingName := desired.GetName() + r.log.V(4).Info("reconciling clusterrolebinding resource", "name", roleBindingName) + fetched := &rbacv1.ClusterRoleBinding{} + exist, err := r.Exists(r.ctx, client.ObjectKeyFromObject(desired), fetched) + if err != nil { + return FromClientError(err, "failed to check %s clusterrolebinding resource already exists", roleBindingName) + } + + if exist && trustManagerCreateRecon { + r.eventRecorder.Eventf(tm, corev1.EventTypeWarning, "ResourceAlreadyExists", "%s clusterrolebinding resource already exists, maybe from previous installation", roleBindingName) + } + if exist && hasObjectChanged(desired, fetched) { + r.log.V(1).Info("clusterrolebinding has been modified, updating to desired state", "name", roleBindingName) + if err := r.UpdateWithRetry(r.ctx, desired); err != nil { + return FromClientError(err, "failed to update %s clusterrolebinding resource", roleBindingName) + } + r.eventRecorder.Eventf(tm, corev1.EventTypeNormal, "Reconciled", "clusterrolebinding resource %s reconciled back to desired state", roleBindingName) + } else { + r.log.V(4).Info("clusterrolebinding resource already exists and is in expected state", "name", roleBindingName) + } + if !exist { + if err := r.Create(r.ctx, desired); err != nil { + return FromClientError(err, "failed to create %s clusterrolebinding resource", roleBindingName) + } + r.eventRecorder.Eventf(tm, corev1.EventTypeNormal, "Reconciled", "clusterrolebinding resource %s created", roleBindingName) + } + + return nil +} + +func (r *Reconciler) getClusterRoleBindingObject(serviceAccount, trustNamespace string, resourceLabels map[string]string) *rbacv1.ClusterRoleBinding { + clusterRoleBinding := decodeClusterRoleBindingObjBytes(assets.MustAsset(clusterRoleBindingAssetName)) + updateResourceLabels(clusterRoleBinding, resourceLabels) + updateServiceAccountNamespaceInRBACBindingObject[*rbacv1.ClusterRoleBinding](clusterRoleBinding, serviceAccount, trustNamespace) + return clusterRoleBinding +} + +func (r *Reconciler) createOrApplyRoleForLeases(tm *v1alpha1.TrustManager, trustNamespace string, resourceLabels map[string]string, trustManagerCreateRecon bool) error { + desired := r.getRoleForLeasesObject(trustNamespace, resourceLabels) + + roleName := fmt.Sprintf("%s/%s", desired.GetNamespace(), desired.GetName()) + r.log.V(4).Info("reconciling role for lease resource", "name", roleName) + fetched := &rbacv1.Role{} + exist, err := r.Exists(r.ctx, client.ObjectKeyFromObject(desired), fetched) + if err != nil { + return FromClientError(err, "failed to check %s role resource already exists", roleName) + } + + if exist && trustManagerCreateRecon { + r.eventRecorder.Eventf(tm, corev1.EventTypeWarning, "ResourceAlreadyExists", "%s role resource already exists, maybe from previous installation", roleName) + } + if exist && hasObjectChanged(desired, fetched) { + r.log.V(1).Info("role has been modified, updating to desired state", "name", roleName) + if err := r.UpdateWithRetry(r.ctx, desired); err != nil { + return FromClientError(err, "failed to update %s role resource", roleName) + } + r.eventRecorder.Eventf(tm, corev1.EventTypeNormal, "Reconciled", "role resource %s reconciled back to desired state", roleName) + } else { + r.log.V(4).Info("role resource already exists and is in expected state", "name", roleName) + } + if !exist { + if err := r.Create(r.ctx, desired); err != nil { + return FromClientError(err, "failed to create %s role resource", roleName) + } + r.eventRecorder.Eventf(tm, corev1.EventTypeNormal, "Reconciled", "role resource %s created", roleName) + } + + return nil +} + +func (r *Reconciler) getRoleForLeasesObject(roleNamespace string, resourceLabels map[string]string) *rbacv1.Role { + role := decodeRoleObjBytes(assets.MustAsset(roleLeasesAssetName)) + updateNamespace(role, roleNamespace) + updateResourceLabels(role, resourceLabels) + return role +} + +func (r *Reconciler) createOrApplyRoleBindingForLeases(tm *v1alpha1.TrustManager, serviceAccount, trustNamespace string, resourceLabels map[string]string, trustManagerCreateRecon bool) error { + desired := r.getRoleBindingForLeasesObject(serviceAccount, trustNamespace, resourceLabels) + + roleBindingName := fmt.Sprintf("%s/%s", desired.GetNamespace(), desired.GetName()) + r.log.V(4).Info("reconciling rolebinding for lease resource", "name", roleBindingName) + fetched := &rbacv1.RoleBinding{} + exist, err := r.Exists(r.ctx, client.ObjectKeyFromObject(desired), fetched) + if err != nil { + return FromClientError(err, "failed to check %s rolebinding resource already exists", roleBindingName) + } + + if exist && trustManagerCreateRecon { + r.eventRecorder.Eventf(tm, corev1.EventTypeWarning, "ResourceAlreadyExists", "%s rolebinding resource already exists, maybe from previous installation", roleBindingName) + } + if exist && hasObjectChanged(desired, fetched) { + r.log.V(1).Info("rolebinding has been modified, updating to desired state", "name", roleBindingName) + if err := r.UpdateWithRetry(r.ctx, desired); err != nil { + return FromClientError(err, "failed to update %s rolebinding resource", roleBindingName) + } + r.eventRecorder.Eventf(tm, corev1.EventTypeNormal, "Reconciled", "rolebinding resource %s reconciled back to desired state", roleBindingName) + } else { + r.log.V(4).Info("rolebinding resource already exists and is in expected state", "name", roleBindingName) + } + if !exist { + if err := r.Create(r.ctx, desired); err != nil { + return FromClientError(err, "failed to create %s rolebinding resource", roleBindingName) + } + r.eventRecorder.Eventf(tm, corev1.EventTypeNormal, "Reconciled", "rolebinding resource %s created", roleBindingName) + } + + return nil +} + +func (r *Reconciler) getRoleBindingForLeasesObject(serviceAccount, trustNamespace string, resourceLabels map[string]string) *rbacv1.RoleBinding { + roleBinding := decodeRoleBindingObjBytes(assets.MustAsset(roleBindingLeasesAssetName)) + updateNamespace(roleBinding, trustNamespace) + updateResourceLabels(roleBinding, resourceLabels) + updateServiceAccountNamespaceInRBACBindingObject[*rbacv1.RoleBinding](roleBinding, serviceAccount, trustNamespace) + return roleBinding +} + +func (r *Reconciler) createOrApplySecretTargetsRBAC(tm *v1alpha1.TrustManager, serviceAccount, trustNamespace string, resourceLabels map[string]string, trustManagerCreateRecon bool) error { + // Create the secret targets ClusterRole + desiredRole := decodeClusterRoleObjBytes(assets.MustAsset(secretTargetsRoleAssetName)) + updateResourceLabels(desiredRole, resourceLabels) + + roleName := desiredRole.GetName() + r.log.V(4).Info("reconciling secret targets clusterrole resource", "name", roleName) + fetchedRole := &rbacv1.ClusterRole{} + exist, err := r.Exists(r.ctx, client.ObjectKeyFromObject(desiredRole), fetchedRole) + if err != nil { + return FromClientError(err, "failed to check %s clusterrole resource already exists", roleName) + } + + if exist && trustManagerCreateRecon { + r.eventRecorder.Eventf(tm, corev1.EventTypeWarning, "ResourceAlreadyExists", "%s clusterrole resource already exists, maybe from previous installation", roleName) + } + if exist && hasObjectChanged(desiredRole, fetchedRole) { + r.log.V(1).Info("secret targets clusterrole has been modified, updating to desired state", "name", roleName) + if err := r.UpdateWithRetry(r.ctx, desiredRole); err != nil { + return FromClientError(err, "failed to update %s clusterrole resource", roleName) + } + r.eventRecorder.Eventf(tm, corev1.EventTypeNormal, "Reconciled", "clusterrole resource %s reconciled back to desired state", roleName) + } + if !exist { + if err := r.Create(r.ctx, desiredRole); err != nil { + return FromClientError(err, "failed to create %s clusterrole resource", roleName) + } + r.eventRecorder.Eventf(tm, corev1.EventTypeNormal, "Reconciled", "clusterrole resource %s created", roleName) + } + + // Create the secret targets ClusterRoleBinding + desiredBinding := decodeClusterRoleBindingObjBytes(assets.MustAsset(secretTargetsRoleBindingName)) + updateResourceLabels(desiredBinding, resourceLabels) + updateServiceAccountNamespaceInRBACBindingObject[*rbacv1.ClusterRoleBinding](desiredBinding, serviceAccount, trustNamespace) + + bindingName := desiredBinding.GetName() + r.log.V(4).Info("reconciling secret targets clusterrolebinding resource", "name", bindingName) + fetchedBinding := &rbacv1.ClusterRoleBinding{} + exist, err = r.Exists(r.ctx, client.ObjectKeyFromObject(desiredBinding), fetchedBinding) + if err != nil { + return FromClientError(err, "failed to check %s clusterrolebinding resource already exists", bindingName) + } + + if exist && trustManagerCreateRecon { + r.eventRecorder.Eventf(tm, corev1.EventTypeWarning, "ResourceAlreadyExists", "%s clusterrolebinding resource already exists, maybe from previous installation", bindingName) + } + if exist && hasObjectChanged(desiredBinding, fetchedBinding) { + r.log.V(1).Info("secret targets clusterrolebinding has been modified, updating to desired state", "name", bindingName) + if err := r.UpdateWithRetry(r.ctx, desiredBinding); err != nil { + return FromClientError(err, "failed to update %s clusterrolebinding resource", bindingName) + } + r.eventRecorder.Eventf(tm, corev1.EventTypeNormal, "Reconciled", "clusterrolebinding resource %s reconciled back to desired state", bindingName) + } + if !exist { + if err := r.Create(r.ctx, desiredBinding); err != nil { + return FromClientError(err, "failed to create %s clusterrolebinding resource", bindingName) + } + r.eventRecorder.Eventf(tm, corev1.EventTypeNormal, "Reconciled", "clusterrolebinding resource %s created", bindingName) + } + + return nil +} + +// cleanUpSecretTargetsRBAC removes secret targets ClusterRole and ClusterRoleBinding +// when the SecretTargets policy is changed from Custom to Disabled. +func (r *Reconciler) cleanUpSecretTargetsRBAC(tm *v1alpha1.TrustManager) error { + secretTargetsRole := decodeClusterRoleObjBytes(assets.MustAsset(secretTargetsRoleAssetName)) + fetchedRole := &rbacv1.ClusterRole{} + exist, err := r.Exists(r.ctx, client.ObjectKeyFromObject(secretTargetsRole), fetchedRole) + if err != nil { + return FromClientError(err, "failed to check if secret targets clusterrole exists for cleanup") + } + if exist { + if err := r.Delete(r.ctx, fetchedRole); err != nil { + return FromClientError(err, "failed to delete secret targets clusterrole during cleanup") + } + r.log.V(1).Info("deleted secret targets clusterrole", "name", fetchedRole.GetName()) + } + + secretTargetsBinding := decodeClusterRoleBindingObjBytes(assets.MustAsset(secretTargetsRoleBindingName)) + fetchedBinding := &rbacv1.ClusterRoleBinding{} + exist, err = r.Exists(r.ctx, client.ObjectKeyFromObject(secretTargetsBinding), fetchedBinding) + if err != nil { + return FromClientError(err, "failed to check if secret targets clusterrolebinding exists for cleanup") + } + if exist { + if err := r.Delete(r.ctx, fetchedBinding); err != nil { + return FromClientError(err, "failed to delete secret targets clusterrolebinding during cleanup") + } + r.log.V(1).Info("deleted secret targets clusterrolebinding", "name", fetchedBinding.GetName()) + } + + return nil +} diff --git a/pkg/controller/trustmanager/serviceaccounts.go b/pkg/controller/trustmanager/serviceaccounts.go new file mode 100644 index 000000000..4025716ce --- /dev/null +++ b/pkg/controller/trustmanager/serviceaccounts.go @@ -0,0 +1,45 @@ +package trustmanager + +import ( + "fmt" + + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/openshift/cert-manager-operator/api/operator/v1alpha1" + "github.com/openshift/cert-manager-operator/pkg/operator/assets" +) + +func (r *Reconciler) createOrApplyServiceAccounts(tm *v1alpha1.TrustManager, trustNamespace string, resourceLabels map[string]string, trustManagerCreateRecon bool) error { + desired := r.getServiceAccountObject(trustNamespace, resourceLabels) + + serviceAccountName := fmt.Sprintf("%s/%s", desired.GetNamespace(), desired.GetName()) + r.log.V(4).Info("reconciling serviceaccount resource", "name", serviceAccountName) + fetched := &corev1.ServiceAccount{} + exist, err := r.Exists(r.ctx, client.ObjectKeyFromObject(desired), fetched) + if err != nil { + return FromClientError(err, "failed to check %s serviceaccount resource already exists", serviceAccountName) + } + + if exist { + if trustManagerCreateRecon { + r.eventRecorder.Eventf(tm, corev1.EventTypeWarning, "ResourceAlreadyExists", "%s serviceaccount resource already exists, maybe from previous installation", serviceAccountName) + } + r.log.V(4).Info("serviceaccount resource already exists and is in expected state", "name", serviceAccountName) + } + if !exist { + if err := r.Create(r.ctx, desired); err != nil { + return FromClientError(err, "failed to create %s serviceaccount resource", serviceAccountName) + } + r.eventRecorder.Eventf(tm, corev1.EventTypeNormal, "Reconciled", "serviceaccount resource %s created", serviceAccountName) + } + + return nil +} + +func (r *Reconciler) getServiceAccountObject(trustNamespace string, resourceLabels map[string]string) *corev1.ServiceAccount { + serviceAccount := decodeServiceAccountObjBytes(assets.MustAsset(serviceAccountAssetName)) + updateNamespace(serviceAccount, trustNamespace) + updateResourceLabels(serviceAccount, resourceLabels) + return serviceAccount +} diff --git a/pkg/controller/trustmanager/services.go b/pkg/controller/trustmanager/services.go new file mode 100644 index 000000000..2042d0c92 --- /dev/null +++ b/pkg/controller/trustmanager/services.go @@ -0,0 +1,65 @@ +package trustmanager + +import ( + "fmt" + + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/openshift/cert-manager-operator/api/operator/v1alpha1" + "github.com/openshift/cert-manager-operator/pkg/operator/assets" +) + +func (r *Reconciler) createOrApplyServices(tm *v1alpha1.TrustManager, trustNamespace string, resourceLabels map[string]string, trustManagerCreateRecon bool) error { + metricsService := r.getMetricsServiceObject(trustNamespace, resourceLabels) + if err := r.createOrApplyService(tm, metricsService, trustManagerCreateRecon); err != nil { + return err + } + + // If defaultCAPackage is enabled, ensure the ConfigMap for CA bundle injection exists + if tm.Spec.TrustManagerConfig.DefaultCAPackage.Policy == v1alpha1.DefaultCAPackagePolicyEnabled { + if err := r.createOrApplyDefaultCAPackageConfigMap(tm, trustNamespace, resourceLabels); err != nil { + r.log.Error(err, "failed to reconcile default CA package configmap") + return err + } + } + + return nil +} + +func (r *Reconciler) createOrApplyService(tm *v1alpha1.TrustManager, svc *corev1.Service, trustManagerCreateRecon bool) error { + serviceName := fmt.Sprintf("%s/%s", svc.GetNamespace(), svc.GetName()) + r.log.V(4).Info("reconciling service resource", "name", serviceName) + fetched := &corev1.Service{} + exist, err := r.Exists(r.ctx, client.ObjectKeyFromObject(svc), fetched) + if err != nil { + return FromClientError(err, "failed to check %s service resource already exists", serviceName) + } + + if exist && trustManagerCreateRecon { + r.eventRecorder.Eventf(tm, corev1.EventTypeWarning, "ResourceAlreadyExists", "%s service resource already exists, maybe from previous installation", serviceName) + } + if exist && hasObjectChanged(svc, fetched) { + r.log.V(1).Info("service has been modified, updating to desired state", "name", serviceName) + if err := r.UpdateWithRetry(r.ctx, svc); err != nil { + return FromClientError(err, "failed to update %s service resource", serviceName) + } + r.eventRecorder.Eventf(tm, corev1.EventTypeNormal, "Reconciled", "service resource %s reconciled back to desired state", serviceName) + } else { + r.log.V(4).Info("service resource already exists and is in expected state", "name", serviceName) + } + if !exist { + if err := r.Create(r.ctx, svc); err != nil { + return FromClientError(err, "failed to create %s service resource", serviceName) + } + r.eventRecorder.Eventf(tm, corev1.EventTypeNormal, "Reconciled", "service resource %s created", serviceName) + } + return nil +} + +func (r *Reconciler) getMetricsServiceObject(trustNamespace string, resourceLabels map[string]string) *corev1.Service { + service := decodeServiceObjBytes(assets.MustAsset(metricsServiceAssetName)) + updateNamespace(service, trustNamespace) + updateResourceLabels(service, resourceLabels) + return service +} diff --git a/pkg/controller/trustmanager/utils.go b/pkg/controller/trustmanager/utils.go new file mode 100644 index 000000000..155769183 --- /dev/null +++ b/pkg/controller/trustmanager/utils.go @@ -0,0 +1,360 @@ +package trustmanager + +import ( + "context" + "fmt" + "reflect" + "unsafe" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + metav1validation "k8s.io/apimachinery/pkg/apis/meta/v1/validation" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" + utilerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/client-go/util/retry" + "k8s.io/kubernetes/pkg/apis/core" + corevalidation "k8s.io/kubernetes/pkg/apis/core/validation" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + "github.com/openshift/cert-manager-operator/api/operator/v1alpha1" +) + +var ( + localScheme = runtime.NewScheme() + codecs = serializer.NewCodecFactory(localScheme) +) + +func init() { + if err := appsv1.AddToScheme(localScheme); err != nil { + panic(err) + } + if err := corev1.AddToScheme(localScheme); err != nil { + panic(err) + } + if err := rbacv1.AddToScheme(localScheme); err != nil { + panic(err) + } +} + +// 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, tm *v1alpha1.TrustManager) error { + namespacedName := client.ObjectKeyFromObject(tm) + if !controllerutil.ContainsFinalizer(tm, finalizer) { + if !controllerutil.AddFinalizer(tm, 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, tm); 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(tm) + return nil + } + return nil +} + +// removeFinalizer removes finalizers added to trustmanagers.operator.openshift.io resource. +func (r *Reconciler) removeFinalizer(ctx context.Context, tm *v1alpha1.TrustManager, finalizer string) error { + namespacedName := client.ObjectKeyFromObject(tm) + if controllerutil.ContainsFinalizer(tm, finalizer) { + if !controllerutil.RemoveFinalizer(tm, finalizer) { + return fmt.Errorf("failed to create %q trustmanagers.operator.openshift.io object with finalizers removed", namespacedName) + } + + if err := r.UpdateWithRetry(ctx, tm); err != nil { + return fmt.Errorf("failed to remove finalizers on %q trustmanagers.operator.openshift.io with %w", namespacedName, err) + } + return nil + } + + return nil +} + +func containsProcessedAnnotation(tm *v1alpha1.TrustManager) bool { + _, exist := tm.GetAnnotations()[controllerProcessedAnnotation] + return exist +} + +func addProcessedAnnotation(tm *v1alpha1.TrustManager) bool { + annotations := tm.GetAnnotations() + if annotations == nil { + annotations = make(map[string]string, 1) + } + if _, exist := annotations[controllerProcessedAnnotation]; !exist { + annotations[controllerProcessedAnnotation] = "true" + tm.SetAnnotations(annotations) + return true + } + return false +} + +func updateNamespace(obj client.Object, newNamespace string) { + obj.SetNamespace(newNamespace) +} + +func updateResourceLabels(obj client.Object, labels map[string]string) { + obj.SetLabels(labels) +} + +func decodeDeploymentObjBytes(objBytes []byte) *appsv1.Deployment { + obj, err := runtime.Decode(codecs.UniversalDecoder(appsv1.SchemeGroupVersion), objBytes) + if err != nil { + panic(err) + } + return obj.(*appsv1.Deployment) +} + +func decodeClusterRoleObjBytes(objBytes []byte) *rbacv1.ClusterRole { + obj, err := runtime.Decode(codecs.UniversalDecoder(rbacv1.SchemeGroupVersion), objBytes) + if err != nil { + panic(err) + } + return obj.(*rbacv1.ClusterRole) +} + +func decodeClusterRoleBindingObjBytes(objBytes []byte) *rbacv1.ClusterRoleBinding { + obj, err := runtime.Decode(codecs.UniversalDecoder(rbacv1.SchemeGroupVersion), objBytes) + if err != nil { + panic(err) + } + return obj.(*rbacv1.ClusterRoleBinding) +} + +func decodeRoleObjBytes(objBytes []byte) *rbacv1.Role { + obj, err := runtime.Decode(codecs.UniversalDecoder(rbacv1.SchemeGroupVersion), objBytes) + if err != nil { + panic(err) + } + return obj.(*rbacv1.Role) +} + +func decodeRoleBindingObjBytes(objBytes []byte) *rbacv1.RoleBinding { + obj, err := runtime.Decode(codecs.UniversalDecoder(rbacv1.SchemeGroupVersion), objBytes) + if err != nil { + panic(err) + } + return obj.(*rbacv1.RoleBinding) +} + +func decodeServiceObjBytes(objBytes []byte) *corev1.Service { + obj, err := runtime.Decode(codecs.UniversalDecoder(corev1.SchemeGroupVersion), objBytes) + if err != nil { + panic(err) + } + return obj.(*corev1.Service) +} + +func decodeServiceAccountObjBytes(objBytes []byte) *corev1.ServiceAccount { + obj, err := runtime.Decode(codecs.UniversalDecoder(corev1.SchemeGroupVersion), objBytes) + if err != nil { + panic(err) + } + return obj.(*corev1.ServiceAccount) +} + +func hasObjectChanged(desired, fetched client.Object) bool { + if reflect.TypeOf(desired) != reflect.TypeOf(fetched) { + panic("both objects to be compared must be of same type") + } + + var objectModified bool + switch desired.(type) { + case *rbacv1.ClusterRole: + objectModified = rbacRoleRulesModified[*rbacv1.ClusterRole](desired.(*rbacv1.ClusterRole), fetched.(*rbacv1.ClusterRole)) + case *rbacv1.ClusterRoleBinding: + objectModified = rbacRoleBindingRefModified[*rbacv1.ClusterRoleBinding](desired.(*rbacv1.ClusterRoleBinding), fetched.(*rbacv1.ClusterRoleBinding)) || + rbacRoleBindingSubjectsModified[*rbacv1.ClusterRoleBinding](desired.(*rbacv1.ClusterRoleBinding), fetched.(*rbacv1.ClusterRoleBinding)) + case *appsv1.Deployment: + objectModified = deploymentSpecModified(desired.(*appsv1.Deployment), fetched.(*appsv1.Deployment)) + case *rbacv1.Role: + objectModified = rbacRoleRulesModified[*rbacv1.Role](desired.(*rbacv1.Role), fetched.(*rbacv1.Role)) + case *rbacv1.RoleBinding: + objectModified = rbacRoleBindingRefModified[*rbacv1.RoleBinding](desired.(*rbacv1.RoleBinding), fetched.(*rbacv1.RoleBinding)) || + rbacRoleBindingSubjectsModified[*rbacv1.RoleBinding](desired.(*rbacv1.RoleBinding), fetched.(*rbacv1.RoleBinding)) + case *corev1.Service: + objectModified = serviceSpecModified(desired.(*corev1.Service), fetched.(*corev1.Service)) + case *corev1.ConfigMap: + objectModified = configMapDataModified(desired.(*corev1.ConfigMap), fetched.(*corev1.ConfigMap)) + default: + panic(fmt.Sprintf("unsupported object type: %T", desired)) + } + return objectModified || objectMetadataModified(desired, fetched) +} + +func objectMetadataModified(desired, fetched client.Object) bool { + return !reflect.DeepEqual(desired.GetLabels(), fetched.GetLabels()) +} + +func deploymentSpecModified(desired, fetched *appsv1.Deployment) bool { + // check just the fields which are set by the controller and set in static manifest, + // as fields with default values end up in manifest and causes plain check to fail. + if *desired.Spec.Replicas != *fetched.Spec.Replicas || + !reflect.DeepEqual(desired.Spec.Selector.MatchLabels, fetched.Spec.Selector.MatchLabels) { + return true + } + + if !reflect.DeepEqual(desired.Spec.Template.Labels, fetched.Spec.Template.Labels) || + len(desired.Spec.Template.Spec.Containers) != len(fetched.Spec.Template.Spec.Containers) { + return true + } + + desiredContainer := desired.Spec.Template.Spec.Containers[0] + fetchedContainer := fetched.Spec.Template.Spec.Containers[0] + if !reflect.DeepEqual(desiredContainer.Args, fetchedContainer.Args) || + desiredContainer.Name != fetchedContainer.Name || desiredContainer.Image != fetchedContainer.Image || + desiredContainer.ImagePullPolicy != fetchedContainer.ImagePullPolicy { + return true + } + + if len(desiredContainer.Ports) != len(fetchedContainer.Ports) { + return true + } + for _, fetchedPort := range fetchedContainer.Ports { + matched := false + for _, desiredPort := range desiredContainer.Ports { + if fetchedPort.ContainerPort == desiredPort.ContainerPort { + matched = true + break + } + } + if !matched { + return true + } + } + + if !reflect.DeepEqual(desiredContainer.Resources, fetchedContainer.Resources) || + !reflect.DeepEqual(desiredContainer.VolumeMounts, fetchedContainer.VolumeMounts) { + return true + } + + if desired.Spec.Template.Spec.ServiceAccountName != fetched.Spec.Template.Spec.ServiceAccountName || + !reflect.DeepEqual(desired.Spec.Template.Spec.NodeSelector, fetched.Spec.Template.Spec.NodeSelector) || + !reflect.DeepEqual(desired.Spec.Template.Spec.Volumes, fetched.Spec.Template.Spec.Volumes) { + return true + } + + return false +} + +func serviceSpecModified(desired, fetched *corev1.Service) bool { + if desired.Spec.Type != fetched.Spec.Type || + !reflect.DeepEqual(desired.Spec.Ports, fetched.Spec.Ports) || + !reflect.DeepEqual(desired.Spec.Selector, fetched.Spec.Selector) { + return true + } + + return false +} + +func rbacRoleRulesModified[Object *rbacv1.Role | *rbacv1.ClusterRole](desired, fetched Object) bool { + switch typ := any(desired).(type) { + case *rbacv1.ClusterRole: + return !reflect.DeepEqual(any(desired).(*rbacv1.ClusterRole).Rules, any(fetched).(*rbacv1.ClusterRole).Rules) + case *rbacv1.Role: + return !reflect.DeepEqual(any(desired).(*rbacv1.Role).Rules, any(fetched).(*rbacv1.Role).Rules) + default: + panic(fmt.Sprintf("unsupported object type %v", typ)) + } +} + +func rbacRoleBindingRefModified[Object *rbacv1.RoleBinding | *rbacv1.ClusterRoleBinding](desired, fetched Object) bool { + switch typ := any(desired).(type) { + case *rbacv1.ClusterRoleBinding: + return !reflect.DeepEqual(any(desired).(*rbacv1.ClusterRoleBinding).RoleRef, any(fetched).(*rbacv1.ClusterRoleBinding).RoleRef) + case *rbacv1.RoleBinding: + return !reflect.DeepEqual(any(desired).(*rbacv1.RoleBinding).RoleRef, any(fetched).(*rbacv1.RoleBinding).RoleRef) + default: + panic(fmt.Sprintf("unsupported object type %v", typ)) + } +} + +func rbacRoleBindingSubjectsModified[Object *rbacv1.RoleBinding | *rbacv1.ClusterRoleBinding](desired, fetched Object) bool { + switch typ := any(desired).(type) { + case *rbacv1.ClusterRoleBinding: + return !reflect.DeepEqual(any(desired).(*rbacv1.ClusterRoleBinding).Subjects, any(fetched).(*rbacv1.ClusterRoleBinding).Subjects) + case *rbacv1.RoleBinding: + return !reflect.DeepEqual(any(desired).(*rbacv1.RoleBinding).Subjects, any(fetched).(*rbacv1.RoleBinding).Subjects) + default: + panic(fmt.Sprintf("unsupported object type %v", typ)) + } +} + +func configMapDataModified(desired, fetched *corev1.ConfigMap) bool { + return !reflect.DeepEqual(desired.Data, fetched.Data) +} + +func (r *Reconciler) updateCondition(tm *v1alpha1.TrustManager, prependErr error) error { + if err := r.updateStatus(r.ctx, tm); err != nil { + errUpdate := fmt.Errorf("failed to update %s status: %w", tm.GetName(), err) + if prependErr != nil { + return utilerrors.NewAggregate([]error{err, errUpdate}) + } + return errUpdate + } + return prependErr +} + +// validateNodeSelectorConfig validates the NodeSelector configuration. +func validateNodeSelectorConfig(nodeSelector map[string]string, fldPath *field.Path) error { + return metav1validation.ValidateLabels(nodeSelector, fldPath.Child("nodeSelector")).ToAggregate() +} + +func validateTolerationsConfig(tolerations []corev1.Toleration, fldPath *field.Path) error { + // convert corev1.Tolerations to core.Tolerations, required for validation. + convTolerations := *(*[]core.Toleration)(unsafe.Pointer(&tolerations)) + return corevalidation.ValidateTolerations(convTolerations, fldPath.Child("tolerations")).ToAggregate() +} + +func validateResourceRequirements(requirements corev1.ResourceRequirements, fldPath *field.Path) error { + // convert corev1.ResourceRequirements to core.ResourceRequirements, required for validation. + convRequirements := *(*core.ResourceRequirements)(unsafe.Pointer(&requirements)) + return corevalidation.ValidateContainerResourceRequirements(&convRequirements, nil, fldPath.Child("resources"), corevalidation.PodValidationOptions{}).ToAggregate() +} + +func updateServiceAccountNamespaceInRBACBindingObject[Object *rbacv1.RoleBinding | *rbacv1.ClusterRoleBinding](obj Object, serviceAccount, newNamespace string) { + var subjects *[]rbacv1.Subject + switch o := any(obj).(type) { + case *rbacv1.ClusterRoleBinding: + subjects = &o.Subjects + case *rbacv1.RoleBinding: + subjects = &o.Subjects + } + for i := range *subjects { + if (*subjects)[i].Kind == roleBindingSubjectKind && (*subjects)[i].Name == serviceAccount { + (*subjects)[i].Namespace = newNamespace + } + } +} diff --git a/pkg/features/features_test.go b/pkg/features/features_test.go index 357d7af43..2eb844ec8 100644 --- a/pkg/features/features_test.go +++ b/pkg/features/features_test.go @@ -23,7 +23,9 @@ var expectedDefaultFeatureState = map[bool][]featuregate.Feature{ // features DISABLED by default, // list of features which are expected to be disabled at runtime. - false: {}, + false: { + featuregate.Feature("TrustManager"), + }, } func TestFeatureGates(t *testing.T) { @@ -43,7 +45,7 @@ func TestFeatureGates(t *testing.T) { knownOperatorFeatures = append(knownOperatorFeatures, feat) } - assert.Equal(t, knownOperatorFeatures, testFeatureNames, + assert.ElementsMatch(t, knownOperatorFeatures, testFeatureNames, `the list of features known to the operator differ from what is being tested here, it could be that there was a new Feature added to the api which wasn't added to the tests. Please verify "api/operator/v1alpha1" and "pkg/operator/features" have identical features.`) @@ -66,8 +68,9 @@ func TestFeatureGates(t *testing.T) { continue } - assert.Equal(t, spec.PreRelease == "TechPreview", !spec.Default, - "prerelease TechPreview %q feature should default to disabled", + isTechPreviewOrAlpha := spec.PreRelease == "TechPreview" || spec.PreRelease == featuregate.Alpha + assert.Equal(t, isTechPreviewOrAlpha, !spec.Default, + "prerelease TechPreview/Alpha %q feature should default to disabled", feat) } }) 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/assets/bindata.go b/pkg/operator/assets/bindata.go index d83fcb9e1..47bc95cbc 100644 --- a/pkg/operator/assets/bindata.go +++ b/pkg/operator/assets/bindata.go @@ -63,6 +63,15 @@ // bindata/networkpolicies/istio-csr-allow-ingress-to-grpc-networkpolicy.yaml // bindata/networkpolicies/istio-csr-allow-ingress-to-metrics-networkpolicy.yaml // bindata/networkpolicies/istio-csr-deny-all-networkpolicy.yaml +// bindata/trust-manager/trust-manager-clusterrole.yaml +// bindata/trust-manager/trust-manager-clusterrolebinding.yaml +// bindata/trust-manager/trust-manager-deployment.yaml +// bindata/trust-manager/trust-manager-leases-role.yaml +// bindata/trust-manager/trust-manager-leases-rolebinding.yaml +// bindata/trust-manager/trust-manager-metrics-service.yaml +// bindata/trust-manager/trust-manager-secret-targets-role.yaml +// bindata/trust-manager/trust-manager-secret-targets-rolebinding.yaml +// bindata/trust-manager/trust-manager-serviceaccount.yaml package assets import ( @@ -3072,6 +3081,391 @@ func networkpoliciesIstioCsrDenyAllNetworkpolicyYaml() (*asset, error) { return a, nil } +var _trustManagerTrustManagerClusterroleYaml = []byte(`apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: trust-manager + labels: + app: trust-manager + app.kubernetes.io/name: trust-manager + app.kubernetes.io/instance: trust-manager + app.kubernetes.io/managed-by: cert-manager-operator + app.kubernetes.io/part-of: cert-manager-operator +rules: + - apiGroups: + - trust.cert-manager.io + resources: + - bundles + verbs: + - get + - list + - watch + - apiGroups: + - trust.cert-manager.io + resources: + - bundles/status + verbs: + - patch + - apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - create + - update + - patch + - watch + - delete + - apiGroups: + - "" + resources: + - namespaces + verbs: + - get + - list + - watch + - apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +`) + +func trustManagerTrustManagerClusterroleYamlBytes() ([]byte, error) { + return _trustManagerTrustManagerClusterroleYaml, nil +} + +func trustManagerTrustManagerClusterroleYaml() (*asset, error) { + bytes, err := trustManagerTrustManagerClusterroleYamlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "trust-manager/trust-manager-clusterrole.yaml", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _trustManagerTrustManagerClusterrolebindingYaml = []byte(`apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: trust-manager + labels: + app: trust-manager + app.kubernetes.io/name: trust-manager + app.kubernetes.io/instance: trust-manager + app.kubernetes.io/managed-by: cert-manager-operator + app.kubernetes.io/part-of: cert-manager-operator +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: trust-manager +subjects: + - kind: ServiceAccount + name: trust-manager + namespace: cert-manager +`) + +func trustManagerTrustManagerClusterrolebindingYamlBytes() ([]byte, error) { + return _trustManagerTrustManagerClusterrolebindingYaml, nil +} + +func trustManagerTrustManagerClusterrolebindingYaml() (*asset, error) { + bytes, err := trustManagerTrustManagerClusterrolebindingYamlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "trust-manager/trust-manager-clusterrolebinding.yaml", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _trustManagerTrustManagerDeploymentYaml = []byte(`apiVersion: apps/v1 +kind: Deployment +metadata: + name: trust-manager + namespace: cert-manager + labels: + app: trust-manager + app.kubernetes.io/name: trust-manager + app.kubernetes.io/instance: trust-manager + app.kubernetes.io/managed-by: cert-manager-operator + app.kubernetes.io/part-of: cert-manager-operator +spec: + replicas: 1 + selector: + matchLabels: + app: trust-manager + template: + metadata: + labels: + app: trust-manager + spec: + serviceAccountName: trust-manager + containers: + - name: trust-manager + image: trust-manager:latest + imagePullPolicy: IfNotPresent + args: [] + ports: + - containerPort: 6443 + name: webhook + protocol: TCP + - containerPort: 9402 + name: metrics + protocol: TCP + readinessProbe: + httpGet: + path: /readyz + port: 6060 + initialDelaySeconds: 3 + periodSeconds: 7 + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsNonRoot: true + securityContext: + runAsNonRoot: true +`) + +func trustManagerTrustManagerDeploymentYamlBytes() ([]byte, error) { + return _trustManagerTrustManagerDeploymentYaml, nil +} + +func trustManagerTrustManagerDeploymentYaml() (*asset, error) { + bytes, err := trustManagerTrustManagerDeploymentYamlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "trust-manager/trust-manager-deployment.yaml", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _trustManagerTrustManagerLeasesRoleYaml = []byte(`apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: trust-manager:leases + namespace: cert-manager + labels: + app: trust-manager + app.kubernetes.io/name: trust-manager + app.kubernetes.io/instance: trust-manager + app.kubernetes.io/managed-by: cert-manager-operator + app.kubernetes.io/part-of: cert-manager-operator +rules: + - apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - get + - create + - update + - watch + - list +`) + +func trustManagerTrustManagerLeasesRoleYamlBytes() ([]byte, error) { + return _trustManagerTrustManagerLeasesRoleYaml, nil +} + +func trustManagerTrustManagerLeasesRoleYaml() (*asset, error) { + bytes, err := trustManagerTrustManagerLeasesRoleYamlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "trust-manager/trust-manager-leases-role.yaml", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _trustManagerTrustManagerLeasesRolebindingYaml = []byte(`apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: trust-manager:leases + namespace: cert-manager + labels: + app: trust-manager + app.kubernetes.io/name: trust-manager + app.kubernetes.io/instance: trust-manager + app.kubernetes.io/managed-by: cert-manager-operator + app.kubernetes.io/part-of: cert-manager-operator +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: trust-manager:leases +subjects: + - kind: ServiceAccount + name: trust-manager + namespace: cert-manager +`) + +func trustManagerTrustManagerLeasesRolebindingYamlBytes() ([]byte, error) { + return _trustManagerTrustManagerLeasesRolebindingYaml, nil +} + +func trustManagerTrustManagerLeasesRolebindingYaml() (*asset, error) { + bytes, err := trustManagerTrustManagerLeasesRolebindingYamlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "trust-manager/trust-manager-leases-rolebinding.yaml", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _trustManagerTrustManagerMetricsServiceYaml = []byte(`apiVersion: v1 +kind: Service +metadata: + name: trust-manager-metrics + namespace: cert-manager + labels: + app: trust-manager + app.kubernetes.io/name: trust-manager + app.kubernetes.io/instance: trust-manager + app.kubernetes.io/managed-by: cert-manager-operator + app.kubernetes.io/part-of: cert-manager-operator +spec: + type: ClusterIP + selector: + app: trust-manager + ports: + - name: metrics + port: 9402 + targetPort: metrics + protocol: TCP +`) + +func trustManagerTrustManagerMetricsServiceYamlBytes() ([]byte, error) { + return _trustManagerTrustManagerMetricsServiceYaml, nil +} + +func trustManagerTrustManagerMetricsServiceYaml() (*asset, error) { + bytes, err := trustManagerTrustManagerMetricsServiceYamlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "trust-manager/trust-manager-metrics-service.yaml", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _trustManagerTrustManagerSecretTargetsRoleYaml = []byte(`apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: trust-manager:secret-targets + labels: + app: trust-manager + app.kubernetes.io/name: trust-manager + app.kubernetes.io/instance: trust-manager + app.kubernetes.io/managed-by: cert-manager-operator + app.kubernetes.io/part-of: cert-manager-operator +rules: + - apiGroups: + - "" + resources: + - secrets + verbs: + - get + - list + - create + - update + - patch + - watch + - delete +`) + +func trustManagerTrustManagerSecretTargetsRoleYamlBytes() ([]byte, error) { + return _trustManagerTrustManagerSecretTargetsRoleYaml, nil +} + +func trustManagerTrustManagerSecretTargetsRoleYaml() (*asset, error) { + bytes, err := trustManagerTrustManagerSecretTargetsRoleYamlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "trust-manager/trust-manager-secret-targets-role.yaml", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _trustManagerTrustManagerSecretTargetsRolebindingYaml = []byte(`apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: trust-manager:secret-targets + labels: + app: trust-manager + app.kubernetes.io/name: trust-manager + app.kubernetes.io/instance: trust-manager + app.kubernetes.io/managed-by: cert-manager-operator + app.kubernetes.io/part-of: cert-manager-operator +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: trust-manager:secret-targets +subjects: + - kind: ServiceAccount + name: trust-manager + namespace: cert-manager +`) + +func trustManagerTrustManagerSecretTargetsRolebindingYamlBytes() ([]byte, error) { + return _trustManagerTrustManagerSecretTargetsRolebindingYaml, nil +} + +func trustManagerTrustManagerSecretTargetsRolebindingYaml() (*asset, error) { + bytes, err := trustManagerTrustManagerSecretTargetsRolebindingYamlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "trust-manager/trust-manager-secret-targets-rolebinding.yaml", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _trustManagerTrustManagerServiceaccountYaml = []byte(`apiVersion: v1 +kind: ServiceAccount +metadata: + name: trust-manager + namespace: cert-manager + labels: + app: trust-manager + app.kubernetes.io/name: trust-manager + app.kubernetes.io/instance: trust-manager + app.kubernetes.io/managed-by: cert-manager-operator + app.kubernetes.io/part-of: cert-manager-operator +`) + +func trustManagerTrustManagerServiceaccountYamlBytes() ([]byte, error) { + return _trustManagerTrustManagerServiceaccountYaml, nil +} + +func trustManagerTrustManagerServiceaccountYaml() (*asset, error) { + bytes, err := trustManagerTrustManagerServiceaccountYamlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "trust-manager/trust-manager-serviceaccount.yaml", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + // Asset loads and returns the asset for the given name. // It returns an error if the asset could not be found or // could not be loaded. @@ -3187,6 +3581,15 @@ var _bindata = map[string]func() (*asset, error){ "networkpolicies/istio-csr-allow-ingress-to-grpc-networkpolicy.yaml": networkpoliciesIstioCsrAllowIngressToGrpcNetworkpolicyYaml, "networkpolicies/istio-csr-allow-ingress-to-metrics-networkpolicy.yaml": networkpoliciesIstioCsrAllowIngressToMetricsNetworkpolicyYaml, "networkpolicies/istio-csr-deny-all-networkpolicy.yaml": networkpoliciesIstioCsrDenyAllNetworkpolicyYaml, + "trust-manager/trust-manager-clusterrole.yaml": trustManagerTrustManagerClusterroleYaml, + "trust-manager/trust-manager-clusterrolebinding.yaml": trustManagerTrustManagerClusterrolebindingYaml, + "trust-manager/trust-manager-deployment.yaml": trustManagerTrustManagerDeploymentYaml, + "trust-manager/trust-manager-leases-role.yaml": trustManagerTrustManagerLeasesRoleYaml, + "trust-manager/trust-manager-leases-rolebinding.yaml": trustManagerTrustManagerLeasesRolebindingYaml, + "trust-manager/trust-manager-metrics-service.yaml": trustManagerTrustManagerMetricsServiceYaml, + "trust-manager/trust-manager-secret-targets-role.yaml": trustManagerTrustManagerSecretTargetsRoleYaml, + "trust-manager/trust-manager-secret-targets-rolebinding.yaml": trustManagerTrustManagerSecretTargetsRolebindingYaml, + "trust-manager/trust-manager-serviceaccount.yaml": trustManagerTrustManagerServiceaccountYaml, } // AssetDir returns the file names below a certain @@ -3309,6 +3712,17 @@ var _bintree = &bintree{nil, map[string]*bintree{ "istio-csr-allow-ingress-to-metrics-networkpolicy.yaml": {networkpoliciesIstioCsrAllowIngressToMetricsNetworkpolicyYaml, map[string]*bintree{}}, "istio-csr-deny-all-networkpolicy.yaml": {networkpoliciesIstioCsrDenyAllNetworkpolicyYaml, map[string]*bintree{}}, }}, + "trust-manager": {nil, map[string]*bintree{ + "trust-manager-clusterrole.yaml": {trustManagerTrustManagerClusterroleYaml, map[string]*bintree{}}, + "trust-manager-clusterrolebinding.yaml": {trustManagerTrustManagerClusterrolebindingYaml, map[string]*bintree{}}, + "trust-manager-deployment.yaml": {trustManagerTrustManagerDeploymentYaml, map[string]*bintree{}}, + "trust-manager-leases-role.yaml": {trustManagerTrustManagerLeasesRoleYaml, map[string]*bintree{}}, + "trust-manager-leases-rolebinding.yaml": {trustManagerTrustManagerLeasesRolebindingYaml, map[string]*bintree{}}, + "trust-manager-metrics-service.yaml": {trustManagerTrustManagerMetricsServiceYaml, map[string]*bintree{}}, + "trust-manager-secret-targets-role.yaml": {trustManagerTrustManagerSecretTargetsRoleYaml, map[string]*bintree{}}, + "trust-manager-secret-targets-rolebinding.yaml": {trustManagerTrustManagerSecretTargetsRolebindingYaml, map[string]*bintree{}}, + "trust-manager-serviceaccount.yaml": {trustManagerTrustManagerServiceaccountYaml, map[string]*bintree{}}, + }}, }} // RestoreAsset restores an asset under the given directory 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"))} +} diff --git a/pkg/operator/setup_manager.go b/pkg/operator/setup_manager.go index ba8c49602..f7ce32800 100644 --- a/pkg/operator/setup_manager.go +++ b/pkg/operator/setup_manager.go @@ -8,12 +8,17 @@ import ( corev1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/selection" utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientscheme "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" "k8s.io/klog/v2" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/cache" + "sigs.k8s.io/controller-runtime/pkg/client" ctrllog "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/manager" @@ -21,6 +26,8 @@ import ( 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/features" "github.com/openshift/cert-manager-operator/pkg/version" ) @@ -42,20 +49,86 @@ func init() { // +kubebuilder:scaffold:scheme } -// Manager holds the manager resource for the istio-csr controller +// Manager holds the manager resource for the operator controllers type Manager struct { manager manager.Manager } +// newCacheBuilder returns a cache builder function that configures label selectors +// for managed resources from all enabled controllers. When TrustManager feature gate +// is enabled, the cache includes resources from both istiocsr and trustmanager +// controllers. This ensures that label-filtered resource types (Deployments, Services, +// RBAC, etc.) from both controllers are properly cached. +func newCacheBuilder(trustManagerEnabled bool) cache.NewCacheFunc { + return func(config *rest.Config, opts cache.Options) (cache.Cache, error) { + // Determine which label values to include in the cache selector. + // Both istiocsr and trustmanager use the "app" label key for filtering. + managedLabelValues := []string{"cert-manager-istio-csr"} + if trustManagerEnabled { + managedLabelValues = append(managedLabelValues, "trust-manager") + } + + managedResourceLabelReq, err := labels.NewRequirement("app", selection.In, managedLabelValues) + if err != nil { + return nil, fmt.Errorf("invalid cache label requirement for %q: %w", "app", err) + } + managedResourceLabelReqSelector := labels.NewSelector().Add(*managedResourceLabelReq) + + // Configure cache with label selectors for managed resources + opts.ByObject = map[client.Object]cache.ByObject{ + // Explicitly include IstioCSR to ensure the cache properly watches and syncs all IstioCSR objects + &v1alpha1.IstioCSR{}: {}, + // Resources managed by istiocsr controller (with label selectors) + &certmanagerv1.Certificate{}: { + 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, + }, + &networkingv1.NetworkPolicy{}: { + Label: managedResourceLabelReqSelector, + }, + } + + // If TrustManager is enabled, also include TrustManager objects in the cache + if trustManagerEnabled { + opts.ByObject[&v1alpha1.TrustManager{}] = cache.ByObject{} + } + + return cache.New(config, opts) + } +} + // NewControllerManager creates a new manager. func NewControllerManager() (*Manager, error) { setupLog.Info("setting up operator manager", "controller", istiocsr.ControllerName) setupLog.Info("controller", "version", version.Get()) + trustManagerEnabled := features.DefaultFeatureGate.Enabled(v1alpha1.FeatureTrustManager) + mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ Scheme: scheme, - // Use custom cache builder to configure label selectors for managed resources - NewCache: istiocsr.NewCacheBuilder, + // Use combined cache builder to configure label selectors for managed resources + // from all enabled controllers. + NewCache: newCacheBuilder(trustManagerEnabled), Logger: ctrl.Log.WithName("operator-manager"), }) if err != nil { @@ -69,6 +142,19 @@ func NewControllerManager() (*Manager, error) { if err := r.SetupWithManager(mgr); err != nil { return nil, fmt.Errorf("failed to create %s controller: %w", istiocsr.ControllerName, err) } + // Register trust-manager controller if the TrustManager feature gate is enabled + if trustManagerEnabled { + setupLog.Info("TrustManager feature gate is enabled, registering trust-manager controller") + tmr, err := trustmanager.New(mgr) + if err != nil { + return nil, fmt.Errorf("failed to create %s reconciler object: %w", trustmanager.ControllerName, err) + } + if err := tmr.SetupWithManager(mgr); err != nil { + return nil, fmt.Errorf("failed to create %s controller: %w", trustmanager.ControllerName, err) + } + } else { + setupLog.Info("TrustManager feature gate is disabled, skipping trust-manager controller registration") + } // +kubebuilder:scaffold:builder return &Manager{ diff --git a/test/e2e/testdata/trust_manager/trust_manager_template.yaml b/test/e2e/testdata/trust_manager/trust_manager_template.yaml new file mode 100644 index 000000000..e260917f4 --- /dev/null +++ b/test/e2e/testdata/trust_manager/trust_manager_template.yaml @@ -0,0 +1,28 @@ +apiVersion: operator.openshift.io/v1alpha1 +kind: TrustManager +metadata: + name: cluster +spec: + trustManagerConfig: + logLevel: {{.LogLevel}} + logFormat: "{{.LogFormat}}" +{{- if .TrustNamespace}} + trustNamespace: "{{.TrustNamespace}}" +{{- end}} +{{- if .FilterExpiredCertificates}} + filterExpiredCertificates: "{{.FilterExpiredCertificates}}" +{{- end}} +{{- if .SecretTargetsPolicy}} + secretTargets: + policy: "{{.SecretTargetsPolicy}}" +{{- if .AuthorizedSecrets}} + authorizedSecrets: +{{- range .AuthorizedSecrets}} + - "{{.}}" +{{- end}} +{{- end}} +{{- end}} +{{- if .DefaultCAPackagePolicy}} + defaultCAPackage: + policy: "{{.DefaultCAPackagePolicy}}" +{{- end}} diff --git a/test/e2e/trust_manager_test.go b/test/e2e/trust_manager_test.go new file mode 100644 index 000000000..eeff44077 --- /dev/null +++ b/test/e2e/trust_manager_test.go @@ -0,0 +1,526 @@ +//go:build e2e +// +build e2e + +package e2e + +import ( + "context" + "fmt" + "path/filepath" + + "github.com/openshift/cert-manager-operator/api/operator/v1alpha1" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + 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/util/wait" + "k8s.io/client-go/kubernetes" +) + +// TrustManagerConfig holds the template values for the trust-manager CR template. +type TrustManagerConfig struct { + LogLevel int32 + LogFormat string + TrustNamespace string + FilterExpiredCertificates string + SecretTargetsPolicy string + AuthorizedSecrets []string + DefaultCAPackagePolicy string +} + +var _ = Describe("TrustManager", Ordered, Label("Feature:TrustManager"), func() { + ctx := context.TODO() + var clientset *kubernetes.Clientset + + const ( + trustManagerDeploymentName = "trust-manager" + trustManagerServiceAccount = "trust-manager" + trustManagerNamespace = "cert-manager" + trustManagerClusterRoleName = "trust-manager" + trustManagerCRBName = "trust-manager" + trustManagerLeasesRoleName = "trust-manager-leases" + trustManagerLeasesRBName = "trust-manager-leases" + trustManagerMetricsService = "trust-manager-metrics" + trustManagerSecretTargetsCR = "trust-manager-secret-targets" + trustManagerSecretTargetsCRB = "trust-manager-secret-targets" + defaultCAPackageConfigMapName = "trust-manager-default-package" + ) + + defaultTrustManagerConfig := func() TrustManagerConfig { + return TrustManagerConfig{ + LogLevel: 1, + LogFormat: "text", + } + } + + createTrustManagerCR := func(tmConfig TrustManagerConfig) { + By("creating trustmanager.operator.openshift.io resource") + loader.CreateFromFile(AssetFunc(testassets.ReadFile).WithTemplateValues(tmConfig), + filepath.Join("testdata", "trust_manager", "trust_manager_template.yaml"), "") + } + + deleteTrustManagerCR := func(tmConfig TrustManagerConfig) { + By("deleting trustmanager.operator.openshift.io resource") + loader.DeleteFromFile(AssetFunc(testassets.ReadFile).WithTemplateValues(tmConfig), + filepath.Join("testdata", "trust_manager", "trust_manager_template.yaml"), "") + } + + waitForTrustManagerReady := func() { + By("waiting for trust-manager deployment to be available") + err := pollTillDeploymentAvailable(ctx, clientset, trustManagerNamespace, trustManagerDeploymentName) + Expect(err).Should(BeNil()) + + By("waiting for TrustManager CR to become Ready") + err = pollTillTrustManagerReady(ctx) + Expect(err).Should(BeNil()) + } + + waitForTrustManagerDeleted := func() { + By("waiting for trust-manager deployment to be removed") + err := pollTillResourceDeleted(ctx, func() error { + _, err := clientset.AppsV1().Deployments(trustManagerNamespace).Get(ctx, trustManagerDeploymentName, metav1.GetOptions{}) + return err + }) + Expect(err).Should(BeNil()) + } + + 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 lifecycle", func() { + AfterEach(func() { + tmConfig := defaultTrustManagerConfig() + deleteTrustManagerCR(tmConfig) + + By("waiting for trust-manager resources to be cleaned up") + waitForTrustManagerDeleted() + + By("cleaning up cluster-scoped RBAC resources") + clientset.RbacV1().ClusterRoles().DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{ + LabelSelector: "app.kubernetes.io/name=trust-manager", + }) + clientset.RbacV1().ClusterRoleBindings().DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{ + LabelSelector: "app.kubernetes.io/name=trust-manager", + }) + }) + + It("should create all trust-manager resources when TrustManager CR is created", func() { + tmConfig := defaultTrustManagerConfig() + createTrustManagerCR(tmConfig) + waitForTrustManagerReady() + + By("verifying the deployment exists in the cert-manager namespace") + deployment, err := clientset.AppsV1().Deployments(trustManagerNamespace).Get(ctx, trustManagerDeploymentName, metav1.GetOptions{}) + Expect(err).Should(BeNil()) + Expect(deployment.Spec.Template.Spec.Containers).Should(HaveLen(1)) + Expect(deployment.Spec.Template.Spec.Containers[0].Name).Should(Equal(trustManagerDeploymentName)) + + By("verifying the service account exists") + _, err = clientset.CoreV1().ServiceAccounts(trustManagerNamespace).Get(ctx, trustManagerServiceAccount, metav1.GetOptions{}) + Expect(err).Should(BeNil()) + + By("verifying the cluster role exists") + _, err = clientset.RbacV1().ClusterRoles().Get(ctx, trustManagerClusterRoleName, metav1.GetOptions{}) + Expect(err).Should(BeNil()) + + By("verifying the cluster role binding exists") + _, err = clientset.RbacV1().ClusterRoleBindings().Get(ctx, trustManagerCRBName, metav1.GetOptions{}) + Expect(err).Should(BeNil()) + + By("verifying the leases role exists") + _, err = clientset.RbacV1().Roles(trustManagerNamespace).Get(ctx, trustManagerLeasesRoleName, metav1.GetOptions{}) + Expect(err).Should(BeNil()) + + By("verifying the leases role binding exists") + _, err = clientset.RbacV1().RoleBindings(trustManagerNamespace).Get(ctx, trustManagerLeasesRBName, metav1.GetOptions{}) + Expect(err).Should(BeNil()) + + By("verifying the metrics service exists") + _, err = clientset.CoreV1().Services(trustManagerNamespace).Get(ctx, trustManagerMetricsService, metav1.GetOptions{}) + Expect(err).Should(BeNil()) + }) + + It("should set Ready condition to True on successful deployment", func() { + tmConfig := defaultTrustManagerConfig() + createTrustManagerCR(tmConfig) + waitForTrustManagerReady() + + By("fetching the TrustManager CR") + tm, err := certmanageroperatorclient.OperatorV1alpha1().TrustManagers().Get(ctx, "cluster", metav1.GetOptions{}) + Expect(err).Should(BeNil()) + + By("checking Ready condition") + readyCondition := meta.FindStatusCondition(tm.Status.Conditions, v1alpha1.Ready) + Expect(readyCondition).ShouldNot(BeNil()) + Expect(readyCondition.Status).Should(Equal(metav1.ConditionTrue)) + + By("checking Degraded condition is False") + degradedCondition := meta.FindStatusCondition(tm.Status.Conditions, v1alpha1.Degraded) + if degradedCondition != nil { + Expect(degradedCondition.Status).Should(Equal(metav1.ConditionFalse)) + } + + By("checking status fields are populated") + Expect(tm.Status.TrustManagerImage).ShouldNot(BeEmpty()) + }) + + It("should clean up all resources when TrustManager CR is deleted", func() { + tmConfig := defaultTrustManagerConfig() + createTrustManagerCR(tmConfig) + waitForTrustManagerReady() + + By("deleting the TrustManager CR") + deleteTrustManagerCR(tmConfig) + waitForTrustManagerDeleted() + + By("verifying the deployment is removed") + _, err := clientset.AppsV1().Deployments(trustManagerNamespace).Get(ctx, trustManagerDeploymentName, metav1.GetOptions{}) + Expect(apierrors.IsNotFound(err)).Should(BeTrue(), "deployment should be deleted") + + By("verifying the service account is removed") + _, err = clientset.CoreV1().ServiceAccounts(trustManagerNamespace).Get(ctx, trustManagerServiceAccount, metav1.GetOptions{}) + Expect(apierrors.IsNotFound(err)).Should(BeTrue(), "service account should be deleted") + + By("verifying the cluster role is removed") + Eventually(func() bool { + _, err := clientset.RbacV1().ClusterRoles().Get(ctx, trustManagerClusterRoleName, metav1.GetOptions{}) + return apierrors.IsNotFound(err) + }, lowTimeout, fastPollInterval).Should(BeTrue(), "cluster role should be deleted") + + By("verifying the cluster role binding is removed") + Eventually(func() bool { + _, err := clientset.RbacV1().ClusterRoleBindings().Get(ctx, trustManagerCRBName, metav1.GetOptions{}) + return apierrors.IsNotFound(err) + }, lowTimeout, fastPollInterval).Should(BeTrue(), "cluster role binding should be deleted") + }) + }) + + Context("deployment reconciliation", func() { + AfterEach(func() { + tmConfig := defaultTrustManagerConfig() + deleteTrustManagerCR(tmConfig) + waitForTrustManagerDeleted() + + clientset.RbacV1().ClusterRoles().DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{ + LabelSelector: "app.kubernetes.io/name=trust-manager", + }) + clientset.RbacV1().ClusterRoleBindings().DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{ + LabelSelector: "app.kubernetes.io/name=trust-manager", + }) + }) + + It("should reconcile the deployment back to desired state if manually modified", func() { + tmConfig := defaultTrustManagerConfig() + createTrustManagerCR(tmConfig) + waitForTrustManagerReady() + + By("manually scaling down the deployment to zero replicas") + deployment, err := clientset.AppsV1().Deployments(trustManagerNamespace).Get(ctx, trustManagerDeploymentName, metav1.GetOptions{}) + Expect(err).Should(BeNil()) + + modifiedDeployment := deployment.DeepCopy() + zero := int32(0) + modifiedDeployment.Spec.Replicas = &zero + _, err = clientset.AppsV1().Deployments(trustManagerNamespace).Update(ctx, modifiedDeployment, metav1.UpdateOptions{}) + Expect(err).Should(BeNil()) + + By("verifying the deployment is reconciled back to 1 replica") + Eventually(func() bool { + dep, err := clientset.AppsV1().Deployments(trustManagerNamespace).Get(ctx, trustManagerDeploymentName, metav1.GetOptions{}) + if err != nil { + return false + } + return dep.Spec.Replicas != nil && *dep.Spec.Replicas == 1 + }, lowTimeout, fastPollInterval).Should(BeTrue(), "deployment should be reconciled back to 1 replica") + + By("waiting for the deployment to become available again") + err = pollTillDeploymentAvailable(ctx, clientset, trustManagerNamespace, trustManagerDeploymentName) + Expect(err).Should(BeNil()) + }) + + It("should pass configured log level and format to the deployment", func() { + tmConfig := TrustManagerConfig{ + LogLevel: 3, + LogFormat: "json", + } + createTrustManagerCR(tmConfig) + defer deleteTrustManagerCR(tmConfig) + waitForTrustManagerReady() + + By("verifying the deployment has the correct log level and format args") + Eventually(func() bool { + dep, err := clientset.AppsV1().Deployments(trustManagerNamespace).Get(ctx, trustManagerDeploymentName, metav1.GetOptions{}) + if err != nil || len(dep.Spec.Template.Spec.Containers) == 0 { + return false + } + args := dep.Spec.Template.Spec.Containers[0].Args + hasLogLevel := false + hasLogFormat := false + for _, arg := range args { + if arg == "--log-level=3" { + hasLogLevel = true + } + if arg == "--log-format=json" { + hasLogFormat = true + } + } + return hasLogLevel && hasLogFormat + }, lowTimeout, fastPollInterval).Should(BeTrue(), "deployment should have --log-level=3 and --log-format=json args") + }) + }) + + Context("secret targets policy", func() { + AfterEach(func() { + // Clean up any TrustManager CR + tmConfig := defaultTrustManagerConfig() + deleteTrustManagerCR(tmConfig) + waitForTrustManagerDeleted() + + // Clean up cluster-scoped resources + clientset.RbacV1().ClusterRoles().DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{ + LabelSelector: "app.kubernetes.io/name=trust-manager", + }) + clientset.RbacV1().ClusterRoleBindings().DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{ + LabelSelector: "app.kubernetes.io/name=trust-manager", + }) + }) + + It("should create secret targets RBAC when policy is Custom", func() { + tmConfig := TrustManagerConfig{ + LogLevel: 1, + LogFormat: "text", + SecretTargetsPolicy: "Custom", + AuthorizedSecrets: []string{"my-secret"}, + } + createTrustManagerCR(tmConfig) + defer deleteTrustManagerCR(tmConfig) + waitForTrustManagerReady() + + By("verifying secret-targets ClusterRole is created") + Eventually(func() error { + _, err := clientset.RbacV1().ClusterRoles().Get(ctx, trustManagerSecretTargetsCR, metav1.GetOptions{}) + return err + }, lowTimeout, fastPollInterval).Should(Succeed(), "secret-targets ClusterRole should exist") + + By("verifying secret-targets ClusterRoleBinding is created") + Eventually(func() error { + _, err := clientset.RbacV1().ClusterRoleBindings().Get(ctx, trustManagerSecretTargetsCRB, metav1.GetOptions{}) + return err + }, lowTimeout, fastPollInterval).Should(Succeed(), "secret-targets ClusterRoleBinding should exist") + + By("verifying deployment has --secret-targets-enabled arg") + Eventually(func() bool { + dep, err := clientset.AppsV1().Deployments(trustManagerNamespace).Get(ctx, trustManagerDeploymentName, metav1.GetOptions{}) + if err != nil || len(dep.Spec.Template.Spec.Containers) == 0 { + return false + } + for _, arg := range dep.Spec.Template.Spec.Containers[0].Args { + if arg == "--secret-targets-enabled=true" { + return true + } + } + return false + }, lowTimeout, fastPollInterval).Should(BeTrue(), "deployment should have --secret-targets-enabled=true") + }) + + It("should not create secret targets RBAC when policy is Disabled", func() { + tmConfig := defaultTrustManagerConfig() + createTrustManagerCR(tmConfig) + defer deleteTrustManagerCR(tmConfig) + waitForTrustManagerReady() + + By("verifying secret-targets ClusterRole does NOT exist") + Consistently(func() bool { + _, err := clientset.RbacV1().ClusterRoles().Get(ctx, trustManagerSecretTargetsCR, metav1.GetOptions{}) + return apierrors.IsNotFound(err) + }, "30s", fastPollInterval).Should(BeTrue(), "secret-targets ClusterRole should not exist when policy is Disabled") + }) + }) + + Context("default CA package", func() { + AfterEach(func() { + tmConfig := defaultTrustManagerConfig() + deleteTrustManagerCR(tmConfig) + waitForTrustManagerDeleted() + + clientset.RbacV1().ClusterRoles().DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{ + LabelSelector: "app.kubernetes.io/name=trust-manager", + }) + clientset.RbacV1().ClusterRoleBindings().DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{ + LabelSelector: "app.kubernetes.io/name=trust-manager", + }) + }) + + It("should create CA package ConfigMap with injection annotation when enabled", func() { + tmConfig := TrustManagerConfig{ + LogLevel: 1, + LogFormat: "text", + DefaultCAPackagePolicy: "Enabled", + } + createTrustManagerCR(tmConfig) + defer deleteTrustManagerCR(tmConfig) + waitForTrustManagerReady() + + By("verifying default CA package ConfigMap exists") + Eventually(func() error { + cm, err := clientset.CoreV1().ConfigMaps(trustManagerNamespace).Get(ctx, defaultCAPackageConfigMapName, metav1.GetOptions{}) + if err != nil { + return err + } + + By("checking ConfigMap has the trusted CA bundle injection annotation") + annotations := cm.GetAnnotations() + val, exists := annotations["config.openshift.io/inject-trusted-cabundle"] + if !exists || val != "true" { + return fmt.Errorf("expected annotation config.openshift.io/inject-trusted-cabundle=true, got: %v", annotations) + } + return nil + }, lowTimeout, fastPollInterval).Should(Succeed(), "default CA package ConfigMap should exist with injection annotation") + + By("verifying the deployment has the --default-package-location arg") + Eventually(func() bool { + dep, err := clientset.AppsV1().Deployments(trustManagerNamespace).Get(ctx, trustManagerDeploymentName, metav1.GetOptions{}) + if err != nil || len(dep.Spec.Template.Spec.Containers) == 0 { + return false + } + for _, arg := range dep.Spec.Template.Spec.Containers[0].Args { + if arg == "--default-package-location=/var/run/trust-manager/default-package/ca-bundle.crt" { + return true + } + } + return false + }, lowTimeout, fastPollInterval).Should(BeTrue(), "deployment should have --default-package-location arg") + }) + + It("should not create CA package ConfigMap when disabled", func() { + tmConfig := defaultTrustManagerConfig() + createTrustManagerCR(tmConfig) + defer deleteTrustManagerCR(tmConfig) + waitForTrustManagerReady() + + By("verifying default CA package ConfigMap does NOT exist") + Consistently(func() bool { + _, err := clientset.CoreV1().ConfigMaps(trustManagerNamespace).Get(ctx, defaultCAPackageConfigMapName, metav1.GetOptions{}) + return apierrors.IsNotFound(err) + }, "30s", fastPollInterval).Should(BeTrue(), "default CA package ConfigMap should not exist when disabled") + }) + }) + + Context("filter expired certificates", func() { + AfterEach(func() { + tmConfig := defaultTrustManagerConfig() + deleteTrustManagerCR(tmConfig) + waitForTrustManagerDeleted() + + clientset.RbacV1().ClusterRoles().DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{ + LabelSelector: "app.kubernetes.io/name=trust-manager", + }) + clientset.RbacV1().ClusterRoleBindings().DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{ + LabelSelector: "app.kubernetes.io/name=trust-manager", + }) + }) + + It("should pass --filter-expired-certs=true when policy is Enabled", func() { + tmConfig := TrustManagerConfig{ + LogLevel: 1, + LogFormat: "text", + FilterExpiredCertificates: "Enabled", + } + createTrustManagerCR(tmConfig) + defer deleteTrustManagerCR(tmConfig) + waitForTrustManagerReady() + + By("verifying the deployment has --filter-expired-certs=true arg") + Eventually(func() bool { + dep, err := clientset.AppsV1().Deployments(trustManagerNamespace).Get(ctx, trustManagerDeploymentName, metav1.GetOptions{}) + if err != nil || len(dep.Spec.Template.Spec.Containers) == 0 { + return false + } + for _, arg := range dep.Spec.Template.Spec.Containers[0].Args { + if arg == "--filter-expired-certs=true" { + return true + } + } + return false + }, lowTimeout, fastPollInterval).Should(BeTrue(), "deployment should have --filter-expired-certs=true") + }) + + It("should pass --filter-expired-certs=false when policy is Disabled", func() { + tmConfig := defaultTrustManagerConfig() + createTrustManagerCR(tmConfig) + defer deleteTrustManagerCR(tmConfig) + waitForTrustManagerReady() + + By("verifying the deployment has --filter-expired-certs=false arg") + Eventually(func() bool { + dep, err := clientset.AppsV1().Deployments(trustManagerNamespace).Get(ctx, trustManagerDeploymentName, metav1.GetOptions{}) + if err != nil || len(dep.Spec.Template.Spec.Containers) == 0 { + return false + } + for _, arg := range dep.Spec.Template.Spec.Containers[0].Args { + if arg == "--filter-expired-certs=false" { + return true + } + } + return false + }, lowTimeout, fastPollInterval).Should(BeTrue(), "deployment should have --filter-expired-certs=false") + }) + }) +}) + +// pollTillTrustManagerReady polls the TrustManager CR until it has a Ready=True condition. +func pollTillTrustManagerReady(ctx context.Context) error { + return wait.PollUntilContextTimeout(ctx, slowPollInterval, highTimeout, true, func(context.Context) (bool, error) { + tm, err := certmanageroperatorclient.OperatorV1alpha1().TrustManagers().Get(ctx, "cluster", metav1.GetOptions{}) + if err != nil { + if apierrors.IsNotFound(err) { + return false, nil + } + return false, err + } + + readyCondition := meta.FindStatusCondition(tm.Status.Conditions, v1alpha1.Ready) + if readyCondition == nil { + return false, nil + } + + degradedCondition := meta.FindStatusCondition(tm.Status.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 + }) +} + +// pollTillResourceDeleted polls until a get function returns IsNotFound. +func pollTillResourceDeleted(ctx context.Context, getFn func() error) error { + return wait.PollUntilContextTimeout(ctx, fastPollInterval, highTimeout, true, func(context.Context) (bool, error) { + err := getFn() + if err != nil { + if apierrors.IsNotFound(err) { + return true, nil + } + return false, err + } + return false, nil + }) +}