From 10d5cc73988b86e74ee0a07afe4bc64c7af2aa3d Mon Sep 17 00:00:00 2001 From: Jack Henschel Date: Wed, 14 Jun 2023 12:44:57 +0200 Subject: [PATCH] Refactor createNextCR and add unit tests Signed-off-by: Jack Henschel --- internal/controller/sync.go | 67 +++--- internal/controller/sync_test.go | 366 ++++++++++++++++++++++--------- 2 files changed, 302 insertions(+), 131 deletions(-) diff --git a/internal/controller/sync.go b/internal/controller/sync.go index a3cd765..bcae514 100644 --- a/internal/controller/sync.go +++ b/internal/controller/sync.go @@ -48,8 +48,13 @@ const ( ReasonMissingHostname = `MissingHostname` ) +const DefaultCertificateDuration = time.Hour * 24 * 90 // 90 days + // sync reconciles an Openshift route. -func (r *Route) sync(ctx context.Context, req reconcile.Request, route *routev1.Route) (result reconcile.Result, err error) { +func (r *Route) sync(ctx context.Context, req reconcile.Request, route *routev1.Route) (reconcile.Result, error) { + var result reconcile.Result + var err error + log := r.log.WithName("sync").WithValues("route", req, "resourceVersion", route.ObjectMeta.ResourceVersion) defer func() { // Always send a warning event if err is not nil @@ -63,47 +68,59 @@ func (r *Route) sync(ctx context.Context, req reconcile.Request, route *routev1. if r.hasValidCertificate(route) { result, err = reconcile.Result{RequeueAfter: r.getRequeueAfterDuration(route)}, nil log.V(5).Info("route has valid cert") - return + return result, err } // Do we have a revision? If not set revision to 0 revision, err := getCurrentRevision(route) if err != nil { err = r.setRevision(ctx, route, 0) log.V(5).Info("generated revision 0") - return + return result, err } // Do we have a next key? if !r.hasNextPrivateKey(route) { err = r.generateNextPrivateKey(ctx, route) log.V(5).Info("generated next private key for route") - return + return result, err } // Is there a CertificateRequest for the Next revision? If not, make it. hasNext, err := r.hasNextCR(ctx, route, revision) if err != nil { - // err above is the returned err - named returns parameters + bare returns can be confusing - return + return result, err } if !hasNext { - // create CR and return. We own the CR so it will cause a re-reconcile + // generate manifest for new CR log.V(5).Info("route has no matching certificate request", "revision", revision) - err = r.createNextCR(ctx, route, revision) - return + var cr *cmapi.CertificateRequest + cr, err = r.buildNextCR(ctx, route, revision) + if err != nil { + log.V(1).Error(err, "error generating certificate request", "object", req.NamespacedName) + // Not a reconcile error, so don't retry this revision + return result, nil + } + + // create CR and return. We own the CR so it will cause a re-reconcile + _, err = r.certClient.CertmanagerV1().CertificateRequests(route.Namespace).Create(ctx, cr, metav1.CreateOptions{}) + if err != nil { + return result, err + } + r.eventRecorder.Event(route, corev1.EventTypeNormal, ReasonIssuing, "Created new CertificateRequest for Route %s") + return result, nil + } // is the CR Ready and Approved? ready, cr, err := r.certificateRequestReadyAndApproved(ctx, route, revision) if err != nil { - // err above is the returned err - named returns parameters + bare returns can be confusing - return + return result, err } if !ready { log.V(5).Info("cr is not ready yet") - return + return result, nil } // Cert is ready. Populate the route. err = r.populateRoute(ctx, route, cr, revision) log.V(5).Info("populated route cert") - return + return result, err } func (r *Route) hasValidCertificate(route *routev1.Route) bool { @@ -278,12 +295,14 @@ func (r *Route) findNextCR(ctx context.Context, route *routev1.Route, revision i return nil, fmt.Errorf("multiple certificateRequests found for this route at revision " + strconv.Itoa(revision)) } -func (r *Route) createNextCR(ctx context.Context, route *routev1.Route, revision int) error { +// buildNextCR generates the manifest of a Certificate Request that is needed for a given Route and revision +// This method expects that the private key has already been generated and added as an annotation on the route +func (r *Route) buildNextCR(ctx context.Context, route *routev1.Route, revision int) (*cmapi.CertificateRequest, error) { var key crypto.Signer // get private key from route k2, err := utilpki.DecodePrivateKeyBytes([]byte(route.Annotations[cmapi.IsNextPrivateKeySecretLabelKey])) if err != nil { - return err + return nil, err } key = k2 @@ -294,19 +313,16 @@ func (r *Route) createNextCR(ctx context.Context, route *routev1.Route, revision "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") - // Not a reconcile error, so stop. - return nil + return nil, fmt.Errorf("Invalid duration annotation on Route %s/%s", route.Namespace, route.Name) } var dnsNames []string // Get the canonical hostname(s) of the Route (from .spec.host or .spec.subdomain) dnsNames = getRouteHostnames(route) if len(dnsNames) == 0 { - r.log.V(1).Error(fmt.Errorf("Route is not yet initialized with a hostname"), - "object", route.Namespace+"/"+route.Name) - r.eventRecorder.Event(route, corev1.EventTypeWarning, ReasonMissingHostname, "Route is not yet initialized with a hostname") - // Not a reconcile error, so stop. Will be re-tried when the route status changes. - return nil + err := fmt.Errorf("Route is not yet initialized with a hostname") + r.eventRecorder.Event(route, corev1.EventTypeWarning, ReasonMissingHostname, fmt.Sprint(err)) + return nil, err } // Parse out SANs @@ -353,7 +369,7 @@ func (r *Route) createNextCR(ctx context.Context, route *routev1.Route, revision key, ) if err != nil { - return err + return nil, err } csrPEM := pem.EncodeToMemory(&pem.Block{ Type: "CERTIFICATE REQUEST", @@ -389,8 +405,7 @@ func (r *Route) createNextCR(ctx context.Context, route *routev1.Route, revision cr.Spec.Usages = append(cr.Spec.Usages, cmapi.UsageClientAuth) } - _, err = r.certClient.CertmanagerV1().CertificateRequests(route.Namespace).Create(ctx, cr, metav1.CreateOptions{}) - return err + return cr, nil } func (r *Route) certificateRequestReadyAndApproved(ctx context.Context, route *routev1.Route, revision int) (bool, *cmapi.CertificateRequest, error) { @@ -487,7 +502,7 @@ func (r *Route) getRequeueAfterDuration(route *routev1.Route) time.Duration { } func certDurationFromRoute(r *routev1.Route) (time.Duration, error) { - duration := time.Hour * 24 * 90 + duration := DefaultCertificateDuration durationAnnotation, exists := r.Annotations[cmapi.DurationAnnotationKey] if exists { durationOverride, err := time.ParseDuration(durationAnnotation) diff --git a/internal/controller/sync_test.go b/internal/controller/sync_test.go index e02562f..f7c6413 100644 --- a/internal/controller/sync_test.go +++ b/internal/controller/sync_test.go @@ -21,11 +21,14 @@ import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" + "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "encoding/pem" "fmt" "math/big" + "net" + "net/url" "sort" "testing" "time" @@ -36,6 +39,7 @@ import ( fakeroutev1client "github.com/openshift/client-go/route/clientset/versioned/fake" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/tools/record" ) @@ -83,7 +87,7 @@ func TestRoute_hasValidCertificate(t *testing.T) { }{ { name: "valid and up-to-date ecdsa cert is OK", - route: &routev1.Route{ + route: generateRouteStatus(&routev1.Route{ ObjectMeta: metav1.ObjectMeta{ Name: "some-route", Namespace: "some-namespace", @@ -100,20 +104,14 @@ func TestRoute_hasValidCertificate(t *testing.T) { InsecureEdgeTerminationPolicy: routev1.InsecureEdgeTerminationPolicyRedirect, }, }, - Status: routev1.RouteStatus{ - Ingress: []routev1.RouteIngress{ - { - Host: "some-host.some-domain.tld", - }, - }, - }, }, + true), want: true, wantedEvents: nil, }, { name: "route with renew-before annotation overrides the default 2/3 lifetime behaviour", - route: &routev1.Route{ + route: generateRouteStatus(&routev1.Route{ ObjectMeta: metav1.ObjectMeta{ Name: "some-route", Namespace: "some-namespace", @@ -133,20 +131,14 @@ func TestRoute_hasValidCertificate(t *testing.T) { InsecureEdgeTerminationPolicy: routev1.InsecureEdgeTerminationPolicyRedirect, }, }, - Status: routev1.RouteStatus{ - Ingress: []routev1.RouteIngress{ - { - Host: "some-host.some-domain.tld", - }, - }, - }, }, + true), want: false, wantedEvents: []string{"Normal Issuing Issuing cert as the renew-before period has been reached"}, }, { name: "expiring soon ecdsa cert triggers a renewal", - route: &routev1.Route{ + route: generateRouteStatus(&routev1.Route{ ObjectMeta: metav1.ObjectMeta{ Name: "some-route", Namespace: "some-namespace", @@ -163,20 +155,14 @@ func TestRoute_hasValidCertificate(t *testing.T) { InsecureEdgeTerminationPolicy: routev1.InsecureEdgeTerminationPolicyRedirect, }, }, - Status: routev1.RouteStatus{ - Ingress: []routev1.RouteIngress{ - { - Host: "some-host.some-domain.tld", - }, - }, - }, }, + true), want: false, wantedEvents: []string{"Normal Issuing Issuing cert as the existing cert is more than 2/3 through its validity period"}, }, { name: "cert not matching key triggers a renewal", - route: &routev1.Route{ + route: generateRouteStatus(&routev1.Route{ ObjectMeta: metav1.ObjectMeta{ Name: "some-route", Namespace: "some-namespace", @@ -193,20 +179,14 @@ func TestRoute_hasValidCertificate(t *testing.T) { InsecureEdgeTerminationPolicy: routev1.InsecureEdgeTerminationPolicyRedirect, }, }, - Status: routev1.RouteStatus{ - Ingress: []routev1.RouteIngress{ - { - Host: "some-host.some-domain.tld", - }, - }, - }, }, + true), want: false, wantedEvents: []string{"Normal Issuing Issuing cert as the public key does not match the certificate"}, }, { name: "junk data in key triggers a renewal", - route: &routev1.Route{ + route: generateRouteStatus(&routev1.Route{ ObjectMeta: metav1.ObjectMeta{ Name: "some-route", Namespace: "some-namespace", @@ -225,20 +205,14 @@ SOME GARBAGE InsecureEdgeTerminationPolicy: routev1.InsecureEdgeTerminationPolicyRedirect, }, }, - Status: routev1.RouteStatus{ - Ingress: []routev1.RouteIngress{ - { - Host: "some-host.some-domain.tld", - }, - }, - }, }, + true), want: false, wantedEvents: []string{"Normal Issuing Issuing cert as the existing key is invalid: error decoding private key PEM block"}, }, { name: "missing private key triggers a renewal", - route: &routev1.Route{ + route: generateRouteStatus(&routev1.Route{ ObjectMeta: metav1.ObjectMeta{ Name: "some-route", Namespace: "some-namespace", @@ -254,20 +228,14 @@ SOME GARBAGE InsecureEdgeTerminationPolicy: routev1.InsecureEdgeTerminationPolicyRedirect, }, }, - Status: routev1.RouteStatus{ - Ingress: []routev1.RouteIngress{ - { - Host: "some-host.some-domain.tld", - }, - }, - }, }, + true), want: false, wantedEvents: []string{"Normal Issuing Issuing cert as no private key exists"}, }, { name: "junk data in cert triggers a renewal", - route: &routev1.Route{ + route: generateRouteStatus(&routev1.Route{ ObjectMeta: metav1.ObjectMeta{ Name: "some-route", Namespace: "some-namespace", @@ -293,12 +261,13 @@ SOME GARBAGE }, }, }, + true), want: false, wantedEvents: []string{"Normal Issuing Issuing cert as the existing cert is invalid: error decoding certificate PEM block"}, }, { name: "missing cert triggers a renewal", - route: &routev1.Route{ + route: generateRouteStatus(&routev1.Route{ ObjectMeta: metav1.ObjectMeta{ Name: "some-route", Namespace: "some-namespace", @@ -313,20 +282,14 @@ SOME GARBAGE InsecureEdgeTerminationPolicy: routev1.InsecureEdgeTerminationPolicyRedirect, }, }, - Status: routev1.RouteStatus{ - Ingress: []routev1.RouteIngress{ - { - Host: "some-host.some-domain.tld", - }, - }, - }, }, + true), want: false, wantedEvents: []string{"Normal Issuing Issuing cert as no certificate exists"}, }, { name: "missing tls config triggers a renewal", - route: &routev1.Route{ + route: generateRouteStatus(&routev1.Route{ ObjectMeta: metav1.ObjectMeta{ Name: "some-route", Namespace: "some-namespace", @@ -336,42 +299,40 @@ SOME GARBAGE Spec: routev1.RouteSpec{ Host: "some-host.some-domain.tld", }, - Status: routev1.RouteStatus{ - Ingress: []routev1.RouteIngress{ - { - Host: "some-host.some-domain.tld", - }, - }, - }, }, + true), want: false, wantedEvents: []string{"Normal Issuing Issuing cert as no TLS is configured"}, }, { - name: "uninitialized route", - route: &routev1.Route{ + name: "route with changed hostname", + route: generateRouteStatus(&routev1.Route{ ObjectMeta: metav1.ObjectMeta{ - Name: "some-uninitialized-route", + Name: "some-route", Namespace: "some-namespace", CreationTimestamp: metav1.Time{Time: time.Now().Add(-time.Hour * 24 * 30)}, Annotations: map[string]string{cmapi.IssuerNameAnnotationKey: "some-issuer"}, }, Spec: routev1.RouteSpec{ - Host: "some-host.some-domain.tld", - }, - Status: routev1.RouteStatus{ - Ingress: []routev1.RouteIngress{}, + Host: "some-other-host.some-domain.tld", + TLS: &routev1.TLSConfig{ + Termination: routev1.TLSTerminationEdge, + Certificate: string(validEcdsaCertPEM), + Key: string(ecdsaKeyPEM), + CACertificate: string(validEcdsaCertPEM), + InsecureEdgeTerminationPolicy: routev1.InsecureEdgeTerminationPolicyRedirect, + }, }, }, + true), want: false, wantedEvents: []string{ - "Normal Issuing Issuing cert as no TLS is configured", - "Route has not been initialized with a Status", + "Normal Issuing Issuing cert as the hostname does not match the certificate", }, }, { name: "route with subdomain", - route: &routev1.Route{ + route: generateRouteStatus(&routev1.Route{ ObjectMeta: metav1.ObjectMeta{ Name: "some-uninitialized-route", Namespace: "some-namespace", @@ -381,16 +342,12 @@ SOME GARBAGE Spec: routev1.RouteSpec{ Subdomain: "sub-domain", }, - Status: routev1.RouteStatus{ - Ingress: []routev1.RouteIngress{ - { - Host: "sub-domain.ingress.example.com", - }, - }, - }, }, - want: true, - wantedEvents: nil, + true), + want: false, + wantedEvents: []string{ + "Normal Issuing Issuing cert as no TLS is configured", + }, }, } for _, tt := range tests { @@ -436,9 +393,7 @@ func TestRoute_hasNextPrivateKey(t *testing.T) { cmapi.IsNextPrivateKeySecretLabelKey: string(ecdsaKeyPEM), }, }, - Spec: routev1.RouteSpec{ - Host: "some-host.some-domain.tld", - }, + Spec: routev1.RouteSpec{}, }, want: true, wantedEvents: nil, @@ -454,9 +409,7 @@ func TestRoute_hasNextPrivateKey(t *testing.T) { cmapi.IssuerNameAnnotationKey: "some-issuer", }, }, - Spec: routev1.RouteSpec{ - Host: "some-host.some-domain.tld", - }, + Spec: routev1.RouteSpec{}, }, want: false, wantedEvents: nil, @@ -475,9 +428,7 @@ SOME GARBAGE -----END PRIVATE KEY-----`, }, }, - Spec: routev1.RouteSpec{ - Host: "some-host.some-domain.tld", - }, + Spec: routev1.RouteSpec{}, }, want: false, wantedEvents: []string{"Warning InvalidKey Regenerating Next Private Key as the existing key is invalid: error decoding private key PEM block"}, @@ -520,9 +471,7 @@ func TestRoute_generateNextPrivateKey(t *testing.T) { cmapi.IssuerNameAnnotationKey: "some-issuer", }, }, - Spec: routev1.RouteSpec{ - Host: "some-host.some-domain.tld", - }, + Spec: routev1.RouteSpec{}, }, want: nil, wantedEvents: []string{"Normal Issuing Generated Private Key for route"}, @@ -575,9 +524,7 @@ func Test_getCurrentRevision(t *testing.T) { cmapi.CertificateRequestRevisionAnnotationKey: "1337", }, }, - Spec: routev1.RouteSpec{ - Host: "some-host.some-domain.tld", - }, + Spec: routev1.RouteSpec{}, }, want: 1337, wantErr: nil, @@ -593,9 +540,7 @@ func Test_getCurrentRevision(t *testing.T) { cmapi.IssuerNameAnnotationKey: "some-issuer", }, }, - Spec: routev1.RouteSpec{ - Host: "some-host.some-domain.tld", - }, + Spec: routev1.RouteSpec{}, }, want: 0, wantErr: fmt.Errorf("no revision found"), @@ -629,9 +574,7 @@ func TestRoute_setRevision(t *testing.T) { cmapi.IssuerNameAnnotationKey: "some-issuer", }, }, - Spec: routev1.RouteSpec{ - Host: "some-host.some-domain.tld", - }, + Spec: routev1.RouteSpec{}, }, revision: 1337, want: "1337", @@ -654,3 +597,216 @@ func TestRoute_setRevision(t *testing.T) { }) } } + +func TestRoute_buildNextCR(t *testing.T) { + // set up key for test cases + rsaKey, err := rsa.GenerateKey(rand.Reader, 4096) + require.NoError(t, err) + rsaPEM, err := utilpki.EncodePKCS8PrivateKey(rsaKey) + require.NoError(t, err) + + tests := []struct { + name string + route *routev1.Route + revision int + want *cmapi.CertificateRequest + wantErr error + wantCSR *x509.CertificateRequest + wantEvents []string + }{ + { + name: "Basic test with duration and hostname", + revision: 1337, + route: generateRouteStatus(&routev1.Route{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-route", + Namespace: "some-namespace", + Annotations: map[string]string{ + cmapi.DurationAnnotationKey: "42m", + cmapi.IsNextPrivateKeySecretLabelKey: string(rsaPEM), + }, + }, + Spec: routev1.RouteSpec{ + Host: "some-host.some-domain.tld", + }, + Status: routev1.RouteStatus{ + Ingress: []routev1.RouteIngress{ + { + Host: "some-host.some-domain.tld", + Conditions: []routev1.RouteIngressCondition{ + { + Type: "Admitted", + Status: "True", + }, + }, + }, + }, + }, + }, + true), + want: &cmapi.CertificateRequest{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "some-route-", + Namespace: "some-namespace", + Annotations: map[string]string{ + cmapi.CertificateRequestRevisionAnnotationKey: "1338", + }, + }, + Spec: cmapi.CertificateRequestSpec{ + Duration: &metav1.Duration{Duration: 42 * time.Minute}, + IsCA: false, + Usages: []cmapi.KeyUsage{cmapi.UsageServerAuth, cmapi.UsageDigitalSignature, cmapi.UsageKeyEncipherment}, + }, + }, + wantErr: nil, + }, + { + name: "With subdomain and multiple ICs", + revision: 1337, + route: &routev1.Route{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-route-with-subdomain", + Namespace: "some-namespace", + Annotations: map[string]string{ + cmapi.IsNextPrivateKeySecretLabelKey: string(rsaPEM), + }, + }, + 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.CertificateRequest{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "some-route-with-subdomain-", + Namespace: "some-namespace", + Annotations: map[string]string{ + cmapi.CertificateRequestRevisionAnnotationKey: "1338", + }, + }, + Spec: cmapi.CertificateRequestSpec{ + Duration: &metav1.Duration{Duration: DefaultCertificateDuration}, + Usages: []cmapi.KeyUsage{cmapi.UsageServerAuth, cmapi.UsageDigitalSignature, cmapi.UsageKeyEncipherment}, + }, + }, + wantCSR: &x509.CertificateRequest{ + SignatureAlgorithm: x509.SHA256WithRSA, + PublicKeyAlgorithm: x509.RSA, + Subject: pkix.Name{ + CommonName: "", + }, + DNSNames: []string{"some-sub-domain.some-domain.tld", "some-sub-domain.some-other-ic.example.com"}, + IPAddresses: []net.IP{}, + URIs: []*url.URL{}, + }, + wantErr: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + recorder := record.NewFakeRecorder(100) + r := &Route{ + eventRecorder: recorder, + } + // test "buildNextCR" function + cr, err := r.buildNextCR(context.TODO(), tt.route, tt.revision) + + // check that we got the expected error (including nil) + assert.Equal(t, tt.wantErr, err, "buildNextCR()") + + // check that the returned object is as expected + assert.Equal(t, tt.want.ObjectMeta.GenerateName, cr.ObjectMeta.GenerateName) + assert.Equal(t, tt.want.ObjectMeta.Namespace, cr.ObjectMeta.Namespace) + assert.Equal(t, tt.want.ObjectMeta.Annotations, cr.ObjectMeta.Annotations) + assert.Equal(t, tt.want.ObjectMeta.Labels, cr.ObjectMeta.Labels) + assert.Equal(t, tt.want.Spec.Duration, cr.Spec.Duration) + assert.Equal(t, tt.want.Spec.IsCA, cr.Spec.IsCA) + assert.Equal(t, tt.want.Spec.Usages, cr.Spec.Usages) + + // check the CSR + if tt.wantCSR != nil { + csr, err := x509.CreateCertificateRequest(rand.Reader, tt.wantCSR, rsaKey) + assert.NoError(t, err) + csrPEM := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE REQUEST", + Bytes: csr, + }) + assert.Equal(t, cr.Spec.Request, csrPEM) + } + + // check the events that were generated + close(recorder.Events) + if len(tt.wantEvents) > 0 { + var gotEvents []string + for e := range recorder.Events { + gotEvents = append(gotEvents, e) + } + sort.Strings(tt.wantEvents) + sort.Strings(gotEvents) + assert.Equal(t, tt.wantEvents, gotEvents, "createNextCR() events") + } + + }) + } +} + +// trivial logic that re-implements OpenShift's IngressController behavior +func generateRouteStatus(route *routev1.Route, admitted bool) *routev1.Route { + var host string + if route.Spec.Host != "" { + host = route.Spec.Host + } + if route.Spec.Subdomain != "" { + host = route.Spec.Subdomain + ".cert-manager.io" // suffix depends on IC config + } + + var admittedStatus = corev1.ConditionTrue + if admitted == false { + admittedStatus = corev1.ConditionFalse + } + + route.Status = routev1.RouteStatus{ + Ingress: []routev1.RouteIngress{ + { + Host: host, + Conditions: []routev1.RouteIngressCondition{ + { + Type: "Admitted", + Status: admittedStatus, + }, + }, + }, + }, + } + return route +}