diff --git a/pkg/i2gw/providers/gce/extensions/output_extensions.go b/pkg/i2gw/providers/gce/extensions/output_extensions.go index 19b4ca2e..2b47633d 100644 --- a/pkg/i2gw/providers/gce/extensions/output_extensions.go +++ b/pkg/i2gw/providers/gce/extensions/output_extensions.go @@ -36,3 +36,52 @@ func BuildBackendPolicySecurityPolicyConfig(serviceIR intermediate.ProviderSpeci securityPolicy := serviceIR.Gce.SecurityPolicy.Name return &securityPolicy } + +func BuildHealthCheckPolicyConfig(serviceIR intermediate.ProviderSpecificServiceIR) *gkegatewayv1.HealthCheckPolicyConfig { + hcConfig := gkegatewayv1.HealthCheckPolicyConfig{ + CheckIntervalSec: serviceIR.Gce.HealthCheck.CheckIntervalSec, + TimeoutSec: serviceIR.Gce.HealthCheck.TimeoutSec, + HealthyThreshold: serviceIR.Gce.HealthCheck.HealthyThreshold, + UnhealthyThreshold: serviceIR.Gce.HealthCheck.UnhealthyThreshold, + } + commonHc := gkegatewayv1.CommonHealthCheck{ + Port: serviceIR.Gce.HealthCheck.Port, + } + commonHTTPHc := gkegatewayv1.CommonHTTPHealthCheck{ + RequestPath: serviceIR.Gce.HealthCheck.RequestPath, + } + + switch *serviceIR.Gce.HealthCheck.Type { + case "HTTP": + hcConfig.Config = &gkegatewayv1.HealthCheck{ + Type: gkegatewayv1.HTTP, + HTTP: &gkegatewayv1.HTTPHealthCheck{ + CommonHealthCheck: commonHc, + CommonHTTPHealthCheck: commonHTTPHc, + }, + } + + case "HTTPS": + hcConfig.Config = &gkegatewayv1.HealthCheck{ + Type: gkegatewayv1.HTTPS, + HTTPS: &gkegatewayv1.HTTPSHealthCheck{ + CommonHealthCheck: commonHc, + CommonHTTPHealthCheck: commonHTTPHc, + }, + } + + case "HTTP2": + hcConfig.Config = &gkegatewayv1.HealthCheck{ + Type: gkegatewayv1.HTTP2, + HTTP2: &gkegatewayv1.HTTP2HealthCheck{ + CommonHealthCheck: commonHc, + CommonHTTPHealthCheck: commonHTTPHc, + }, + } + + default: + return nil + } + + return &hcConfig +} diff --git a/pkg/i2gw/providers/gce/gateway_converter.go b/pkg/i2gw/providers/gce/gateway_converter.go index 2c973d73..eeabfccc 100644 --- a/pkg/i2gw/providers/gce/gateway_converter.go +++ b/pkg/i2gw/providers/gce/gateway_converter.go @@ -17,6 +17,8 @@ limitations under the License. package gce import ( + "fmt" + gkegatewayv1 "github.com/GoogleCloudPlatform/gke-gateway-api/apis/networking/v1" "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw" "github.com/kubernetes-sigs/ingress2gateway/pkg/i2gw/intermediate" @@ -49,15 +51,24 @@ func (c *irToGatewayResourcesConverter) irToGateway(ir intermediate.IR) (i2gw.Ga func buildGceServiceExtensions(ir intermediate.IR, gatewayResources *i2gw.GatewayResources) { for svcKey, serviceIR := range ir.Services { bePolicy := addBackendPolicyIfConfigured(svcKey, serviceIR) - if bePolicy == nil { - continue + if bePolicy != nil { + obj, err := i2gw.CastToUnstructured(bePolicy) + if err != nil { + notify(notifications.ErrorNotification, "Failed to cast GCPBackendPolicy to unstructured", bePolicy) + continue + } + gatewayResources.GatewayExtensions = append(gatewayResources.GatewayExtensions, *obj) } - obj, err := i2gw.CastToUnstructured(bePolicy) - if err != nil { - notify(notifications.ErrorNotification, "Failed to cast GCPBackendPolicy to unstructured", bePolicy) - continue + + hcPolicy := addHealthCheckPolicyIfConfigured(svcKey, serviceIR) + if hcPolicy != nil { + obj, err := i2gw.CastToUnstructured(hcPolicy) + if err != nil { + notify(notifications.ErrorNotification, "Failed to cast HealthCheckPolicy to unstructured", hcPolicy) + continue + } + gatewayResources.GatewayExtensions = append(gatewayResources.GatewayExtensions, *obj) } - gatewayResources.GatewayExtensions = append(gatewayResources.GatewayExtensions, *obj) } } @@ -65,6 +76,11 @@ func addBackendPolicyIfConfigured(serviceNamespacedName types.NamespacedName, se if serviceIR.Gce == nil { return nil } + // If there is no specification related to GCPBackendPolicy feature, return nil. + if serviceIR.Gce.SessionAffinity == nil && serviceIR.Gce.SecurityPolicy == nil { + return nil + } + backendPolicy := gkegatewayv1.GCPBackendPolicy{ ObjectMeta: metav1.ObjectMeta{ Namespace: serviceNamespacedName.Namespace, @@ -90,3 +106,31 @@ func addBackendPolicyIfConfigured(serviceNamespacedName types.NamespacedName, se return &backendPolicy } + +func addHealthCheckPolicyIfConfigured(serviceNamespacedName types.NamespacedName, serviceIR intermediate.ProviderSpecificServiceIR) *gkegatewayv1.HealthCheckPolicy { + if serviceIR.Gce == nil { + return nil + } + // If there is no specification related to HealthCheckPolicy feature, return nil. + if serviceIR.Gce.HealthCheck == nil { + return nil + } + + healthCheckPolicy := gkegatewayv1.HealthCheckPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: serviceNamespacedName.Namespace, + Name: serviceNamespacedName.Name, + }, + Spec: gkegatewayv1.HealthCheckPolicySpec{ + Default: extensions.BuildHealthCheckPolicyConfig(serviceIR), + TargetRef: gatewayv1alpha2.NamespacedPolicyTargetReference{ + Group: "", + Kind: "Service", + Name: gatewayv1.ObjectName(serviceNamespacedName.Name), + }, + }, + } + healthCheckPolicy.SetGroupVersionKind(HealthCheckPolicyGVK) + fmt.Println(healthCheckPolicy) + return &healthCheckPolicy +} diff --git a/pkg/i2gw/providers/gce/gateway_converter_test.go b/pkg/i2gw/providers/gce/gateway_converter_test.go index ace82377..8237b127 100644 --- a/pkg/i2gw/providers/gce/gateway_converter_test.go +++ b/pkg/i2gw/providers/gce/gateway_converter_test.go @@ -156,6 +156,107 @@ var ( }, }, } + + commonHc = gkegatewayv1.CommonHealthCheck{ + Port: common.PtrTo(testPort), + } + commonHTTPHc = gkegatewayv1.CommonHTTPHealthCheck{ + RequestPath: common.PtrTo(testRequestPath), + } + + testHealthCheckPolicyName = testServiceName + testHealthCheckPolicyHTTP = gkegatewayv1.HealthCheckPolicy{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "networking.gke.io/v1", + Kind: "HealthCheckPolicy", + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: testNamespace, + Name: testHealthCheckPolicyName, + }, + Spec: gkegatewayv1.HealthCheckPolicySpec{ + Default: &gkegatewayv1.HealthCheckPolicyConfig{ + CheckIntervalSec: common.PtrTo(testCheckIntervalSec), + TimeoutSec: common.PtrTo(testTimeoutSec), + HealthyThreshold: common.PtrTo(testHealthyThreshold), + UnhealthyThreshold: common.PtrTo(testUnhealthyThreshold), + Config: &gkegatewayv1.HealthCheck{ + Type: gkegatewayv1.HTTP, + HTTP: &gkegatewayv1.HTTPHealthCheck{ + CommonHealthCheck: commonHc, + CommonHTTPHealthCheck: commonHTTPHc, + }, + }, + }, + TargetRef: v1alpha2.NamespacedPolicyTargetReference{ + Group: "", + Kind: "Service", + Name: gatewayv1.ObjectName(testServiceName), + }, + }, + } + + testHealthCheckPolicyHTTPS = gkegatewayv1.HealthCheckPolicy{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "networking.gke.io/v1", + Kind: "HealthCheckPolicy", + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: testNamespace, + Name: testHealthCheckPolicyName, + }, + Spec: gkegatewayv1.HealthCheckPolicySpec{ + Default: &gkegatewayv1.HealthCheckPolicyConfig{ + CheckIntervalSec: common.PtrTo(testCheckIntervalSec), + TimeoutSec: common.PtrTo(testTimeoutSec), + HealthyThreshold: common.PtrTo(testHealthyThreshold), + UnhealthyThreshold: common.PtrTo(testUnhealthyThreshold), + Config: &gkegatewayv1.HealthCheck{ + Type: gkegatewayv1.HTTPS, + HTTPS: &gkegatewayv1.HTTPSHealthCheck{ + CommonHealthCheck: commonHc, + CommonHTTPHealthCheck: commonHTTPHc, + }, + }, + }, + TargetRef: v1alpha2.NamespacedPolicyTargetReference{ + Group: "", + Kind: "Service", + Name: gatewayv1.ObjectName(testServiceName), + }, + }, + } + + testHealthCheckPolicyHTTP2 = gkegatewayv1.HealthCheckPolicy{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "networking.gke.io/v1", + Kind: "HealthCheckPolicy", + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: testNamespace, + Name: testHealthCheckPolicyName, + }, + Spec: gkegatewayv1.HealthCheckPolicySpec{ + Default: &gkegatewayv1.HealthCheckPolicyConfig{ + CheckIntervalSec: common.PtrTo(testCheckIntervalSec), + TimeoutSec: common.PtrTo(testTimeoutSec), + HealthyThreshold: common.PtrTo(testHealthyThreshold), + UnhealthyThreshold: common.PtrTo(testUnhealthyThreshold), + Config: &gkegatewayv1.HealthCheck{ + Type: gkegatewayv1.HTTP2, + HTTP2: &gkegatewayv1.HTTP2HealthCheck{ + CommonHealthCheck: commonHc, + CommonHTTPHealthCheck: commonHTTPHc, + }, + }, + }, + TargetRef: v1alpha2.NamespacedPolicyTargetReference{ + Group: "", + Kind: "Service", + Name: gatewayv1.ObjectName(testServiceName), + }, + }, + } ) func Test_irToGateway(t *testing.T) { @@ -171,6 +272,18 @@ func Test_irToGateway(t *testing.T) { if err != nil { t.Errorf("Failed to generate unstructured GCP Backend Policy with Security Policy feature: %v", err) } + testHealthCheckPolicyHTTPUnstructured, err := i2gw.CastToUnstructured(&testHealthCheckPolicyHTTP) + if err != nil { + t.Errorf("Failed to generate unstructured Health Check Policy with custom HTTP health check: %v", err) + } + testHealthCheckPolicyHTTPSUnstructured, err := i2gw.CastToUnstructured(&testHealthCheckPolicyHTTPS) + if err != nil { + t.Errorf("Failed to generate unstructured Health Check Policy with custom HTTP health check: %v", err) + } + testHealthCheckPolicyHTTP2Unstructured, err := i2gw.CastToUnstructured(&testHealthCheckPolicyHTTP2) + if err != nil { + t.Errorf("Failed to generate unstructured Health Check Policy with custom HTTP2 health check: %v", err) + } testCases := []struct { name string @@ -287,6 +400,132 @@ func Test_irToGateway(t *testing.T) { }, expectedErrors: field.ErrorList{}, }, + { + name: "ingress with a Backend Config specifying custom HTTP health check", + ir: intermediate.IR{ + Gateways: map[types.NamespacedName]intermediate.GatewayContext{ + {Namespace: testNamespace, Name: testGatewayName}: { + Gateway: testGateway, + }, + }, + HTTPRoutes: map[types.NamespacedName]intermediate.HTTPRouteContext{ + {Namespace: testNamespace, Name: testHTTPRouteName}: { + HTTPRoute: testHTTPRoute, + }, + }, + Services: map[types.NamespacedName]intermediate.ProviderSpecificServiceIR{ + {Namespace: testNamespace, Name: testServiceName}: { + Gce: &intermediate.GceServiceIR{ + HealthCheck: &intermediate.HealthCheckConfig{ + CheckIntervalSec: common.PtrTo(testCheckIntervalSec), + TimeoutSec: common.PtrTo(testTimeoutSec), + HealthyThreshold: common.PtrTo(testHealthyThreshold), + UnhealthyThreshold: common.PtrTo(testUnhealthyThreshold), + Type: common.PtrTo(protocolHTTP), + Port: common.PtrTo(testPort), + RequestPath: common.PtrTo(testRequestPath), + }, + }, + }, + }, + }, + expectedGatewayResources: i2gw.GatewayResources{ + Gateways: map[types.NamespacedName]gatewayv1.Gateway{ + {Namespace: testNamespace, Name: testGatewayName}: testGateway, + }, + HTTPRoutes: map[types.NamespacedName]gatewayv1.HTTPRoute{ + {Namespace: testNamespace, Name: testHTTPRouteName}: testHTTPRoute, + }, + GatewayExtensions: []unstructured.Unstructured{ + *testHealthCheckPolicyHTTPUnstructured, + }, + }, + expectedErrors: field.ErrorList{}, + }, + { + name: "ingress with a Backend Config specifying custom HTTPS health check", + ir: intermediate.IR{ + Gateways: map[types.NamespacedName]intermediate.GatewayContext{ + {Namespace: testNamespace, Name: testGatewayName}: { + Gateway: testGateway, + }, + }, + HTTPRoutes: map[types.NamespacedName]intermediate.HTTPRouteContext{ + {Namespace: testNamespace, Name: testHTTPRouteName}: { + HTTPRoute: testHTTPRoute, + }, + }, + Services: map[types.NamespacedName]intermediate.ProviderSpecificServiceIR{ + {Namespace: testNamespace, Name: testServiceName}: { + Gce: &intermediate.GceServiceIR{ + HealthCheck: &intermediate.HealthCheckConfig{ + CheckIntervalSec: common.PtrTo(testCheckIntervalSec), + TimeoutSec: common.PtrTo(testTimeoutSec), + HealthyThreshold: common.PtrTo(testHealthyThreshold), + UnhealthyThreshold: common.PtrTo(testUnhealthyThreshold), + Type: common.PtrTo(protocolHTTPS), + Port: common.PtrTo(testPort), + RequestPath: common.PtrTo(testRequestPath), + }, + }, + }, + }, + }, + expectedGatewayResources: i2gw.GatewayResources{ + Gateways: map[types.NamespacedName]gatewayv1.Gateway{ + {Namespace: testNamespace, Name: testGatewayName}: testGateway, + }, + HTTPRoutes: map[types.NamespacedName]gatewayv1.HTTPRoute{ + {Namespace: testNamespace, Name: testHTTPRouteName}: testHTTPRoute, + }, + GatewayExtensions: []unstructured.Unstructured{ + *testHealthCheckPolicyHTTPSUnstructured, + }, + }, + expectedErrors: field.ErrorList{}, + }, + { + name: "ingress with a Backend Config specifying custom HTTP2 health check", + ir: intermediate.IR{ + Gateways: map[types.NamespacedName]intermediate.GatewayContext{ + {Namespace: testNamespace, Name: testGatewayName}: { + Gateway: testGateway, + }, + }, + HTTPRoutes: map[types.NamespacedName]intermediate.HTTPRouteContext{ + {Namespace: testNamespace, Name: testHTTPRouteName}: { + HTTPRoute: testHTTPRoute, + }, + }, + Services: map[types.NamespacedName]intermediate.ProviderSpecificServiceIR{ + {Namespace: testNamespace, Name: testServiceName}: { + Gce: &intermediate.GceServiceIR{ + HealthCheck: &intermediate.HealthCheckConfig{ + CheckIntervalSec: common.PtrTo(testCheckIntervalSec), + TimeoutSec: common.PtrTo(testTimeoutSec), + HealthyThreshold: common.PtrTo(testHealthyThreshold), + UnhealthyThreshold: common.PtrTo(testUnhealthyThreshold), + Type: common.PtrTo(protocolHTTP2), + Port: common.PtrTo(testPort), + RequestPath: common.PtrTo(testRequestPath), + }, + }, + }, + }, + }, + expectedGatewayResources: i2gw.GatewayResources{ + Gateways: map[types.NamespacedName]gatewayv1.Gateway{ + {Namespace: testNamespace, Name: testGatewayName}: testGateway, + }, + HTTPRoutes: map[types.NamespacedName]gatewayv1.HTTPRoute{ + {Namespace: testNamespace, Name: testHTTPRouteName}: testHTTPRoute, + }, + GatewayExtensions: []unstructured.Unstructured{ + *testHealthCheckPolicyHTTP2Unstructured, + }, + }, + expectedErrors: field.ErrorList{}, + }, } for _, tc := range testCases { diff --git a/pkg/i2gw/providers/gce/ir_converter_test.go b/pkg/i2gw/providers/gce/ir_converter_test.go index 3ee8ceec..fa323a23 100644 --- a/pkg/i2gw/providers/gce/ir_converter_test.go +++ b/pkg/i2gw/providers/gce/ir_converter_test.go @@ -46,12 +46,21 @@ const ( gExact = gatewayv1.PathMatchExact implSpecificPathType = networkingv1.PathTypeImplementationSpecific - testNamespace = "default" - testHost = "test.mydomain.com" - testServiceName = "test-service" - testBackendConfigName = "test-backendconfig" - testSecurityPolicy = "test-security-policy" - testCookieTTLSec = int64(10) + testNamespace = "default" + testHost = "test.mydomain.com" + testServiceName = "test-service" + testBackendConfigName = "test-backendconfig" + testSecurityPolicy = "test-security-policy" + testCookieTTLSec = int64(10) + testCheckIntervalSec = int64(5) + testTimeoutSec = int64(10) + testHealthyThreshold = int64(2) + testUnhealthyThreshold = int64(3) + protocolHTTP = "HTTP" + protocolHTTPS = "HTTPS" + protocolHTTP2 = "HTTP2" + testPort = int64(8081) + testRequestPath = "/foo" ) func Test_convertToIR(t *testing.T) { @@ -883,6 +892,113 @@ func Test_convertToIR(t *testing.T) { }, expectedErrors: field.ErrorList{}, }, + { + name: "ingress with a Backend Config specifying custom HTTP Health Check", + ingresses: map[types.NamespacedName]*networkingv1.Ingress{ + {Namespace: testNamespace, Name: extIngClassIngressName}: testExtIngress, + }, + services: map[types.NamespacedName]*apiv1.Service{ + {Namespace: testNamespace, Name: testServiceName}: { + ObjectMeta: metav1.ObjectMeta{ + Namespace: testNamespace, + Name: testServiceName, + Annotations: map[string]string{ + backendConfigKey: `{"default":"test-backendconfig"}`, + }, + }, + }, + }, + backendConfigs: map[types.NamespacedName]*backendconfigv1.BackendConfig{ + {Namespace: testNamespace, Name: testBackendConfigName}: { + ObjectMeta: metav1.ObjectMeta{ + Namespace: testNamespace, + Name: testBackendConfigName, + }, + Spec: backendconfigv1.BackendConfigSpec{ + HealthCheck: &backendconfigv1.HealthCheckConfig{ + CheckIntervalSec: common.PtrTo(testCheckIntervalSec), + TimeoutSec: common.PtrTo(testTimeoutSec), + HealthyThreshold: common.PtrTo(testHealthyThreshold), + UnhealthyThreshold: common.PtrTo(testUnhealthyThreshold), + Type: common.PtrTo(protocolHTTP), + Port: common.PtrTo(testPort), + RequestPath: common.PtrTo(testRequestPath), + }, + }, + }, + }, + expectedIR: intermediate.IR{ + Gateways: map[types.NamespacedName]intermediate.GatewayContext{ + {Namespace: testNamespace, Name: gceIngressClass}: { + Gateway: gatewayv1.Gateway{ + ObjectMeta: metav1.ObjectMeta{Name: gceIngressClass, Namespace: testNamespace}, + Spec: gatewayv1.GatewaySpec{ + GatewayClassName: gceL7GlobalExternalManagedGatewayClass, + Listeners: []gatewayv1.Listener{{ + Name: "test-mydomain-com-http", + Port: 80, + Protocol: gatewayv1.HTTPProtocolType, + Hostname: common.PtrTo(gatewayv1.Hostname(testHost)), + }}, + }, + }, + }, + }, + HTTPRoutes: map[types.NamespacedName]intermediate.HTTPRouteContext{ + {Namespace: testNamespace, Name: fmt.Sprintf("%s-test-mydomain-com", extIngClassIngressName)}: { + HTTPRoute: gatewayv1.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{Name: fmt.Sprintf("%s-test-mydomain-com", extIngClassIngressName), Namespace: testNamespace}, + Spec: gatewayv1.HTTPRouteSpec{ + CommonRouteSpec: gatewayv1.CommonRouteSpec{ + ParentRefs: []gatewayv1.ParentReference{{ + Name: gceIngressClass, + }}, + }, + Hostnames: []gatewayv1.Hostname{gatewayv1.Hostname(testHost)}, + Rules: []gatewayv1.HTTPRouteRule{ + { + Matches: []gatewayv1.HTTPRouteMatch{ + { + Path: &gatewayv1.HTTPPathMatch{ + Type: common.PtrTo(gPathPrefix), + Value: common.PtrTo("/"), + }, + }, + }, + BackendRefs: []gatewayv1.HTTPBackendRef{ + { + BackendRef: gatewayv1.BackendRef{ + BackendObjectReference: gatewayv1.BackendObjectReference{ + Name: gatewayv1.ObjectName(testServiceName), + Port: common.PtrTo(gatewayv1.PortNumber(80)), + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + Services: map[types.NamespacedName]intermediate.ProviderSpecificServiceIR{ + {Namespace: testNamespace, Name: testServiceName}: { + Gce: &intermediate.GceServiceIR{ + HealthCheck: &intermediate.HealthCheckConfig{ + CheckIntervalSec: common.PtrTo(testCheckIntervalSec), + TimeoutSec: common.PtrTo(testTimeoutSec), + HealthyThreshold: common.PtrTo(testHealthyThreshold), + UnhealthyThreshold: common.PtrTo(testUnhealthyThreshold), + Type: common.PtrTo(protocolHTTP), + Port: common.PtrTo(testPort), + RequestPath: common.PtrTo(testRequestPath), + }, + }, + }, + }, + }, + expectedErrors: field.ErrorList{}, + }, } for _, tc := range testCases { diff --git a/pkg/i2gw/providers/gce/types.go b/pkg/i2gw/providers/gce/types.go index 974bb745..82acd7de 100644 --- a/pkg/i2gw/providers/gce/types.go +++ b/pkg/i2gw/providers/gce/types.go @@ -28,8 +28,16 @@ const ( betaBackendConfigKey = "beta.cloud.google.com/backend-config" ) -var GCPBackendPolicyGVK = schema.GroupVersionKind{ - Group: "networking.gke.io", - Version: "v1", - Kind: "GCPBackendPolicy", -} +var ( + GCPBackendPolicyGVK = schema.GroupVersionKind{ + Group: "networking.gke.io", + Version: "v1", + Kind: "GCPBackendPolicy", + } + + HealthCheckPolicyGVK = schema.GroupVersionKind{ + Group: "networking.gke.io", + Version: "v1", + Kind: "HealthCheckPolicy", + } +)