Skip to content

Commit

Permalink
Support Route.spec.subdomain extension
Browse files Browse the repository at this point in the history
Signed-off-by: Jack Henschel <[email protected]>
  • Loading branch information
jacksgt committed Jun 14, 2023
1 parent d7a6e49 commit d92e4ed
Show file tree
Hide file tree
Showing 2 changed files with 146 additions and 18 deletions.
57 changes: 50 additions & 7 deletions internal/controller/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const (
ReasonInvalidKey = `InvalidKey`
ReasonInvalidValue = `InvalidValue`
ReasonInternalReconcileError = `InternalReconcileError`
ReasonMissingHostname = `MissingHostname`
)

// sync reconciles an Openshift route.
Expand Down Expand Up @@ -145,9 +146,12 @@ func (r *Route) hasValidCertificate(route *routev1.Route) bool {
return false
}
// Cert matches Route hostname?
if err := cert.VerifyHostname(route.Spec.Host); err != nil {
r.eventRecorder.Event(route, corev1.EventTypeNormal, ReasonIssuing, "Issuing cert as the hostname does not match the certificate")
return false
hostnames := getRouteHostnames(route)
for _, host := range hostnames {
if err := cert.VerifyHostname(host); err != nil {
r.eventRecorder.Event(route, corev1.EventTypeNormal, ReasonIssuing, "Issuing cert as the hostname does not match the certificate")
return false
}
}
// Still not after the renew-before window?
if metav1.HasAnnotation(route.ObjectMeta, cmapi.RenewBeforeAnnotationKey) {
Expand Down Expand Up @@ -294,15 +298,22 @@ func (r *Route) createNextCR(ctx context.Context, route *routev1.Route, revision
return nil
}

// Parse out SANs
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
}

// Parse out SANs
if metav1.HasAnnotation(route.ObjectMeta, cmapi.AltNamesAnnotationKey) {
altNames := strings.Split(route.Annotations[cmapi.AltNamesAnnotationKey], ",")
dnsNames = append(dnsNames, altNames...)
}
if len(route.Spec.Host) > 0 {
dnsNames = append(dnsNames, route.Spec.Host)
}
var ipSans []net.IP
if metav1.HasAnnotation(route.ObjectMeta, cmapi.IPSANAnnotationKey) {
ipAddresses := strings.Split(route.Annotations[cmapi.IPSANAnnotationKey], ",")
Expand Down Expand Up @@ -487,3 +498,35 @@ func certDurationFromRoute(r *routev1.Route) (time.Duration, error) {
}
return duration, nil
}

// This function returns the hostnames that have been admitted by an Ingress Controller.
// Usually this is just `.spec.host`, but as of OpenShift 4.11 users may also specify `.spec.subdomain`,
// in which case the fully qualified hostname is derived from the hostname of the Ingress Controller.
// In both cases, the final hostname is reflected in `.status.ingress[].host`.
// Note that a Route can be admitted by multiple ingress controllers, so it may have multiple hostnames.
func getRouteHostnames(r *routev1.Route) []string {
hostnames := []string{}
for _, ing := range r.Status.Ingress {
// Iterate over all Ingress Controllers which have admitted the Route
for i := range ing.Conditions {
if ing.Conditions[i].Type == "Admitted" && ing.Conditions[i].Status == "True" {
// The same hostname can be exposed by multiple Ingress routers,
// but we only want a list of unique hostnames.
if !stringInSlice(hostnames, ing.Host) {
hostnames = append(hostnames, ing.Host)
}
}
}
}

return hostnames
}

func stringInSlice(slice []string, s string) bool {
for i := range slice {
if slice[i] == s {
return true
}
}
return false
}
107 changes: 96 additions & 11 deletions internal/controller/sync_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,13 @@ func TestRoute_hasValidCertificate(t *testing.T) {
InsecureEdgeTerminationPolicy: routev1.InsecureEdgeTerminationPolicyRedirect,
},
},
Status: routev1.RouteStatus{
Ingress: []routev1.RouteIngress{
{
Host: "some-host.some-domain.tld",
},
},
},
},
want: true,
wantedEvents: nil,
Expand All @@ -126,6 +133,13 @@ func TestRoute_hasValidCertificate(t *testing.T) {
InsecureEdgeTerminationPolicy: routev1.InsecureEdgeTerminationPolicyRedirect,
},
},
Status: routev1.RouteStatus{
Ingress: []routev1.RouteIngress{
{
Host: "some-host.some-domain.tld",
},
},
},
},
want: false,
wantedEvents: []string{"Normal Issuing Issuing cert as the renew-before period has been reached"},
Expand All @@ -149,6 +163,13 @@ func TestRoute_hasValidCertificate(t *testing.T) {
InsecureEdgeTerminationPolicy: routev1.InsecureEdgeTerminationPolicyRedirect,
},
},
Status: routev1.RouteStatus{
Ingress: []routev1.RouteIngress{
{
Host: "some-host.some-domain.tld",
},
},
},
},
want: false,
wantedEvents: []string{"Normal Issuing Issuing cert as the existing cert is more than 2/3 through its validity period"},
Expand All @@ -172,6 +193,13 @@ func TestRoute_hasValidCertificate(t *testing.T) {
InsecureEdgeTerminationPolicy: routev1.InsecureEdgeTerminationPolicyRedirect,
},
},
Status: routev1.RouteStatus{
Ingress: []routev1.RouteIngress{
{
Host: "some-host.some-domain.tld",
},
},
},
},
want: false,
wantedEvents: []string{"Normal Issuing Issuing cert as the public key does not match the certificate"},
Expand All @@ -197,6 +225,13 @@ SOME GARBAGE
InsecureEdgeTerminationPolicy: routev1.InsecureEdgeTerminationPolicyRedirect,
},
},
Status: routev1.RouteStatus{
Ingress: []routev1.RouteIngress{
{
Host: "some-host.some-domain.tld",
},
},
},
},
want: false,
wantedEvents: []string{"Normal Issuing Issuing cert as the existing key is invalid: error decoding private key PEM block"},
Expand All @@ -219,6 +254,13 @@ SOME GARBAGE
InsecureEdgeTerminationPolicy: routev1.InsecureEdgeTerminationPolicyRedirect,
},
},
Status: routev1.RouteStatus{
Ingress: []routev1.RouteIngress{
{
Host: "some-host.some-domain.tld",
},
},
},
},
want: false,
wantedEvents: []string{"Normal Issuing Issuing cert as no private key exists"},
Expand All @@ -243,6 +285,13 @@ SOME GARBAGE
InsecureEdgeTerminationPolicy: routev1.InsecureEdgeTerminationPolicyRedirect,
},
},
Status: routev1.RouteStatus{
Ingress: []routev1.RouteIngress{
{
Host: "some-host.some-domain.tld",
},
},
},
},
want: false,
wantedEvents: []string{"Normal Issuing Issuing cert as the existing cert is invalid: error decoding certificate PEM block"},
Expand All @@ -264,6 +313,13 @@ SOME GARBAGE
InsecureEdgeTerminationPolicy: routev1.InsecureEdgeTerminationPolicyRedirect,
},
},
Status: routev1.RouteStatus{
Ingress: []routev1.RouteIngress{
{
Host: "some-host.some-domain.tld",
},
},
},
},
want: false,
wantedEvents: []string{"Normal Issuing Issuing cert as no certificate exists"},
Expand All @@ -280,32 +336,61 @@ SOME GARBAGE
Spec: routev1.RouteSpec{
Host: "some-host.some-domain.tld",
},
Status: routev1.RouteStatus{
Ingress: []routev1.RouteIngress{
{
Host: "some-host.some-domain.tld",
},
},
},
},
want: false,
wantedEvents: []string{"Normal Issuing Issuing cert as no TLS is configured"},
},
{
name: "changed route hostname triggers new certificate",
name: "uninitialized route",
route: &routev1.Route{
ObjectMeta: metav1.ObjectMeta{
Name: "some-route",
Name: "some-uninitialized-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-other-host.some-domain.tld",
TLS: &routev1.TLSConfig{
Termination: routev1.TLSTerminationEdge,
Certificate: string(validEcdsaCertPEM),
Key: string(ecdsaKeyPEM),
CACertificate: string(validEcdsaCertPEM),
InsecureEdgeTerminationPolicy: routev1.InsecureEdgeTerminationPolicyRedirect,
Host: "some-host.some-domain.tld",
},
Status: routev1.RouteStatus{
Ingress: []routev1.RouteIngress{},
},
},
want: false,
wantedEvents: []string{
"Normal Issuing Issuing cert as no TLS is configured",
"Route has not been initialized with a Status",
},
},
{
name: "route with subdomain",
route: &routev1.Route{
ObjectMeta: metav1.ObjectMeta{
Name: "some-uninitialized-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{
Subdomain: "sub-domain",
},
Status: routev1.RouteStatus{
Ingress: []routev1.RouteIngress{
{
Host: "sub-domain.ingress.example.com",
},
},
},
},
want: false,
wantedEvents: []string{"Normal Issuing Issuing cert as the hostname does not match the certificate"},
want: true,
wantedEvents: nil,
},
}
for _, tt := range tests {
Expand Down

0 comments on commit d92e4ed

Please sign in to comment.