This is an automated email from the ASF dual-hosted git repository. squakez pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/camel-k.git
commit 9a30911d41f02688f153ea9f894ab05518591dd4 Author: Keerthan <[email protected]> AuthorDate: Wed Sep 16 00:04:17 2026 +0530 Fix #5857: Add cert-manager auto-discovery to the Ingress trait --- .github/workflows/cert-manager.yml | 91 ++++++++ docs/modules/ROOT/partials/apis/camel-k-crds.adoc | 26 +++ docs/modules/traits/pages/ingress.adoc | 17 ++ e2e/cert-manager/certmanager_test.go | 85 +++++++ e2e/cert-manager/files/PlatformHttpServer.java | 26 +++ .../cert-manager/setup/cluster-issuer.yaml | 29 +-- .../cert-manager/setup/setup.sh | 36 ++- helm/camel-k/crds/camel-k-crds.yaml | 136 +++++++++++ helm/camel-k/templates/rbacs-descoped.yaml | 32 +++ helm/camel-k/templates/rbacs-namespaced.yaml | 31 +++ pkg/apis/camel/v1/trait/ingress.go | 11 + pkg/apis/camel/v1/trait/zz_generated.deepcopy.go | 5 + pkg/internal/client.go | 20 ++ .../camel.apache.org_integrationplatforms.yaml | 34 +++ .../camel.apache.org_integrationprofiles.yaml | 34 +++ .../config/crd/bases/camel.apache.org_pipes.yaml | 34 +++ .../config/rbac/descoped/kustomization.yaml | 2 + ...operator-cluster-role-binding-certmanager.yaml} | 36 ++- .../operator-cluster-role-certmanager.yaml} | 39 ++-- .../config/rbac/namespaced/kustomization.yaml | 2 + ...yaml => operator-role-binding-certmanager.yaml} | 36 ++- ...ization.yaml => operator-role-certmanager.yaml} | 39 ++-- pkg/trait/ingress.go | 113 ++++++++- pkg/trait/ingress_test.go | 142 ++++++++++++ pkg/util/certmanager/enabled.go | 163 +++++++++++++ pkg/util/certmanager/enabled_test.go | 252 +++++++++++++++++++++ script/Makefile | 6 + 27 files changed, 1336 insertions(+), 141 deletions(-) diff --git a/.github/workflows/cert-manager.yml b/.github/workflows/cert-manager.yml new file mode 100644 index 000000000..e83e1d46e --- /dev/null +++ b/.github/workflows/cert-manager.yml @@ -0,0 +1,91 @@ +# --------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# --------------------------------------------------------------------------- + +name: cert-manager + +env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + +on: + pull_request: + branches: + - main + - "release-*" + paths-ignore: + - 'docs/**' + - 'java/**' + - 'proposals/**' + - '**.adoc' + - '**.md' + - 'KEYS' + - 'LICENSE' + - 'NOTICE' + push: + branches: + - main + - "release-*" + paths-ignore: + - 'docs/**' + - 'java/**' + - 'proposals/**' + - '**.adoc' + - '**.md' + - 'KEYS' + - 'LICENSE' + - 'NOTICE' + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: true + +jobs: + test: + runs-on: ubuntu-latest + steps: + + - name: Checkout code + uses: actions/checkout@v7 + with: + persist-credentials: false + submodules: recursive + + - name: Infra setting + uses: ./.github/actions/infra-setting + + - name: Install cert-manager + shell: bash + run: | + ./e2e/cert-manager/setup/setup.sh + + - name: Create operator namespace + shell: bash + run: | + kubectl create ns camel-k + + - name: Registry setting + uses: ./.github/actions/registry-setting + + - name: Install operator + shell: bash + run: | + make install-k8s-global + + - name: Run test + shell: bash + run: | + DO_TEST_PREBUILD=false GOTESTFMT="-json 2>&1 | gotestfmt" make test-cert-manager diff --git a/docs/modules/ROOT/partials/apis/camel-k-crds.adoc b/docs/modules/ROOT/partials/apis/camel-k-crds.adoc index 04c41b894..9509b4a77 100644 --- a/docs/modules/ROOT/partials/apis/camel-k-crds.adoc +++ b/docs/modules/ROOT/partials/apis/camel-k-crds.adoc @@ -7872,6 +7872,32 @@ string To configure tls secret name +|`tlsCertManagerAuto` + +bool +| + + +To automatically discover and configure a cert-manager Issuer or ClusterIssuer +for TLS certificate issuance, in place of manually providing a secret. + +|`tlsIssuerName` + +string +| + + +The name of the cert-manager Issuer or ClusterIssuer to use for automatic +TLS certificate issuance. If set, cert-manager availability and the issuer's +existence are verified, bypassing auto-discovery. + +|`tlsIssuerKind` + +string +| + + +The kind of the cert-manager issuer to use: `Issuer` (namespaced) or +`ClusterIssuer` (cluster-scoped). Defaults to `ClusterIssuer` when +tlsIssuerName is set without a kind. + |=== diff --git a/docs/modules/traits/pages/ingress.adoc b/docs/modules/traits/pages/ingress.adoc index 86ffa8e10..cd4c55891 100755 --- a/docs/modules/traits/pages/ingress.adoc +++ b/docs/modules/traits/pages/ingress.adoc @@ -72,6 +72,23 @@ One of `Exact`, `Prefix`, `ImplementationSpecific` (default to `Prefix`). | string | To configure tls secret name +| ingress.tlsCertManagerAuto +| bool +| To automatically discover and configure a cert-manager Issuer or ClusterIssuer +for TLS certificate issuance, in place of manually providing a secret. + +| ingress.tlsIssuerName +| string +| The name of the cert-manager Issuer or ClusterIssuer to use for automatic +TLS certificate issuance. If set, cert-manager availability and the issuer's +existence are verified, bypassing auto-discovery. + +| ingress.tlsIssuerKind +| string +| The kind of the cert-manager issuer to use: `Issuer` (namespaced) or +`ClusterIssuer` (cluster-scoped). Defaults to `ClusterIssuer` when +tlsIssuerName is set without a kind. + |=== NOTE: the variable names are "snake case" if you're using in `kamel` CLI, for example `trait.myParam` has to be translated as `-t trait.my-param` diff --git a/e2e/cert-manager/certmanager_test.go b/e2e/cert-manager/certmanager_test.go new file mode 100644 index 000000000..7a1eed7e6 --- /dev/null +++ b/e2e/cert-manager/certmanager_test.go @@ -0,0 +1,85 @@ +//go:build integration +// +build integration + +// To enable compilation of this file in Goland, go to "Settings -> Go -> Vendoring & Build Tags -> Custom Tags" and add "integration" + +/* +Licensed to the Apache Software Foundation (ASF) under one or more +contributor license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright ownership. +The ASF licenses this file to You under the Apache License, Version 2.0 +(the "License"); you may not use this file except in compliance with +the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package certmanager + +import ( + "context" + "crypto/x509" + "encoding/pem" + "testing" + "time" + + . "github.com/apache/camel-k/v2/e2e/support" + v1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1" + "github.com/apache/camel-k/v2/pkg/util/certmanager" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// TestIngressCertManagerAutoDiscovery verifies that the Ingress trait, when +// tls-cert-manager-auto is enabled, discovers the self-signed ClusterIssuer +// installed by setup/setup.sh, annotates the generated Ingress accordingly, +// and that cert-manager actually issues a certificate into the derived +// "<service>-tls" secret for the configured host. +func TestIngressCertManagerAutoDiscovery(t *testing.T) { + t.Parallel() + WithNewTestNamespace(t, func(ctx context.Context, g *WithT, ns string) { + host := "cert-manager-it.example.com" + integrationName := "platform-http-server" + + g.Expect(KamelRun(t, ctx, ns, "files/PlatformHttpServer.java", + "-t", "ingress.enabled=true", + "-t", "ingress.host="+host, + "-t", "ingress.tls-cert-manager-auto=true", + ).Execute()).To(Succeed()) + + g.Eventually(IntegrationConditionStatus(t, ctx, ns, integrationName, + v1.IntegrationConditionReady), TestTimeoutMedium).Should(Equal(corev1.ConditionTrue)) + + // The Ingress trait should have auto-discovered the ClusterIssuer installed + // by setup.sh and annotated the Ingress accordingly. + g.Eventually(func() (string, error) { + ingress, err := TestClient(t).NetworkingV1().Ingresses(ns).Get(ctx, integrationName, metav1.GetOptions{}) + if err != nil { + return "", err + } + + return ingress.Annotations[certmanager.AnnotationClusterIssuer], nil + }, TestTimeoutShort).Should(Equal("selfsigned-cluster-issuer")) + + // cert-manager should pick up the annotation and populate the derived secret. + g.Eventually(SecretByName(t, ctx, ns, integrationName+"-tls"), TestTimeoutMedium).ShouldNot(BeNil()) + + certSecret := SecretByName(t, ctx, ns, integrationName+"-tls")() + g.Expect(certSecret.Data).To(HaveKey("tls.crt")) + g.Expect(certSecret.Data).To(HaveKey("tls.key")) + + block, _ := pem.Decode(certSecret.Data["tls.crt"]) + g.Expect(block).NotTo(BeNil()) + cert, err := x509.ParseCertificate(block.Bytes) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(cert.DNSNames).To(ContainElement(host)) + g.Expect(time.Now()).To(BeTemporally("<", cert.NotAfter)) + }) +} diff --git a/e2e/cert-manager/files/PlatformHttpServer.java b/e2e/cert-manager/files/PlatformHttpServer.java new file mode 100644 index 000000000..31d8d19f3 --- /dev/null +++ b/e2e/cert-manager/files/PlatformHttpServer.java @@ -0,0 +1,26 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import org.apache.camel.builder.RouteBuilder; + +public class PlatformHttpServer extends RouteBuilder { + @Override + public void configure() throws Exception { + from("platform-http:/hello?httpMethodRestrict=GET") + .setBody(simple("Hello ${header.name}")); + } +} diff --git a/pkg/resources/config/rbac/namespaced/kustomization.yaml b/e2e/cert-manager/setup/cluster-issuer.yaml similarity index 58% copy from pkg/resources/config/rbac/namespaced/kustomization.yaml copy to e2e/cert-manager/setup/cluster-issuer.yaml index 1859bd544..ae17ef94b 100644 --- a/pkg/resources/config/rbac/namespaced/kustomization.yaml +++ b/e2e/cert-manager/setup/cluster-issuer.yaml @@ -15,26 +15,9 @@ # limitations under the License. # --------------------------------------------------------------------------- -# -# rbac resources applicable for all kubernetes platforms - namespaced operator -# -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -resources: -- operator-role-events.yaml -- operator-role.yaml -- operator-role-keda.yaml -- operator-role-knative.yaml -- operator-role-leases.yaml -- operator-role-openshift.yaml -- operator-role-podmonitors.yaml -- operator-role-strimzi.yaml -- operator-role-binding.yaml -- operator-role-binding-events.yaml -- operator-role-binding-keda.yaml -- operator-role-binding-knative.yaml -- operator-role-binding-leases.yaml -- operator-role-binding-openshift.yaml -- operator-role-binding-podmonitors.yaml -- operator-role-binding-strimzi.yaml +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: selfsigned-cluster-issuer +spec: + selfSigned: {} diff --git a/pkg/resources/config/rbac/namespaced/kustomization.yaml b/e2e/cert-manager/setup/setup.sh old mode 100644 new mode 100755 similarity index 55% copy from pkg/resources/config/rbac/namespaced/kustomization.yaml copy to e2e/cert-manager/setup/setup.sh index 1859bd544..d7ab4caa9 --- a/pkg/resources/config/rbac/namespaced/kustomization.yaml +++ b/e2e/cert-manager/setup/setup.sh @@ -1,3 +1,5 @@ +#!/bin/bash + # --------------------------------------------------------------------------- # Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed with @@ -6,7 +8,7 @@ # (the "License"); you may not use this file except in compliance with # the License. You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, @@ -15,26 +17,14 @@ # limitations under the License. # --------------------------------------------------------------------------- -# -# rbac resources applicable for all kubernetes platforms - namespaced operator -# -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization +CERT_MANAGER_VERSION="v1.21.2" +TIMEOUT="150s" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/${CERT_MANAGER_VERSION}/cert-manager.yaml +kubectl wait --for=condition=available deployment/cert-manager -n cert-manager --timeout=$TIMEOUT +kubectl wait --for=condition=available deployment/cert-manager-webhook -n cert-manager --timeout=$TIMEOUT +kubectl wait --for=condition=available deployment/cert-manager-cainjector -n cert-manager --timeout=$TIMEOUT -resources: -- operator-role-events.yaml -- operator-role.yaml -- operator-role-keda.yaml -- operator-role-knative.yaml -- operator-role-leases.yaml -- operator-role-openshift.yaml -- operator-role-podmonitors.yaml -- operator-role-strimzi.yaml -- operator-role-binding.yaml -- operator-role-binding-events.yaml -- operator-role-binding-keda.yaml -- operator-role-binding-knative.yaml -- operator-role-binding-leases.yaml -- operator-role-binding-openshift.yaml -- operator-role-binding-podmonitors.yaml -- operator-role-binding-strimzi.yaml +# Install a self-signed ClusterIssuer for test purposes +kubectl apply -f $SCRIPT_DIR/cluster-issuer.yaml diff --git a/helm/camel-k/crds/camel-k-crds.yaml b/helm/camel-k/crds/camel-k-crds.yaml index 2f972f821..97974af6d 100644 --- a/helm/camel-k/crds/camel-k-crds.yaml +++ b/helm/camel-k/crds/camel-k-crds.yaml @@ -4693,11 +4693,28 @@ spec: items: type: string type: array + tlsCertManagerAuto: + description: |- + To automatically discover and configure a cert-manager Issuer or ClusterIssuer + for TLS certificate issuance, in place of manually providing a secret. + type: boolean tlsHosts: description: To configure tls hosts items: type: string type: array + tlsIssuerKind: + description: |- + The kind of the cert-manager issuer to use: `Issuer` (namespaced) or + `ClusterIssuer` (cluster-scoped). Defaults to `ClusterIssuer` when + tlsIssuerName is set without a kind. + type: string + tlsIssuerName: + description: |- + The name of the cert-manager Issuer or ClusterIssuer to use for automatic + TLS certificate issuance. If set, cert-manager availability and the issuer's + existence are verified, bypassing auto-discovery. + type: string tlsSecretName: description: To configure tls secret name type: string @@ -7238,11 +7255,28 @@ spec: items: type: string type: array + tlsCertManagerAuto: + description: |- + To automatically discover and configure a cert-manager Issuer or ClusterIssuer + for TLS certificate issuance, in place of manually providing a secret. + type: boolean tlsHosts: description: To configure tls hosts items: type: string type: array + tlsIssuerKind: + description: |- + The kind of the cert-manager issuer to use: `Issuer` (namespaced) or + `ClusterIssuer` (cluster-scoped). Defaults to `ClusterIssuer` when + tlsIssuerName is set without a kind. + type: string + tlsIssuerName: + description: |- + The name of the cert-manager Issuer or ClusterIssuer to use for automatic + TLS certificate issuance. If set, cert-manager availability and the issuer's + existence are verified, bypassing auto-discovery. + type: string tlsSecretName: description: To configure tls secret name type: string @@ -9744,11 +9778,28 @@ spec: items: type: string type: array + tlsCertManagerAuto: + description: |- + To automatically discover and configure a cert-manager Issuer or ClusterIssuer + for TLS certificate issuance, in place of manually providing a secret. + type: boolean tlsHosts: description: To configure tls hosts items: type: string type: array + tlsIssuerKind: + description: |- + The kind of the cert-manager issuer to use: `Issuer` (namespaced) or + `ClusterIssuer` (cluster-scoped). Defaults to `ClusterIssuer` when + tlsIssuerName is set without a kind. + type: string + tlsIssuerName: + description: |- + The name of the cert-manager Issuer or ClusterIssuer to use for automatic + TLS certificate issuance. If set, cert-manager availability and the issuer's + existence are verified, bypassing auto-discovery. + type: string tlsSecretName: description: To configure tls secret name type: string @@ -12238,11 +12289,28 @@ spec: items: type: string type: array + tlsCertManagerAuto: + description: |- + To automatically discover and configure a cert-manager Issuer or ClusterIssuer + for TLS certificate issuance, in place of manually providing a secret. + type: boolean tlsHosts: description: To configure tls hosts items: type: string type: array + tlsIssuerKind: + description: |- + The kind of the cert-manager issuer to use: `Issuer` (namespaced) or + `ClusterIssuer` (cluster-scoped). Defaults to `ClusterIssuer` when + tlsIssuerName is set without a kind. + type: string + tlsIssuerName: + description: |- + The name of the cert-manager Issuer or ClusterIssuer to use for automatic + TLS certificate issuance. If set, cert-manager availability and the issuer's + existence are verified, bypassing auto-discovery. + type: string tlsSecretName: description: To configure tls secret name type: string @@ -21794,11 +21862,28 @@ spec: items: type: string type: array + tlsCertManagerAuto: + description: |- + To automatically discover and configure a cert-manager Issuer or ClusterIssuer + for TLS certificate issuance, in place of manually providing a secret. + type: boolean tlsHosts: description: To configure tls hosts items: type: string type: array + tlsIssuerKind: + description: |- + The kind of the cert-manager issuer to use: `Issuer` (namespaced) or + `ClusterIssuer` (cluster-scoped). Defaults to `ClusterIssuer` when + tlsIssuerName is set without a kind. + type: string + tlsIssuerName: + description: |- + The name of the cert-manager Issuer or ClusterIssuer to use for automatic + TLS certificate issuance. If set, cert-manager availability and the issuer's + existence are verified, bypassing auto-discovery. + type: string tlsSecretName: description: To configure tls secret name type: string @@ -24176,11 +24261,28 @@ spec: items: type: string type: array + tlsCertManagerAuto: + description: |- + To automatically discover and configure a cert-manager Issuer or ClusterIssuer + for TLS certificate issuance, in place of manually providing a secret. + type: boolean tlsHosts: description: To configure tls hosts items: type: string type: array + tlsIssuerKind: + description: |- + The kind of the cert-manager issuer to use: `Issuer` (namespaced) or + `ClusterIssuer` (cluster-scoped). Defaults to `ClusterIssuer` when + tlsIssuerName is set without a kind. + type: string + tlsIssuerName: + description: |- + The name of the cert-manager Issuer or ClusterIssuer to use for automatic + TLS certificate issuance. If set, cert-manager availability and the issuer's + existence are verified, bypassing auto-discovery. + type: string tlsSecretName: description: To configure tls secret name type: string @@ -35095,11 +35197,28 @@ spec: items: type: string type: array + tlsCertManagerAuto: + description: |- + To automatically discover and configure a cert-manager Issuer or ClusterIssuer + for TLS certificate issuance, in place of manually providing a secret. + type: boolean tlsHosts: description: To configure tls hosts items: type: string type: array + tlsIssuerKind: + description: |- + The kind of the cert-manager issuer to use: `Issuer` (namespaced) or + `ClusterIssuer` (cluster-scoped). Defaults to `ClusterIssuer` when + tlsIssuerName is set without a kind. + type: string + tlsIssuerName: + description: |- + The name of the cert-manager Issuer or ClusterIssuer to use for automatic + TLS certificate issuance. If set, cert-manager availability and the issuer's + existence are verified, bypassing auto-discovery. + type: string tlsSecretName: description: To configure tls secret name type: string @@ -37399,11 +37518,28 @@ spec: items: type: string type: array + tlsCertManagerAuto: + description: |- + To automatically discover and configure a cert-manager Issuer or ClusterIssuer + for TLS certificate issuance, in place of manually providing a secret. + type: boolean tlsHosts: description: To configure tls hosts items: type: string type: array + tlsIssuerKind: + description: |- + The kind of the cert-manager issuer to use: `Issuer` (namespaced) or + `ClusterIssuer` (cluster-scoped). Defaults to `ClusterIssuer` when + tlsIssuerName is set without a kind. + type: string + tlsIssuerName: + description: |- + The name of the cert-manager Issuer or ClusterIssuer to use for automatic + TLS certificate issuance. If set, cert-manager availability and the issuer's + existence are verified, bypassing auto-discovery. + type: string tlsSecretName: description: To configure tls secret name type: string diff --git a/helm/camel-k/templates/rbacs-descoped.yaml b/helm/camel-k/templates/rbacs-descoped.yaml index 8cdd8aaf6..1c515a1ce 100644 --- a/helm/camel-k/templates/rbacs-descoped.yaml +++ b/helm/camel-k/templates/rbacs-descoped.yaml @@ -217,6 +217,23 @@ rules: --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole +metadata: + labels: + app: camel-k + name: {{ include "camel-k.fullname" . }}-operator-certmanager +rules: +- apiGroups: + - cert-manager.io + resources: + - issuers + - clusterissuers + verbs: + - get + - list + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole metadata: labels: app: camel-k @@ -474,6 +491,21 @@ subjects: --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding +metadata: + labels: + app: camel-k + name: {{ include "camel-k.fullname" . }}-operator-certmanager +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "camel-k.fullname" . }}-operator-certmanager +subjects: +- kind: ServiceAccount + name: {{ include "camel-k.fullname" . }}-operator + namespace: '{{ .Release.Namespace }}' +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding metadata: labels: app: camel-k diff --git a/helm/camel-k/templates/rbacs-namespaced.yaml b/helm/camel-k/templates/rbacs-namespaced.yaml index 530539539..d43d5db1f 100644 --- a/helm/camel-k/templates/rbacs-namespaced.yaml +++ b/helm/camel-k/templates/rbacs-namespaced.yaml @@ -211,6 +211,23 @@ rules: --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role +metadata: + labels: + app: camel-k + name: {{ include "camel-k.fullname" . }}-operator-certmanager +rules: +- apiGroups: + - cert-manager.io + resources: + - issuers + - clusterissuers + verbs: + - get + - list + - watch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role metadata: labels: app: camel-k @@ -467,6 +484,20 @@ subjects: --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding +metadata: + labels: + app: camel-k + name: {{ include "camel-k.fullname" . }}-operator-certmanager +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ include "camel-k.fullname" . }}-operator-certmanager +subjects: +- kind: ServiceAccount + name: {{ include "camel-k.fullname" . }}-operator +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding metadata: labels: app: camel-k diff --git a/pkg/apis/camel/v1/trait/ingress.go b/pkg/apis/camel/v1/trait/ingress.go index 0768998ca..364a05013 100644 --- a/pkg/apis/camel/v1/trait/ingress.go +++ b/pkg/apis/camel/v1/trait/ingress.go @@ -55,4 +55,15 @@ type IngressTrait struct { TLSHosts []string `json:"tlsHosts,omitempty" property:"tls-hosts"` // To configure tls secret name TLSSecretName string `json:"tlsSecretName,omitempty" property:"tls-secret-name"` + // To automatically discover and configure a cert-manager Issuer or ClusterIssuer + // for TLS certificate issuance, in place of manually providing a secret. + TLSCertManagerAuto *bool `json:"tlsCertManagerAuto,omitempty" property:"tls-cert-manager-auto"` + // The name of the cert-manager Issuer or ClusterIssuer to use for automatic + // TLS certificate issuance. If set, cert-manager availability and the issuer's + // existence are verified, bypassing auto-discovery. + TLSIssuerName string `json:"tlsIssuerName,omitempty" property:"tls-issuer-name"` + // The kind of the cert-manager issuer to use: `Issuer` (namespaced) or + // `ClusterIssuer` (cluster-scoped). Defaults to `ClusterIssuer` when + // tlsIssuerName is set without a kind. + TLSIssuerKind string `json:"tlsIssuerKind,omitempty" property:"tls-issuer-kind"` } diff --git a/pkg/apis/camel/v1/trait/zz_generated.deepcopy.go b/pkg/apis/camel/v1/trait/zz_generated.deepcopy.go index 03e7c8716..190cbfade 100644 --- a/pkg/apis/camel/v1/trait/zz_generated.deepcopy.go +++ b/pkg/apis/camel/v1/trait/zz_generated.deepcopy.go @@ -544,6 +544,11 @@ func (in *IngressTrait) DeepCopyInto(out *IngressTrait) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.TLSCertManagerAuto != nil { + in, out := &in.TLSCertManagerAuto, &out.TLSCertManagerAuto + *out = new(bool) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IngressTrait. diff --git a/pkg/internal/client.go b/pkg/internal/client.go index 4366a2ef8..2c15d3892 100644 --- a/pkg/internal/client.go +++ b/pkg/internal/client.go @@ -151,6 +151,7 @@ type FakeClient struct { enabledOpenshift bool enabledKnativeServing bool enabledKnativeEventing bool + enabledCertManager bool } func (c *FakeClient) Intercept(intercept *interceptor.Funcs) { @@ -202,6 +203,10 @@ func (c *FakeClient) EnableOpenshiftDiscovery() { c.enabledOpenshift = true } +func (c *FakeClient) EnableCertManagerDiscovery() { + c.enabledCertManager = true +} + func (c *FakeClient) DisableKnativeServing() { c.enabledKnativeServing = false } @@ -225,6 +230,7 @@ func (c *FakeClient) Discovery() discovery.DiscoveryInterfaces { enabledOpenshift: c.enabledOpenshift, enabledKnativeServing: c.enabledKnativeServing, enabledKnativeEventing: c.enabledKnativeEventing, + enabledCertManager: c.enabledCertManager, } } @@ -276,6 +282,7 @@ type FakeDiscovery struct { enabledOpenshift bool enabledKnativeServing bool enabledKnativeEventing bool + enabledCertManager bool } func (f *FakeDiscovery) ServerResourcesForGroupVersion(groupVersion string) (*metav1.APIResourceList, error) { @@ -292,6 +299,19 @@ func (f *FakeDiscovery) ServerResourcesForGroupVersion(groupVersion string) (*me } } + // used to verify if cert-manager is installed + if groupVersion == "cert-manager.io/v1" { + if f.enabledCertManager { + return &metav1.APIResourceList{ + GroupVersion: "cert-manager.io/v1", + }, nil + } + + return nil, k8serrors.NewNotFound(schema.GroupResource{ + Group: "cert-manager.io", + }, "") + } + // used to verify if Knative Serving is installed if f.enabledKnativeServing { if groupVersion == "serving.knative.dev/v1" && !util.StringSliceExists(f.disabledGroups, groupVersion) { diff --git a/pkg/resources/config/crd/bases/camel.apache.org_integrationplatforms.yaml b/pkg/resources/config/crd/bases/camel.apache.org_integrationplatforms.yaml index 1b96264f9..cd875bf7e 100644 --- a/pkg/resources/config/crd/bases/camel.apache.org_integrationplatforms.yaml +++ b/pkg/resources/config/crd/bases/camel.apache.org_integrationplatforms.yaml @@ -1385,11 +1385,28 @@ spec: items: type: string type: array + tlsCertManagerAuto: + description: |- + To automatically discover and configure a cert-manager Issuer or ClusterIssuer + for TLS certificate issuance, in place of manually providing a secret. + type: boolean tlsHosts: description: To configure tls hosts items: type: string type: array + tlsIssuerKind: + description: |- + The kind of the cert-manager issuer to use: `Issuer` (namespaced) or + `ClusterIssuer` (cluster-scoped). Defaults to `ClusterIssuer` when + tlsIssuerName is set without a kind. + type: string + tlsIssuerName: + description: |- + The name of the cert-manager Issuer or ClusterIssuer to use for automatic + TLS certificate issuance. If set, cert-manager availability and the issuer's + existence are verified, bypassing auto-discovery. + type: string tlsSecretName: description: To configure tls secret name type: string @@ -3930,11 +3947,28 @@ spec: items: type: string type: array + tlsCertManagerAuto: + description: |- + To automatically discover and configure a cert-manager Issuer or ClusterIssuer + for TLS certificate issuance, in place of manually providing a secret. + type: boolean tlsHosts: description: To configure tls hosts items: type: string type: array + tlsIssuerKind: + description: |- + The kind of the cert-manager issuer to use: `Issuer` (namespaced) or + `ClusterIssuer` (cluster-scoped). Defaults to `ClusterIssuer` when + tlsIssuerName is set without a kind. + type: string + tlsIssuerName: + description: |- + The name of the cert-manager Issuer or ClusterIssuer to use for automatic + TLS certificate issuance. If set, cert-manager availability and the issuer's + existence are verified, bypassing auto-discovery. + type: string tlsSecretName: description: To configure tls secret name type: string diff --git a/pkg/resources/config/crd/bases/camel.apache.org_integrationprofiles.yaml b/pkg/resources/config/crd/bases/camel.apache.org_integrationprofiles.yaml index 7a893b272..af5c8dd2f 100644 --- a/pkg/resources/config/crd/bases/camel.apache.org_integrationprofiles.yaml +++ b/pkg/resources/config/crd/bases/camel.apache.org_integrationprofiles.yaml @@ -1310,11 +1310,28 @@ spec: items: type: string type: array + tlsCertManagerAuto: + description: |- + To automatically discover and configure a cert-manager Issuer or ClusterIssuer + for TLS certificate issuance, in place of manually providing a secret. + type: boolean tlsHosts: description: To configure tls hosts items: type: string type: array + tlsIssuerKind: + description: |- + The kind of the cert-manager issuer to use: `Issuer` (namespaced) or + `ClusterIssuer` (cluster-scoped). Defaults to `ClusterIssuer` when + tlsIssuerName is set without a kind. + type: string + tlsIssuerName: + description: |- + The name of the cert-manager Issuer or ClusterIssuer to use for automatic + TLS certificate issuance. If set, cert-manager availability and the issuer's + existence are verified, bypassing auto-discovery. + type: string tlsSecretName: description: To configure tls secret name type: string @@ -3804,11 +3821,28 @@ spec: items: type: string type: array + tlsCertManagerAuto: + description: |- + To automatically discover and configure a cert-manager Issuer or ClusterIssuer + for TLS certificate issuance, in place of manually providing a secret. + type: boolean tlsHosts: description: To configure tls hosts items: type: string type: array + tlsIssuerKind: + description: |- + The kind of the cert-manager issuer to use: `Issuer` (namespaced) or + `ClusterIssuer` (cluster-scoped). Defaults to `ClusterIssuer` when + tlsIssuerName is set without a kind. + type: string + tlsIssuerName: + description: |- + The name of the cert-manager Issuer or ClusterIssuer to use for automatic + TLS certificate issuance. If set, cert-manager availability and the issuer's + existence are verified, bypassing auto-discovery. + type: string tlsSecretName: description: To configure tls secret name type: string diff --git a/pkg/resources/config/crd/bases/camel.apache.org_pipes.yaml b/pkg/resources/config/crd/bases/camel.apache.org_pipes.yaml index 7cc12f6b9..df85d293c 100644 --- a/pkg/resources/config/crd/bases/camel.apache.org_pipes.yaml +++ b/pkg/resources/config/crd/bases/camel.apache.org_pipes.yaml @@ -8418,11 +8418,28 @@ spec: items: type: string type: array + tlsCertManagerAuto: + description: |- + To automatically discover and configure a cert-manager Issuer or ClusterIssuer + for TLS certificate issuance, in place of manually providing a secret. + type: boolean tlsHosts: description: To configure tls hosts items: type: string type: array + tlsIssuerKind: + description: |- + The kind of the cert-manager issuer to use: `Issuer` (namespaced) or + `ClusterIssuer` (cluster-scoped). Defaults to `ClusterIssuer` when + tlsIssuerName is set without a kind. + type: string + tlsIssuerName: + description: |- + The name of the cert-manager Issuer or ClusterIssuer to use for automatic + TLS certificate issuance. If set, cert-manager availability and the issuer's + existence are verified, bypassing auto-discovery. + type: string tlsSecretName: description: To configure tls secret name type: string @@ -10722,11 +10739,28 @@ spec: items: type: string type: array + tlsCertManagerAuto: + description: |- + To automatically discover and configure a cert-manager Issuer or ClusterIssuer + for TLS certificate issuance, in place of manually providing a secret. + type: boolean tlsHosts: description: To configure tls hosts items: type: string type: array + tlsIssuerKind: + description: |- + The kind of the cert-manager issuer to use: `Issuer` (namespaced) or + `ClusterIssuer` (cluster-scoped). Defaults to `ClusterIssuer` when + tlsIssuerName is set without a kind. + type: string + tlsIssuerName: + description: |- + The name of the cert-manager Issuer or ClusterIssuer to use for automatic + TLS certificate issuance. If set, cert-manager availability and the issuer's + existence are verified, bypassing auto-discovery. + type: string tlsSecretName: description: To configure tls secret name type: string diff --git a/pkg/resources/config/rbac/descoped/kustomization.yaml b/pkg/resources/config/rbac/descoped/kustomization.yaml index 68f35074a..54c7ebb7e 100644 --- a/pkg/resources/config/rbac/descoped/kustomization.yaml +++ b/pkg/resources/config/rbac/descoped/kustomization.yaml @@ -22,6 +22,7 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: +- operator-cluster-role-certmanager.yaml - operator-cluster-role-events.yaml - operator-cluster-role.yaml - operator-cluster-role-keda.yaml @@ -30,6 +31,7 @@ resources: - operator-cluster-role-openshift.yaml - operator-cluster-role-podmonitors.yaml - operator-cluster-role-strimzi.yaml +- operator-cluster-role-binding-certmanager.yaml - operator-cluster-role-binding-events.yaml - operator-cluster-role-binding-keda.yaml - operator-cluster-role-binding-knative.yaml diff --git a/pkg/resources/config/rbac/namespaced/kustomization.yaml b/pkg/resources/config/rbac/descoped/operator-cluster-role-binding-certmanager.yaml similarity index 58% copy from pkg/resources/config/rbac/namespaced/kustomization.yaml copy to pkg/resources/config/rbac/descoped/operator-cluster-role-binding-certmanager.yaml index 1859bd544..0b444f5c4 100644 --- a/pkg/resources/config/rbac/namespaced/kustomization.yaml +++ b/pkg/resources/config/rbac/descoped/operator-cluster-role-binding-certmanager.yaml @@ -15,26 +15,16 @@ # limitations under the License. # --------------------------------------------------------------------------- -# -# rbac resources applicable for all kubernetes platforms - namespaced operator -# -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -resources: -- operator-role-events.yaml -- operator-role.yaml -- operator-role-keda.yaml -- operator-role-knative.yaml -- operator-role-leases.yaml -- operator-role-openshift.yaml -- operator-role-podmonitors.yaml -- operator-role-strimzi.yaml -- operator-role-binding.yaml -- operator-role-binding-events.yaml -- operator-role-binding-keda.yaml -- operator-role-binding-knative.yaml -- operator-role-binding-leases.yaml -- operator-role-binding-openshift.yaml -- operator-role-binding-podmonitors.yaml -- operator-role-binding-strimzi.yaml +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: camel-k-operator-certmanager + labels: + app: "camel-k" +subjects: +- kind: ServiceAccount + name: camel-k-operator +roleRef: + kind: ClusterRole + name: camel-k-operator-certmanager + apiGroup: rbac.authorization.k8s.io diff --git a/pkg/resources/config/rbac/namespaced/kustomization.yaml b/pkg/resources/config/rbac/descoped/operator-cluster-role-certmanager.yaml similarity index 58% copy from pkg/resources/config/rbac/namespaced/kustomization.yaml copy to pkg/resources/config/rbac/descoped/operator-cluster-role-certmanager.yaml index 1859bd544..60fc584e3 100644 --- a/pkg/resources/config/rbac/namespaced/kustomization.yaml +++ b/pkg/resources/config/rbac/descoped/operator-cluster-role-certmanager.yaml @@ -15,26 +15,19 @@ # limitations under the License. # --------------------------------------------------------------------------- -# -# rbac resources applicable for all kubernetes platforms - namespaced operator -# -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -resources: -- operator-role-events.yaml -- operator-role.yaml -- operator-role-keda.yaml -- operator-role-knative.yaml -- operator-role-leases.yaml -- operator-role-openshift.yaml -- operator-role-podmonitors.yaml -- operator-role-strimzi.yaml -- operator-role-binding.yaml -- operator-role-binding-events.yaml -- operator-role-binding-keda.yaml -- operator-role-binding-knative.yaml -- operator-role-binding-leases.yaml -- operator-role-binding-openshift.yaml -- operator-role-binding-podmonitors.yaml -- operator-role-binding-strimzi.yaml +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: camel-k-operator-certmanager + labels: + app: "camel-k" +rules: +- apiGroups: + - "cert-manager.io" + resources: + - issuers + - clusterissuers + verbs: + - get + - list + - watch diff --git a/pkg/resources/config/rbac/namespaced/kustomization.yaml b/pkg/resources/config/rbac/namespaced/kustomization.yaml index 1859bd544..e50bc5c5a 100644 --- a/pkg/resources/config/rbac/namespaced/kustomization.yaml +++ b/pkg/resources/config/rbac/namespaced/kustomization.yaml @@ -22,6 +22,7 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: +- operator-role-certmanager.yaml - operator-role-events.yaml - operator-role.yaml - operator-role-keda.yaml @@ -31,6 +32,7 @@ resources: - operator-role-podmonitors.yaml - operator-role-strimzi.yaml - operator-role-binding.yaml +- operator-role-binding-certmanager.yaml - operator-role-binding-events.yaml - operator-role-binding-keda.yaml - operator-role-binding-knative.yaml diff --git a/pkg/resources/config/rbac/namespaced/kustomization.yaml b/pkg/resources/config/rbac/namespaced/operator-role-binding-certmanager.yaml similarity index 58% copy from pkg/resources/config/rbac/namespaced/kustomization.yaml copy to pkg/resources/config/rbac/namespaced/operator-role-binding-certmanager.yaml index 1859bd544..0ae88e3c8 100644 --- a/pkg/resources/config/rbac/namespaced/kustomization.yaml +++ b/pkg/resources/config/rbac/namespaced/operator-role-binding-certmanager.yaml @@ -15,26 +15,16 @@ # limitations under the License. # --------------------------------------------------------------------------- -# -# rbac resources applicable for all kubernetes platforms - namespaced operator -# -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -resources: -- operator-role-events.yaml -- operator-role.yaml -- operator-role-keda.yaml -- operator-role-knative.yaml -- operator-role-leases.yaml -- operator-role-openshift.yaml -- operator-role-podmonitors.yaml -- operator-role-strimzi.yaml -- operator-role-binding.yaml -- operator-role-binding-events.yaml -- operator-role-binding-keda.yaml -- operator-role-binding-knative.yaml -- operator-role-binding-leases.yaml -- operator-role-binding-openshift.yaml -- operator-role-binding-podmonitors.yaml -- operator-role-binding-strimzi.yaml +kind: RoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: camel-k-operator-certmanager + labels: + app: "camel-k" +subjects: +- kind: ServiceAccount + name: camel-k-operator +roleRef: + kind: Role + name: camel-k-operator-certmanager + apiGroup: rbac.authorization.k8s.io diff --git a/pkg/resources/config/rbac/namespaced/kustomization.yaml b/pkg/resources/config/rbac/namespaced/operator-role-certmanager.yaml similarity index 58% copy from pkg/resources/config/rbac/namespaced/kustomization.yaml copy to pkg/resources/config/rbac/namespaced/operator-role-certmanager.yaml index 1859bd544..178420a8a 100644 --- a/pkg/resources/config/rbac/namespaced/kustomization.yaml +++ b/pkg/resources/config/rbac/namespaced/operator-role-certmanager.yaml @@ -15,26 +15,19 @@ # limitations under the License. # --------------------------------------------------------------------------- -# -# rbac resources applicable for all kubernetes platforms - namespaced operator -# -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization - -resources: -- operator-role-events.yaml -- operator-role.yaml -- operator-role-keda.yaml -- operator-role-knative.yaml -- operator-role-leases.yaml -- operator-role-openshift.yaml -- operator-role-podmonitors.yaml -- operator-role-strimzi.yaml -- operator-role-binding.yaml -- operator-role-binding-events.yaml -- operator-role-binding-keda.yaml -- operator-role-binding-knative.yaml -- operator-role-binding-leases.yaml -- operator-role-binding-openshift.yaml -- operator-role-binding-podmonitors.yaml -- operator-role-binding-strimzi.yaml +kind: Role +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: camel-k-operator-certmanager + labels: + app: "camel-k" +rules: +- apiGroups: + - "cert-manager.io" + resources: + - issuers + - clusterissuers + verbs: + - get + - list + - watch diff --git a/pkg/trait/ingress.go b/pkg/trait/ingress.go index 943b7b1e7..5ef9247dd 100644 --- a/pkg/trait/ingress.go +++ b/pkg/trait/ingress.go @@ -23,6 +23,7 @@ import ( v1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1" traitv1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1/trait" + "github.com/apache/camel-k/v2/pkg/util/certmanager" corev1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" "k8s.io/utils/ptr" @@ -124,11 +125,38 @@ func (t *ingressTrait) Apply(e *Environment) error { ingress.Spec.IngressClassName = &t.IngressClassName } - if len(t.TLSHosts) > 0 && t.TLSSecretName != "" { + tlsHosts := t.TLSHosts + secretName := t.TLSSecretName + + // The cert-manager path only activates when the user has not manually provided a + // secret name. It may fall back to t.Host so that auto-discovery also works for the + // common single-host case, without changing the pre-existing manual TLS behavior + // (which requires TLSHosts to be set explicitly and never considers t.Host). + if secretName == "" { + if len(tlsHosts) == 0 && t.Host != "" { + tlsHosts = []string{t.Host} + } + + if len(tlsHosts) > 0 { + annotationKey, issuerName, err := t.resolveCertManagerIssuer(e) + if err != nil { + return err + } + if issuerName != "" { + if ingress.Annotations == nil { + ingress.Annotations = map[string]string{} + } + ingress.Annotations[annotationKey] = issuerName + secretName = service.Name + "-tls" + } + } + } + + if len(tlsHosts) > 0 && secretName != "" { ingress.Spec.TLS = []networkingv1.IngressTLS{ { - Hosts: t.TLSHosts, - SecretName: t.TLSSecretName, + Hosts: tlsHosts, + SecretName: secretName, }, } } @@ -147,6 +175,85 @@ func (t *ingressTrait) Apply(e *Environment) error { return nil } +// resolveCertManagerIssuer determines which cert-manager Issuer or ClusterIssuer +// annotation to apply to the Ingress, if any. It returns an empty issuerName when +// no annotation should be applied (cert-manager auto-discovery is disabled, cert-manager +// is not installed, or no issuer is found). A forced TLSIssuerName is verified to exist +// and returns an error if it does not; auto-discovery degrades to a no-op instead. +func (t *ingressTrait) resolveCertManagerIssuer(e *Environment) (annotationKey, issuerName string, err error) { + namespace := e.Integration.Namespace + + if t.TLSIssuerName != "" { + installed, err := certmanager.IsInstalled(e.Client) + if err != nil { + return "", "", err + } + if !installed { + return "", "", fmt.Errorf("cert-manager is not installed but tlsIssuerName %q was set", t.TLSIssuerName) + } + + kind := t.TLSIssuerKind + if kind == "" { + kind = "ClusterIssuer" + } + + switch kind { + case "Issuer": + exists, err := certmanager.GetIssuer(e.Ctx, e.Client, namespace, t.TLSIssuerName) + if err != nil { + return "", "", err + } + if !exists { + return "", "", fmt.Errorf("issuer %q not found in namespace %q", t.TLSIssuerName, namespace) + } + + return certmanager.AnnotationIssuer, t.TLSIssuerName, nil + case "ClusterIssuer": + exists, err := certmanager.GetClusterIssuer(e.Ctx, e.Client, t.TLSIssuerName) + if err != nil { + return "", "", err + } + if !exists { + return "", "", fmt.Errorf("clusterissuer %q not found", t.TLSIssuerName) + } + + return certmanager.AnnotationClusterIssuer, t.TLSIssuerName, nil + default: + return "", "", fmt.Errorf("invalid tlsIssuerKind %q: must be %q or %q", kind, "Issuer", "ClusterIssuer") + } + } + + if !ptr.Deref(t.TLSCertManagerAuto, false) { + return "", "", nil + } + + installed, err := certmanager.IsInstalled(e.Client) + if err != nil { + return "", "", err + } + if !installed { + return "", "", nil + } + + clusterIssuers, err := certmanager.ListClusterIssuers(e.Ctx, e.Client) + if err != nil { + return "", "", err + } + if len(clusterIssuers) > 0 { + return certmanager.AnnotationClusterIssuer, clusterIssuers[0], nil + } + + issuers, err := certmanager.ListIssuers(e.Ctx, e.Client, namespace) + if err != nil { + return "", "", err + } + if len(issuers) > 0 { + return certmanager.AnnotationIssuer, issuers[0], nil + } + + return "", "", nil +} + func (t *ingressTrait) getPaths(service *corev1.Service) []networkingv1.HTTPIngressPath { createIngressPath := func(path string) networkingv1.HTTPIngressPath { return networkingv1.HTTPIngressPath{ diff --git a/pkg/trait/ingress_test.go b/pkg/trait/ingress_test.go index 763cfcbd9..56731b2c2 100644 --- a/pkg/trait/ingress_test.go +++ b/pkg/trait/ingress_test.go @@ -18,6 +18,7 @@ limitations under the License. package trait import ( + "context" "testing" "github.com/stretchr/testify/assert" @@ -26,10 +27,13 @@ import ( corev1 "k8s.io/api/core/v1" networkingv1 "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/utils/ptr" v1 "github.com/apache/camel-k/v2/pkg/apis/camel/v1" + "github.com/apache/camel-k/v2/pkg/internal" + "github.com/apache/camel-k/v2/pkg/util/certmanager" "github.com/apache/camel-k/v2/pkg/util/kubernetes" ) @@ -335,6 +339,144 @@ func TestConfigureTLSWithoutSecretNameIngressTraitWDoesSucceed(t *testing.T) { assert.Equal(t, "service-name(hostname) -> service-name(http)", conditions[0].Message) } +func TestApplyIngressTraitCertManagerAutoNotInstalledDoesNoop(t *testing.T) { + ingressTrait, environment := createNominalIngressTest() + ingressTrait.TLSCertManagerAuto = ptr.To(true) + environment.Ctx = context.Background() + fakeClient, err := internal.NewFakeClient() + require.NoError(t, err) + environment.Client = fakeClient + + err = ingressTrait.Apply(environment) + + require.NoError(t, err) + environment.Resources.Visit(func(resource runtime.Object) { + if ingress, ok := resource.(*networkingv1.Ingress); ok { + assert.Nil(t, ingress.Spec.TLS) + assert.NotContains(t, ingress.Annotations, certmanager.AnnotationClusterIssuer) + assert.NotContains(t, ingress.Annotations, certmanager.AnnotationIssuer) + } + }) +} + +func TestApplyIngressTraitCertManagerAutoNoIssuerDoesNoop(t *testing.T) { + ingressTrait, environment := createNominalIngressTest() + ingressTrait.TLSCertManagerAuto = ptr.To(true) + environment.Ctx = context.Background() + fakeClient, err := internal.NewFakeClient() + require.NoError(t, err) + fakeClient.(*internal.FakeClient).EnableCertManagerDiscovery() + environment.Client = fakeClient + + err = ingressTrait.Apply(environment) + + require.NoError(t, err) + environment.Resources.Visit(func(resource runtime.Object) { + if ingress, ok := resource.(*networkingv1.Ingress); ok { + assert.Nil(t, ingress.Spec.TLS) + assert.NotContains(t, ingress.Annotations, certmanager.AnnotationClusterIssuer) + } + }) +} + +func TestApplyIngressTraitCertManagerAutoClusterIssuerFoundDoesSucceed(t *testing.T) { + ingressTrait, environment := createNominalIngressTest() + ingressTrait.TLSCertManagerAuto = ptr.To(true) + environment.Ctx = context.Background() + + clusterIssuer := newClusterIssuer("letsencrypt-prod") + fakeClient, err := internal.NewFakeClient(clusterIssuer) + require.NoError(t, err) + fakeClient.(*internal.FakeClient).EnableCertManagerDiscovery() + environment.Client = fakeClient + + err = ingressTrait.Apply(environment) + + require.NoError(t, err) + environment.Resources.Visit(func(resource runtime.Object) { + if ingress, ok := resource.(*networkingv1.Ingress); ok { + assert.Equal(t, "letsencrypt-prod", ingress.Annotations[certmanager.AnnotationClusterIssuer]) + require.NotNil(t, ingress.Spec.TLS) + assert.Equal(t, []string{"hostname"}, ingress.Spec.TLS[0].Hosts) + assert.Equal(t, "service-name-tls", ingress.Spec.TLS[0].SecretName) + } + }) +} + +func TestApplyIngressTraitForcedIssuerExistsDoesSucceed(t *testing.T) { + ingressTrait, environment := createNominalIngressTest() + ingressTrait.TLSIssuerName = "my-issuer" + ingressTrait.TLSIssuerKind = "Issuer" + environment.Ctx = context.Background() + environment.Integration.Namespace = "namespace" + + issuer := newIssuer("my-issuer", "namespace") + fakeClient, err := internal.NewFakeClient(issuer) + require.NoError(t, err) + fakeClient.(*internal.FakeClient).EnableCertManagerDiscovery() + environment.Client = fakeClient + + err = ingressTrait.Apply(environment) + + require.NoError(t, err) + environment.Resources.Visit(func(resource runtime.Object) { + if ingress, ok := resource.(*networkingv1.Ingress); ok { + assert.Equal(t, "my-issuer", ingress.Annotations[certmanager.AnnotationIssuer]) + require.NotNil(t, ingress.Spec.TLS) + assert.Equal(t, "service-name-tls", ingress.Spec.TLS[0].SecretName) + } + }) +} + +func TestApplyIngressTraitForcedIssuerMissingDoesNotSucceed(t *testing.T) { + ingressTrait, environment := createNominalIngressTest() + ingressTrait.TLSIssuerName = "missing-issuer" + environment.Ctx = context.Background() + + fakeClient, err := internal.NewFakeClient() + require.NoError(t, err) + fakeClient.(*internal.FakeClient).EnableCertManagerDiscovery() + environment.Client = fakeClient + + err = ingressTrait.Apply(environment) + + require.Error(t, err) + assert.Contains(t, err.Error(), "missing-issuer") + assert.Contains(t, err.Error(), "not found") +} + +func TestApplyIngressTraitForcedIssuerCertManagerNotInstalledDoesNotSucceed(t *testing.T) { + ingressTrait, environment := createNominalIngressTest() + ingressTrait.TLSIssuerName = "my-issuer" + environment.Ctx = context.Background() + + fakeClient, err := internal.NewFakeClient() + require.NoError(t, err) + environment.Client = fakeClient + + err = ingressTrait.Apply(environment) + + require.Error(t, err) + assert.Contains(t, err.Error(), "cert-manager is not installed") +} + +func newClusterIssuer(name string) *unstructured.Unstructured { + u := &unstructured.Unstructured{} + u.SetGroupVersionKind(certmanager.ClusterIssuerGVK) + u.SetName(name) + + return u +} + +func newIssuer(name, namespace string) *unstructured.Unstructured { + u := &unstructured.Unstructured{} + u.SetGroupVersionKind(certmanager.IssuerGVK) + u.SetName(name) + u.SetNamespace(namespace) + + return u +} + func createNominalIngressTestWithIngressClassName(ingressClassName string) (*ingressTrait, *Environment) { trait, environment := createNominalIngressTest() trait.IngressClassName = ingressClassName diff --git a/pkg/util/certmanager/enabled.go b/pkg/util/certmanager/enabled.go new file mode 100644 index 000000000..627002280 --- /dev/null +++ b/pkg/util/certmanager/enabled.go @@ -0,0 +1,163 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one or more +contributor license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright ownership. +The ASF licenses this file to You under the Apache License, Version 2.0 +(the "License"); you may not use this file except in compliance with +the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package certmanager + +import ( + "context" + "sort" + + k8serrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/kubernetes" + ctrl "sigs.k8s.io/controller-runtime/pkg/client" + + kubernetesutil "github.com/apache/camel-k/v2/pkg/util/kubernetes" +) + +const ( + // CertManagerAPIGroup is the API group for cert-manager. + CertManagerAPIGroup = "cert-manager.io" + // CertManagerAPIVersion is the current API version for cert-manager. + CertManagerAPIVersion = "v1" + + // AnnotationClusterIssuer is the Ingress annotation to specify a ClusterIssuer. + AnnotationClusterIssuer = "cert-manager.io/cluster-issuer" + // AnnotationIssuer is the Ingress annotation to specify a namespaced Issuer. + AnnotationIssuer = "cert-manager.io/issuer" +) + +var ( + // ClusterIssuerGVK is the GroupVersionKind for cert-manager ClusterIssuer. + ClusterIssuerGVK = schema.GroupVersionKind{ + Group: CertManagerAPIGroup, + Version: CertManagerAPIVersion, + Kind: "ClusterIssuer", + } + + // IssuerGVK is the GroupVersionKind for cert-manager Issuer. + IssuerGVK = schema.GroupVersionKind{ + Group: CertManagerAPIGroup, + Version: CertManagerAPIVersion, + Kind: "Issuer", + } +) + +func isResourceNotFoundError(err error) bool { + if err == nil { + return false + } + + return k8serrors.IsNotFound(err) || meta.IsNoMatchError(err) || kubernetesutil.IsUnknownAPIError(err) +} + +// IsInstalled returns true if connected to a cluster with cert-manager installed. +func IsInstalled(c kubernetes.Interface) (bool, error) { + _, err := c.Discovery().ServerResourcesForGroupVersion(schema.GroupVersion{ + Group: CertManagerAPIGroup, + Version: CertManagerAPIVersion, + }.String()) + if isResourceNotFoundError(err) { + return false, nil + } else if err != nil { + return false, err + } + + return true, nil +} + +// ListClusterIssuers returns all ClusterIssuer names available in the cluster. +func ListClusterIssuers(ctx context.Context, c ctrl.Reader) ([]string, error) { + list := &unstructured.UnstructuredList{} + list.SetGroupVersionKind(schema.GroupVersionKind{ + Group: CertManagerAPIGroup, + Version: CertManagerAPIVersion, + Kind: "ClusterIssuerList", + }) + + if err := c.List(ctx, list); err != nil { + if isResourceNotFoundError(err) { + return nil, nil + } + + return nil, err + } + + names := make([]string, 0, len(list.Items)) + for _, item := range list.Items { + names = append(names, item.GetName()) + } + sort.Strings(names) + + return names, nil +} + +// GetClusterIssuer checks if a specific ClusterIssuer exists in the cluster. +func GetClusterIssuer(ctx context.Context, c ctrl.Reader, name string) (bool, error) { + _, err := kubernetesutil.GetUnstructured(ctx, c, ClusterIssuerGVK, name, "") + if err != nil { + if isResourceNotFoundError(err) { + return false, nil + } + + return false, err + } + + return true, nil +} + +// ListIssuers returns all Issuer names available in a specific namespace. +func ListIssuers(ctx context.Context, c ctrl.Reader, namespace string) ([]string, error) { + list := &unstructured.UnstructuredList{} + list.SetGroupVersionKind(schema.GroupVersionKind{ + Group: CertManagerAPIGroup, + Version: CertManagerAPIVersion, + Kind: "IssuerList", + }) + + if err := c.List(ctx, list, ctrl.InNamespace(namespace)); err != nil { + if isResourceNotFoundError(err) { + return nil, nil + } + + return nil, err + } + + names := make([]string, 0, len(list.Items)) + for _, item := range list.Items { + names = append(names, item.GetName()) + } + sort.Strings(names) + + return names, nil +} + +// GetIssuer checks if a specific namespaced Issuer exists. +func GetIssuer(ctx context.Context, c ctrl.Reader, namespace, name string) (bool, error) { + _, err := kubernetesutil.GetUnstructured(ctx, c, IssuerGVK, name, namespace) + if err != nil { + if isResourceNotFoundError(err) { + return false, nil + } + + return false, err + } + + return true, nil +} diff --git a/pkg/util/certmanager/enabled_test.go b/pkg/util/certmanager/enabled_test.go new file mode 100644 index 000000000..65a0c60ef --- /dev/null +++ b/pkg/util/certmanager/enabled_test.go @@ -0,0 +1,252 @@ +/* +Licensed to the Apache Software Foundation (ASF) under one or more +contributor license agreements. See the NOTICE file distributed with +this work for additional information regarding copyright ownership. +The ASF licenses this file to You under the Apache License, Version 2.0 +(the "License"); you may not use this file except in compliance with +the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package certmanager + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + ctrl "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/apache/camel-k/v2/pkg/internal" +) + +type errorMockReader struct { + ctrl.Reader + listErr error + getErr error +} + +func (m *errorMockReader) List(ctx context.Context, list ctrl.ObjectList, opts ...ctrl.ListOption) error { + if m.listErr != nil { + return m.listErr + } + + return m.Reader.List(ctx, list, opts...) +} + +func (m *errorMockReader) Get(ctx context.Context, key ctrl.ObjectKey, obj ctrl.Object, opts ...ctrl.GetOption) error { + if m.getErr != nil { + return m.getErr + } + + return m.Reader.Get(ctx, key, obj, opts...) +} + +func newUnstructuredClusterIssuer(name string) *unstructured.Unstructured { + u := &unstructured.Unstructured{} + u.SetGroupVersionKind(ClusterIssuerGVK) + u.SetName(name) + + return u +} + +func newUnstructuredIssuer(namespace, name string) *unstructured.Unstructured { + u := &unstructured.Unstructured{} + u.SetGroupVersionKind(IssuerGVK) + u.SetNamespace(namespace) + u.SetName(name) + + return u +} + +func TestListClusterIssuers(t *testing.T) { + ci1 := newUnstructuredClusterIssuer("letsencrypt-staging") + ci2 := newUnstructuredClusterIssuer("letsencrypt-prod") + + c, err := internal.NewFakeClient(ci1, ci2) + require.NoError(t, err) + + names, err := ListClusterIssuers(context.Background(), c) + require.NoError(t, err) + assert.Equal(t, []string{"letsencrypt-prod", "letsencrypt-staging"}, names) +} + +func TestGetClusterIssuer(t *testing.T) { + ci := newUnstructuredClusterIssuer("my-cluster-issuer") + + c, err := internal.NewFakeClient(ci) + require.NoError(t, err) + + exists, err := GetClusterIssuer(context.Background(), c, "my-cluster-issuer") + require.NoError(t, err) + assert.True(t, exists) + + notExists, err := GetClusterIssuer(context.Background(), c, "non-existent") + require.NoError(t, err) + assert.False(t, notExists) +} + +func TestListIssuers(t *testing.T) { + i1 := newUnstructuredIssuer("ns1", "issuer-b") + i2 := newUnstructuredIssuer("ns1", "issuer-a") + i3 := newUnstructuredIssuer("ns2", "issuer-other") + + c, err := internal.NewFakeClient(i1, i2, i3) + require.NoError(t, err) + + names, err := ListIssuers(context.Background(), c, "ns1") + require.NoError(t, err) + assert.Equal(t, []string{"issuer-a", "issuer-b"}, names) + + namesOther, err := ListIssuers(context.Background(), c, "ns2") + require.NoError(t, err) + assert.Equal(t, []string{"issuer-other"}, namesOther) + + namesEmpty, err := ListIssuers(context.Background(), c, "ns-empty") + require.NoError(t, err) + assert.Empty(t, namesEmpty) +} + +func TestGetIssuer(t *testing.T) { + i := newUnstructuredIssuer("test-ns", "my-issuer") + + c, err := internal.NewFakeClient(i) + require.NoError(t, err) + + exists, err := GetIssuer(context.Background(), c, "test-ns", "my-issuer") + require.NoError(t, err) + assert.True(t, exists) + + notExistsWrongNs, err := GetIssuer(context.Background(), c, "wrong-ns", "my-issuer") + require.NoError(t, err) + assert.False(t, notExistsWrongNs) + + notExistsWrongName, err := GetIssuer(context.Background(), c, "test-ns", "other-issuer") + require.NoError(t, err) + assert.False(t, notExistsWrongName) +} + +func TestCRDsAbsentEntirely(t *testing.T) { + baseClient, err := internal.NewFakeClient() + require.NoError(t, err) + + noKindMatchErr := &meta.NoKindMatchError{ + GroupKind: schema.GroupKind{Group: CertManagerAPIGroup, Kind: "ClusterIssuerList"}, + SearchedVersions: []string{"v1"}, + } + + reader := &errorMockReader{ + Reader: baseClient, + listErr: noKindMatchErr, + getErr: noKindMatchErr, + } + + // 1. ListClusterIssuers should return (nil, nil) + ciList, err := ListClusterIssuers(context.Background(), reader) + assert.NoError(t, err) + assert.Nil(t, ciList) + + // 2. ListIssuers should return (nil, nil) + iList, err := ListIssuers(context.Background(), reader, "my-ns") + assert.NoError(t, err) + assert.Nil(t, iList) + + // 3. GetClusterIssuer should return (false, nil) + ciExists, err := GetClusterIssuer(context.Background(), reader, "my-ci") + assert.NoError(t, err) + assert.False(t, ciExists) + + // 4. GetIssuer should return (false, nil) + iExists, err := GetIssuer(context.Background(), reader, "my-ns", "my-i") + assert.NoError(t, err) + assert.False(t, iExists) +} + +func TestCRDsAbsentViaUnknownAPIError(t *testing.T) { + baseClient, err := internal.NewFakeClient() + require.NoError(t, err) + + unknownAPIErr := fmt.Errorf("no matches for kind \"ClusterIssuerList\" in version \"cert-manager.io/v1\"") + + reader := &errorMockReader{ + Reader: baseClient, + listErr: unknownAPIErr, + getErr: unknownAPIErr, + } + + ciList, err := ListClusterIssuers(context.Background(), reader) + assert.NoError(t, err) + assert.Nil(t, ciList) + + ciExists, err := GetClusterIssuer(context.Background(), reader, "my-ci") + assert.NoError(t, err) + assert.False(t, ciExists) +} + +func TestCRDsAbsentViaNotFoundError(t *testing.T) { + baseClient, err := internal.NewFakeClient() + require.NoError(t, err) + + notFoundErr := k8serrors.NewNotFound( + schema.GroupResource{Group: CertManagerAPIGroup, Resource: "clusterissuers"}, + "my-ci", + ) + + reader := &errorMockReader{ + Reader: baseClient, + listErr: notFoundErr, + getErr: notFoundErr, + } + + ciList, err := ListClusterIssuers(context.Background(), reader) + assert.NoError(t, err) + assert.Nil(t, ciList) + + ciExists, err := GetClusterIssuer(context.Background(), reader, "my-ci") + assert.NoError(t, err) + assert.False(t, ciExists) +} + +func TestGenericErrorPropagated(t *testing.T) { + baseClient, err := internal.NewFakeClient() + require.NoError(t, err) + + authErr := errors.New("connection refused: dial tcp 10.0.0.1:443") + + reader := &errorMockReader{ + Reader: baseClient, + listErr: authErr, + getErr: authErr, + } + + // Any non-absence error MUST be returned and NOT swallowed + ciList, err := ListClusterIssuers(context.Background(), reader) + assert.ErrorIs(t, err, authErr) + assert.Nil(t, ciList) + + iList, err := ListIssuers(context.Background(), reader, "my-ns") + assert.ErrorIs(t, err, authErr) + assert.Nil(t, iList) + + ciExists, err := GetClusterIssuer(context.Background(), reader, "my-ci") + assert.ErrorIs(t, err, authErr) + assert.False(t, ciExists) + + iExists, err := GetIssuer(context.Background(), reader, "my-ns", "my-i") + assert.ErrorIs(t, err, authErr) + assert.False(t, iExists) +} diff --git a/script/Makefile b/script/Makefile index 478678972..16cc56de7 100644 --- a/script/Makefile +++ b/script/Makefile @@ -339,6 +339,12 @@ test-telemetry: test-gateway: go test -timeout 10m -v ./e2e/gateway -tags=integration $(GOTESTFMT) +# +# Cert-manager tests that require the presence of cert-manager +# +test-cert-manager: + go test -timeout 10m -v ./e2e/cert-manager -tags=integration $(GOTESTFMT) + # # Quarkus native test (requires certain CPU and memory conditions) #
