From d36d691a0a431abc39bbe2b7635d7b618a7319fa Mon Sep 17 00:00:00 2001 From: Tao Yi Date: Mon, 13 Jan 2025 17:46:10 +0800 Subject: [PATCH] Feat: Add Name in ServiceOptions and allow specifying ingress service name of dataplane (#966) * Support specifying name of dataplane ingress service * add integration test and changelog --- CHANGELOG.md | 7 ++ api/v1beta1/dataplane_types.go | 4 + api/v1beta1/zz_generated.deepcopy.go | 5 + ...ateway-operator.konghq.com_dataplanes.yaml | 5 + ...ator.konghq.com_gatewayconfigurations.yaml | 5 + ...ateway-operator.konghq.com_dataplanes.yaml | 5 + controller/dataplane/owned_resources.go | 9 +- controller/dataplane/owned_resources_test.go | 23 ++++ controller/pkg/builder/builder.go | 7 ++ docs/api-reference.md | 3 + pkg/utils/kubernetes/reduce/reduce.go | 20 +++ pkg/utils/kubernetes/resources/services.go | 25 +++- pkg/utils/test/predicates.go | 16 +++ test/integration/test_dataplane.go | 115 ++++++++++++++++++ .../zz_generated.registered_testcases.go | 1 + 15 files changed, 247 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab2f31620..1dd3fb7b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,13 @@ ## Unreleased +### Added + +- Added `Name` field in `ServiceOptions` to allow specifying name of the + owning service. Currently specifying ingress service of `DataPlane` is + supported. + [#966](https://github.com/Kong/gateway-operator/pull/966) + ### Changed - `KonnectExtension` does not require `spec.serverHostname` to be set by a user diff --git a/api/v1beta1/dataplane_types.go b/api/v1beta1/dataplane_types.go index 7f7b1a494..9df356a98 100644 --- a/api/v1beta1/dataplane_types.go +++ b/api/v1beta1/dataplane_types.go @@ -260,6 +260,10 @@ type ServiceOptions struct { // +kubebuilder:validation:Enum=LoadBalancer;ClusterIP Type corev1.ServiceType `json:"type,omitempty" protobuf:"bytes,4,opt,name=type,casttype=ServiceType"` + // Name defines the name of the service. + // If Name is empty, the controller will generate a service name from the owning object. + Name *string `json:"name,omitempty"` + // Annotations is an unstructured key value map stored with a resource that may be // set by external tools to store and retrieve arbitrary metadata. They are not // queryable and should be preserved when modifying objects. diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index b4c0f42b1..938b313a7 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -1019,6 +1019,11 @@ func (in *Scaling) DeepCopy() *Scaling { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServiceOptions) DeepCopyInto(out *ServiceOptions) { *out = *in + if in.Name != nil { + in, out := &in.Name, &out.Name + *out = new(string) + **out = **in + } if in.Annotations != nil { in, out := &in.Annotations, &out.Annotations *out = make(map[string]string, len(*in)) diff --git a/config/crd/bases/gateway-operator.konghq.com_dataplanes.yaml b/config/crd/bases/gateway-operator.konghq.com_dataplanes.yaml index 7b3d1f05c..a2441cf45 100644 --- a/config/crd/bases/gateway-operator.konghq.com_dataplanes.yaml +++ b/config/crd/bases/gateway-operator.konghq.com_dataplanes.yaml @@ -8989,6 +8989,11 @@ spec: - Cluster - Local type: string + name: + description: |- + Name defines the name of the service. + If Name is empty, the controller will generate a service name from the owning object. + type: string ports: description: |- Ports defines the list of ports that are exposed by the service. diff --git a/config/crd/bases/gateway-operator.konghq.com_gatewayconfigurations.yaml b/config/crd/bases/gateway-operator.konghq.com_gatewayconfigurations.yaml index 5d79cfff5..9d3cf898f 100644 --- a/config/crd/bases/gateway-operator.konghq.com_gatewayconfigurations.yaml +++ b/config/crd/bases/gateway-operator.konghq.com_gatewayconfigurations.yaml @@ -17345,6 +17345,11 @@ spec: - Cluster - Local type: string + name: + description: |- + Name defines the name of the service. + If Name is empty, the controller will generate a service name from the owning object. + type: string type: default: LoadBalancer description: |- diff --git a/config/crd/dataplane/gateway-operator.konghq.com_dataplanes.yaml b/config/crd/dataplane/gateway-operator.konghq.com_dataplanes.yaml index 7b3d1f05c..a2441cf45 100644 --- a/config/crd/dataplane/gateway-operator.konghq.com_dataplanes.yaml +++ b/config/crd/dataplane/gateway-operator.konghq.com_dataplanes.yaml @@ -8989,6 +8989,11 @@ spec: - Cluster - Local type: string + name: + description: |- + Name defines the name of the service. + If Name is empty, the controller will generate a service name from the owning object. + type: string ports: description: |- Ports defines the list of ports that are exposed by the service. diff --git a/controller/dataplane/owned_resources.go b/controller/dataplane/owned_resources.go index 192026f65..1be8e62a5 100644 --- a/controller/dataplane/owned_resources.go +++ b/controller/dataplane/owned_resources.go @@ -295,7 +295,14 @@ func ensureIngressServiceForDataPlane( } count := len(services) - if count > 1 { + if serviceName := k8sresources.GetDataPlaneIngressServiceName(dataPlane); serviceName != "" { + if count > 1 || (count == 1 && services[0].Name != serviceName) { + if err := k8sreduce.ReduceServicesByName(ctx, cl, services, serviceName, dataplane.OwnedObjectPreDeleteHook); err != nil { + return op.Noop, nil, err + } + return op.Noop, nil, errors.New("DataPlane ingress services with different names reduced") + } + } else if count > 1 { if err := k8sreduce.ReduceServices(ctx, cl, services, dataplane.OwnedObjectPreDeleteHook); err != nil { return op.Noop, nil, err } diff --git a/controller/dataplane/owned_resources_test.go b/controller/dataplane/owned_resources_test.go index 8fca676ef..75d9c4e3a 100644 --- a/controller/dataplane/owned_resources_test.go +++ b/controller/dataplane/owned_resources_test.go @@ -30,6 +30,7 @@ func TestEnsureIngressServiceForDataPlane(t *testing.T) { existingServiceModifier func(*testing.T, context.Context, client.Client, *corev1.Service) expectedCreatedOrUpdated op.Result expectedServiceType corev1.ServiceType + expectedServiceName string expectedServicePorts []corev1.ServicePort expectedAnnotations map[string]string expectedLabels map[string]string @@ -264,6 +265,24 @@ func TestEnsureIngressServiceForDataPlane(t *testing.T) { expectedServiceType: corev1.ServiceTypeLoadBalancer, expectedServicePorts: k8sresources.DefaultDataPlaneIngressServicePorts, }, + { + name: "should create service with specified name if name is specified", + dataplane: builder.NewDataPlaneBuilder(). + WithObjectMeta(metav1.ObjectMeta{ + Namespace: "default", + Name: "ingress-service-specified", + }).WithIngressServiceName("ingress-service-1"). + WithIngressServiceType(corev1.ServiceTypeLoadBalancer). + Build(), + existingServiceModifier: func(t *testing.T, ctx context.Context, c client.Client, svc *corev1.Service) { + require.NoError(t, dataplane.OwnedObjectPreDeleteHook(ctx, c, svc)) + require.NoError(t, c.Delete(ctx, svc)) + }, + expectedCreatedOrUpdated: op.Created, + expectedServiceType: corev1.ServiceTypeLoadBalancer, + expectedServiceName: "ingress-service-1", + expectedServicePorts: k8sresources.DefaultDataPlaneIngressServicePorts, + }, } for _, tc := range testCases { @@ -293,6 +312,10 @@ func TestEnsureIngressServiceForDataPlane(t *testing.T) { ) require.NoError(t, err) require.Equal(t, tc.expectedCreatedOrUpdated, res) + // check service name. + if tc.expectedServiceName != "" { + require.Equal(t, tc.expectedServiceName, svc.Name, "should have the same name") + } // check service type. require.Equal(t, tc.expectedServiceType, svc.Spec.Type, "should have the same service type") // check service ports. diff --git a/controller/pkg/builder/builder.go b/controller/pkg/builder/builder.go index ea6367b47..9437be477 100644 --- a/controller/pkg/builder/builder.go +++ b/controller/pkg/builder/builder.go @@ -57,6 +57,13 @@ func (b *testDataPlaneBuilder) WithIngressServiceType(typ corev1.ServiceType) *t return b } +// WithIngressServiceName sets the Name of the Ingress service. +func (b *testDataPlaneBuilder) WithIngressServiceName(name string) *testDataPlaneBuilder { + b.initIngressServiceOptions() + b.dataplane.Spec.DataPlaneOptions.Network.Services.Ingress.Name = &name + return b +} + // WithIngressServiceExternalTrafficPolicy sets the ExternalTrafficPolicy of the Ingress service. func (b *testDataPlaneBuilder) WithIngressServiceExternalTrafficPolicy(typ corev1.ServiceExternalTrafficPolicyType) *testDataPlaneBuilder { b.initIngressServiceOptions() diff --git a/docs/api-reference.md b/docs/api-reference.md index b4dbe1f7c..26ecac7d2 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -2616,6 +2616,7 @@ DataPlaneServiceOptions contains Services related DataPlane configuration. | --- | --- | | `ports` _[DataPlaneServicePort](#dataplaneserviceport) array_ | Ports defines the list of ports that are exposed by the service. The ports field allows defining the name, port and targetPort of the underlying service ports, while the protocol is defaulted to TCP, as it is the only protocol currently supported. | | `type` _[ServiceType](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.28/#servicetype-v1-core)_ | Type determines how the Service is exposed. Defaults to `LoadBalancer`.

Valid options are `LoadBalancer` and `ClusterIP`.

`ClusterIP` allocates a cluster-internal IP address for load-balancing to endpoints.

`LoadBalancer` builds on NodePort and creates an external load-balancer (if supported in the current cloud) which routes to the same endpoints as the clusterIP.

More info: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types | +| `name` _string_ | Name defines the name of the service. If Name is empty, the controller will generate a service name from the owning object. | | `annotations` _object (keys:string, values:string)_ | Annotations is an unstructured key value map stored with a resource that may be set by external tools to store and retrieve arbitrary metadata. They are not queryable and should be preserved when modifying objects.

More info: http://kubernetes.io/docs/user-guide/annotations | | `externalTrafficPolicy` _[ServiceExternalTrafficPolicy](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.28/#serviceexternaltrafficpolicy-v1-core)_ | ExternalTrafficPolicy describes how nodes distribute service traffic they receive on one of the Service's "externally-facing" addresses (NodePorts, ExternalIPs, and LoadBalancer IPs). If set to "Local", the proxy will configure the service in a way that assumes that external load balancers will take care of balancing the service traffic between nodes, and so each node will deliver traffic only to the node-local endpoints of the service, without masquerading the client source IP. (Traffic mistakenly sent to a node with no endpoints will be dropped.) The default value, "Cluster", uses the standard behavior of routing to all endpoints evenly (possibly modified by topology and other features). Note that traffic sent to an External IP or LoadBalancer IP from within the cluster will always get "Cluster" semantics, but clients sending to a NodePort from within the cluster may need to take traffic policy into account when picking a node.

More info: https://kubernetes.io/docs/tasks/access-application-cluster/create-external-load-balancer/#preserving-the-client-source-ip | @@ -2776,6 +2777,7 @@ such as the annotations. | Field | Description | | --- | --- | | `type` _[ServiceType](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.28/#servicetype-v1-core)_ | Type determines how the Service is exposed. Defaults to `LoadBalancer`.

Valid options are `LoadBalancer` and `ClusterIP`.

`ClusterIP` allocates a cluster-internal IP address for load-balancing to endpoints.

`LoadBalancer` builds on NodePort and creates an external load-balancer (if supported in the current cloud) which routes to the same endpoints as the clusterIP.

More info: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types | +| `name` _string_ | Name defines the name of the service. If Name is empty, the controller will generate a service name from the owning object. | | `annotations` _object (keys:string, values:string)_ | Annotations is an unstructured key value map stored with a resource that may be set by external tools to store and retrieve arbitrary metadata. They are not queryable and should be preserved when modifying objects.

More info: http://kubernetes.io/docs/user-guide/annotations | | `externalTrafficPolicy` _[ServiceExternalTrafficPolicy](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.28/#serviceexternaltrafficpolicy-v1-core)_ | ExternalTrafficPolicy describes how nodes distribute service traffic they receive on one of the Service's "externally-facing" addresses (NodePorts, ExternalIPs, and LoadBalancer IPs). If set to "Local", the proxy will configure the service in a way that assumes that external load balancers will take care of balancing the service traffic between nodes, and so each node will deliver traffic only to the node-local endpoints of the service, without masquerading the client source IP. (Traffic mistakenly sent to a node with no endpoints will be dropped.) The default value, "Cluster", uses the standard behavior of routing to all endpoints evenly (possibly modified by topology and other features). Note that traffic sent to an External IP or LoadBalancer IP from within the cluster will always get "Cluster" semantics, but clients sending to a NodePort from within the cluster may need to take traffic policy into account when picking a node.

More info: https://kubernetes.io/docs/tasks/access-application-cluster/create-external-load-balancer/#preserving-the-client-source-ip | @@ -3062,6 +3064,7 @@ such as the annotations. | Field | Description | | --- | --- | | `type` _[ServiceType](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.28/#servicetype-v1-core)_ | Type determines how the Service is exposed. Defaults to `LoadBalancer`.

Valid options are `LoadBalancer` and `ClusterIP`.

`ClusterIP` allocates a cluster-internal IP address for load-balancing to endpoints.

`LoadBalancer` builds on NodePort and creates an external load-balancer (if supported in the current cloud) which routes to the same endpoints as the clusterIP.

More info: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types | +| `name` _string_ | Name defines the name of the service. If Name is empty, the controller will generate a service name from the owning object. | | `annotations` _object (keys:string, values:string)_ | Annotations is an unstructured key value map stored with a resource that may be set by external tools to store and retrieve arbitrary metadata. They are not queryable and should be preserved when modifying objects.

More info: http://kubernetes.io/docs/user-guide/annotations | | `externalTrafficPolicy` _[ServiceExternalTrafficPolicy](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.28/#serviceexternaltrafficpolicy-v1-core)_ | ExternalTrafficPolicy describes how nodes distribute service traffic they receive on one of the Service's "externally-facing" addresses (NodePorts, ExternalIPs, and LoadBalancer IPs). If set to "Local", the proxy will configure the service in a way that assumes that external load balancers will take care of balancing the service traffic between nodes, and so each node will deliver traffic only to the node-local endpoints of the service, without masquerading the client source IP. (Traffic mistakenly sent to a node with no endpoints will be dropped.) The default value, "Cluster", uses the standard behavior of routing to all endpoints evenly (possibly modified by topology and other features). Note that traffic sent to an External IP or LoadBalancer IP from within the cluster will always get "Cluster" semantics, but clients sending to a NodePort from within the cluster may need to take traffic policy into account when picking a node.

More info: https://kubernetes.io/docs/tasks/access-application-cluster/create-external-load-balancer/#preserving-the-client-source-ip | diff --git a/pkg/utils/kubernetes/reduce/reduce.go b/pkg/utils/kubernetes/reduce/reduce.go index adfdc0146..6a45a9ccb 100644 --- a/pkg/utils/kubernetes/reduce/reduce.go +++ b/pkg/utils/kubernetes/reduce/reduce.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/samber/lo" admregv1 "k8s.io/api/admissionregistration/v1" appsv1 "k8s.io/api/apps/v1" autoscalingv2 "k8s.io/api/autoscaling/v2" @@ -132,6 +133,25 @@ func ReduceServices(ctx context.Context, k8sClient client.Client, services []cor return nil } +// ReduceServicesByName deletes all service in the list except the one with specified name (if exists). +// It accepts optional preDeleteHooks which are executed before every Service delete operation. +func ReduceServicesByName(ctx context.Context, k8sClient client.Client, services []corev1.Service, name string, preDeleteHooks ...PreDeleteHook) error { + filteredServices := lo.Filter(services, func(svc corev1.Service, _ int) bool { + return svc.Name != name + }) + for _, service := range filteredServices { + for _, hook := range preDeleteHooks { + if err := hook(ctx, k8sClient, &service); err != nil { + return fmt.Errorf("failed to execute pre delete hook: %w", err) + } + } + if err := k8sClient.Delete(ctx, &service); client.IgnoreNotFound(err) != nil { + return err + } + } + return nil +} + // +kubebuilder:rbac:groups=networking.k8s.io,resources=networkpolicies,verbs=delete // ReduceNetworkPolicies detects the best NetworkPolicy in the set and deletes all the others. diff --git a/pkg/utils/kubernetes/resources/services.go b/pkg/utils/kubernetes/resources/services.go index 143dd2c8a..247e82551 100644 --- a/pkg/utils/kubernetes/resources/services.go +++ b/pkg/utils/kubernetes/resources/services.go @@ -49,8 +49,8 @@ func GenerateNewServiceForCertificateConfig(namespace, name string) *corev1.Serv func GenerateNewIngressServiceForDataPlane(dataplane *operatorv1beta1.DataPlane, opts ...ServiceOpt) (*corev1.Service, error) { svc := &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ - Namespace: dataplane.Namespace, - GenerateName: k8sutils.TrimGenerateName(fmt.Sprintf("%s-ingress-%s-", consts.DataPlanePrefix, dataplane.Name)), + Namespace: dataplane.Namespace, + Labels: map[string]string{ "app": dataplane.Name, consts.DataPlaneServiceTypeLabel: string(consts.DataPlaneIngressServiceLabelValue), @@ -65,6 +65,14 @@ func GenerateNewIngressServiceForDataPlane(dataplane *operatorv1beta1.DataPlane, }, } + // Assign the service name if the DataPlane specifies name of ingress service. + if serviceName := GetDataPlaneIngressServiceName(dataplane); serviceName != "" { + svc.Name = serviceName + } else { + // If the service name is not specified, use the generated name. + svc.GenerateName = k8sutils.TrimGenerateName(fmt.Sprintf("%s-ingress-%s-", consts.DataPlanePrefix, dataplane.Name)) + } + setDataPlaneIngressServiceExternalTrafficPolicy(dataplane, svc) LabelObjectAsDataPlaneManaged(svc) @@ -288,3 +296,16 @@ func GenerateNewAdmissionWebhookServiceForControlPlane(cp *operatorv1beta1.Contr return svc, nil } + +// GetDataPlaneIngressServiceName fetches the specified name of ingress service of dataplane. +// If the service name is not specified, it returns an empty string. +func GetDataPlaneIngressServiceName(dataPlane *operatorv1beta1.DataPlane) string { + if dataPlane == nil { + return "" + } + dpServices := dataPlane.Spec.DataPlaneOptions.Network.Services + if dpServices == nil || dpServices.Ingress == nil || dpServices.Ingress.Name == nil { + return "" + } + return *dpServices.Ingress.Name +} diff --git a/pkg/utils/test/predicates.go b/pkg/utils/test/predicates.go index dd8475f2c..1d9d07f9d 100644 --- a/pkg/utils/test/predicates.go +++ b/pkg/utils/test/predicates.go @@ -470,6 +470,22 @@ func DataPlaneHasActiveService(t *testing.T, ctx context.Context, dataplaneName }, clients.OperatorClient) } +// DataPlaneHasActiveServiceWithName is a helper function for tests that returns a function +// that can be used to check if a DataPlane has an active proxy service with the specified name. +// Should be used in conjunction with require.Eventually or assert.Eventually. +func DataPlaneHasActiveServiceWithName(t *testing.T, ctx context.Context, dataplaneName types.NamespacedName, ret *corev1.Service, clients K8sClients, matchingLabels client.MatchingLabels, name string) func() bool { + return DataPlanePredicate(t, ctx, dataplaneName, func(dataplane *operatorv1beta1.DataPlane) bool { + services := MustListDataPlaneServices(t, ctx, dataplane, clients.MgrClient, matchingLabels) + if len(services) == 1 && services[0].Name == name { + if ret != nil { + *ret = services[0] + } + return true + } + return false + }, clients.OperatorClient) +} + // DataPlaneServiceHasNActiveEndpoints is a helper function for tests that returns a function // that can be used to check if a Service has active endpoints. // Should be used in conjunction with require.Eventually or assert.Eventually. diff --git a/test/integration/test_dataplane.go b/test/integration/test_dataplane.go index 01b66944b..d3ad3c27a 100644 --- a/test/integration/test_dataplane.go +++ b/test/integration/test_dataplane.go @@ -1033,3 +1033,118 @@ func TestDataPlaneServiceExternalTrafficPolicy(t *testing.T) { }), waitTime, tickTime) verifyEventuallyExternalTrafficPolicy(t, dataplaneName, corev1.ServiceExternalTrafficPolicyLocal) } + +func TestDataPlaneSpecifyingServiceName(t *testing.T) { + t.Parallel() + namespace, cleaner := helpers.SetupTestEnv(t, GetCtx(), GetEnv()) + + serviceName := "ingress-service-" + uuid.NewString() + t.Logf("deploying dataplane resource with service name specified to %s", serviceName) + dataplaneName := types.NamespacedName{ + Namespace: namespace.Name, + Name: uuid.NewString(), + } + dataplane := &operatorv1beta1.DataPlane{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: dataplaneName.Namespace, + Name: dataplaneName.Name, + }, + Spec: operatorv1beta1.DataPlaneSpec{ + DataPlaneOptions: operatorv1beta1.DataPlaneOptions{ + Deployment: operatorv1beta1.DataPlaneDeploymentOptions{ + DeploymentOptions: operatorv1beta1.DeploymentOptions{ + PodTemplateSpec: &corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Env: []corev1.EnvVar{ + { + Name: "TEST_ENV", + Value: "test", + }, + }, + Name: consts.DataPlaneProxyContainerName, + Image: helpers.GetDefaultDataPlaneImage(), + }, + }, + }, + }, + }, + }, + Network: operatorv1beta1.DataPlaneNetworkOptions{ + Services: &operatorv1beta1.DataPlaneServices{ + Ingress: &operatorv1beta1.DataPlaneServiceOptions{ + ServiceOptions: operatorv1beta1.ServiceOptions{ + Name: &serviceName, + }, + }, + }, + }, + }, + }, + } + dataplaneClient := GetClients().OperatorClient.ApisV1beta1().DataPlanes(namespace.Name) + dataplane, err := dataplaneClient.Create(GetCtx(), dataplane, metav1.CreateOptions{}) + require.NoError(t, err) + cleaner.Add(dataplane) + + t.Log("verifying that dataplane is provisioned") + require.Eventually(t, testutils.DataPlaneIsReady(t, GetCtx(), dataplaneName, GetClients().OperatorClient), waitTime, tickTime) + + t.Logf("verifying that ingress service with the specified name '%s' is created", serviceName) + require.Eventually(t, testutils.DataPlaneHasActiveServiceWithName(t, GetCtx(), dataplaneName, nil, clients, client.MatchingLabels{ + consts.GatewayOperatorManagedByLabel: consts.DataPlaneManagedLabelValue, + consts.DataPlaneServiceTypeLabel: string(consts.DataPlaneIngressServiceLabelValue), + }, serviceName, + ), waitTime, tickTime) + + t.Log("verifying dataplane services receive IP addresses") + var dataplaneIP string + require.Eventually(t, func() bool { + dataplaneService, err := GetClients().K8sClient.CoreV1().Services(dataplane.Namespace).Get(GetCtx(), serviceName, metav1.GetOptions{}) + require.NoError(t, err) + if len(dataplaneService.Status.LoadBalancer.Ingress) > 0 { + dataplaneIP = dataplaneService.Status.LoadBalancer.Ingress[0].IP + return true + } + return false + }, waitTime, tickTime) + + require.Eventually(t, Expect404WithNoRouteFunc(t, GetCtx(), "http://"+dataplaneIP), waitTime, tickTime) + + oldServiceName := serviceName + serviceName = "ingress-service-" + uuid.NewString() + t.Logf("updating ingress service name from '%s' to '%s' in dataplane", oldServiceName, serviceName) + require.Eventually(t, + testutils.DataPlaneUpdateEventually(t, GetCtx(), dataplaneName, clients, func(dp *operatorv1beta1.DataPlane) { + dp.Spec.Network.Services.Ingress.Name = &serviceName + }), + time.Minute, time.Second, + ) + + t.Logf("verifying that ingress service with the new name '%s' is created", serviceName) + require.Eventually(t, testutils.DataPlaneHasActiveServiceWithName(t, GetCtx(), dataplaneName, nil, clients, client.MatchingLabels{ + consts.GatewayOperatorManagedByLabel: consts.DataPlaneManagedLabelValue, + consts.DataPlaneServiceTypeLabel: string(consts.DataPlaneIngressServiceLabelValue), + }, serviceName, + ), waitTime, tickTime) + + t.Logf("verifying that the old ingress service '%s' is deleted", oldServiceName) + require.Eventually(t, func() bool { + _, err := clients.K8sClient.CoreV1().Services(dataplane.Namespace).Get(GetCtx(), oldServiceName, metav1.GetOptions{}) + return err != nil && k8serrors.IsNotFound(err) + }, waitTime, tickTime) + + t.Log("verifying dataplane services receive IP addresses after service name is updated") + require.Eventually(t, func() bool { + dataplaneService, err := GetClients().K8sClient.CoreV1().Services(dataplane.Namespace).Get(GetCtx(), serviceName, metav1.GetOptions{}) + require.NoError(t, err) + if len(dataplaneService.Status.LoadBalancer.Ingress) > 0 { + dataplaneIP = dataplaneService.Status.LoadBalancer.Ingress[0].IP + return true + } + return false + }, waitTime, tickTime) + + require.Eventually(t, Expect404WithNoRouteFunc(t, GetCtx(), "http://"+dataplaneIP), waitTime, tickTime) +} diff --git a/test/integration/zz_generated.registered_testcases.go b/test/integration/zz_generated.registered_testcases.go index 8900affa5..2267ba26b 100644 --- a/test/integration/zz_generated.registered_testcases.go +++ b/test/integration/zz_generated.registered_testcases.go @@ -14,6 +14,7 @@ func init() { TestDataPlaneHorizontalScaling, TestDataPlanePodDisruptionBudget, TestDataPlaneServiceExternalTrafficPolicy, + TestDataPlaneSpecifyingServiceName, TestDataPlaneUpdate, TestDataPlaneValidation, TestDataPlaneVolumeMounts,