diff --git a/bundle/manifests/cert-manager-acme-issuer-sample_console.openshift.io_v1_consoleyamlsample.yaml b/bindata/console/cert-manager-acme-issuer-sample.yaml similarity index 87% rename from bundle/manifests/cert-manager-acme-issuer-sample_console.openshift.io_v1_consoleyamlsample.yaml rename to bindata/console/cert-manager-acme-issuer-sample.yaml index 31f27a2e0..40e3d7e4a 100644 --- a/bundle/manifests/cert-manager-acme-issuer-sample_console.openshift.io_v1_consoleyamlsample.yaml +++ b/bindata/console/cert-manager-acme-issuer-sample.yaml @@ -1,16 +1,13 @@ apiVersion: console.openshift.io/v1 kind: ConsoleYAMLSample metadata: - annotations: - capability.openshift.io/name: Console name: cert-manager-acme-issuer-sample spec: - description: An example ACME Issuer for Let's Encrypt production certificates with - HTTP-01 challenge targetResource: apiVersion: cert-manager.io/v1 kind: Issuer title: Example ACME Issuer (Let's Encrypt) + description: An example ACME Issuer for Let's Encrypt production certificates with HTTP-01 challenge yaml: |- apiVersion: cert-manager.io/v1 kind: Issuer diff --git a/bundle/manifests/cert-manager-certificate-sample_console.openshift.io_v1_consoleyamlsample.yaml b/bindata/console/cert-manager-certificate-sample.yaml similarity index 90% rename from bundle/manifests/cert-manager-certificate-sample_console.openshift.io_v1_consoleyamlsample.yaml rename to bindata/console/cert-manager-certificate-sample.yaml index 64b78b490..b83b514ac 100644 --- a/bundle/manifests/cert-manager-certificate-sample_console.openshift.io_v1_consoleyamlsample.yaml +++ b/bindata/console/cert-manager-certificate-sample.yaml @@ -1,15 +1,13 @@ apiVersion: console.openshift.io/v1 kind: ConsoleYAMLSample metadata: - annotations: - capability.openshift.io/name: Console name: cert-manager-certificate-sample spec: - description: A simple Certificate example targetResource: apiVersion: cert-manager.io/v1 kind: Certificate title: Example Certificate + description: A simple Certificate example yaml: |- apiVersion: cert-manager.io/v1 kind: Certificate @@ -24,3 +22,4 @@ spec: issuerRef: name: selfsigned-issuer kind: Issuer + diff --git a/bindata/console/cert-manager-example-quickstart.yaml b/bindata/console/cert-manager-example-quickstart.yaml new file mode 100644 index 000000000..673421ad3 --- /dev/null +++ b/bindata/console/cert-manager-example-quickstart.yaml @@ -0,0 +1,199 @@ +apiVersion: console.openshift.io/v1 +kind: ConsoleQuickStart +metadata: + name: cert-manager-example + annotations: + include.release.openshift.io/ibm-cloud-managed: "true" + include.release.openshift.io/self-managed-high-availability: "true" + include.release.openshift.io/single-node-developer: "true" +spec: + displayName: cert-manager Operator for Red Hat OpenShift Example + tags: + - example + - operator + - certificates + durationMinutes: 10 + prerequisites: + - You completed the "Install the cert-manager Operator for Red Hat OpenShift" quick start. + - You have a namespace in which to deploy the example CRs. + description: |- + Create and issue TLS certificates using the cert-manager Operator for Red Hat OpenShift + introduction: |- + # cert-manager Operator for Red Hat OpenShift + + The cert-manager Operator for Red Hat OpenShift enables you to create and sign TLS certificates from an external PKI for your workloads + running on an OpenShift cluster. + + ### Expected Learning + + With this Quick Start, you will learn about the following CRDs provided by the cert-manager operator: + 1. **Issuer** - defines a certificate authority that can sign certificates + 2. **Certificate** - defines the desired certificate and its properties + + This Quick Start will walk you through creating your first certificate: + - Create a self-signed Issuer (for testing) + - Create a Certificate signed by that Issuer + - View the generated certificate in a Kubernetes Secret + - Learn how to use it in your applications + + **Note**: For production, you would use Let's Encrypt (ACME), HashiCorp Vault, or your organization's CA instead of self-signed certificates. + tasks: + - title: Navigate to installed cert-manager operator + description: |- + ### To navigate to the installed operator: + 1. Go to the **Installed Operators** from the [Ecosystem]{{highlight qs-nav-ecosystem}} section of the navigation. + 2. In the **Search by name** field, type `cert-manager`. + 3. Look for **cert-manager Operator for Red Hat OpenShift**. If you had completed the prerequisite Quick Start, the tile should appear. + 4. Click on the installed operator + + You will be brought to the **Operator Details** page and be presented with **Provided APIs** + review: + instructions: |- + #### Verify you see a list of **Provided APIs**: + The list should include `Issuer`, `ClusterIssuer`, and `Certificate` + failedTaskHelp: This task isn't verified yet. Try the task again. + summary: + success: You are in the right place, and ready to start the rest of the Quick Start + failed: Try the steps again. + - title: Select a project + description: |- + ### Create or select a project to work in + 1. Find the **Project** dropdown menu at the top of the screen. + 2. Select or create the project in which you want to work in. + + **Note**: For this example, we'll create an `Issuer` which is namespace-scoped. If you want to issue certificates + across multiple namespaces, you can create a `ClusterIssuer` instead. + review: + instructions: |- + #### Verify the name in the **Project** dropdown menu is the expected project + failedTaskHelp: Try the task again. + summary: + success: You are in the right place. + failed: Try the steps again. + - title: Create a self-signed Issuer + description: |- + ### To create a self-signed Issuer + + An Issuer represents a certificate authority that can sign certificates. We'll create a self-signed Issuer + for this example. This is useful for testing and development. + + 1. Find the `Issuer` Custom Resource in the list of **Provided APIs** or in the top side-scrolling menu bar. + - From the list of **Provided APIs**, click the **Create instance** link. + - From the **top side-scrolling menu bar**, click **Issuer** and then click **Create Issuer**. + + 2. Switch to **YAML view** in the editor. + + 3. On the right sidebar, look for the **Samples** section and select **"Example Self-Signed Issuer"**. + + 4. Click **Try it** to populate the editor with the sample YAML. + + 5. Click the **Create** button to create the Issuer. + review: + instructions: |- + #### Verify the Issuer was successfully created: + 1. You should see the Issuer listed with the name `selfsigned-issuer` + 2. Check that the **Ready** condition shows **True** in the Conditions section + failedTaskHelp: This task isn't verified yet. Try the task again. + summary: + success: You just created a self-signed Issuer! Now we can create certificates. + failed: Try the steps again. + - title: Create a Certificate + description: |- + ### To create a Certificate + + Now we'll create a certificate that will be signed by our Issuer. + + 1. Find the `Certificate` Custom Resource in the list of **Provided APIs** or in the top side-scrolling menu bar. + - From the list of **Provided APIs**, click the **Create instance** link. + - From the **top side-scrolling menu bar**, click **Certificate** and then click **Create Certificate**. + + 2. Switch to **YAML view** in the editor. + + 3. On the right sidebar, look for the **Samples** section and select **"Example Certificate"**. + + 4. Click **Try it** to populate the editor with the sample YAML. + + 5. Click the **Create** button to create the Certificate. + review: + instructions: |- + #### Verify the Certificate was successfully created: + 1. You should see the Certificate listed with the name `example-cert`. + 2. Check that the **Ready** condition shows **True**. + 3. Navigate to the [Workloads]{{highlight qs-nav-workloads}} section and click **Secrets** + 4. You should see a new Secret named `example-tls`. + failedTaskHelp: This task isn't verified yet. Try the task again. + summary: + success: You just created your first certificate! cert-manager has issued it and stored it in a Secret. + failed: Try the steps again. + - title: View the certificate + description: |- + ### To inspect the certificate + + Let's look at the Secret that contains the certificate. + + 1. Make sure you're in the [Workloads]{{highlight qs-nav-workloads}} section, click **Secrets**. + 2. Click on the **example-tls** Secret. + 3. You should see the certificate data with keys: + - `tls.crt` - The certificate. + - `tls.key` - The private key. + - `ca.crt` - The CA certificate. + review: + instructions: |- + #### Verify you can see the certificate data: + Is the Secret `example-tls` present with `tls.crt` and `tls.key`? + failedTaskHelp: This task isn't verified yet. Try the task again. + summary: + success: Great! Your certificate is ready to use. + failed: Try the steps again. + - title: Use the certificate + description: |- + ### How to use certificates + + Now you can use this certificate in your applications. Here's a simple example for an OpenShift Route: + + ```yaml + apiVersion: route.openshift.io/v1 + kind: Route + metadata: + name: my-app + spec: + to: + kind: Service + name: my-service + tls: + termination: edge + externalCertificate: + name: example-tls + ``` + + Or in an Ingress: + + ```yaml + apiVersion: networking.k8s.io/v1 + kind: Ingress + metadata: + name: my-app + spec: + tls: + - secretName: example-tls + rules: + - host: example.com + ``` + + **Note**: cert-manager will automatically renew certificates before they expire! + review: + instructions: |- + #### Do you understand how to use certificates? + Certificates can be referenced in Routes and Ingress resources. + failedTaskHelp: Review the examples above. + summary: + success: You now know how to create and use certificates! + failed: Review the examples again. + conclusion: |- + Great job! You've successfully created your first Issuer and Certificate. + + ### Next Steps: + + - For production, use Let's Encrypt (ACME Issuer) instead of self-signed certificates + - Explore ClusterIssuer for cluster-wide certificate management + - Check out the [cert-manager documentation](https://docs.redhat.com/en/documentation/openshift_container_platform/latest/html/security_and_compliance/cert-manager-operator-for-red-hat-openshift) to learn more diff --git a/bundle/manifests/cert-manager-issuer-sample_console.openshift.io_v1_consoleyamlsample.yaml b/bindata/console/cert-manager-issuer-sample.yaml similarity index 88% rename from bundle/manifests/cert-manager-issuer-sample_console.openshift.io_v1_consoleyamlsample.yaml rename to bindata/console/cert-manager-issuer-sample.yaml index 3e8fffad4..3bc9ebd71 100644 --- a/bundle/manifests/cert-manager-issuer-sample_console.openshift.io_v1_consoleyamlsample.yaml +++ b/bindata/console/cert-manager-issuer-sample.yaml @@ -1,15 +1,13 @@ apiVersion: console.openshift.io/v1 kind: ConsoleYAMLSample metadata: - annotations: - capability.openshift.io/name: Console name: cert-manager-issuer-sample spec: - description: A simple self-signed Issuer for development and testing targetResource: apiVersion: cert-manager.io/v1 kind: Issuer title: Example Self-Signed Issuer + description: A simple self-signed Issuer for development and testing yaml: |- apiVersion: cert-manager.io/v1 kind: Issuer @@ -18,3 +16,4 @@ spec: namespace: default spec: selfSigned: {} + diff --git a/bundle/manifests/cert-manager-example_console.openshift.io_v1_consolequickstart.yaml b/bundle/manifests/cert-manager-example_console.openshift.io_v1_consolequickstart.yaml deleted file mode 100644 index 37847c281..000000000 --- a/bundle/manifests/cert-manager-example_console.openshift.io_v1_consolequickstart.yaml +++ /dev/null @@ -1,203 +0,0 @@ -apiVersion: console.openshift.io/v1 -kind: ConsoleQuickStart -metadata: - annotations: - capability.openshift.io/name: Console - include.release.openshift.io/ibm-cloud-managed: "true" - include.release.openshift.io/self-managed-high-availability: "true" - include.release.openshift.io/single-node-developer: "true" - name: cert-manager-example -spec: - conclusion: |- - Great job! You've successfully created your first Issuer and Certificate. - - ### Next Steps: - - - For production, use Let's Encrypt (ACME Issuer) instead of self-signed certificates - - Explore ClusterIssuer for cluster-wide certificate management - - Check out the [cert-manager documentation](https://docs.redhat.com/en/documentation/openshift_container_platform/latest/html/security_and_compliance/cert-manager-operator-for-red-hat-openshift) to learn more - description: Create and issue TLS certificates using the cert-manager Operator for - Red Hat OpenShift - displayName: cert-manager Operator for Red Hat OpenShift Example - durationMinutes: 10 - introduction: |- - # cert-manager Operator for Red Hat OpenShift - - The cert-manager Operator for Red Hat OpenShift enables you to create and sign TLS certificates from an external PKI for your workloads - running on an OpenShift cluster. - - ### Expected Learning - - With this Quick Start, you will learn about the following CRDs provided by the cert-manager operator: - 1. **Issuer** - defines a certificate authority that can sign certificates - 2. **Certificate** - defines the desired certificate and its properties - - This Quick Start will walk you through creating your first certificate: - - Create a self-signed Issuer (for testing) - - Create a Certificate signed by that Issuer - - View the generated certificate in a Kubernetes Secret - - Learn how to use it in your applications - - **Note**: For production, you would use Let's Encrypt (ACME), HashiCorp Vault, or your organization's CA instead of self-signed certificates. - prerequisites: - - You completed the "Install the cert-manager Operator for Red Hat OpenShift" quick - start. - - You have a namespace in which to deploy the example CRs. - tags: - - example - - operator - - certificates - tasks: - - description: |- - ### To navigate to the installed operator: - 1. Go to the **Installed Operators** from the [Ecosystem]{{highlight qs-nav-ecosystem}} section of the navigation. - 2. In the **Search by name** field, type `cert-manager`. - 3. Look for **cert-manager Operator for Red Hat OpenShift**. If you had completed the prerequisite Quick Start, the tile should appear. - 4. Click on the installed operator - - You will be brought to the **Operator Details** page and be presented with **Provided APIs** - review: - failedTaskHelp: This task isn't verified yet. Try the task again. - instructions: |- - #### Verify you see a list of **Provided APIs**: - The list should include `Issuer`, `ClusterIssuer`, and `Certificate` - summary: - failed: Try the steps again. - success: You are in the right place, and ready to start the rest of the Quick - Start - title: Navigate to installed cert-manager operator - - description: |- - ### Create or select a project to work in - 1. Find the **Project** dropdown menu at the top of the screen. - 2. Select or create the project in which you want to work in. - - **Note**: For this example, we'll create an `Issuer` which is namespace-scoped. If you want to issue certificates - across multiple namespaces, you can create a `ClusterIssuer` instead. - review: - failedTaskHelp: Try the task again. - instructions: '#### Verify the name in the **Project** dropdown menu is the - expected project' - summary: - failed: Try the steps again. - success: You are in the right place. - title: Select a project - - description: |- - ### To create a self-signed Issuer - - An Issuer represents a certificate authority that can sign certificates. We'll create a self-signed Issuer - for this example. This is useful for testing and development. - - 1. Find the `Issuer` Custom Resource in the list of **Provided APIs** or in the top side-scrolling menu bar. - - From the list of **Provided APIs**, click the **Create instance** link. - - From the **top side-scrolling menu bar**, click **Issuer** and then click **Create Issuer**. - - 2. Switch to **YAML view** in the editor. - - 3. On the right sidebar, look for the **Samples** section and select **"Example Self-Signed Issuer"**. - - 4. Click **Try it** to populate the editor with the sample YAML. - - 5. Click the **Create** button to create the Issuer. - review: - failedTaskHelp: This task isn't verified yet. Try the task again. - instructions: |- - #### Verify the Issuer was successfully created: - 1. You should see the Issuer listed with the name `selfsigned-issuer` - 2. Check that the **Ready** condition shows **True** in the Conditions section - summary: - failed: Try the steps again. - success: You just created a self-signed Issuer! Now we can create certificates. - title: Create a self-signed Issuer - - description: |- - ### To create a Certificate - - Now we'll create a certificate that will be signed by our Issuer. - - 1. Find the `Certificate` Custom Resource in the list of **Provided APIs** or in the top side-scrolling menu bar. - - From the list of **Provided APIs**, click the **Create instance** link. - - From the **top side-scrolling menu bar**, click **Certificate** and then click **Create Certificate**. - - 2. Switch to **YAML view** in the editor. - - 3. On the right sidebar, look for the **Samples** section and select **"Example Certificate"**. - - 4. Click **Try it** to populate the editor with the sample YAML. - - 5. Click the **Create** button to create the Certificate. - review: - failedTaskHelp: This task isn't verified yet. Try the task again. - instructions: |- - #### Verify the Certificate was successfully created: - 1. You should see the Certificate listed with the name `example-cert`. - 2. Check that the **Ready** condition shows **True**. - 3. Navigate to the [Workloads]{{highlight qs-nav-workloads}} section and click **Secrets** - 4. You should see a new Secret named `example-tls`. - summary: - failed: Try the steps again. - success: You just created your first certificate! cert-manager has issued it - and stored it in a Secret. - title: Create a Certificate - - description: |- - ### To inspect the certificate - - Let's look at the Secret that contains the certificate. - - 1. Make sure you're in the [Workloads]{{highlight qs-nav-workloads}} section, click **Secrets**. - 2. Click on the **example-tls** Secret. - 3. You should see the certificate data with keys: - - `tls.crt` - The certificate. - - `tls.key` - The private key. - - `ca.crt` - The CA certificate. - review: - failedTaskHelp: This task isn't verified yet. Try the task again. - instructions: |- - #### Verify you can see the certificate data: - Is the Secret `example-tls` present with `tls.crt` and `tls.key`? - summary: - failed: Try the steps again. - success: Great! Your certificate is ready to use. - title: View the certificate - - description: |- - ### How to use certificates - - Now you can use this certificate in your applications. Here's a simple example for an OpenShift Route: - - ```yaml - apiVersion: route.openshift.io/v1 - kind: Route - metadata: - name: my-app - spec: - to: - kind: Service - name: my-service - tls: - termination: edge - externalCertificate: - name: example-tls - ``` - - Or in an Ingress: - - ```yaml - apiVersion: networking.k8s.io/v1 - kind: Ingress - metadata: - name: my-app - spec: - tls: - - secretName: example-tls - rules: - - host: example.com - ``` - - **Note**: cert-manager will automatically renew certificates before they expire! - review: - failedTaskHelp: Review the examples above. - instructions: |- - #### Do you understand how to use certificates? - Certificates can be referenced in Routes and Ingress resources. - summary: - failed: Review the examples again. - success: You now know how to create and use certificates! - title: Use the certificate diff --git a/bundle/manifests/cert-manager-operator.clusterserviceversion.yaml b/bundle/manifests/cert-manager-operator.clusterserviceversion.yaml index 2832f06b5..eb405f098 100644 --- a/bundle/manifests/cert-manager-operator.clusterserviceversion.yaml +++ b/bundle/manifests/cert-manager-operator.clusterserviceversion.yaml @@ -568,6 +568,7 @@ spec: - config.openshift.io resources: - apiservers + - clusterversions verbs: - get - list @@ -587,6 +588,19 @@ spec: - patch - update - watch + - apiGroups: + - console.openshift.io + resources: + - consolequickstarts + - consoleyamlsamples + verbs: + - create + - delete + - get + - list + - patch + - update + - watch - apiGroups: - coordination.k8s.io resources: diff --git a/config/console/cert-manager-acme-issuer-sample.yaml b/config/console/cert-manager-acme-issuer-sample.yaml index ae04d2fab..40e3d7e4a 100644 --- a/config/console/cert-manager-acme-issuer-sample.yaml +++ b/config/console/cert-manager-acme-issuer-sample.yaml @@ -2,8 +2,6 @@ apiVersion: console.openshift.io/v1 kind: ConsoleYAMLSample metadata: name: cert-manager-acme-issuer-sample - annotations: - capability.openshift.io/name: Console spec: targetResource: apiVersion: cert-manager.io/v1 diff --git a/config/console/cert-manager-certificate-sample.yaml b/config/console/cert-manager-certificate-sample.yaml index 23b565989..b83b514ac 100644 --- a/config/console/cert-manager-certificate-sample.yaml +++ b/config/console/cert-manager-certificate-sample.yaml @@ -2,8 +2,6 @@ apiVersion: console.openshift.io/v1 kind: ConsoleYAMLSample metadata: name: cert-manager-certificate-sample - annotations: - capability.openshift.io/name: Console spec: targetResource: apiVersion: cert-manager.io/v1 diff --git a/config/console/cert-manager-example-quickstart.yaml b/config/console/cert-manager-example-quickstart.yaml index f1f192aa3..673421ad3 100644 --- a/config/console/cert-manager-example-quickstart.yaml +++ b/config/console/cert-manager-example-quickstart.yaml @@ -3,7 +3,6 @@ kind: ConsoleQuickStart metadata: name: cert-manager-example annotations: - capability.openshift.io/name: Console include.release.openshift.io/ibm-cloud-managed: "true" include.release.openshift.io/self-managed-high-availability: "true" include.release.openshift.io/single-node-developer: "true" diff --git a/config/console/cert-manager-issuer-sample.yaml b/config/console/cert-manager-issuer-sample.yaml index 3100ed5b7..3bc9ebd71 100644 --- a/config/console/cert-manager-issuer-sample.yaml +++ b/config/console/cert-manager-issuer-sample.yaml @@ -2,8 +2,6 @@ apiVersion: console.openshift.io/v1 kind: ConsoleYAMLSample metadata: name: cert-manager-issuer-sample - annotations: - capability.openshift.io/name: Console spec: targetResource: apiVersion: cert-manager.io/v1 diff --git a/config/manifests/kustomization.yaml b/config/manifests/kustomization.yaml index 0a4b22e0f..1bbf49121 100644 --- a/config/manifests/kustomization.yaml +++ b/config/manifests/kustomization.yaml @@ -5,7 +5,6 @@ resources: - ../default - ../samples - ../scorecard -- ../console # [WEBHOOK] To enable webhooks, uncomment all the sections with [WEBHOOK] prefix. # Do NOT uncomment sections with prefix [CERTMANAGER], as OLM does not support cert-manager. diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 60136088a..1511acc78 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -159,6 +159,7 @@ rules: - config.openshift.io resources: - apiservers + - clusterversions verbs: - get - list @@ -178,6 +179,19 @@ rules: - patch - update - watch +- apiGroups: + - console.openshift.io + resources: + - consolequickstarts + - consoleyamlsamples + verbs: + - create + - delete + - get + - list + - patch + - update + - watch - apiGroups: - coordination.k8s.io resources: diff --git a/pkg/controller/certmanager/cert_manager_controller_set.go b/pkg/controller/certmanager/cert_manager_controller_set.go index 08981cb33..ed4cba67a 100644 --- a/pkg/controller/certmanager/cert_manager_controller_set.go +++ b/pkg/controller/certmanager/cert_manager_controller_set.go @@ -1,9 +1,11 @@ package certmanager import ( + "k8s.io/client-go/dynamic" "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes" + configv1client "github.com/openshift/client-go/config/clientset/versioned" configinformers "github.com/openshift/client-go/config/informers/externalversions" "github.com/openshift/library-go/pkg/controller/factory" "github.com/openshift/library-go/pkg/operator/events" @@ -24,6 +26,7 @@ type CertManagerControllerSet struct { certManagerCAInjectorDeploymentController factory.Controller certManagerNetworkPolicyStaticResourcesController factory.Controller certManagerNetworkPolicyUserDefinedController factory.Controller + consoleResourcesController factory.Controller } func NewCertManagerControllerSet( @@ -34,6 +37,8 @@ func NewCertManagerControllerSet( operatorClient v1helpers.OperatorClientWithFinalizers, certManagerOperatorInformers certmanoperatorinformers.SharedInformerFactory, kubeClientContainer *resourceapply.ClientHolder, + configClient configv1client.Interface, + dynamicClient dynamic.Interface, eventRecorder events.Recorder, targetVersion string, versionRecorder status.VersionGetter, @@ -49,6 +54,7 @@ func NewCertManagerControllerSet( certManagerCAInjectorDeploymentController: NewCertManagerCAInjectorDeploymentController(operatorClient, certManagerOperatorInformers, infraInformers, kubeClient, kubeInformersForTargetNamespace, eventRecorder, targetVersion, versionRecorder, trustedCAConfigmapName, cloudCredentialsSecretName), certManagerNetworkPolicyStaticResourcesController: NewCertManagerNetworkPolicyStaticResourcesController(operatorClient, kubeClientContainer, kubeInformersForNamespaces, certManagerOperatorInformers, eventRecorder), certManagerNetworkPolicyUserDefinedController: NewCertManagerNetworkPolicyUserDefinedController(operatorClient, certManagerOperatorInformers, kubeClient, kubeInformersForNamespaces, eventRecorder), + consoleResourcesController: NewConsoleResourcesController(operatorClient, configClient, dynamicClient, eventRecorder), } } @@ -62,5 +68,6 @@ func (c *CertManagerControllerSet) ToArray() []factory.Controller { c.certManagerCAInjectorDeploymentController, c.certManagerNetworkPolicyStaticResourcesController, c.certManagerNetworkPolicyUserDefinedController, + c.consoleResourcesController, } } diff --git a/pkg/controller/certmanager/console_resources.go b/pkg/controller/certmanager/console_resources.go new file mode 100644 index 000000000..02683d630 --- /dev/null +++ b/pkg/controller/certmanager/console_resources.go @@ -0,0 +1,152 @@ +package certmanager + +import ( + "context" + "errors" + "fmt" + "slices" + "time" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic" + "k8s.io/klog/v2" + + configv1 "github.com/openshift/api/config/v1" + configv1client "github.com/openshift/client-go/config/clientset/versioned" + "github.com/openshift/library-go/pkg/controller/factory" + "github.com/openshift/library-go/pkg/operator/events" + "github.com/openshift/library-go/pkg/operator/resource/resourceapply" + "github.com/openshift/library-go/pkg/operator/resource/resourceread" + "github.com/openshift/library-go/pkg/operator/v1helpers" + + "github.com/openshift/cert-manager-operator/pkg/operator/assets" +) + +const ( + consoleResourcesControllerName = operatorName + "-console-resources" +) + +var ( + ConsoleYAMLSampleGVR = schema.GroupVersionResource{ + Group: "console.openshift.io", + Version: "v1", + Resource: "consoleyamlsamples", + } + + ConsoleQuickStartGVR = schema.GroupVersionResource{ + Group: "console.openshift.io", + Version: "v1", + Resource: "consolequickstarts", + } + + consoleAssetFiles = []string{ + "console/cert-manager-acme-issuer-sample.yaml", + "console/cert-manager-certificate-sample.yaml", + "console/cert-manager-issuer-sample.yaml", + "console/cert-manager-example-quickstart.yaml", + } +) + +// +kubebuilder:rbac:groups=config.openshift.io,resources=clusterversions,verbs=get;list;watch +// +kubebuilder:rbac:groups=console.openshift.io,resources=consoleyamlsamples;consolequickstarts,verbs=get;list;watch;create;update;patch;delete + +// consoleResourcesController conditionally creates Console resources +// when the Console capability is enabled on the cluster. +type consoleResourcesController struct { + configClient configv1client.Interface + dynamicClient dynamic.Interface + eventRecorder events.Recorder + consoleAssets []consoleAsset +} + +type consoleAsset struct { + obj *unstructured.Unstructured + gvr schema.GroupVersionResource +} + +func NewConsoleResourcesController( + operatorClient v1helpers.OperatorClient, + configClient configv1client.Interface, + dynamicClient dynamic.Interface, + eventRecorder events.Recorder, +) factory.Controller { + c := &consoleResourcesController{ + configClient: configClient, + dynamicClient: dynamicClient, + eventRecorder: eventRecorder.WithComponentSuffix("console-resources"), + } + + for _, file := range consoleAssetFiles { + data, err := assets.Asset(file) + if err != nil { + panic(fmt.Sprintf("failed to read console asset %s: %v", file, err)) + } + obj := resourceread.ReadUnstructuredOrDie(data) + gvr, err := gvrForKind(obj.GetKind()) + if err != nil { + panic(fmt.Sprintf("console asset %s: %v", file, err)) + } + c.consoleAssets = append(c.consoleAssets, consoleAsset{obj: obj, gvr: gvr}) + } + + return factory.New(). + WithInformers(operatorClient.Informer()). + ResyncEvery(10*time.Minute). + WithSync(c.sync). + ToController(consoleResourcesControllerName, c.eventRecorder) +} + +func (c *consoleResourcesController) sync(ctx context.Context, _ factory.SyncContext) error { + enabled, err := c.hasConsoleCapability(ctx) + if err != nil { + return fmt.Errorf("failed to check Console capability: %w", err) + } + + if !enabled { + klog.V(4).Info("Console capability is not enabled, skipping Console resource creation") + return nil + } + + var errs []error + for _, asset := range c.consoleAssets { + _, _, err := resourceapply.ApplyUnstructuredResourceImproved( + ctx, c.dynamicClient, c.eventRecorder, + asset.obj, nil, asset.gvr, nil, nil, + ) + if err != nil { + errs = append(errs, fmt.Errorf("failed to apply %s/%s: %w", asset.obj.GetKind(), asset.obj.GetName(), err)) + } + } + + return errors.Join(errs...) +} + +func (c *consoleResourcesController) hasConsoleCapability(ctx context.Context) (bool, error) { + version, err := c.configClient.ConfigV1().ClusterVersions().Get(ctx, "version", metav1.GetOptions{}) + if apierrors.IsNotFound(err) { + klog.V(4).Info("ClusterVersion resource not available (e.g. MicroShift); assuming Console capability is disabled") + return false, nil + } + if err != nil { + return false, fmt.Errorf("failed to get ClusterVersion: %w", err) + } + + return slices.Contains( + version.Status.Capabilities.EnabledCapabilities, + configv1.ClusterVersionCapabilityConsole, + ), nil +} + +func gvrForKind(kind string) (schema.GroupVersionResource, error) { + switch kind { + case "ConsoleYAMLSample": + return ConsoleYAMLSampleGVR, nil + case "ConsoleQuickStart": + return ConsoleQuickStartGVR, nil + default: + return schema.GroupVersionResource{}, fmt.Errorf("unsupported console resource kind: %s", kind) + } +} diff --git a/pkg/controller/certmanager/console_resources_test.go b/pkg/controller/certmanager/console_resources_test.go new file mode 100644 index 000000000..910151bb1 --- /dev/null +++ b/pkg/controller/certmanager/console_resources_test.go @@ -0,0 +1,449 @@ +package certmanager + +import ( + "context" + "fmt" + "strings" + "testing" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + dynamicfake "k8s.io/client-go/dynamic/fake" + clienttesting "k8s.io/client-go/testing" + "k8s.io/utils/clock" + + configv1 "github.com/openshift/api/config/v1" + configfake "github.com/openshift/client-go/config/clientset/versioned/fake" + "github.com/openshift/library-go/pkg/operator/events" + "github.com/openshift/library-go/pkg/operator/resource/resourceread" + + "github.com/openshift/cert-manager-operator/pkg/operator/assets" +) + +func clusterVersionWithCapabilities(caps ...configv1.ClusterVersionCapability) *configv1.ClusterVersion { + return &configv1.ClusterVersion{ + ObjectMeta: metav1.ObjectMeta{Name: "version"}, + Status: configv1.ClusterVersionStatus{ + Capabilities: configv1.ClusterVersionCapabilitiesStatus{ + EnabledCapabilities: caps, + }, + }, + } +} + +func buildConsoleAssets(t *testing.T) []consoleAsset { + t.Helper() + result := make([]consoleAsset, 0, len(consoleAssetFiles)) + for _, file := range consoleAssetFiles { + data, err := assets.Asset(file) + if err != nil { + t.Fatalf("failed to load asset %s: %v", file, err) + } + obj := resourceread.ReadUnstructuredOrDie(data) + gvr, err := gvrForKind(obj.GetKind()) + if err != nil { + t.Fatalf("gvrForKind(%s): %v", obj.GetKind(), err) + } + result = append(result, consoleAsset{obj: obj, gvr: gvr}) + } + return result +} + +func TestHasConsoleCapability(t *testing.T) { + tests := []struct { + name string + caps []configv1.ClusterVersionCapability + expected bool + }{ + { + name: "console enabled", + caps: []configv1.ClusterVersionCapability{configv1.ClusterVersionCapabilityConsole, configv1.ClusterVersionCapabilityIngress}, + expected: true, + }, + { + name: "console disabled", + caps: []configv1.ClusterVersionCapability{configv1.ClusterVersionCapabilityIngress, configv1.ClusterVersionCapabilityStorage}, + expected: false, + }, + { + name: "no capabilities", + caps: nil, + expected: false, + }, + { + name: "console only capability", + caps: []configv1.ClusterVersionCapability{configv1.ClusterVersionCapabilityConsole}, + expected: true, + }, + { + name: "empty capabilities list", + caps: []configv1.ClusterVersionCapability{}, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cv := clusterVersionWithCapabilities(tt.caps...) + client := configfake.NewClientset(cv) + c := &consoleResourcesController{configClient: client} + + got, err := c.hasConsoleCapability(context.Background()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != tt.expected { + t.Errorf("hasConsoleCapability() = %v, want %v", got, tt.expected) + } + }) + } +} + +func TestHasConsoleCapabilityClusterVersionNotFound(t *testing.T) { + client := configfake.NewClientset() + c := &consoleResourcesController{configClient: client} + + got, err := c.hasConsoleCapability(context.Background()) + if err != nil { + t.Fatalf("expected no error when ClusterVersion is not found (e.g. MicroShift), got: %v", err) + } + if got { + t.Error("expected false when ClusterVersion does not exist") + } +} + +func TestHasConsoleCapabilityResourceNotFound(t *testing.T) { + client := configfake.NewClientset() + client.PrependReactor("get", "clusterversions", func(action clienttesting.Action) (bool, runtime.Object, error) { + return true, nil, &apierrors.StatusError{ + ErrStatus: metav1.Status{ + Status: metav1.StatusFailure, + Code: 404, + Reason: metav1.StatusReasonNotFound, + Message: "the server could not find the requested resource", + }, + } + }) + c := &consoleResourcesController{configClient: client} + + got, err := c.hasConsoleCapability(context.Background()) + if err != nil { + t.Fatalf("expected no error for resource-type-not-found (MicroShift), got: %v", err) + } + if got { + t.Error("expected false when ClusterVersion resource type does not exist") + } +} + +func TestSyncConsoleDisabledWithAssets(t *testing.T) { + cv := clusterVersionWithCapabilities(configv1.ClusterVersionCapabilityIngress, configv1.ClusterVersionCapabilityStorage) + configClient := configfake.NewClientset(cv) + + scheme := runtime.NewScheme() + dynClient := dynamicfake.NewSimpleDynamicClient(scheme) + + c := &consoleResourcesController{ + configClient: configClient, + dynamicClient: dynClient, + eventRecorder: events.NewInMemoryRecorder("test", clock.RealClock{}), + consoleAssets: buildConsoleAssets(t), + } + + if err := c.sync(context.Background(), nil); err != nil { + t.Fatalf("sync() returned error: %v", err) + } + + if len(dynClient.Actions()) != 0 { + t.Errorf("expected no dynamic client actions when Console disabled even with assets loaded, got %d", len(dynClient.Actions())) + } +} + +func TestSyncConsoleEnabled(t *testing.T) { + cv := clusterVersionWithCapabilities(configv1.ClusterVersionCapabilityConsole) + configClient := configfake.NewClientset(cv) + + scheme := runtime.NewScheme() + dynClient := dynamicfake.NewSimpleDynamicClient(scheme) + + c := &consoleResourcesController{ + configClient: configClient, + dynamicClient: dynClient, + eventRecorder: events.NewInMemoryRecorder("test", clock.RealClock{}), + consoleAssets: buildConsoleAssets(t), + } + + err := c.sync(context.Background(), nil) + // ApplyUnstructuredResourceImproved will fail with the fake dynamic client + // because it does a Get first and the fake doesn't have the right GVR scheme. + // What matters is that it ATTEMPTED to call the dynamic client (not skipped). + if len(dynClient.Actions()) == 0 && err == nil { + t.Error("expected dynamic client actions when Console is enabled") + } +} + +func TestSyncSkipsWhenClusterVersionMissing(t *testing.T) { + configClient := configfake.NewClientset() + + scheme := runtime.NewScheme() + dynClient := dynamicfake.NewSimpleDynamicClient(scheme) + + c := &consoleResourcesController{ + configClient: configClient, + dynamicClient: dynClient, + eventRecorder: events.NewInMemoryRecorder("test", clock.RealClock{}), + consoleAssets: buildConsoleAssets(t), + } + + err := c.sync(context.Background(), nil) + if err != nil { + t.Fatalf("sync() should succeed when ClusterVersion is missing (e.g. MicroShift), got: %v", err) + } + + if len(dynClient.Actions()) != 0 { + t.Errorf("expected no dynamic client actions when ClusterVersion is missing, got %d", len(dynClient.Actions())) + } +} + +func TestSyncErrorPropagationFromCapabilityCheck(t *testing.T) { + configClient := configfake.NewClientset() + configClient.PrependReactor("get", "clusterversions", func(action clienttesting.Action) (bool, runtime.Object, error) { + return true, nil, fmt.Errorf("connection refused") + }) + + scheme := runtime.NewScheme() + dynClient := dynamicfake.NewSimpleDynamicClient(scheme) + + c := &consoleResourcesController{ + configClient: configClient, + dynamicClient: dynClient, + eventRecorder: events.NewInMemoryRecorder("test", clock.RealClock{}), + consoleAssets: buildConsoleAssets(t), + } + + err := c.sync(context.Background(), nil) + if err == nil { + t.Fatal("sync() should return error on transient failure") + } + if !strings.Contains(err.Error(), "failed to check Console capability") { + t.Errorf("error should wrap capability check failure, got: %v", err) + } + + if len(dynClient.Actions()) != 0 { + t.Errorf("expected no dynamic client actions when capability check fails, got %d", len(dynClient.Actions())) + } +} + +func TestSyncAttemptsAllAssetsOnError(t *testing.T) { + cv := clusterVersionWithCapabilities(configv1.ClusterVersionCapabilityConsole) + configClient := configfake.NewClientset(cv) + + scheme := runtime.NewScheme() + dynClient := dynamicfake.NewSimpleDynamicClient(scheme) + + testAssets := buildConsoleAssets(t) + + c := &consoleResourcesController{ + configClient: configClient, + dynamicClient: dynClient, + eventRecorder: events.NewInMemoryRecorder("test", clock.RealClock{}), + consoleAssets: testAssets, + } + + _ = c.sync(context.Background(), nil) + + // With the fake client, ApplyUnstructuredResourceImproved does a Get first. + // Each asset should produce at least one Get action. With continue-on-error, + // we expect actions for ALL assets, not just the first one. + getActions := 0 + for _, action := range dynClient.Actions() { + if action.GetVerb() == "get" { + getActions++ + } + } + + if getActions < len(testAssets) { + t.Errorf("expected at least %d get actions (one per asset), got %d; sync may be aborting early", + len(testAssets), getActions) + } +} + +func TestSyncNoAssetsNoError(t *testing.T) { + cv := clusterVersionWithCapabilities(configv1.ClusterVersionCapabilityConsole) + configClient := configfake.NewClientset(cv) + + scheme := runtime.NewScheme() + dynClient := dynamicfake.NewSimpleDynamicClient(scheme) + + c := &consoleResourcesController{ + configClient: configClient, + dynamicClient: dynClient, + eventRecorder: events.NewInMemoryRecorder("test", clock.RealClock{}), + consoleAssets: nil, + } + + if err := c.sync(context.Background(), nil); err != nil { + t.Fatalf("sync() with no assets should succeed, got: %v", err) + } + + if len(dynClient.Actions()) != 0 { + t.Errorf("expected no dynamic client actions with empty assets, got %d", len(dynClient.Actions())) + } +} + +func TestSyncReappliesWithoutError(t *testing.T) { + cv := clusterVersionWithCapabilities(configv1.ClusterVersionCapabilityConsole) + configClient := configfake.NewClientset(cv) + + scheme := runtime.NewScheme() + scheme.AddKnownTypeWithName( + schema.GroupVersionKind{Group: "console.openshift.io", Version: "v1", Kind: "ConsoleYAMLSample"}, + &unstructured.Unstructured{}, + ) + scheme.AddKnownTypeWithName( + schema.GroupVersionKind{Group: "console.openshift.io", Version: "v1", Kind: "ConsoleYAMLSampleList"}, + &unstructured.UnstructuredList{}, + ) + scheme.AddKnownTypeWithName( + schema.GroupVersionKind{Group: "console.openshift.io", Version: "v1", Kind: "ConsoleQuickStart"}, + &unstructured.Unstructured{}, + ) + scheme.AddKnownTypeWithName( + schema.GroupVersionKind{Group: "console.openshift.io", Version: "v1", Kind: "ConsoleQuickStartList"}, + &unstructured.UnstructuredList{}, + ) + dynClient := dynamicfake.NewSimpleDynamicClient(scheme) + + c := &consoleResourcesController{ + configClient: configClient, + dynamicClient: dynClient, + eventRecorder: events.NewInMemoryRecorder("test", clock.RealClock{}), + consoleAssets: buildConsoleAssets(t), + } + + err1 := c.sync(context.Background(), nil) + actionsAfterFirst := len(dynClient.Actions()) + + err2 := c.sync(context.Background(), nil) + actionsAfterSecond := len(dynClient.Actions()) + + if err1 != nil { + t.Fatalf("first sync() failed: %v", err1) + } + if err2 != nil { + t.Fatalf("second sync() failed: %v", err2) + } + + if actionsAfterSecond <= actionsAfterFirst { + t.Error("second sync should have produced additional actions (idempotent re-apply)") + } +} + +func TestSyncReturnsAggregatedErrors(t *testing.T) { + cv := clusterVersionWithCapabilities(configv1.ClusterVersionCapabilityConsole) + configClient := configfake.NewClientset(cv) + + scheme := runtime.NewScheme() + dynClient := dynamicfake.NewSimpleDynamicClient(scheme) + dynClient.PrependReactor("*", "*", func(action clienttesting.Action) (bool, runtime.Object, error) { + return true, nil, fmt.Errorf("simulated error for %s", action.GetResource().Resource) + }) + + c := &consoleResourcesController{ + configClient: configClient, + dynamicClient: dynClient, + eventRecorder: events.NewInMemoryRecorder("test", clock.RealClock{}), + consoleAssets: buildConsoleAssets(t), + } + + err := c.sync(context.Background(), nil) + if err == nil { + t.Fatal("sync() should return error when all assets fail") + } + + errStr := err.Error() + if !strings.Contains(errStr, "ConsoleYAMLSample") { + t.Errorf("aggregated error should mention ConsoleYAMLSample, got: %v", err) + } + if !strings.Contains(errStr, "ConsoleQuickStart") { + t.Errorf("aggregated error should mention ConsoleQuickStart, got: %v", err) + } +} + +func TestGvrForKind(t *testing.T) { + tests := []struct { + kind string + want schema.GroupVersionResource + wantErr bool + }{ + { + kind: "ConsoleYAMLSample", + want: ConsoleYAMLSampleGVR, + }, + { + kind: "ConsoleQuickStart", + want: ConsoleQuickStartGVR, + }, + { + kind: "ConsolePlugin", + wantErr: true, + }, + { + kind: "Unknown", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.kind, func(t *testing.T) { + got, err := gvrForKind(tt.kind) + if tt.wantErr { + if err == nil { + t.Errorf("gvrForKind(%q) expected error, got %v", tt.kind, got) + } + return + } + if err != nil { + t.Fatalf("gvrForKind(%q) unexpected error: %v", tt.kind, err) + } + if got != tt.want { + t.Errorf("gvrForKind(%q) = %v, want %v", tt.kind, got, tt.want) + } + }) + } +} + +func TestConsoleAssetContent(t *testing.T) { + expectedAssets := []struct { + file string + kind string + name string + }{ + {"console/cert-manager-acme-issuer-sample.yaml", "ConsoleYAMLSample", "cert-manager-acme-issuer-sample"}, + {"console/cert-manager-certificate-sample.yaml", "ConsoleYAMLSample", "cert-manager-certificate-sample"}, + {"console/cert-manager-issuer-sample.yaml", "ConsoleYAMLSample", "cert-manager-issuer-sample"}, + {"console/cert-manager-example-quickstart.yaml", "ConsoleQuickStart", "cert-manager-example"}, + } + + for _, expected := range expectedAssets { + t.Run(expected.file, func(t *testing.T) { + data, err := assets.Asset(expected.file) + if err != nil { + t.Fatalf("asset not found: %v", err) + } + obj := resourceread.ReadUnstructuredOrDie(data) + + if obj.GetKind() != expected.kind { + t.Errorf("kind = %q, want %q", obj.GetKind(), expected.kind) + } + if obj.GetName() != expected.name { + t.Errorf("name = %q, want %q", obj.GetName(), expected.name) + } + if obj.GetAPIVersion() != "console.openshift.io/v1" { + t.Errorf("apiVersion = %q, want %q", obj.GetAPIVersion(), "console.openshift.io/v1") + } + }) + } +} diff --git a/pkg/operator/assets/bindata.go b/pkg/operator/assets/bindata.go index 6183e9b47..820344cdf 100644 --- a/pkg/operator/assets/bindata.go +++ b/pkg/operator/assets/bindata.go @@ -43,6 +43,10 @@ // bindata/cert-manager-deployment/webhook/cert-manager-webhook-subjectaccessreviews-crb.yaml // bindata/cert-manager-deployment/webhook/cert-manager-webhook-svc.yaml // bindata/cert-manager-deployment/webhook/cert-manager-webhook-validatingwebhookconfiguration.yaml +// bindata/console/cert-manager-acme-issuer-sample.yaml +// bindata/console/cert-manager-certificate-sample.yaml +// bindata/console/cert-manager-example-quickstart.yaml +// bindata/console/cert-manager-issuer-sample.yaml // bindata/istio-csr/cert-manager-istio-csr-clusterrole.yaml // bindata/istio-csr/cert-manager-istio-csr-clusterrolebinding.yaml // bindata/istio-csr/cert-manager-istio-csr-deployment.yaml @@ -2295,6 +2299,342 @@ func certManagerDeploymentWebhookCertManagerWebhookValidatingwebhookconfiguratio return a, nil } +var _consoleCertManagerAcmeIssuerSampleYaml = []byte(`apiVersion: console.openshift.io/v1 +kind: ConsoleYAMLSample +metadata: + name: cert-manager-acme-issuer-sample +spec: + targetResource: + apiVersion: cert-manager.io/v1 + kind: Issuer + title: Example ACME Issuer (Let's Encrypt) + description: An example ACME Issuer for Let's Encrypt production certificates with HTTP-01 challenge + yaml: |- + apiVersion: cert-manager.io/v1 + kind: Issuer + metadata: + name: letsencrypt-prod + namespace: default + spec: + acme: + server: https://acme-v02.api.letsencrypt.org/directory + privateKeySecretRef: + name: letsencrypt-prod-account-key + solvers: + - http01: + ingress: + ingressClassName: openshift-default +`) + +func consoleCertManagerAcmeIssuerSampleYamlBytes() ([]byte, error) { + return _consoleCertManagerAcmeIssuerSampleYaml, nil +} + +func consoleCertManagerAcmeIssuerSampleYaml() (*asset, error) { + bytes, err := consoleCertManagerAcmeIssuerSampleYamlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "console/cert-manager-acme-issuer-sample.yaml", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _consoleCertManagerCertificateSampleYaml = []byte(`apiVersion: console.openshift.io/v1 +kind: ConsoleYAMLSample +metadata: + name: cert-manager-certificate-sample +spec: + targetResource: + apiVersion: cert-manager.io/v1 + kind: Certificate + title: Example Certificate + description: A simple Certificate example + yaml: |- + apiVersion: cert-manager.io/v1 + kind: Certificate + metadata: + name: example-cert + namespace: default + spec: + secretName: example-tls + commonName: example.com + dnsNames: + - example.com + issuerRef: + name: selfsigned-issuer + kind: Issuer + +`) + +func consoleCertManagerCertificateSampleYamlBytes() ([]byte, error) { + return _consoleCertManagerCertificateSampleYaml, nil +} + +func consoleCertManagerCertificateSampleYaml() (*asset, error) { + bytes, err := consoleCertManagerCertificateSampleYamlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "console/cert-manager-certificate-sample.yaml", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _consoleCertManagerExampleQuickstartYaml = []byte(`apiVersion: console.openshift.io/v1 +kind: ConsoleQuickStart +metadata: + name: cert-manager-example + annotations: + include.release.openshift.io/ibm-cloud-managed: "true" + include.release.openshift.io/self-managed-high-availability: "true" + include.release.openshift.io/single-node-developer: "true" +spec: + displayName: cert-manager Operator for Red Hat OpenShift Example + tags: + - example + - operator + - certificates + durationMinutes: 10 + prerequisites: + - You completed the "Install the cert-manager Operator for Red Hat OpenShift" quick start. + - You have a namespace in which to deploy the example CRs. + description: |- + Create and issue TLS certificates using the cert-manager Operator for Red Hat OpenShift + introduction: |- + # cert-manager Operator for Red Hat OpenShift + + The cert-manager Operator for Red Hat OpenShift enables you to create and sign TLS certificates from an external PKI for your workloads + running on an OpenShift cluster. + + ### Expected Learning + + With this Quick Start, you will learn about the following CRDs provided by the cert-manager operator: + 1. **Issuer** - defines a certificate authority that can sign certificates + 2. **Certificate** - defines the desired certificate and its properties + + This Quick Start will walk you through creating your first certificate: + - Create a self-signed Issuer (for testing) + - Create a Certificate signed by that Issuer + - View the generated certificate in a Kubernetes Secret + - Learn how to use it in your applications + + **Note**: For production, you would use Let's Encrypt (ACME), HashiCorp Vault, or your organization's CA instead of self-signed certificates. + tasks: + - title: Navigate to installed cert-manager operator + description: |- + ### To navigate to the installed operator: + 1. Go to the **Installed Operators** from the [Ecosystem]{{highlight qs-nav-ecosystem}} section of the navigation. + 2. In the **Search by name** field, type ` + "`" + `cert-manager` + "`" + `. + 3. Look for **cert-manager Operator for Red Hat OpenShift**. If you had completed the prerequisite Quick Start, the tile should appear. + 4. Click on the installed operator + + You will be brought to the **Operator Details** page and be presented with **Provided APIs** + review: + instructions: |- + #### Verify you see a list of **Provided APIs**: + The list should include ` + "`" + `Issuer` + "`" + `, ` + "`" + `ClusterIssuer` + "`" + `, and ` + "`" + `Certificate` + "`" + ` + failedTaskHelp: This task isn't verified yet. Try the task again. + summary: + success: You are in the right place, and ready to start the rest of the Quick Start + failed: Try the steps again. + - title: Select a project + description: |- + ### Create or select a project to work in + 1. Find the **Project** dropdown menu at the top of the screen. + 2. Select or create the project in which you want to work in. + + **Note**: For this example, we'll create an ` + "`" + `Issuer` + "`" + ` which is namespace-scoped. If you want to issue certificates + across multiple namespaces, you can create a ` + "`" + `ClusterIssuer` + "`" + ` instead. + review: + instructions: |- + #### Verify the name in the **Project** dropdown menu is the expected project + failedTaskHelp: Try the task again. + summary: + success: You are in the right place. + failed: Try the steps again. + - title: Create a self-signed Issuer + description: |- + ### To create a self-signed Issuer + + An Issuer represents a certificate authority that can sign certificates. We'll create a self-signed Issuer + for this example. This is useful for testing and development. + + 1. Find the ` + "`" + `Issuer` + "`" + ` Custom Resource in the list of **Provided APIs** or in the top side-scrolling menu bar. + - From the list of **Provided APIs**, click the **Create instance** link. + - From the **top side-scrolling menu bar**, click **Issuer** and then click **Create Issuer**. + + 2. Switch to **YAML view** in the editor. + + 3. On the right sidebar, look for the **Samples** section and select **"Example Self-Signed Issuer"**. + + 4. Click **Try it** to populate the editor with the sample YAML. + + 5. Click the **Create** button to create the Issuer. + review: + instructions: |- + #### Verify the Issuer was successfully created: + 1. You should see the Issuer listed with the name ` + "`" + `selfsigned-issuer` + "`" + ` + 2. Check that the **Ready** condition shows **True** in the Conditions section + failedTaskHelp: This task isn't verified yet. Try the task again. + summary: + success: You just created a self-signed Issuer! Now we can create certificates. + failed: Try the steps again. + - title: Create a Certificate + description: |- + ### To create a Certificate + + Now we'll create a certificate that will be signed by our Issuer. + + 1. Find the ` + "`" + `Certificate` + "`" + ` Custom Resource in the list of **Provided APIs** or in the top side-scrolling menu bar. + - From the list of **Provided APIs**, click the **Create instance** link. + - From the **top side-scrolling menu bar**, click **Certificate** and then click **Create Certificate**. + + 2. Switch to **YAML view** in the editor. + + 3. On the right sidebar, look for the **Samples** section and select **"Example Certificate"**. + + 4. Click **Try it** to populate the editor with the sample YAML. + + 5. Click the **Create** button to create the Certificate. + review: + instructions: |- + #### Verify the Certificate was successfully created: + 1. You should see the Certificate listed with the name ` + "`" + `example-cert` + "`" + `. + 2. Check that the **Ready** condition shows **True**. + 3. Navigate to the [Workloads]{{highlight qs-nav-workloads}} section and click **Secrets** + 4. You should see a new Secret named ` + "`" + `example-tls` + "`" + `. + failedTaskHelp: This task isn't verified yet. Try the task again. + summary: + success: You just created your first certificate! cert-manager has issued it and stored it in a Secret. + failed: Try the steps again. + - title: View the certificate + description: |- + ### To inspect the certificate + + Let's look at the Secret that contains the certificate. + + 1. Make sure you're in the [Workloads]{{highlight qs-nav-workloads}} section, click **Secrets**. + 2. Click on the **example-tls** Secret. + 3. You should see the certificate data with keys: + - ` + "`" + `tls.crt` + "`" + ` - The certificate. + - ` + "`" + `tls.key` + "`" + ` - The private key. + - ` + "`" + `ca.crt` + "`" + ` - The CA certificate. + review: + instructions: |- + #### Verify you can see the certificate data: + Is the Secret ` + "`" + `example-tls` + "`" + ` present with ` + "`" + `tls.crt` + "`" + ` and ` + "`" + `tls.key` + "`" + `? + failedTaskHelp: This task isn't verified yet. Try the task again. + summary: + success: Great! Your certificate is ready to use. + failed: Try the steps again. + - title: Use the certificate + description: |- + ### How to use certificates + + Now you can use this certificate in your applications. Here's a simple example for an OpenShift Route: + + ` + "`" + `` + "`" + `` + "`" + `yaml + apiVersion: route.openshift.io/v1 + kind: Route + metadata: + name: my-app + spec: + to: + kind: Service + name: my-service + tls: + termination: edge + externalCertificate: + name: example-tls + ` + "`" + `` + "`" + `` + "`" + ` + + Or in an Ingress: + + ` + "`" + `` + "`" + `` + "`" + `yaml + apiVersion: networking.k8s.io/v1 + kind: Ingress + metadata: + name: my-app + spec: + tls: + - secretName: example-tls + rules: + - host: example.com + ` + "`" + `` + "`" + `` + "`" + ` + + **Note**: cert-manager will automatically renew certificates before they expire! + review: + instructions: |- + #### Do you understand how to use certificates? + Certificates can be referenced in Routes and Ingress resources. + failedTaskHelp: Review the examples above. + summary: + success: You now know how to create and use certificates! + failed: Review the examples again. + conclusion: |- + Great job! You've successfully created your first Issuer and Certificate. + + ### Next Steps: + + - For production, use Let's Encrypt (ACME Issuer) instead of self-signed certificates + - Explore ClusterIssuer for cluster-wide certificate management + - Check out the [cert-manager documentation](https://docs.redhat.com/en/documentation/openshift_container_platform/latest/html/security_and_compliance/cert-manager-operator-for-red-hat-openshift) to learn more +`) + +func consoleCertManagerExampleQuickstartYamlBytes() ([]byte, error) { + return _consoleCertManagerExampleQuickstartYaml, nil +} + +func consoleCertManagerExampleQuickstartYaml() (*asset, error) { + bytes, err := consoleCertManagerExampleQuickstartYamlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "console/cert-manager-example-quickstart.yaml", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _consoleCertManagerIssuerSampleYaml = []byte(`apiVersion: console.openshift.io/v1 +kind: ConsoleYAMLSample +metadata: + name: cert-manager-issuer-sample +spec: + targetResource: + apiVersion: cert-manager.io/v1 + kind: Issuer + title: Example Self-Signed Issuer + description: A simple self-signed Issuer for development and testing + yaml: |- + apiVersion: cert-manager.io/v1 + kind: Issuer + metadata: + name: selfsigned-issuer + namespace: default + spec: + selfSigned: {} + +`) + +func consoleCertManagerIssuerSampleYamlBytes() ([]byte, error) { + return _consoleCertManagerIssuerSampleYaml, nil +} + +func consoleCertManagerIssuerSampleYaml() (*asset, error) { + bytes, err := consoleCertManagerIssuerSampleYamlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "console/cert-manager-issuer-sample.yaml", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + var _istioCsrCertManagerIstioCsrClusterroleYaml = []byte(`kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: @@ -3772,6 +4112,10 @@ var _bindata = map[string]func() (*asset, error){ "cert-manager-deployment/webhook/cert-manager-webhook-subjectaccessreviews-crb.yaml": certManagerDeploymentWebhookCertManagerWebhookSubjectaccessreviewsCrbYaml, "cert-manager-deployment/webhook/cert-manager-webhook-svc.yaml": certManagerDeploymentWebhookCertManagerWebhookSvcYaml, "cert-manager-deployment/webhook/cert-manager-webhook-validatingwebhookconfiguration.yaml": certManagerDeploymentWebhookCertManagerWebhookValidatingwebhookconfigurationYaml, + "console/cert-manager-acme-issuer-sample.yaml": consoleCertManagerAcmeIssuerSampleYaml, + "console/cert-manager-certificate-sample.yaml": consoleCertManagerCertificateSampleYaml, + "console/cert-manager-example-quickstart.yaml": consoleCertManagerExampleQuickstartYaml, + "console/cert-manager-issuer-sample.yaml": consoleCertManagerIssuerSampleYaml, "istio-csr/cert-manager-istio-csr-clusterrole.yaml": istioCsrCertManagerIstioCsrClusterroleYaml, "istio-csr/cert-manager-istio-csr-clusterrolebinding.yaml": istioCsrCertManagerIstioCsrClusterrolebindingYaml, "istio-csr/cert-manager-istio-csr-deployment.yaml": istioCsrCertManagerIstioCsrDeploymentYaml, @@ -3903,6 +4247,12 @@ var _bintree = &bintree{nil, map[string]*bintree{ "cert-manager-webhook-validatingwebhookconfiguration.yaml": {certManagerDeploymentWebhookCertManagerWebhookValidatingwebhookconfigurationYaml, map[string]*bintree{}}, }}, }}, + "console": {nil, map[string]*bintree{ + "cert-manager-acme-issuer-sample.yaml": {consoleCertManagerAcmeIssuerSampleYaml, map[string]*bintree{}}, + "cert-manager-certificate-sample.yaml": {consoleCertManagerCertificateSampleYaml, map[string]*bintree{}}, + "cert-manager-example-quickstart.yaml": {consoleCertManagerExampleQuickstartYaml, map[string]*bintree{}}, + "cert-manager-issuer-sample.yaml": {consoleCertManagerIssuerSampleYaml, map[string]*bintree{}}, + }}, "istio-csr": {nil, map[string]*bintree{ "cert-manager-istio-csr-clusterrole.yaml": {istioCsrCertManagerIstioCsrClusterroleYaml, map[string]*bintree{}}, "cert-manager-istio-csr-clusterrolebinding.yaml": {istioCsrCertManagerIstioCsrClusterrolebindingYaml, map[string]*bintree{}}, diff --git a/pkg/operator/starter.go b/pkg/operator/starter.go index 188f73d35..7663cf257 100644 --- a/pkg/operator/starter.go +++ b/pkg/operator/starter.go @@ -6,6 +6,7 @@ import ( "time" apiextensionsclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" + "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" "k8s.io/klog/v2" ctrl "sigs.k8s.io/controller-runtime" @@ -87,6 +88,11 @@ func RunOperator(ctx context.Context, cc *controllercmd.ControllerContext) error return err } + dynamicClient, err := dynamic.NewForConfig(cc.KubeConfig) + if err != nil { + return err + } + infraGVR := configv1.GroupVersion.WithResource("infrastructures") optInfraInformer, err := utils.InitInformerIfAvailable( utils.NewResourceDiscoverer(infraGVR, configClient.Discovery()), @@ -106,6 +112,8 @@ func RunOperator(ctx context.Context, cc *controllercmd.ControllerContext) error operatorClient, certManagerInformers, resourceapply.NewKubeClientHolder(kubeClient).WithAPIExtensionsClient(apiExtensionsClient), + configClient, + dynamicClient, cc.EventRecorder, status.VersionForOperandFromEnv(), versionRecorder, diff --git a/test/e2e/console_resources_test.go b/test/e2e/console_resources_test.go new file mode 100644 index 000000000..d64c40bbe --- /dev/null +++ b/test/e2e/console_resources_test.go @@ -0,0 +1,125 @@ +//go:build e2e +// +build e2e + +package e2e + +import ( + "context" + "fmt" + "slices" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + configapiv1 "github.com/openshift/api/config/v1" + "github.com/openshift/library-go/pkg/operator/resource/resourceread" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/wait" + + "github.com/openshift/cert-manager-operator/pkg/controller/certmanager" + "github.com/openshift/cert-manager-operator/pkg/operator/assets" +) + +var consoleExpectedAssets = []struct { + file string + name string + gvr schema.GroupVersionResource +}{ + {file: "console/cert-manager-acme-issuer-sample.yaml", name: "cert-manager-acme-issuer-sample", gvr: certmanager.ConsoleYAMLSampleGVR}, + {file: "console/cert-manager-certificate-sample.yaml", name: "cert-manager-certificate-sample", gvr: certmanager.ConsoleYAMLSampleGVR}, + {file: "console/cert-manager-issuer-sample.yaml", name: "cert-manager-issuer-sample", gvr: certmanager.ConsoleYAMLSampleGVR}, + {file: "console/cert-manager-example-quickstart.yaml", name: "cert-manager-example", gvr: certmanager.ConsoleQuickStartGVR}, +} + +func hasConsoleCapability(ctx context.Context) (bool, error) { + version, err := configClient.ClusterVersions().Get(ctx, "version", metav1.GetOptions{}) + if err != nil { + return false, fmt.Errorf("failed to get ClusterVersion: %w", err) + } + + return slices.Contains( + version.Status.Capabilities.EnabledCapabilities, + configapiv1.ClusterVersionCapabilityConsole, + ), nil +} + +var _ = Describe("Console Resources", Label("Platform:Generic"), Ordered, func() { + var ( + ctx context.Context + consoleEnabled bool + ) + + BeforeAll(func() { + ctx = context.Background() + + By("detecting Console capability on the cluster") + enabled, err := hasConsoleCapability(ctx) + Expect(err).NotTo(HaveOccurred(), "failed to check Console capability") + consoleEnabled = enabled + + if consoleEnabled { + fmt.Fprintf(GinkgoWriter, "Console capability is ENABLED on this cluster\n") + } else { + fmt.Fprintf(GinkgoWriter, "Console capability is DISABLED on this cluster\n") + } + }) + + 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("when Console capability is enabled", func() { + It("should create all console resources with correct content", func() { + if !consoleEnabled { + Skip("Console capability is not enabled on this cluster") + } + + for _, asset := range consoleExpectedAssets { + By(fmt.Sprintf("verifying %s exists", asset.name)) + + var live *unstructured.Unstructured + err := wait.PollUntilContextTimeout(ctx, fastPollInterval, lowTimeout, true, func(ctx context.Context) (bool, error) { + obj, getErr := loader.DynamicClient.Resource(asset.gvr).Get(ctx, asset.name, metav1.GetOptions{}) + if getErr != nil { + if apierrors.IsNotFound(getErr) { + return false, nil + } + return false, getErr + } + live = obj + return true, nil + }) + Expect(err).NotTo(HaveOccurred(), "timed out waiting for %s to be created", asset.name) + + By(fmt.Sprintf("validating content of %s", asset.name)) + data, err := assets.Asset(asset.file) + Expect(err).NotTo(HaveOccurred(), "failed to load bindata asset %s", asset.file) + expected := resourceread.ReadUnstructuredOrDie(data) + + Expect(live.Object["spec"]).To(Equal(expected.Object["spec"]), + "spec mismatch for %s", asset.name) + } + }) + }) + + Context("when Console capability is disabled", func() { + It("should not create any console resources", func() { + if consoleEnabled { + Skip("Console capability is enabled on this cluster") + } + + for _, asset := range consoleExpectedAssets { + By(fmt.Sprintf("verifying %s does not exist", asset.name)) + _, err := loader.DynamicClient.Resource(asset.gvr).Get(ctx, asset.name, metav1.GetOptions{}) + Expect(apierrors.IsNotFound(err)).To(BeTrue(), + "expected %s to not exist on consoleless cluster, but got: %v", asset.name, err) + } + }) + }) +}) diff --git a/test/go.mod b/test/go.mod index 77c538f77..d9dc68f9b 100644 --- a/test/go.mod +++ b/test/go.mod @@ -22,6 +22,7 @@ require ( github.com/openshift/api v0.0.0-20260513085653-694421e64aee github.com/openshift/cert-manager-operator v0.0.0-00010101000000-000000000000 github.com/openshift/client-go v0.0.0-20260330134249-7e1499aaacd7 + github.com/openshift/library-go v0.0.0-20260512161954-889c2cd3e381 github.com/stretchr/testify v1.11.1 github.com/tidwall/gjson v1.18.0 github.com/vmware-archive/yaml-patch v0.0.11 @@ -102,13 +103,14 @@ require ( github.com/nxadm/tail v1.4.11 // indirect github.com/onsi/ginkgo v1.16.5 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/openshift/library-go v0.0.0-20260512161954-889c2cd3e381 // indirect + github.com/operator-framework/operator-lib v0.19.0 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/procfs v0.19.2 // indirect + github.com/robfig/cron v1.2.0 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/tidwall/match v1.1.1 // indirect @@ -147,11 +149,13 @@ require ( k8s.io/component-helpers v0.35.2 // indirect k8s.io/controller-manager v0.35.2 // indirect k8s.io/klog/v2 v2.140.0 // indirect + k8s.io/kube-aggregator v0.35.2 // indirect k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect k8s.io/kubelet v0.35.2 // indirect k8s.io/kubernetes v1.35.2 // indirect sigs.k8s.io/gateway-api v1.5.0 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect + sigs.k8s.io/kube-storage-version-migrator v0.0.6-0.20230721195810-5c8923c5ff96 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect ) diff --git a/test/go.sum b/test/go.sum index 393df543e..9731197f0 100644 --- a/test/go.sum +++ b/test/go.sum @@ -237,6 +237,8 @@ github.com/openshift/jetstack-cert-manager v1.20.3 h1:WAOKjcWJxXLZ2VBXdDLXuf2RB6 github.com/openshift/jetstack-cert-manager v1.20.3/go.mod h1:Aqf5P0xRh9aey1p10m2c3UAk/Vb/FBPyH3WQxJRm+7Y= github.com/openshift/library-go v0.0.0-20260512161954-889c2cd3e381 h1:DuRbvQFYd/2HzMCL+s5Z1kCXh+mdL7Q9UZG99/Sa/ys= github.com/openshift/library-go v0.0.0-20260512161954-889c2cd3e381/go.mod h1:3bi4pLpYRdVd1aEhsHfRTJkwxwPLfRZ+ZePn3RmJd2k= +github.com/operator-framework/operator-lib v0.19.0 h1:az6ogYj21rtU0SF9uYctRLyKp2dtlqTsmpfehFy6Ce8= +github.com/operator-framework/operator-lib v0.19.0/go.mod h1:KxycAjFnHt0DBtHmH3Jm7yHcY5sdrshPKTqM/HKAQ08= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -244,6 +246,8 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.74.0 h1:AHzMWDxNiAVscJL6+4wkvFRTpMnJqiaZFEKA/osaBXE= +github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.74.0/go.mod h1:wAR5JopumPtAZnu0Cjv2PSqV4p4QB09LMhc6fZZTXuA= github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= @@ -252,6 +256,8 @@ github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTU github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= +github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= +github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= @@ -412,6 +418,8 @@ k8s.io/controller-manager v0.35.2 h1:EpLIwm4bBgoFwXiVULgNxtpUF9cbXUkcGkZ6vUlurYQ k8s.io/controller-manager v0.35.2/go.mod h1:CknUpFd1A0S9h+J9eQZXvZt05ssXMnsYCCLRghm/gp4= k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= +k8s.io/kube-aggregator v0.35.2 h1:bnF7E238wUOVaPpTyKrqGCAEXOAJ6HRTARvJTZ0UIC0= +k8s.io/kube-aggregator v0.35.2/go.mod h1:7Xl9zFJFsFIrPnwBfu7hve+G5QgLsDZRIedc8gA1mq4= k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a h1:xCeOEAOoGYl2jnJoHkC3hkbPJgdATINPMAxaynU2Ovg= k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0= k8s.io/kubelet v0.35.2 h1:qF9jOe1j6vT4bVQZ6nnTTA5uu5NCnyR10o9IkW8Z0JQ= @@ -426,6 +434,8 @@ sigs.k8s.io/gateway-api v1.5.0 h1:duoo14Ky/fJXpjpmyMISE2RTBGnfCg8zICfTYLTnBJA= sigs.k8s.io/gateway-api v1.5.0/go.mod h1:GvCETiaMAlLym5CovLxGjS0NysqFk3+Yuq3/rh6QL2o= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/kube-storage-version-migrator v0.0.6-0.20230721195810-5c8923c5ff96 h1:PFWFSkpArPNJxFX4ZKWAk9NSeRoZaXschn+ULa4xVek= +sigs.k8s.io/kube-storage-version-migrator v0.0.6-0.20230721195810-5c8923c5ff96/go.mod h1:EOBQyBowOUsd7U4CJnMHNE0ri+zCXyouGdLwC/jZU+I= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/structured-merge-diff/v6 v6.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80uLmm0wJkk8=