diff --git a/pkg/route/OWNERS b/pkg/route/OWNERS new file mode 100644 index 0000000000..58f1af8214 --- /dev/null +++ b/pkg/route/OWNERS @@ -0,0 +1,14 @@ +approvers: + - frobware + - knobunc + - Miciah + - sgreene570 + - smarterclayton +reviewers: + - danehans + - frobware + - knobunc + - Miciah + - sgreene570 + - smarterclayton +component: Routing diff --git a/pkg/route/routeapihelpers/routeapihelpers.go b/pkg/route/routeapihelpers/routeapihelpers.go new file mode 100644 index 0000000000..4f108144af --- /dev/null +++ b/pkg/route/routeapihelpers/routeapihelpers.go @@ -0,0 +1,44 @@ +// Package routeapihelpers contains utilities for handling OpenShift route objects. +package routeapihelpers + +import ( + "fmt" + "net/url" + + routev1 "github.com/openshift/api/route/v1" + corev1 "k8s.io/api/core/v1" +) + +// IngressURI calculates an admitted ingress URI. +// If 'host' is nonempty, only the ingress for that host is considered. +// If 'host' is empty, the first admitted ingress is used. +func IngressURI(route *routev1.Route, host string) (*url.URL, *routev1.RouteIngress, error) { + scheme := "http" + if route.Spec.TLS != nil { + scheme = "https" + } + + for _, ingress := range route.Status.Ingress { + if host == "" || host == ingress.Host { + uri := &url.URL{ + Scheme: scheme, + Host: ingress.Host, + } + + for _, condition := range ingress.Conditions { + if condition.Type == routev1.RouteAdmitted && condition.Status == corev1.ConditionTrue { + return uri, &ingress, nil + } + } + + if host == ingress.Host { + return uri, &ingress, fmt.Errorf("ingress for host %s in route %s in namespace %s is not admitted", host, route.ObjectMeta.Name, route.ObjectMeta.Namespace) + } + } + } + + if host == "" { + return nil, nil, fmt.Errorf("no admitted ingress for route %s in namespace %s", route.ObjectMeta.Name, route.ObjectMeta.Namespace) + } + return nil, nil, fmt.Errorf("no ingress for host %s in route %s in namespace %s", host, route.ObjectMeta.Name, route.ObjectMeta.Namespace) +} diff --git a/pkg/route/routeapihelpers/routeapihelpers_test.go b/pkg/route/routeapihelpers/routeapihelpers_test.go new file mode 100644 index 0000000000..ba6b8a662b --- /dev/null +++ b/pkg/route/routeapihelpers/routeapihelpers_test.go @@ -0,0 +1,301 @@ +package routeapihelpers + +import ( + "net/url" + "reflect" + "regexp" + "testing" + + routev1 "github.com/openshift/api/route/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestIngressURI(t *testing.T) { + for _, testCase := range []struct { + name string + route *routev1.Route + host string + uri *url.URL + ingress *routev1.RouteIngress + error string + }{ + { + name: "no ingress", + route: &routev1.Route{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "example-namespace", + Name: "example-name", + }, + }, + error: "^no admitted ingress for route example-name in namespace example-namespace$", + }, + { + name: "no admitted ingress, host-agnostic", + route: &routev1.Route{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "example-namespace", + Name: "example-name", + }, + Status: routev1.RouteStatus{ + Ingress: []routev1.RouteIngress{ + { + Host: "example.com", + RouterName: "example-router", + }, + }, + }, + }, + error: "^no admitted ingress for route example-name in namespace example-namespace$", + }, + { + name: "explicitly non-admitted ingress, host-agnostic", + route: &routev1.Route{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "example-namespace", + Name: "example-name", + }, + Status: routev1.RouteStatus{ + Ingress: []routev1.RouteIngress{ + { + Host: "example.com", + RouterName: "example-router", + Conditions: []routev1.RouteIngressCondition{ + { + Type: routev1.RouteAdmitted, + Status: corev1.ConditionFalse, + Reason: "ExampleReason", + Message: "Example message", + }, + }, + }, + }, + }, + }, + error: "^no admitted ingress for route example-name in namespace example-namespace$", + }, + { + name: "no admitted ingress, unrecognized host", + route: &routev1.Route{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "example-namespace", + Name: "example-name", + }, + Status: routev1.RouteStatus{ + Ingress: []routev1.RouteIngress{ + { + Host: "example.com", + RouterName: "example-router", + }, + }, + }, + }, + host: "a.example.com", + error: "^no ingress for host a.example.com in route example-name in namespace example-namespace$", + }, + { + name: "no admitted ingress, host not admitted", + route: &routev1.Route{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "example-namespace", + Name: "example-name", + }, + Status: routev1.RouteStatus{ + Ingress: []routev1.RouteIngress{ + { + Host: "example.com", + RouterName: "example-router", + }, + }, + }, + }, + host: "example.com", + uri: &url.URL{ + Scheme: "http", + Host: "example.com", + }, + ingress: &routev1.RouteIngress{ + Host: "example.com", + RouterName: "example-router", + }, + error: "^ingress for host example.com in route example-name in namespace example-namespace is not admitted$", + }, + { + name: "admitted ingress, host-agnostic", + route: &routev1.Route{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "example-namespace", + Name: "example-name", + }, + Status: routev1.RouteStatus{ + Ingress: []routev1.RouteIngress{ + { + Host: "a.example.com", + RouterName: "example-router", + }, + { + Host: "b.example.com", + RouterName: "example-router", + Conditions: []routev1.RouteIngressCondition{ + { + Type: routev1.RouteAdmitted, + Status: corev1.ConditionTrue, + Reason: "ExampleReason", + Message: "Example message", + }, + }, + }, + { + Host: "c.example.com", + RouterName: "example-router", + Conditions: []routev1.RouteIngressCondition{ + { + Type: routev1.RouteAdmitted, + Status: corev1.ConditionTrue, + Reason: "ExampleReason", + Message: "Example message", + }, + }, + }, + }, + }, + }, + uri: &url.URL{ + Scheme: "http", + Host: "b.example.com", + }, + ingress: &routev1.RouteIngress{ + Host: "b.example.com", + RouterName: "example-router", + Conditions: []routev1.RouteIngressCondition{ + { + Type: routev1.RouteAdmitted, + Status: corev1.ConditionTrue, + Reason: "ExampleReason", + Message: "Example message", + }, + }, + }, + }, + { + name: "admitted ingress, host-specific", + route: &routev1.Route{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "example-namespace", + Name: "example-name", + }, + Status: routev1.RouteStatus{ + Ingress: []routev1.RouteIngress{ + { + Host: "a.example.com", + RouterName: "example-router", + }, + { + Host: "b.example.com", + RouterName: "example-router", + Conditions: []routev1.RouteIngressCondition{ + { + Type: routev1.RouteAdmitted, + Status: corev1.ConditionTrue, + Reason: "ExampleReason", + Message: "Example message", + }, + }, + }, + { + Host: "c.example.com", + RouterName: "example-router", + Conditions: []routev1.RouteIngressCondition{ + { + Type: routev1.RouteAdmitted, + Status: corev1.ConditionTrue, + Reason: "ExampleReason", + Message: "Example message", + }, + }, + }, + }, + }, + }, + host: "c.example.com", + uri: &url.URL{ + Scheme: "http", + Host: "c.example.com", + }, + ingress: &routev1.RouteIngress{ + Host: "c.example.com", + RouterName: "example-router", + Conditions: []routev1.RouteIngressCondition{ + { + Type: routev1.RouteAdmitted, + Status: corev1.ConditionTrue, + Reason: "ExampleReason", + Message: "Example message", + }, + }, + }, + }, + { + name: "admitted ingress, TLS", + route: &routev1.Route{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "example-namespace", + Name: "example-name", + }, + Spec: routev1.RouteSpec{ + TLS: &routev1.TLSConfig{}, + }, + Status: routev1.RouteStatus{ + Ingress: []routev1.RouteIngress{ + { + Host: "example.com", + RouterName: "example-router", + Conditions: []routev1.RouteIngressCondition{ + { + Type: routev1.RouteAdmitted, + Status: corev1.ConditionTrue, + Reason: "ExampleReason", + Message: "Example message", + }, + }, + }, + }, + }, + }, + uri: &url.URL{ + Scheme: "https", + Host: "example.com", + }, + ingress: &routev1.RouteIngress{ + Host: "example.com", + RouterName: "example-router", + Conditions: []routev1.RouteIngressCondition{ + { + Type: routev1.RouteAdmitted, + Status: corev1.ConditionTrue, + Reason: "ExampleReason", + Message: "Example message", + }, + }, + }, + }, + } { + t.Run(testCase.name, func(t *testing.T) { + uri, ingress, err := IngressURI(testCase.route, testCase.host) + if testCase.error != "" && err == nil { + t.Fatalf("returned no error, expected %s", testCase.error) + } else if testCase.error == "" && err != nil { + t.Fatalf("expected no error, returned %v", err) + } else if err != nil && !regexp.MustCompile(testCase.error).MatchString(err.Error()) { + t.Fatal(err) + } + + if !reflect.DeepEqual(uri, testCase.uri) { + t.Fatal(uri) + } + if !reflect.DeepEqual(ingress, testCase.ingress) { + t.Fatal(ingress) + } + }) + } +}