From 702e517ef8d58b336ec2f5c5354f25387af9c23f Mon Sep 17 00:00:00 2001 From: Ashley Davis Date: Tue, 1 Oct 2024 18:14:52 +0100 Subject: [PATCH] add most remaining annotations and improve integration/e2e tests Signed-off-by: Ashley Davis --- internal/controller/sync.go | 87 ++- internal/controller/sync_test.go | 1219 ++++++++++++++++++++++++++++++ make/test-smoke.mk | 4 +- test/test-smoke.sh | 65 +- 4 files changed, 1344 insertions(+), 31 deletions(-) diff --git a/internal/controller/sync.go b/internal/controller/sync.go index af9e224..9fd8fd4 100644 --- a/internal/controller/sync.go +++ b/internal/controller/sync.go @@ -239,6 +239,17 @@ func (r *RouteController) getCertificateForRoute(ctx context.Context, route *rou // buildNextCert generates the manifest of a Certificate that is needed for a given Route (based on the annotations) func (r *RouteController) buildNextCert(ctx context.Context, route *routev1.Route) (*cmapi.Certificate, error) { + var issuerName string + if metav1.HasAnnotation(route.ObjectMeta, cmapi.IngressIssuerNameAnnotationKey) { + issuerName = route.Annotations[cmapi.IngressIssuerNameAnnotationKey] + } else { + issuerName = route.Annotations[cmapi.IssuerNameAnnotationKey] + } + + if issuerName == "" { + return nil, fmt.Errorf("missing issuer-name annotation on %s/%s", route.Namespace, route.Name) + } + // Extract various pieces of information from the Route annotations duration, err := certDurationFromRoute(route) if err != nil { @@ -246,7 +257,18 @@ func (r *RouteController) buildNextCert(ctx context.Context, route *routev1.Rout "object", route.Namespace+"/"+route.Name, cmapi.DurationAnnotationKey, route.Annotations[cmapi.DurationAnnotationKey]) r.eventRecorder.Event(route, corev1.EventTypeWarning, ReasonInvalidKey, "annotation "+cmapi.DurationAnnotationKey+": "+route.Annotations[cmapi.DurationAnnotationKey]+" is not a valid duration") - return nil, fmt.Errorf("Invalid duration annotation on Route %s/%s", route.Namespace, route.Name) + return nil, fmt.Errorf("invalid duration annotation on Route %s/%s", route.Namespace, route.Name) + } + + var renewBefore time.Duration + if metav1.HasAnnotation(route.ObjectMeta, cmapi.RenewBeforeAnnotationKey) { + renewBeforeAnnotation := route.Annotations[cmapi.RenewBeforeAnnotationKey] + + var err error + renewBefore, err = time.ParseDuration(renewBeforeAnnotation) + if err != nil { + return nil, fmt.Errorf("invalid renew-before annotation %q on Route %s/%s", renewBeforeAnnotation, route.Namespace, route.Name) + } } var privateKeyAlgorithm cmapi.PrivateKeyAlgorithm @@ -263,7 +285,7 @@ func (r *RouteController) buildNextCert(ctx context.Context, route *routev1.Rout case "ed25519": privateKeyAlgorithm = cmapi.Ed25519KeyAlgorithm default: - r.log.Info("unknown private key algorithm, defaulting to RSA", "algorithm", privateKeyAlgorithmStrRaw) + r.log.V(1).Info("unknown private key algorithm, defaulting to RSA", "algorithm", privateKeyAlgorithmStrRaw) privateKeyAlgorithm = cmapi.RSAKeyAlgorithm } @@ -273,10 +295,20 @@ func (r *RouteController) buildNextCert(ctx context.Context, route *routev1.Rout privateKeySize, err = strconv.Atoi(privateKeySizeStr) if err != nil { r.eventRecorder.Event(route, corev1.EventTypeWarning, ReasonInvalidPrivateKeySize, "invalid private key size:"+privateKeySizeStr) - return nil, fmt.Errorf("invalid private key size, %s: %v", privateKeySizeStr, err) + return nil, fmt.Errorf("invalid private key size annotation %q on %s/%s", privateKeySizeStr, route.Namespace, route.Name) } } + var privateKeyRotationPolicy cmapi.PrivateKeyRotationPolicy + + if metav1.HasAnnotation(route.ObjectMeta, cmapi.PrivateKeyRotationPolicyAnnotationKey) { + // Don't validate the policy here because that would mean we'd need to update this codebase + // if cert-manager adds new values. Just rely on cert-manager validation when the cert is + // created + // This is brittle; ideally, cert-manager should expose a function for this + privateKeyRotationPolicy = cmapi.PrivateKeyRotationPolicy(route.Annotations[cmapi.PrivateKeyRotationPolicyAnnotationKey]) + } + var dnsNames []string // Get the canonical hostname(s) of the Route (from .spec.host or .spec.subdomain) dnsNames = getRouteHostnames(route) @@ -327,6 +359,7 @@ func (r *RouteController) buildNextCert(ctx context.Context, route *routev1.Rout if metav1.HasAnnotation(route.ObjectMeta, cmapi.EmailsAnnotationKey) { emailAddresses = strings.Split(route.Annotations[cmapi.EmailsAnnotationKey], ",") } + var organizations []string if metav1.HasAnnotation(route.ObjectMeta, cmapi.SubjectOrganizationsAnnotationKey) { subjectOrganizations, err := cmutil.SplitWithEscapeCSV(route.Annotations[cmapi.SubjectOrganizationsAnnotationKey]) @@ -340,6 +373,7 @@ func (r *RouteController) buildNextCert(ctx context.Context, route *routev1.Rout return nil, err } } + var organizationalUnits []string if metav1.HasAnnotation(route.ObjectMeta, cmapi.SubjectOrganizationalUnitsAnnotationKey) { subjectOrganizationalUnits, err := cmutil.SplitWithEscapeCSV(route.Annotations[cmapi.SubjectOrganizationalUnitsAnnotationKey]) @@ -354,6 +388,7 @@ func (r *RouteController) buildNextCert(ctx context.Context, route *routev1.Rout } } + var countries []string if metav1.HasAnnotation(route.ObjectMeta, cmapi.SubjectCountriesAnnotationKey) { subjectCountries, err := cmutil.SplitWithEscapeCSV(route.Annotations[cmapi.SubjectCountriesAnnotationKey]) @@ -367,6 +402,7 @@ func (r *RouteController) buildNextCert(ctx context.Context, route *routev1.Rout return nil, err } } + var provinces []string if metav1.HasAnnotation(route.ObjectMeta, cmapi.SubjectProvincesAnnotationKey) { subjectProvinces, err := cmutil.SplitWithEscapeCSV(route.Annotations[cmapi.SubjectProvincesAnnotationKey]) @@ -380,6 +416,7 @@ func (r *RouteController) buildNextCert(ctx context.Context, route *routev1.Rout return nil, err } } + var localities []string if metav1.HasAnnotation(route.ObjectMeta, cmapi.SubjectLocalitiesAnnotationKey) { subjectLocalities, err := cmutil.SplitWithEscapeCSV(route.Annotations[cmapi.SubjectLocalitiesAnnotationKey]) @@ -393,6 +430,7 @@ func (r *RouteController) buildNextCert(ctx context.Context, route *routev1.Rout return nil, err } } + var postalCodes []string if metav1.HasAnnotation(route.ObjectMeta, cmapi.SubjectPostalCodesAnnotationKey) { subjectPostalCodes, err := cmutil.SplitWithEscapeCSV(route.Annotations[cmapi.SubjectPostalCodesAnnotationKey]) @@ -406,6 +444,7 @@ func (r *RouteController) buildNextCert(ctx context.Context, route *routev1.Rout return nil, err } } + var streetAddresses []string if metav1.HasAnnotation(route.ObjectMeta, cmapi.SubjectStreetAddressesAnnotationKey) { subjectStreetAddresses, err := cmutil.SplitWithEscapeCSV(route.Annotations[cmapi.SubjectStreetAddressesAnnotationKey]) @@ -419,15 +458,23 @@ func (r *RouteController) buildNextCert(ctx context.Context, route *routev1.Rout return nil, err } } + var serialNumber string if metav1.HasAnnotation(route.ObjectMeta, cmapi.SubjectSerialNumberAnnotationKey) { serialNumber = route.Annotations[cmapi.SubjectSerialNumberAnnotationKey] } - var issuerName string - if metav1.HasAnnotation(route.ObjectMeta, cmapi.IngressIssuerNameAnnotationKey) { - issuerName = route.Annotations[cmapi.IngressIssuerNameAnnotationKey] - } else { - issuerName = route.Annotations[cmapi.IssuerNameAnnotationKey] + + var revisionHistoryLimit *int32 + if metav1.HasAnnotation(route.ObjectMeta, cmapi.RevisionHistoryLimitAnnotationKey) { + historyLimitRaw := route.Annotations[cmapi.RevisionHistoryLimitAnnotationKey] + + parsedLimit, err := strconv.ParseInt(historyLimitRaw, 10, 32) + if err != nil { + return nil, fmt.Errorf("invalid revision-history-limit annotation %q on %s/%s", historyLimitRaw, route.Namespace, route.Name) + } + + typedLimit := int32(parsedLimit) + revisionHistoryLimit = &typedLimit } secretName := route.Name + "-tls-cert" @@ -446,12 +493,11 @@ func (r *RouteController) buildNextCert(ctx context.Context, route *routev1.Rout }, }, Spec: cmapi.CertificateSpec{ - SecretName: secretName, - Duration: &metav1.Duration{Duration: duration}, - EmailAddresses: emailAddresses, - // RenewBefore? - // RevisionHistoryLimit? - CommonName: route.Annotations[cmapi.CommonNameAnnotationKey], + SecretName: secretName, + Duration: &metav1.Duration{Duration: duration}, + RenewBefore: &metav1.Duration{Duration: renewBefore}, + RevisionHistoryLimit: revisionHistoryLimit, + CommonName: route.Annotations[cmapi.CommonNameAnnotationKey], Subject: &cmapi.X509Subject{ Countries: countries, Localities: localities, @@ -463,13 +509,14 @@ func (r *RouteController) buildNextCert(ctx context.Context, route *routev1.Rout StreetAddresses: streetAddresses, }, PrivateKey: &cmapi.CertificatePrivateKey{ - Algorithm: privateKeyAlgorithm, - Size: privateKeySize, - // RotationPolicy? + Algorithm: privateKeyAlgorithm, + Size: privateKeySize, + RotationPolicy: privateKeyRotationPolicy, }, - DNSNames: dnsNames, - URIs: uriSANs, - IPAddresses: ipSANs, + EmailAddresses: emailAddresses, + DNSNames: dnsNames, + URIs: uriSANs, + IPAddresses: ipSANs, IssuerRef: cmmeta.ObjectReference{ Name: issuerName, Kind: route.Annotations[cmapi.IssuerKindAnnotationKey], diff --git a/internal/controller/sync_test.go b/internal/controller/sync_test.go index 835b886..723de44 100644 --- a/internal/controller/sync_test.go +++ b/internal/controller/sync_test.go @@ -17,18 +17,22 @@ limitations under the License. package controller import ( + "context" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/x509" "crypto/x509/pkix" "encoding/pem" + "fmt" "math/big" "sort" + "strconv" "testing" "time" cmapi "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" + cmmeta "github.com/cert-manager/cert-manager/pkg/apis/meta/v1" utilpki "github.com/cert-manager/cert-manager/pkg/util/pki" routev1 "github.com/openshift/api/route/v1" "github.com/stretchr/testify/assert" @@ -393,3 +397,1218 @@ func generateRouteStatus(route *routev1.Route, admitted bool) *routev1.Route { } return route } + +func TestRoute_buildNextCertificate(t *testing.T) { + domain := "some-host.some-domain.tld" + domainSlice := []string{domain} + + tests := []struct { + name string + route *routev1.Route + want *cmapi.Certificate + wantErr error + wantEvents []string + }{ + { + name: "Basic test with duration and hostname", + route: generateRouteStatus(&routev1.Route{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-route", + Namespace: "some-namespace", + Annotations: map[string]string{ + cmapi.IssuerNameAnnotationKey: "self-signed-issuer", + cmapi.DurationAnnotationKey: "42m", + }, + }, + Spec: routev1.RouteSpec{ + Host: domain, + }, + Status: routev1.RouteStatus{ + Ingress: []routev1.RouteIngress{ + { + Host: domain, + Conditions: []routev1.RouteIngressCondition{ + { + Type: "Admitted", + Status: "True", + }, + }, + }, + }, + }, + }, + true), + want: &cmapi.Certificate{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "some-route-", + Namespace: "some-namespace", + }, + Spec: cmapi.CertificateSpec{ + Duration: &metav1.Duration{Duration: 42 * time.Minute}, + DNSNames: domainSlice, + IsCA: false, + Usages: []cmapi.KeyUsage{cmapi.UsageServerAuth, cmapi.UsageDigitalSignature, cmapi.UsageKeyEncipherment}, + }, + }, + wantErr: nil, + }, + + { + name: "Basic test with issuer name + kind", + route: generateRouteStatus(&routev1.Route{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-route", + Namespace: "some-namespace", + Annotations: map[string]string{ + cmapi.IssuerNameAnnotationKey: "self-signed-issuer", + cmapi.IssuerKindAnnotationKey: "SomeIssuer", + }, + }, + Spec: routev1.RouteSpec{ + Host: domain, + }, + Status: routev1.RouteStatus{ + Ingress: []routev1.RouteIngress{ + { + Host: domain, + Conditions: []routev1.RouteIngressCondition{ + { + Type: "Admitted", + Status: "True", + }, + }, + }, + }, + }, + }, + true), + want: &cmapi.Certificate{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "some-route-", + Namespace: "some-namespace", + }, + Spec: cmapi.CertificateSpec{ + Duration: &metav1.Duration{Duration: DefaultCertificateDuration}, + DNSNames: domainSlice, + IsCA: false, + Usages: []cmapi.KeyUsage{cmapi.UsageServerAuth, cmapi.UsageDigitalSignature, cmapi.UsageKeyEncipherment}, + + IssuerRef: cmmeta.ObjectReference{ + Name: "self-signed-issuer", + Kind: "SomeIssuer", + }, + }, + }, + wantErr: nil, + }, + + { + name: "Basic test with issuer name, kind + group", + route: generateRouteStatus(&routev1.Route{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-route", + Namespace: "some-namespace", + Annotations: map[string]string{ + cmapi.IssuerNameAnnotationKey: "self-signed-issuer", + cmapi.IssuerKindAnnotationKey: "SomeIssuer", + cmapi.IssuerGroupAnnotationKey: "group.example.com", + }, + }, + Spec: routev1.RouteSpec{ + Host: domain, + }, + Status: routev1.RouteStatus{ + Ingress: []routev1.RouteIngress{ + { + Host: domain, + Conditions: []routev1.RouteIngressCondition{ + { + Type: "Admitted", + Status: "True", + }, + }, + }, + }, + }, + }, + true), + want: &cmapi.Certificate{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "some-route-", + Namespace: "some-namespace", + }, + Spec: cmapi.CertificateSpec{ + Duration: &metav1.Duration{Duration: DefaultCertificateDuration}, + DNSNames: domainSlice, + IsCA: false, + Usages: []cmapi.KeyUsage{cmapi.UsageServerAuth, cmapi.UsageDigitalSignature, cmapi.UsageKeyEncipherment}, + + IssuerRef: cmmeta.ObjectReference{ + Name: "self-signed-issuer", + Kind: "SomeIssuer", + Group: "group.example.com", + }, + }, + }, + wantErr: nil, + }, + + { + name: "Basic test with alternate ingress issuer name annotation", + route: generateRouteStatus(&routev1.Route{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-route", + Namespace: "some-namespace", + Annotations: map[string]string{ + cmapi.IngressIssuerNameAnnotationKey: "self-signed-issuer", + cmapi.IssuerKindAnnotationKey: "Issuer", + cmapi.IssuerGroupAnnotationKey: "external-issuer.io", + }, + }, + Spec: routev1.RouteSpec{ + Host: domain, + }, + Status: routev1.RouteStatus{ + Ingress: []routev1.RouteIngress{ + { + Host: domain, + Conditions: []routev1.RouteIngressCondition{ + { + Type: "Admitted", + Status: "True", + }, + }, + }, + }, + }, + }, + true), + want: &cmapi.Certificate{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "some-route-", + Namespace: "some-namespace", + }, + Spec: cmapi.CertificateSpec{ + Duration: &metav1.Duration{Duration: DefaultCertificateDuration}, + DNSNames: domainSlice, + IsCA: false, + Usages: []cmapi.KeyUsage{cmapi.UsageServerAuth, cmapi.UsageDigitalSignature, cmapi.UsageKeyEncipherment}, + + IssuerRef: cmmeta.ObjectReference{ + Name: "self-signed-issuer", + Kind: "Issuer", + Group: "external-issuer.io", + }, + }, + }, + wantErr: nil, + }, + + { + name: "With subdomain and multiple ICs", + route: &routev1.Route{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-route-with-subdomain", + Namespace: "some-namespace", + Annotations: map[string]string{ + cmapi.IssuerNameAnnotationKey: "self-signed-issuer", + }, + }, + Spec: routev1.RouteSpec{ + Subdomain: "some-sub-domain", + }, + Status: routev1.RouteStatus{ + Ingress: []routev1.RouteIngress{ + { + Host: "some-sub-domain.some-domain.tld", // suffix depends on IC config + Conditions: []routev1.RouteIngressCondition{ + { + Type: "Admitted", + Status: "True", + }, + }, + }, + { + Host: "some-sub-domain.some-other-ic.example.com", + Conditions: []routev1.RouteIngressCondition{ + { + Type: "Admitted", + Status: "True", + }, + }, + }, + { + Host: "some-sub-domain.not-admitted.example.com", + Conditions: []routev1.RouteIngressCondition{ + { + Type: "Admitted", + Status: "False", + }, + }, + }, + }, + }, + }, + want: &cmapi.Certificate{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "some-route-with-subdomain-", + Namespace: "some-namespace", + }, + Spec: cmapi.CertificateSpec{ + Duration: &metav1.Duration{Duration: DefaultCertificateDuration}, + Usages: []cmapi.KeyUsage{cmapi.UsageServerAuth, cmapi.UsageDigitalSignature, cmapi.UsageKeyEncipherment}, + IsCA: false, + + DNSNames: []string{ + "some-sub-domain.some-domain.tld", + "some-sub-domain.some-other-ic.example.com", + }, + }, + }, + wantErr: nil, + }, + + { + name: "With ECDSA private key algorithm annotation", + route: generateRouteStatus(&routev1.Route{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-route", + Namespace: "some-namespace", + Annotations: map[string]string{ + cmapi.IssuerNameAnnotationKey: "self-signed-issuer", + cmapi.PrivateKeyAlgorithmAnnotationKey: string(cmapi.ECDSAKeyAlgorithm), + }, + }, + Spec: routev1.RouteSpec{ + Host: domain, + }, + Status: routev1.RouteStatus{ + Ingress: []routev1.RouteIngress{ + { + Host: domain, + Conditions: []routev1.RouteIngressCondition{ + { + Type: "Admitted", + Status: "True", + }, + }, + }, + }, + }, + }, + true), + want: &cmapi.Certificate{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "some-route-", + Namespace: "some-namespace", + }, + Spec: cmapi.CertificateSpec{ + Usages: []cmapi.KeyUsage{cmapi.UsageServerAuth, cmapi.UsageDigitalSignature, cmapi.UsageKeyEncipherment}, + Duration: &metav1.Duration{Duration: DefaultCertificateDuration}, + IsCA: false, + DNSNames: domainSlice, + + PrivateKey: &cmapi.CertificatePrivateKey{ + Algorithm: cmapi.ECDSAKeyAlgorithm, + }, + }, + }, + wantErr: nil, + }, + + { + name: "With ECDSA P-384 private key algorithm and size annotation", + route: generateRouteStatus(&routev1.Route{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-route", + Namespace: "some-namespace", + Annotations: map[string]string{ + cmapi.IssuerNameAnnotationKey: "self-signed-issuer", + cmapi.PrivateKeyAlgorithmAnnotationKey: string(cmapi.ECDSAKeyAlgorithm), + cmapi.PrivateKeySizeAnnotationKey: strconv.Itoa(384), + }, + }, + Spec: routev1.RouteSpec{ + Host: domain, + }, + Status: routev1.RouteStatus{ + Ingress: []routev1.RouteIngress{ + { + Host: domain, + Conditions: []routev1.RouteIngressCondition{ + { + Type: "Admitted", + Status: "True", + }, + }, + }, + }, + }, + }, + true), + want: &cmapi.Certificate{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "some-route-", + Namespace: "some-namespace", + }, + Spec: cmapi.CertificateSpec{ + Usages: []cmapi.KeyUsage{cmapi.UsageServerAuth, cmapi.UsageDigitalSignature, cmapi.UsageKeyEncipherment}, + Duration: &metav1.Duration{Duration: DefaultCertificateDuration}, + IsCA: false, + DNSNames: domainSlice, + + PrivateKey: &cmapi.CertificatePrivateKey{ + Algorithm: cmapi.ECDSAKeyAlgorithm, + Size: 384, + }, + }, + }, + wantErr: nil, + }, + + { + name: "With ECDSA P-521 private key algorithm and size annotation", + route: generateRouteStatus(&routev1.Route{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-route", + Namespace: "some-namespace", + Annotations: map[string]string{ + cmapi.IssuerNameAnnotationKey: "self-signed-issuer", + cmapi.PrivateKeyAlgorithmAnnotationKey: string(cmapi.ECDSAKeyAlgorithm), + cmapi.PrivateKeySizeAnnotationKey: strconv.Itoa(521), + }, + }, + Spec: routev1.RouteSpec{ + Host: domain, + }, + Status: routev1.RouteStatus{ + Ingress: []routev1.RouteIngress{ + { + Host: domain, + Conditions: []routev1.RouteIngressCondition{ + { + Type: "Admitted", + Status: "True", + }, + }, + }, + }, + }, + }, + true), + want: &cmapi.Certificate{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "some-route-", + Namespace: "some-namespace", + }, + Spec: cmapi.CertificateSpec{ + Usages: []cmapi.KeyUsage{cmapi.UsageServerAuth, cmapi.UsageDigitalSignature, cmapi.UsageKeyEncipherment}, + Duration: &metav1.Duration{Duration: DefaultCertificateDuration}, + IsCA: false, + DNSNames: domainSlice, + + PrivateKey: &cmapi.CertificatePrivateKey{ + Algorithm: cmapi.ECDSAKeyAlgorithm, + Size: 521, + }, + }, + }, + wantErr: nil, + }, + + { + name: "With RSA private key algorithm annotation", + route: generateRouteStatus(&routev1.Route{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-route", + Namespace: "some-namespace", + Annotations: map[string]string{ + cmapi.IssuerNameAnnotationKey: "self-signed-issuer", + cmapi.PrivateKeyAlgorithmAnnotationKey: string(cmapi.RSAKeyAlgorithm), + }, + }, + Spec: routev1.RouteSpec{ + Host: domain, + }, + Status: routev1.RouteStatus{ + Ingress: []routev1.RouteIngress{ + { + Host: domain, + Conditions: []routev1.RouteIngressCondition{ + { + Type: "Admitted", + Status: "True", + }, + }, + }, + }, + }, + }, + true), + want: &cmapi.Certificate{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "some-route-", + Namespace: "some-namespace", + }, + Spec: cmapi.CertificateSpec{ + Usages: []cmapi.KeyUsage{cmapi.UsageServerAuth, cmapi.UsageDigitalSignature, cmapi.UsageKeyEncipherment}, + Duration: &metav1.Duration{Duration: DefaultCertificateDuration}, + IsCA: false, + DNSNames: domainSlice, + PrivateKey: &cmapi.CertificatePrivateKey{ + Algorithm: cmapi.RSAKeyAlgorithm, + }, + }, + }, + wantErr: nil, + }, + + { + name: "With RSA 3072 private key algorithm and size annotation", + route: generateRouteStatus(&routev1.Route{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-route", + Namespace: "some-namespace", + Annotations: map[string]string{ + cmapi.IssuerNameAnnotationKey: "self-signed-issuer", + cmapi.PrivateKeyAlgorithmAnnotationKey: string(cmapi.RSAKeyAlgorithm), + cmapi.PrivateKeySizeAnnotationKey: strconv.Itoa(3072), + }, + }, + Spec: routev1.RouteSpec{ + Host: domain, + }, + Status: routev1.RouteStatus{ + Ingress: []routev1.RouteIngress{ + { + Host: domain, + Conditions: []routev1.RouteIngressCondition{ + { + Type: "Admitted", + Status: "True", + }, + }, + }, + }, + }, + }, + true), + want: &cmapi.Certificate{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "some-route-", + Namespace: "some-namespace", + }, + Spec: cmapi.CertificateSpec{ + Usages: []cmapi.KeyUsage{cmapi.UsageServerAuth, cmapi.UsageDigitalSignature, cmapi.UsageKeyEncipherment}, + Duration: &metav1.Duration{Duration: DefaultCertificateDuration}, + IsCA: false, + DNSNames: domainSlice, + PrivateKey: &cmapi.CertificatePrivateKey{ + Algorithm: cmapi.RSAKeyAlgorithm, + Size: 3072, + }, + }, + }, + wantErr: nil, + }, + + { + name: "With Ed25519 private key algorithm and size annotation", + route: generateRouteStatus(&routev1.Route{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-route", + Namespace: "some-namespace", + Annotations: map[string]string{ + cmapi.IssuerNameAnnotationKey: "self-signed-issuer", + cmapi.PrivateKeyAlgorithmAnnotationKey: string(cmapi.Ed25519KeyAlgorithm), + }, + }, + Spec: routev1.RouteSpec{ + Host: domain, + }, + Status: routev1.RouteStatus{ + Ingress: []routev1.RouteIngress{ + { + Host: domain, + Conditions: []routev1.RouteIngressCondition{ + { + Type: "Admitted", + Status: "True", + }, + }, + }, + }, + }, + }, + true), + want: &cmapi.Certificate{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "some-route-", + Namespace: "some-namespace", + }, + Spec: cmapi.CertificateSpec{ + Usages: []cmapi.KeyUsage{cmapi.UsageServerAuth, cmapi.UsageDigitalSignature, cmapi.UsageKeyEncipherment}, + Duration: &metav1.Duration{Duration: DefaultCertificateDuration}, + IsCA: false, + DNSNames: domainSlice, + PrivateKey: &cmapi.CertificatePrivateKey{ + Algorithm: cmapi.Ed25519KeyAlgorithm, + }, + }, + }, + wantErr: nil, + }, + + { + name: "With subject annotations", + route: &routev1.Route{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-route-with-subject-annotations", + Namespace: "some-namespace", + Annotations: map[string]string{ + cmapi.IssuerNameAnnotationKey: "self-signed-issuer", + + cmapi.SubjectOrganizationsAnnotationKey: "Company 1,Company 2", + cmapi.SubjectOrganizationalUnitsAnnotationKey: "Tech Division,Other Division", + cmapi.SubjectCountriesAnnotationKey: "Country 1,Country 2", + cmapi.SubjectProvincesAnnotationKey: "Province 1,Province 2", + cmapi.SubjectStreetAddressesAnnotationKey: "123 Example St,456 Example Ave", + cmapi.SubjectLocalitiesAnnotationKey: "City 1,City 2", + cmapi.SubjectPostalCodesAnnotationKey: "123ABC,456DEF", + cmapi.SubjectSerialNumberAnnotationKey: "10978342379280287615", + }, + }, + Spec: routev1.RouteSpec{ + Host: domain, + }, + Status: routev1.RouteStatus{ + Ingress: []routev1.RouteIngress{ + { + Host: domain, + Conditions: []routev1.RouteIngressCondition{ + { + Type: "Admitted", + Status: "True", + }, + }, + }, + }, + }, + }, + want: &cmapi.Certificate{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "some-route-with-subject-annotations-", + Namespace: "some-namespace", + }, + Spec: cmapi.CertificateSpec{ + Duration: &metav1.Duration{Duration: DefaultCertificateDuration}, + Usages: []cmapi.KeyUsage{cmapi.UsageServerAuth, cmapi.UsageDigitalSignature, cmapi.UsageKeyEncipherment}, + IsCA: false, + DNSNames: domainSlice, + + Subject: &cmapi.X509Subject{ + Organizations: []string{"Company 1", "Company 2"}, + OrganizationalUnits: []string{"Tech Division", "Other Division"}, + Countries: []string{"Country 1", "Country 2"}, + Provinces: []string{"Province 1", "Province 2"}, + StreetAddresses: []string{"123 Example St", "456 Example Ave"}, + Localities: []string{"City 1", "City 2"}, + PostalCodes: []string{"123ABC", "456DEF"}, + SerialNumber: "10978342379280287615", + }, + }, + }, + wantErr: nil, + }, + + { + name: "With custom URI SAN", + route: generateRouteStatus(&routev1.Route{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-route", + Namespace: "some-namespace", + Annotations: map[string]string{ + cmapi.IssuerNameAnnotationKey: "self-signed-issuer", + cmapi.URISANAnnotationKey: "spiffe://example.com/myuri", + }, + }, + Spec: routev1.RouteSpec{ + Host: domain, + }, + Status: routev1.RouteStatus{ + Ingress: []routev1.RouteIngress{ + { + Host: domain, + Conditions: []routev1.RouteIngressCondition{ + { + Type: "Admitted", + Status: "True", + }, + }, + }, + }, + }, + }, + true), + want: &cmapi.Certificate{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "some-route-", + Namespace: "some-namespace", + }, + Spec: cmapi.CertificateSpec{ + Duration: &metav1.Duration{Duration: DefaultCertificateDuration}, + DNSNames: domainSlice, + IsCA: false, + Usages: []cmapi.KeyUsage{cmapi.UsageServerAuth, cmapi.UsageDigitalSignature, cmapi.UsageKeyEncipherment}, + + URIs: []string{"spiffe://example.com/myuri"}, + }, + }, + wantErr: nil, + }, + + { + name: "With extra DNS names", + route: generateRouteStatus(&routev1.Route{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-route", + Namespace: "some-namespace", + Annotations: map[string]string{ + cmapi.IssuerNameAnnotationKey: "self-signed-issuer", + cmapi.AltNamesAnnotationKey: "example.com,another.example.com", + }, + }, + Spec: routev1.RouteSpec{ + Host: domain, + }, + Status: routev1.RouteStatus{ + Ingress: []routev1.RouteIngress{ + { + Host: domain, + Conditions: []routev1.RouteIngressCondition{ + { + Type: "Admitted", + Status: "True", + }, + }, + }, + }, + }, + }, + true), + want: &cmapi.Certificate{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "some-route-", + Namespace: "some-namespace", + }, + Spec: cmapi.CertificateSpec{ + Duration: &metav1.Duration{Duration: DefaultCertificateDuration}, + IsCA: false, + Usages: []cmapi.KeyUsage{cmapi.UsageServerAuth, cmapi.UsageDigitalSignature, cmapi.UsageKeyEncipherment}, + + DNSNames: []string{domain, "example.com", "another.example.com"}, + }, + }, + wantErr: nil, + }, + + { + name: "With custom IPv4 address", + route: generateRouteStatus(&routev1.Route{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-route", + Namespace: "some-namespace", + Annotations: map[string]string{ + cmapi.IssuerNameAnnotationKey: "self-signed-issuer", + cmapi.IPSANAnnotationKey: "169.50.50.50", + }, + }, + Spec: routev1.RouteSpec{ + Host: domain, + }, + Status: routev1.RouteStatus{ + Ingress: []routev1.RouteIngress{ + { + Host: domain, + Conditions: []routev1.RouteIngressCondition{ + { + Type: "Admitted", + Status: "True", + }, + }, + }, + }, + }, + }, + true), + want: &cmapi.Certificate{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "some-route-", + Namespace: "some-namespace", + }, + Spec: cmapi.CertificateSpec{ + Duration: &metav1.Duration{Duration: DefaultCertificateDuration}, + DNSNames: domainSlice, + IsCA: false, + Usages: []cmapi.KeyUsage{cmapi.UsageServerAuth, cmapi.UsageDigitalSignature, cmapi.UsageKeyEncipherment}, + + IPAddresses: []string{"169.50.50.50"}, + }, + }, + wantErr: nil, + }, + + { + name: "With custom IPv6 address", + route: generateRouteStatus(&routev1.Route{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-route", + Namespace: "some-namespace", + Annotations: map[string]string{ + cmapi.IssuerNameAnnotationKey: "self-signed-issuer", + cmapi.IPSANAnnotationKey: "2a02:ec80:300:ed1a::1", + }, + }, + Spec: routev1.RouteSpec{ + Host: domain, + }, + Status: routev1.RouteStatus{ + Ingress: []routev1.RouteIngress{ + { + Host: domain, + Conditions: []routev1.RouteIngressCondition{ + { + Type: "Admitted", + Status: "True", + }, + }, + }, + }, + }, + }, + true), + want: &cmapi.Certificate{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "some-route-", + Namespace: "some-namespace", + }, + Spec: cmapi.CertificateSpec{ + Duration: &metav1.Duration{Duration: DefaultCertificateDuration}, + DNSNames: domainSlice, + IsCA: false, + Usages: []cmapi.KeyUsage{cmapi.UsageServerAuth, cmapi.UsageDigitalSignature, cmapi.UsageKeyEncipherment}, + + IPAddresses: []string{"2a02:ec80:300:ed1a::1"}, + }, + }, + wantErr: nil, + }, + + { + name: "With custom mixed IP addresses", + route: generateRouteStatus(&routev1.Route{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-route", + Namespace: "some-namespace", + Annotations: map[string]string{ + cmapi.IssuerNameAnnotationKey: "self-signed-issuer", + cmapi.IPSANAnnotationKey: "169.50.50.50,2a02:ec80:300:ed1a::1,::ffff:192.168.0.1", + }, + }, + Spec: routev1.RouteSpec{ + Host: domain, + }, + Status: routev1.RouteStatus{ + Ingress: []routev1.RouteIngress{ + { + Host: domain, + Conditions: []routev1.RouteIngressCondition{ + { + Type: "Admitted", + Status: "True", + }, + }, + }, + }, + }, + }, + true), + want: &cmapi.Certificate{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "some-route-", + Namespace: "some-namespace", + }, + Spec: cmapi.CertificateSpec{ + Duration: &metav1.Duration{Duration: DefaultCertificateDuration}, + DNSNames: domainSlice, + IsCA: false, + Usages: []cmapi.KeyUsage{cmapi.UsageServerAuth, cmapi.UsageDigitalSignature, cmapi.UsageKeyEncipherment}, + + IPAddresses: []string{"169.50.50.50", "2a02:ec80:300:ed1a::1", "192.168.0.1"}, + }, + }, + wantErr: nil, + }, + + { + name: "With custom emails", + route: generateRouteStatus(&routev1.Route{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-route", + Namespace: "some-namespace", + Annotations: map[string]string{ + cmapi.IssuerNameAnnotationKey: "self-signed-issuer", + cmapi.EmailsAnnotationKey: "test@example.com,hello@example.com", + }, + }, + Spec: routev1.RouteSpec{ + Host: domain, + }, + Status: routev1.RouteStatus{ + Ingress: []routev1.RouteIngress{ + { + Host: domain, + Conditions: []routev1.RouteIngressCondition{ + { + Type: "Admitted", + Status: "True", + }, + }, + }, + }, + }, + }, + true), + want: &cmapi.Certificate{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "some-route-", + Namespace: "some-namespace", + }, + Spec: cmapi.CertificateSpec{ + Duration: &metav1.Duration{Duration: DefaultCertificateDuration}, + DNSNames: domainSlice, + IsCA: false, + Usages: []cmapi.KeyUsage{cmapi.UsageServerAuth, cmapi.UsageDigitalSignature, cmapi.UsageKeyEncipherment}, + + EmailAddresses: []string{"test@example.com", "hello@example.com"}, + }, + }, + wantErr: nil, + }, + + { + name: "With all SAN fields", + route: generateRouteStatus(&routev1.Route{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-route", + Namespace: "some-namespace", + Annotations: map[string]string{ + cmapi.IssuerNameAnnotationKey: "self-signed-issuer", + + cmapi.AltNamesAnnotationKey: "example.com,another.example.com", + cmapi.URISANAnnotationKey: "spiffe://example.com/myuri", + cmapi.IPSANAnnotationKey: "169.50.50.50,2a02:ec80:300:ed1a::1,::ffff:192.168.0.1", + cmapi.EmailsAnnotationKey: "test@example.com,hello@example.com", + }, + }, + Spec: routev1.RouteSpec{ + Host: domain, + }, + Status: routev1.RouteStatus{ + Ingress: []routev1.RouteIngress{ + { + Host: domain, + Conditions: []routev1.RouteIngressCondition{ + { + Type: "Admitted", + Status: "True", + }, + }, + }, + }, + }, + }, + true), + want: &cmapi.Certificate{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "some-route-", + Namespace: "some-namespace", + }, + Spec: cmapi.CertificateSpec{ + Duration: &metav1.Duration{Duration: DefaultCertificateDuration}, + IsCA: false, + Usages: []cmapi.KeyUsage{cmapi.UsageServerAuth, cmapi.UsageDigitalSignature, cmapi.UsageKeyEncipherment}, + + DNSNames: []string{domain, "example.com", "another.example.com"}, + URIs: []string{"spiffe://example.com/myuri"}, + IPAddresses: []string{"169.50.50.50", "2a02:ec80:300:ed1a::1", "192.168.0.1"}, + EmailAddresses: []string{"test@example.com", "hello@example.com"}, + }, + }, + wantErr: nil, + }, + + { + name: "With custom renewBefore", + route: generateRouteStatus(&routev1.Route{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-route", + Namespace: "some-namespace", + Annotations: map[string]string{ + cmapi.IssuerNameAnnotationKey: "self-signed-issuer", + cmapi.RenewBeforeAnnotationKey: "30m", + }, + }, + Spec: routev1.RouteSpec{ + Host: domain, + }, + Status: routev1.RouteStatus{ + Ingress: []routev1.RouteIngress{ + { + Host: domain, + Conditions: []routev1.RouteIngressCondition{ + { + Type: "Admitted", + Status: "True", + }, + }, + }, + }, + }, + }, + true), + want: &cmapi.Certificate{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "some-route-", + Namespace: "some-namespace", + }, + Spec: cmapi.CertificateSpec{ + Duration: &metav1.Duration{Duration: DefaultCertificateDuration}, + DNSNames: domainSlice, + IsCA: false, + Usages: []cmapi.KeyUsage{cmapi.UsageServerAuth, cmapi.UsageDigitalSignature, cmapi.UsageKeyEncipherment}, + + RenewBefore: &metav1.Duration{Duration: 30 * time.Minute}, + }, + }, + wantErr: nil, + }, + + { + name: "missing issuer-name is an error", + route: generateRouteStatus(&routev1.Route{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-route", + Namespace: "some-namespace", + Annotations: map[string]string{ + cmapi.RenewBeforeAnnotationKey: "30m", + }, + }, + Spec: routev1.RouteSpec{ + Host: domain, + }, + Status: routev1.RouteStatus{ + Ingress: []routev1.RouteIngress{ + { + Host: domain, + Conditions: []routev1.RouteIngressCondition{ + { + Type: "Admitted", + Status: "True", + }, + }, + }, + }, + }, + }, + true), + want: nil, + wantErr: fmt.Errorf("missing issuer-name annotation on some-namespace/some-route"), + }, + + { + name: "invalid duration is an error", + route: generateRouteStatus(&routev1.Route{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-route", + Namespace: "some-namespace", + Annotations: map[string]string{ + cmapi.IssuerNameAnnotationKey: "self-signed-issuer", + cmapi.DurationAnnotationKey: "not-a-time", + }, + }, + Spec: routev1.RouteSpec{ + Host: domain, + }, + Status: routev1.RouteStatus{ + Ingress: []routev1.RouteIngress{ + { + Host: domain, + Conditions: []routev1.RouteIngressCondition{ + { + Type: "Admitted", + Status: "True", + }, + }, + }, + }, + }, + }, + true), + want: nil, + wantErr: fmt.Errorf("invalid duration annotation on Route %s/%s", "some-namespace", "some-route"), + }, + + { + name: "invalid renew-before is an error", + route: generateRouteStatus(&routev1.Route{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-route", + Namespace: "some-namespace", + Annotations: map[string]string{ + cmapi.IssuerNameAnnotationKey: "self-signed-issuer", + cmapi.RenewBeforeAnnotationKey: "not-a-time", + }, + }, + Spec: routev1.RouteSpec{ + Host: domain, + }, + Status: routev1.RouteStatus{ + Ingress: []routev1.RouteIngress{ + { + Host: domain, + Conditions: []routev1.RouteIngressCondition{ + { + Type: "Admitted", + Status: "True", + }, + }, + }, + }, + }, + }, + true), + want: nil, + wantErr: fmt.Errorf("invalid renew-before annotation %q on Route %s/%s", "not-a-time", "some-namespace", "some-route"), + }, + + { + name: "invalid private key size is an error", + route: generateRouteStatus(&routev1.Route{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-route", + Namespace: "some-namespace", + Annotations: map[string]string{ + cmapi.IssuerNameAnnotationKey: "self-signed-issuer", + cmapi.PrivateKeySizeAnnotationKey: "not-a-number", + }, + }, + Spec: routev1.RouteSpec{ + Host: domain, + }, + Status: routev1.RouteStatus{ + Ingress: []routev1.RouteIngress{ + { + Host: domain, + Conditions: []routev1.RouteIngressCondition{ + { + Type: "Admitted", + Status: "True", + }, + }, + }, + }, + }, + }, + true), + want: nil, + wantErr: fmt.Errorf("invalid private key size annotation %q on %s/%s", "not-a-number", "some-namespace", "some-route"), + }, + + { + name: "invalid revision history limit is an error", + route: generateRouteStatus(&routev1.Route{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-route", + Namespace: "some-namespace", + Annotations: map[string]string{ + cmapi.IssuerNameAnnotationKey: "self-signed-issuer", + cmapi.RevisionHistoryLimitAnnotationKey: "not-a-number", + }, + }, + Spec: routev1.RouteSpec{ + Host: domain, + }, + Status: routev1.RouteStatus{ + Ingress: []routev1.RouteIngress{ + { + Host: domain, + Conditions: []routev1.RouteIngressCondition{ + { + Type: "Admitted", + Status: "True", + }, + }, + }, + }, + }, + }, + true), + want: nil, + wantErr: fmt.Errorf("invalid revision-history-limit annotation %q on %s/%s", "not-a-number", "some-namespace", "some-route"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + recorder := record.NewFakeRecorder(100) + r := &RouteController{ + eventRecorder: recorder, + } + + // test "buildNextCR" function + cert, err := r.buildNextCert(context.TODO(), tt.route) + + // check that we got the expected error (including nil) + assert.Equal(t, tt.wantErr, err, "buildNextCert()") + + if tt.wantErr != nil || err != nil { + return + } + + // check that the returned object is as expected + + if tt.want.Spec.IssuerRef.Name != "" { + // only check issuerRef if it was specified on want; this saves copying lots + // of issuerRefs around + assert.Equal(t, tt.want.Spec.IssuerRef, cert.Spec.IssuerRef) + } + + assert.Equal(t, tt.want.ObjectMeta.GenerateName, cert.ObjectMeta.GenerateName) + assert.Equal(t, tt.want.ObjectMeta.Namespace, cert.ObjectMeta.Namespace) + assert.Equal(t, tt.want.ObjectMeta.Annotations, cert.ObjectMeta.Annotations) + assert.Equal(t, tt.want.ObjectMeta.Labels, cert.ObjectMeta.Labels) + assert.Equal(t, tt.want.Spec.Duration, cert.Spec.Duration) + assert.Equal(t, tt.want.Spec.IsCA, cert.Spec.IsCA) + assert.Equal(t, tt.want.Spec.Usages, cert.Spec.Usages) + assert.Equal(t, tt.want.Spec.DNSNames, cert.Spec.DNSNames) + assert.Equal(t, tt.want.Spec.EmailAddresses, cert.Spec.EmailAddresses) + assert.Equal(t, tt.want.Spec.IPAddresses, cert.Spec.IPAddresses) + assert.Equal(t, tt.want.Spec.URIs, cert.Spec.URIs) + + if tt.want.Spec.PrivateKey != nil { + assert.Equal(t, tt.want.Spec.PrivateKey, cert.Spec.PrivateKey) + } + + if tt.want.Spec.Subject != nil { + assert.Equal(t, tt.want.Spec.Subject, cert.Spec.Subject) + } + + if tt.want.Spec.RenewBefore != nil { + assert.Equal(t, tt.want.Spec.RenewBefore, cert.Spec.RenewBefore) + } + + close(recorder.Events) + }) + } +} diff --git a/make/test-smoke.mk b/make/test-smoke.mk index ba29dbc..e6da0d6 100644 --- a/make/test-smoke.mk +++ b/make/test-smoke.mk @@ -58,5 +58,5 @@ test-smoke-deps: install .PHONY: test-smoke ## Smoke end-to-end tests ## @category Testing -test-smoke: test-smoke-deps | kind-cluster - ./test/test-smoke.sh +test-smoke: test-smoke-deps | kind-cluster $(NEEDS_YQ) + ./test/test-smoke.sh $(YQ) diff --git a/test/test-smoke.sh b/test/test-smoke.sh index 6f38ba1..bd3ed6c 100755 --- a/test/test-smoke.sh +++ b/test/test-smoke.sh @@ -18,6 +18,8 @@ set -o errexit set -o nounset set -o pipefail +YQ=${1:-yq} + # Create a self-signed CA certificate and Issuer cat < /dev/null && echo "Found 'provinces = [Ontario]' in Certificate YAML" +echo "$cert_yaml" | $YQ eval --exit-status 'select(.spec.subject.streetAddresses[0] == "1725 Slough Avenue")' > /dev/null && echo "Found 'streetAddresses = [1725 Slough Avenue]' in Certificate YAML" +echo "$cert_yaml" | $YQ eval --exit-status 'select(.spec.subject.countries[0] == "UK")' > /dev/null && echo "Found 'countries = [UK]' in Certificate YAML" +echo "$cert_yaml" | $YQ eval --exit-status 'select(.spec.subject.postalCodes[0] == "SW1A 2AA")' > /dev/null && echo "Found 'postal codes = [SW1A 2AA]' in Certificate YAML" +echo "$cert_yaml" | $YQ eval --exit-status 'select(.spec.subject.organizations[0] == "cert-manager")' > /dev/null && echo "Found 'organizations = [cert-manager]' in Certificate YAML" +echo "$cert_yaml" | $YQ eval --exit-status 'select(.spec.subject.organizationalUnits[0] == "my-ou")' > /dev/null && echo "Found 'organizationalUnits = [my-ou]' in Certificate YAML" + +echo "$cert_yaml" | $YQ eval --exit-status 'select(.spec.privateKey.rotationPolicy == "Always")' > /dev/null && echo "Found 'rotationPolicy == Always' in Certificate YAML" + +echo "$cert_yaml" | $YQ eval --exit-status 'select(.spec.renewBefore == "30m0s")' > /dev/null && echo "Found 'renewBefore == 30m0s' in Certificate YAML" + +echo "$cert_yaml" | $YQ eval --exit-status 'select(.spec.revisionHistoryLimit == 2)' > /dev/null && echo "Found 'revisionHistoryLimit == 2' in Certificate YAML" kubectl delete route "$route_name"