diff --git a/CHANGELOG.md b/CHANGELOG.md index ae68b18c4..b0fbe048c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ - (Feature) (Scheduler) Add Status Conditions - (Bugfix) Versioning Alignment - (Feature) (Scheduler) Merge Strategy +- (Feature) (Networking) Endpoints Destination ## [1.2.42](https://github.com/arangodb/kube-arangodb/tree/1.2.42) (2024-07-23) - (Maintenance) Go 1.22.4 & Kubernetes 1.29.6 libraries diff --git a/docs/api/ArangoRoute.V1Alpha1.md b/docs/api/ArangoRoute.V1Alpha1.md index 2dc3b2302..fea374ee0 100644 --- a/docs/api/ArangoRoute.V1Alpha1.md +++ b/docs/api/ArangoRoute.V1Alpha1.md @@ -18,19 +18,72 @@ Deployment specifies the ArangoDeployment object name ### .spec.destination.authentication.passMode -Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.42/pkg/apis/networking/v1alpha1/route_spec_destination_authentication.go#L28) +Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.42/pkg/apis/networking/v1alpha1/route_spec_destination_authentication.go#L32) + +PassMode define authorization details pass mode when authorization was successful + +Possible Values: +* `"override"` (default) - Generates new token for the user +* `"pass"` - Pass token provided by the user +* `"remove"` - Removes authorization details from the request *** ### .spec.destination.authentication.type -Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.42/pkg/apis/networking/v1alpha1/route_spec_destination_authentication.go#L29) +Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.42/pkg/apis/networking/v1alpha1/route_spec_destination_authentication.go#L37) + +Type of the authentication + +Possible Values: +* `"optional"` (default) - Authentication is header is validated and passed to the service. In case if is unauthorized, requests is still passed +* `"required"` - Authentication is header is validated and passed to the service. In case if is unauthorized, returns 403 + +*** + +### .spec.destination.endpoints.checksum + +Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.42/pkg/apis/shared/v1/object.go#L61) + +UID keeps the information about object Checksum + +*** + +### .spec.destination.endpoints.name + +Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.42/pkg/apis/shared/v1/object.go#L52) + +Name of the object + +*** + +### .spec.destination.endpoints.namespace + +Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.42/pkg/apis/shared/v1/object.go#L55) + +Namespace of the object. Should default to the namespace of the parent object + +*** + +### .spec.destination.endpoints.port + +Type: `intstr.IntOrString` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.42/pkg/apis/networking/v1alpha1/route_spec_destination_endpoint.go#L36) + +Port defines Port or Port Name used as destination + +*** + +### .spec.destination.endpoints.uid + +Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.42/pkg/apis/shared/v1/object.go#L58) + +UID keeps the information about object UID *** ### .spec.destination.path -Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.42/pkg/apis/networking/v1alpha1/route_spec_destination.go#L36) +Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.42/pkg/apis/networking/v1alpha1/route_spec_destination.go#L39) Path defines service path used for overrides @@ -38,7 +91,7 @@ Path defines service path used for overrides ### .spec.destination.schema -Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.42/pkg/apis/networking/v1alpha1/route_spec_destination.go#L30) +Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.42/pkg/apis/networking/v1alpha1/route_spec_destination.go#L33) Schema defines HTTP/S schema used for connection @@ -70,13 +123,10 @@ Namespace of the object. Should default to the namespace of the parent object ### .spec.destination.service.port -Type: `intstr.IntOrString` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.42/pkg/apis/networking/v1alpha1/route_spec_destination_service.go#L36) +Type: `intstr.IntOrString` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.42/pkg/apis/networking/v1alpha1/route_spec_destination_service.go#L35) Port defines Port or Port Name used as destination -Links: -* [Documentation](https://kubernetes.io/docs/tasks/administer-cluster/sysctl-cluster/) - *** ### .spec.destination.service.uid @@ -169,7 +219,7 @@ Type: `integer` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1. ### .status.target.path -Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.42/pkg/apis/networking/v1alpha1/route_status_target.go#L40) +Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.42/pkg/apis/networking/v1alpha1/route_status_target.go#L43) Path specifies request path override @@ -181,3 +231,11 @@ Type: `boolean` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1. Insecure allows Insecure traffic +*** + +### .status.target.type + +Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.42/pkg/apis/networking/v1alpha1/route_status_target.go#L34) + +Type define destination type + diff --git a/pkg/apis/networking/v1alpha1/route_spec_destination.go b/pkg/apis/networking/v1alpha1/route_spec_destination.go index 055fd6cbf..1dfa0ab75 100644 --- a/pkg/apis/networking/v1alpha1/route_spec_destination.go +++ b/pkg/apis/networking/v1alpha1/route_spec_destination.go @@ -26,6 +26,9 @@ type ArangoRouteSpecDestination struct { // Service defines service upstream reference Service *ArangoRouteSpecDestinationService `json:"service,omitempty"` + // Endpoints defines service upstream reference - which is used to find endpoints + Endpoints *ArangoRouteSpecDestinationEndpoints `json:"endpoints,omitempty"` + // Schema defines HTTP/S schema used for connection Schema *ArangoRouteSpecDestinationSchema `json:"schema,omitempty"` @@ -47,6 +50,14 @@ func (a *ArangoRouteSpecDestination) GetService() *ArangoRouteSpecDestinationSer return a.Service } +func (a *ArangoRouteSpecDestination) GetEndpoints() *ArangoRouteSpecDestinationEndpoints { + if a == nil || a.Endpoints == nil { + return nil + } + + return a.Endpoints +} + func (a *ArangoRouteSpecDestination) GetSchema() *ArangoRouteSpecDestinationSchema { if a == nil || a.Schema == nil { return nil @@ -85,7 +96,9 @@ func (a *ArangoRouteSpecDestination) Validate() error { } if err := shared.WithErrors( + shared.ValidateExclusiveFields(a, 1, "Service", "Endpoints"), shared.ValidateOptionalInterfacePath("service", a.Service), + shared.ValidateOptionalInterfacePath("endpoints", a.Endpoints), shared.ValidateOptionalInterfacePath("schema", a.Schema), shared.ValidateOptionalInterfacePath("tls", a.TLS), shared.ValidateOptionalInterfacePath("authentication", a.Authentication), diff --git a/pkg/apis/networking/v1alpha1/route_spec_destination_authentication.go b/pkg/apis/networking/v1alpha1/route_spec_destination_authentication.go index 9ef89aba8..00ac298df 100644 --- a/pkg/apis/networking/v1alpha1/route_spec_destination_authentication.go +++ b/pkg/apis/networking/v1alpha1/route_spec_destination_authentication.go @@ -25,8 +25,16 @@ import ( ) type ArangoRouteSpecDestinationAuthentication struct { + // PassMode define authorization details pass mode when authorization was successful + // +doc/enum: override|Generates new token for the user + // +doc/enum: pass|Pass token provided by the user + // +doc/enum: remove|Removes authorization details from the request PassMode *ArangoRouteSpecAuthenticationPassMode `json:"passMode,omitempty"` - Type *ArangoRouteSpecAuthenticationType `json:"type,omitempty"` + + // Type of the authentication + // +doc/enum: optional|Authentication is header is validated and passed to the service. In case if is unauthorized, requests is still passed + // +doc/enum: required|Authentication is header is validated and passed to the service. In case if is unauthorized, returns 403 + Type *ArangoRouteSpecAuthenticationType `json:"type,omitempty"` } func (a *ArangoRouteSpecDestinationAuthentication) GetType() ArangoRouteSpecAuthenticationType { diff --git a/pkg/apis/networking/v1alpha1/route_spec_destination_endpoint.go b/pkg/apis/networking/v1alpha1/route_spec_destination_endpoint.go new file mode 100644 index 000000000..bd7d9e147 --- /dev/null +++ b/pkg/apis/networking/v1alpha1/route_spec_destination_endpoint.go @@ -0,0 +1,59 @@ +// +// DISCLAIMER +// +// Copyright 2024 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/util/intstr" + + shared "github.com/arangodb/kube-arangodb/pkg/apis/shared" + sharedApi "github.com/arangodb/kube-arangodb/pkg/apis/shared/v1" +) + +type ArangoRouteSpecDestinationEndpoints struct { + // Keeps information on the service, which maps then to the endpoints + *sharedApi.Object `json:",inline,omitempty"` + + // Port defines Port or Port Name used as destination + // +doc/type: intstr.IntOrString + Port *intstr.IntOrString `json:"port,omitempty"` +} + +func (a *ArangoRouteSpecDestinationEndpoints) GetPort() *intstr.IntOrString { + if a == nil || a.Port == nil { + return nil + } + + return a.Port +} + +func (a *ArangoRouteSpecDestinationEndpoints) Validate() error { + if a == nil { + a = &ArangoRouteSpecDestinationEndpoints{} + } + + if err := shared.WithErrors(a.Object.Validate(), shared.ValidateRequiredPath("port", a.Port, func(i intstr.IntOrString) error { + return nil + })); err != nil { + return err + } + + return nil +} diff --git a/pkg/apis/networking/v1alpha1/route_spec_destination_service.go b/pkg/apis/networking/v1alpha1/route_spec_destination_service.go index 9b2c0d9f4..e44a4cbbd 100644 --- a/pkg/apis/networking/v1alpha1/route_spec_destination_service.go +++ b/pkg/apis/networking/v1alpha1/route_spec_destination_service.go @@ -32,7 +32,6 @@ type ArangoRouteSpecDestinationService struct { // Port defines Port or Port Name used as destination // +doc/type: intstr.IntOrString - // +doc/link: Documentation|https://kubernetes.io/docs/tasks/administer-cluster/sysctl-cluster/ Port *intstr.IntOrString `json:"port,omitempty"` } diff --git a/pkg/apis/networking/v1alpha1/route_status_target.go b/pkg/apis/networking/v1alpha1/route_status_target.go index 26d67b1d3..8ec626b07 100644 --- a/pkg/apis/networking/v1alpha1/route_status_target.go +++ b/pkg/apis/networking/v1alpha1/route_status_target.go @@ -30,6 +30,9 @@ type ArangoRouteStatusTarget struct { // Destinations keeps target destinations Destinations ArangoRouteStatusTargetDestinations `json:"destinations,omitempty"` + // Type define destination type + Type ArangoRouteStatusTargetType `json:"type,omitempty"` + // TLS Keeps target TLS Settings (if not nil, TLS is enabled) TLS *ArangoRouteStatusTargetTLS `json:"TLS,omitempty"` @@ -64,5 +67,5 @@ func (a *ArangoRouteStatusTarget) Hash() string { if a == nil { return "" } - return util.SHA256FromStringArray(a.Destinations.Hash(), a.TLS.Hash(), a.Path, a.Authentication.Hash()) + return util.SHA256FromStringArray(a.Destinations.Hash(), a.Type.Hash(), a.TLS.Hash(), a.Path, a.Authentication.Hash()) } diff --git a/pkg/apis/networking/v1alpha1/route_status_target_type.go b/pkg/apis/networking/v1alpha1/route_status_target_type.go new file mode 100644 index 000000000..d88340012 --- /dev/null +++ b/pkg/apis/networking/v1alpha1/route_status_target_type.go @@ -0,0 +1,34 @@ +// +// DISCLAIMER +// +// Copyright 2024 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package v1alpha1 + +import "github.com/arangodb/kube-arangodb/pkg/util" + +type ArangoRouteStatusTargetType string + +func (a ArangoRouteStatusTargetType) Hash() string { + return util.SHA256FromString(string(a)) +} + +const ( + ArangoRouteStatusTargetServiceType ArangoRouteStatusTargetType = "service" + ArangoRouteStatusTargetEndpointsType ArangoRouteStatusTargetType = "endpoints" +) diff --git a/pkg/apis/networking/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/networking/v1alpha1/zz_generated.deepcopy.go index 31e9cff51..fa67283f8 100644 --- a/pkg/apis/networking/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/networking/v1alpha1/zz_generated.deepcopy.go @@ -132,6 +132,11 @@ func (in *ArangoRouteSpecDestination) DeepCopyInto(out *ArangoRouteSpecDestinati *out = new(ArangoRouteSpecDestinationService) (*in).DeepCopyInto(*out) } + if in.Endpoints != nil { + in, out := &in.Endpoints, &out.Endpoints + *out = new(ArangoRouteSpecDestinationEndpoints) + (*in).DeepCopyInto(*out) + } if in.Schema != nil { in, out := &in.Schema, &out.Schema *out = new(ArangoRouteSpecDestinationSchema) @@ -191,6 +196,32 @@ func (in *ArangoRouteSpecDestinationAuthentication) DeepCopy() *ArangoRouteSpecD return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ArangoRouteSpecDestinationEndpoints) DeepCopyInto(out *ArangoRouteSpecDestinationEndpoints) { + *out = *in + if in.Object != nil { + in, out := &in.Object, &out.Object + *out = new(v1.Object) + (*in).DeepCopyInto(*out) + } + if in.Port != nil { + in, out := &in.Port, &out.Port + *out = new(intstr.IntOrString) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ArangoRouteSpecDestinationEndpoints. +func (in *ArangoRouteSpecDestinationEndpoints) DeepCopy() *ArangoRouteSpecDestinationEndpoints { + if in == nil { + return nil + } + out := new(ArangoRouteSpecDestinationEndpoints) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ArangoRouteSpecDestinationService) DeepCopyInto(out *ArangoRouteSpecDestinationService) { *out = *in diff --git a/pkg/apis/shared/validate.go b/pkg/apis/shared/validate.go index 431b5f241..b9da30205 100644 --- a/pkg/apis/shared/validate.go +++ b/pkg/apis/shared/validate.go @@ -24,11 +24,13 @@ import ( "fmt" "reflect" "regexp" + "strings" "github.com/google/uuid" core "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" + "github.com/arangodb/kube-arangodb/pkg/util" "github.com/arangodb/kube-arangodb/pkg/util/errors" ) @@ -243,3 +245,108 @@ func ValidateServiceType(st core.ServiceType) error { } return errors.Errorf("Unsupported service type %s", st) } + +// ValidateExclusiveFields check if fields are defined in exclusive way +func ValidateExclusiveFields(in any, expected int, fields ...string) error { + v := reflect.ValueOf(in) + t := v.Type() + + if expected > len(fields) { + return errors.Errorf("Expected more fields than allowed") + } + + if t.Kind() == reflect.Pointer { + if !v.IsValid() { + return errors.Errorf("Invalid reference") + } + + if v.IsZero() { + // Skip in case of zero value + return nil + } + + // We got a ptr, detach type + return ValidateExclusiveFields(v.Elem().Interface(), expected, fields...) + } + + if t.Kind() == reflect.Struct { + foundFields := map[string]bool{} + for _, field := range fields { + tf, ok := t.FieldByName(field) + if !ok { + continue + } + + n := field + if tg, ok := tf.Tag.Lookup("json"); ok { + p := strings.Split(tg, ",")[0] + if p != "" { + n = p + } + } + foundFields[n] = false + + vf := v.FieldByName(field) + if !vf.IsValid() { + continue + } + if vf.IsZero() { + // Empty or nil + continue + } + + foundFields[n] = true + } + + existing := util.Sort(util.FlattenList(util.FormatList(util.Extract(foundFields), func(a util.KV[string, bool]) []string { + if a.V { + return []string{a.K} + } + + return nil + })), func(i, j string) bool { + return i < j + }) + + missing := util.Sort(util.FlattenList(util.FormatList(util.Extract(foundFields), func(a util.KV[string, bool]) []string { + if !a.V { + return []string{a.K} + } + + return nil + })), func(i, j string) bool { + return i < j + }) + + all := util.SortKeys(foundFields) + + if len(existing) != expected { + // We did not get all fields, check condition + if len(existing) == 0 { + return errors.Errorf("Elements not provided. Expected %d. Possible: %s", + expected, + strings.Join(all, ", "), + ) + } + if len(existing) < expected { + return errors.Errorf("Not enough elements provided. Expected %d, got %d. Defined: %s, Additionally Possible: %s", + expected, + len(existing), + strings.Join(existing, ", "), + strings.Join(missing, ", "), + ) + } + if len(existing) > expected { + return errors.Errorf("Too many elements provided. Expected %d, got %d. Defined: %s", + expected, + len(existing), + strings.Join(existing, ", "), + ) + } + } + + return nil + } + + return errors.Errorf("Invalid reference") +} diff --git a/pkg/apis/shared/validate_test.go b/pkg/apis/shared/validate_test.go index 31b7f6787..4a0dc4e93 100644 --- a/pkg/apis/shared/validate_test.go +++ b/pkg/apis/shared/validate_test.go @@ -42,3 +42,41 @@ func Test_ValidateAPIPath(t *testing.T) { require.NoError(t, ValidateAPIPath("/api/test/2/")) require.Error(t, ValidateAPIPath("/&/")) } + +func Test_ValidateExclusiveFields(t *testing.T) { + type z struct { + A string `json:"a,omitempty"` + B string `json:"b,omitempty"` + C string `json:"c,omitempty"` + D string `json:"d,omitempty"` + } + + require.EqualError(t, ValidateExclusiveFields(z{}, 1, "A"), "Elements not provided. Expected 1. Possible: a") + + require.NoError(t, ValidateExclusiveFields(z{ + A: "test", + }, 1, "A")) + + require.EqualError(t, ValidateExclusiveFields(z{ + A: "test", + }, 2, "A"), "Expected more fields than allowed") + + require.EqualError(t, ValidateExclusiveFields(z{ + A: "test", + }, 2, "A", "B"), "Not enough elements provided. Expected 2, got 1. Defined: a, Additionally Possible: b") + + require.NoError(t, ValidateExclusiveFields(z{ + A: "test", + B: "test", + }, 2, "A", "B")) + + require.EqualError(t, ValidateExclusiveFields(z{ + A: "test", + B: "test", + }, 1, "A", "B"), "Too many elements provided. Expected 1, got 2. Defined: a, b") + + require.NoError(t, ValidateExclusiveFields(z{ + A: "test", + D: "test", + }, 2, "A", "B", "C", "D")) +} diff --git a/pkg/crd/crds/networking-route.schema.generated.yaml b/pkg/crd/crds/networking-route.schema.generated.yaml index d973e59fb..9d82639de 100644 --- a/pkg/crd/crds/networking-route.schema.generated.yaml +++ b/pkg/crd/crds/networking-route.schema.generated.yaml @@ -13,8 +13,37 @@ v1alpha1: description: Authentication defines auth methods properties: passMode: + description: PassMode define authorization details pass mode when authorization was successful + enum: + - override + - pass + - remove type: string type: + description: Type of the authentication + enum: + - optional + - required + type: string + type: object + endpoints: + description: Endpoints defines service upstream reference - which is used to find endpoints + properties: + checksum: + description: UID keeps the information about object Checksum + type: string + name: + description: Name of the object + type: string + namespace: + description: Namespace of the object. Should default to the namespace of the parent object + type: string + port: + description: Port defines Port or Port Name used as destination + type: string + x-kubernetes-int-or-string: true + uid: + description: UID keeps the information about object UID type: string type: object path: diff --git a/pkg/deployment/cleanup.go b/pkg/deployment/cleanup.go index f0e359bd2..6e673ccfd 100644 --- a/pkg/deployment/cleanup.go +++ b/pkg/deployment/cleanup.go @@ -44,7 +44,7 @@ func (d *Deployment) removePodFinalizers(ctx context.Context, cachedStatus inspe if err := cachedStatus.Pod().V1().Iterate(func(pod *core.Pod) error { log.Str("pod", pod.GetName()).Info("Removing Pod Finalizer") - if count, err := k8sutil.RemovePodFinalizers(ctx, cachedStatus, d.PodsModInterface(), pod, constants.ManagedFinalizers(), true); err != nil { + if count, err := k8sutil.RemoveSelectedFinalizers[*core.Pod](ctx, cachedStatus.Pod().V1().Read(), cachedStatus.PodsModInterface().V1(), pod, constants.ManagedFinalizers(), true); err != nil { log.Err(err).Warn("Failed to remove pod finalizers") return err } else if count > 0 { @@ -78,7 +78,7 @@ func (d *Deployment) removePVCFinalizers(ctx context.Context, cachedStatus inspe if err := cachedStatus.PersistentVolumeClaim().V1().Iterate(func(pvc *core.PersistentVolumeClaim) error { log.Str("pvc", pvc.GetName()).Info("Removing PVC Finalizer") - if count, err := k8sutil.RemovePVCFinalizers(ctx, cachedStatus, d.PersistentVolumeClaimsModInterface(), pvc, constants.ManagedFinalizers(), true); err != nil { + if count, err := k8sutil.RemoveSelectedFinalizers[*core.PersistentVolumeClaim](ctx, cachedStatus.PersistentVolumeClaim().V1().Read(), cachedStatus.PersistentVolumeClaimsModInterface().V1(), pvc, constants.ManagedFinalizers(), true); err != nil { log.Err(err).Warn("Failed to remove PVC finalizers") return err } else if count > 0 { diff --git a/pkg/deployment/context_impl.go b/pkg/deployment/context_impl.go index d62659885..a2c1b0668 100644 --- a/pkg/deployment/context_impl.go +++ b/pkg/deployment/context_impl.go @@ -399,7 +399,7 @@ func (d *Deployment) RemovePodFinalizers(ctx context.Context, podName string) er return errors.WithStack(err) } - _, err = k8sutil.RemovePodFinalizers(ctx, d.GetCachedStatus(), d.PodsModInterface(), p, p.GetFinalizers(), true) + _, err = k8sutil.RemoveSelectedFinalizers[*core.Pod](ctx, d.GetCachedStatus().Pod().V1().Read(), d.GetCachedStatus().PodsModInterface().V1(), p, p.GetFinalizers(), true) if err != nil { return errors.WithStack(err) } diff --git a/pkg/deployment/deployment_finalizers.go b/pkg/deployment/deployment_finalizers.go index 45192a410..badf51a57 100644 --- a/pkg/deployment/deployment_finalizers.go +++ b/pkg/deployment/deployment_finalizers.go @@ -26,7 +26,6 @@ import ( meta "k8s.io/apimachinery/pkg/apis/meta/v1" api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" - "github.com/arangodb/kube-arangodb/pkg/generated/clientset/versioned" "github.com/arangodb/kube-arangodb/pkg/util/constants" "github.com/arangodb/kube-arangodb/pkg/util/errors" "github.com/arangodb/kube-arangodb/pkg/util/globals" @@ -90,7 +89,8 @@ func (d *Deployment) runDeploymentFinalizers(ctx context.Context, cachedStatus i } // Remove finalizers (if needed) if len(removalList) > 0 { - if err := removeDeploymentFinalizers(ctx, d.deps.Client.Arango(), updated, removalList); err != nil { + c := d.deps.Client.Arango().DatabaseV1().ArangoDeployments(updated.GetNamespace()) + if _, err := k8sutil.RemoveSelectedFinalizers[*api.ArangoDeployment](ctx, c, c, updated, removalList, false); err != nil { d.log.Err(err).Debug("Failed to update ArangoDeployment (to remove finalizers)") return errors.WithStack(err) } @@ -116,36 +116,3 @@ func (d *Deployment) inspectRemoveChildFinalizers(ctx context.Context, _ *api.Ar return retry, nil } - -// removeDeploymentFinalizers removes the given finalizers from the given PVC. -func removeDeploymentFinalizers(ctx context.Context, cli versioned.Interface, - depl *api.ArangoDeployment, finalizers []string) error { - depls := cli.DatabaseV1().ArangoDeployments(depl.GetNamespace()) - getFunc := func() (meta.Object, error) { - ctxChild, cancel := globals.GetGlobalTimeouts().Kubernetes().WithTimeout(ctx) - defer cancel() - - result, err := depls.Get(ctxChild, depl.GetName(), meta.GetOptions{}) - if err != nil { - return nil, errors.WithStack(err) - } - return result, nil - } - updateFunc := func(updated meta.Object) error { - updatedDepl := updated.(*api.ArangoDeployment) - ctxChild, cancel := globals.GetGlobalTimeouts().Kubernetes().WithTimeout(ctx) - defer cancel() - - result, err := depls.Update(ctxChild, updatedDepl, meta.UpdateOptions{}) - if err != nil { - return errors.WithStack(err) - } - *depl = *result - return nil - } - ignoreNotFound := false - if _, err := k8sutil.RemoveFinalizers(finalizers, getFunc, updateFunc, ignoreNotFound); err != nil { - return errors.WithStack(err) - } - return nil -} diff --git a/pkg/deployment/resources/pod_finalizers.go b/pkg/deployment/resources/pod_finalizers.go index 5e8a53bc8..2ce78d1d7 100644 --- a/pkg/deployment/resources/pod_finalizers.go +++ b/pkg/deployment/resources/pod_finalizers.go @@ -115,7 +115,7 @@ func (r *Resources) runPodFinalizers(ctx context.Context, p *core.Pod, memberSta } // Remove finalizers (if needed) if len(removalList) > 0 { - if _, err := k8sutil.RemovePodFinalizers(ctx, r.context.ACS().CurrentClusterCache(), r.context.ACS().CurrentClusterCache().PodsModInterface().V1(), p, removalList, false); err != nil { + if _, err := k8sutil.RemoveSelectedFinalizers[*core.Pod](ctx, r.context.ACS().CurrentClusterCache().Pod().V1().Read(), r.context.ACS().CurrentClusterCache().PodsModInterface().V1(), p, removalList, false); err != nil { log.Err(err).Debug("Failed to update pod (to remove finalizers)") return 0, errors.WithStack(err) } diff --git a/pkg/deployment/resources/pod_inspector.go b/pkg/deployment/resources/pod_inspector.go index 1d6e2e3b8..bf85c699a 100644 --- a/pkg/deployment/resources/pod_inspector.go +++ b/pkg/deployment/resources/pod_inspector.go @@ -117,7 +117,7 @@ func (r *Resources) InspectPods(ctx context.Context, cachedStatus inspectorInter // Strange, pod belongs to us, but we have no member for it. // Remove all finalizers, so it can be removed. log.Str("pod", pod.GetName()).Warn("Pod belongs to this deployment, but we don't know the member. Removing all finalizers") - _, err := k8sutil.RemovePodFinalizers(ctx, r.context.ACS().CurrentClusterCache(), cachedStatus.PodsModInterface().V1(), pod, pod.GetFinalizers(), false) + _, err := k8sutil.RemoveSelectedFinalizers[*core.Pod](ctx, r.context.ACS().CurrentClusterCache().Pod().V1().Read(), r.context.ACS().CurrentClusterCache().PodsModInterface().V1(), pod, pod.GetFinalizers(), false) if err != nil { log.Str("pod", pod.GetName()).Err(err).Debug("Failed to update pod (to remove all finalizers)") return errors.WithStack(err) diff --git a/pkg/deployment/resources/pvc_finalizers.go b/pkg/deployment/resources/pvc_finalizers.go index 537b6e877..8610758e1 100644 --- a/pkg/deployment/resources/pvc_finalizers.go +++ b/pkg/deployment/resources/pvc_finalizers.go @@ -58,7 +58,7 @@ func (r *Resources) runPVCFinalizers(ctx context.Context, p *core.PersistentVolu } // Remove finalizers (if needed) if len(removalList) > 0 { - _, err := k8sutil.RemovePVCFinalizers(ctx, r.context.ACS().CurrentClusterCache(), r.context.ACS().CurrentClusterCache().PersistentVolumeClaimsModInterface().V1(), p, removalList, false) + _, err := k8sutil.RemoveSelectedFinalizers[*core.PersistentVolumeClaim](ctx, r.context.ACS().CurrentClusterCache().PersistentVolumeClaim().V1().Read(), r.context.ACS().CurrentClusterCache().PersistentVolumeClaimsModInterface().V1(), p, removalList, false) if err != nil { log.Err(err).Debug("Failed to update PVC (to remove finalizers)") return 0, errors.WithStack(err) diff --git a/pkg/deployment/resources/pvc_inspector.go b/pkg/deployment/resources/pvc_inspector.go index f1aad19e3..f369f3be2 100644 --- a/pkg/deployment/resources/pvc_inspector.go +++ b/pkg/deployment/resources/pvc_inspector.go @@ -77,7 +77,7 @@ func (r *Resources) InspectPVCs(ctx context.Context, cachedStatus inspectorInter // Strange, pvc belongs to us, but we have no member for it. // Remove all finalizers, so it can be removed. log.Str("pvc", pvc.GetName()).Warn("PVC belongs to this deployment, but we don't know the member. Removing all finalizers") - _, err := k8sutil.RemovePVCFinalizers(ctx, r.context.ACS().CurrentClusterCache(), cachedStatus.PersistentVolumeClaimsModInterface().V1(), pvc, pvc.GetFinalizers(), false) + _, err := k8sutil.RemoveSelectedFinalizers[*core.PersistentVolumeClaim](ctx, r.context.ACS().CurrentClusterCache().PersistentVolumeClaim().V1().Read(), r.context.ACS().CurrentClusterCache().PersistentVolumeClaimsModInterface().V1(), pvc, pvc.GetFinalizers(), false) if err != nil { log.Str("pvc", pvc.GetName()).Err(err).Debug("Failed to update PVC (to remove all finalizers)") return errors.WithStack(err) diff --git a/pkg/handlers/networking/route/handler.go b/pkg/handlers/networking/route/handler.go index fc5ea603b..bd6009a8b 100644 --- a/pkg/handlers/networking/route/handler.go +++ b/pkg/handlers/networking/route/handler.go @@ -101,7 +101,7 @@ func (h *handler) HandleSpecValidity(ctx context.Context, item operation.Item, e logger.Err(err).Warn("Invalid Spec on %s", item.String()) - if status.Conditions.Update(networkingApi.SpecValidCondition, false, "Spec is invalid", "Spec is invalid") { + if status.Conditions.Update(networkingApi.SpecValidCondition, false, "Spec is invalid", err.Error()) { return true, operator.Stop("Invalid spec") } return false, operator.Stop("Invalid spec") diff --git a/pkg/handlers/networking/route/handler_destination.go b/pkg/handlers/networking/route/handler_destination.go index dfa1e65fd..f6232c2f2 100644 --- a/pkg/handlers/networking/route/handler_destination.go +++ b/pkg/handlers/networking/route/handler_destination.go @@ -22,157 +22,20 @@ package route import ( "context" - "fmt" - - core "k8s.io/api/core/v1" - meta "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/intstr" api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" networkingApi "github.com/arangodb/kube-arangodb/pkg/apis/networking/v1alpha1" operator "github.com/arangodb/kube-arangodb/pkg/operatorV2" "github.com/arangodb/kube-arangodb/pkg/operatorV2/operation" - "github.com/arangodb/kube-arangodb/pkg/util" ) func (h *handler) HandleArangoDestination(ctx context.Context, item operation.Item, extension *networkingApi.ArangoRoute, status *networkingApi.ArangoRouteStatus, deployment *api.ArangoDeployment) (*operator.Condition, bool, error) { if dest := extension.Spec.GetDestination(); dest != nil { if svc := dest.GetService(); svc != nil { - port := svc.Port - - if port == nil { - return &operator.Condition{ - Status: false, - Reason: "Destination Not Found", - Message: "Missing Port definition", - }, false, nil - } - - s, err := util.WithKubernetesContextTimeoutP2A2(ctx, h.kubeClient.CoreV1().Services(svc.GetNamespace(extension)).Get, svc.GetName(), meta.GetOptions{}) - if err != nil { - if api.IsNotFound(err) { - return &operator.Condition{ - Status: false, - Reason: "Destination Not Found", - Message: fmt.Sprintf("Service `%s/%s` Not found", svc.GetNamespace(extension), svc.GetName()), - }, false, nil - } - - return &operator.Condition{ - Status: false, - Reason: "Destination Not Found", - Message: fmt.Sprintf("Unknown error for service `%s/%s`: %s", svc.GetNamespace(extension), svc.GetName(), err.Error()), - }, false, nil - } - - if !svc.Equals(s) { - return &operator.Condition{ - Status: false, - Reason: "Destination Not Found", - Message: fmt.Sprintf("Service `%s/%s` Changed", svc.GetNamespace(extension), svc.GetName()), - }, false, nil - } - - var destPort int32 - - if port.Type == intstr.Int { - p, ok := util.PickFromList(s.Spec.Ports, func(v core.ServicePort) bool { - return v.Port == port.IntVal - }) - if !ok { - return &operator.Condition{ - Status: false, - Reason: "Destination Not Found", - Message: fmt.Sprintf("Port `%d` not defined on Service `%s/%s`", port.IntVal, svc.GetNamespace(extension), svc.GetName()), - }, false, nil - } - - destPort = p.Port - } else if port.Type == intstr.String && port.StrVal != "" { - p, ok := util.PickFromList(s.Spec.Ports, func(v core.ServicePort) bool { - return v.Name == port.StrVal - }) - if !ok { - return &operator.Condition{ - Status: false, - Reason: "Destination Not Found", - Message: fmt.Sprintf("Port `%s` not defined on Service `%s/%s`", port.StrVal, svc.GetNamespace(extension), svc.GetName()), - }, false, nil - } - - destPort = p.Port - } else { - return &operator.Condition{ - Status: false, - Reason: "Destination Not Found", - Message: "Unknown Port definition", - }, false, nil - } - - if destPort == -1 { - return &operator.Condition{ - Status: false, - Reason: "Destination Not Found", - Message: fmt.Sprintf("Unable to discover port on Service `%s/%s`", svc.GetNamespace(extension), svc.GetName()), - }, false, nil - } - - var target networkingApi.ArangoRouteStatusTarget - - target.Path = dest.GetPath() - - // Render Auth Settings - - target.Authentication.Type = dest.GetAuthentication().GetType() - target.Authentication.PassMode = dest.GetAuthentication().GetPassMode() - - if dest.Schema.Get() == networkingApi.ArangoRouteSpecDestinationSchemaHTTPS { - target.TLS = &networkingApi.ArangoRouteStatusTargetTLS{ - Insecure: util.NewType(extension.Spec.Destination.GetTLS().GetInsecure()), - } - } - - if ip := s.Spec.ClusterIP; ip != "" { - target.Destinations = networkingApi.ArangoRouteStatusTargetDestinations{ - networkingApi.ArangoRouteStatusTargetDestination{ - Host: ip, - Port: destPort, - }, - } - } else { - if domain := deployment.Spec.ClusterDomain; domain != nil { - target.Destinations = networkingApi.ArangoRouteStatusTargetDestinations{ - networkingApi.ArangoRouteStatusTargetDestination{ - Host: fmt.Sprintf("%s.%s.svc.%s", s.GetName(), s.GetNamespace(), *domain), - Port: destPort, - }, - } - } else { - target.Destinations = networkingApi.ArangoRouteStatusTargetDestinations{ - networkingApi.ArangoRouteStatusTargetDestination{ - Host: fmt.Sprintf("%s.%s.svc", s.GetName(), s.GetNamespace()), - Port: destPort, - }, - } - } - } - - if status.Target.Hash() == target.Hash() { - return &operator.Condition{ - Status: true, - Reason: "Destination Found", - Message: "Destination Found", - Hash: target.Hash(), - }, false, nil - } - - status.Target = &target - return &operator.Condition{ - Status: true, - Reason: "Destination Found", - Message: "Destination Found", - Hash: target.Hash(), - }, true, nil + return h.HandleArangoDestinationService(ctx, item, extension, status, deployment, dest, svc) + } + if endpoints := dest.GetEndpoints(); endpoints != nil { + return h.HandleArangoDestinationEndpoints(ctx, item, extension, status, deployment, dest, endpoints) } } diff --git a/pkg/handlers/networking/route/handler_destination_endpoints.go b/pkg/handlers/networking/route/handler_destination_endpoints.go new file mode 100644 index 000000000..c67a9a8a1 --- /dev/null +++ b/pkg/handlers/networking/route/handler_destination_endpoints.go @@ -0,0 +1,180 @@ +// +// DISCLAIMER +// +// Copyright 2024 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package route + +import ( + "context" + "fmt" + + core "k8s.io/api/core/v1" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + + api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" + networkingApi "github.com/arangodb/kube-arangodb/pkg/apis/networking/v1alpha1" + operator "github.com/arangodb/kube-arangodb/pkg/operatorV2" + "github.com/arangodb/kube-arangodb/pkg/operatorV2/operation" + "github.com/arangodb/kube-arangodb/pkg/util" +) + +func (h *handler) HandleArangoDestinationEndpoints(ctx context.Context, item operation.Item, extension *networkingApi.ArangoRoute, status *networkingApi.ArangoRouteStatus, deployment *api.ArangoDeployment, dest *networkingApi.ArangoRouteSpecDestination, endpoints *networkingApi.ArangoRouteSpecDestinationEndpoints) (*operator.Condition, bool, error) { + port := endpoints.Port + + if port == nil { + return &operator.Condition{ + Status: false, + Reason: "Destination Not Found", + Message: "Missing Port definition", + }, false, nil + } + + s, err := util.WithKubernetesContextTimeoutP2A2(ctx, h.kubeClient.CoreV1().Services(endpoints.GetNamespace(extension)).Get, endpoints.GetName(), meta.GetOptions{}) + if err != nil { + if api.IsNotFound(err) { + return &operator.Condition{ + Status: false, + Reason: "Destination Not Found", + Message: fmt.Sprintf("Service `%s/%s` Not found", endpoints.GetNamespace(extension), endpoints.GetName()), + }, false, nil + } + + return &operator.Condition{ + Status: false, + Reason: "Destination Not Found", + Message: fmt.Sprintf("Unknown error for service `%s/%s`: %s", endpoints.GetNamespace(extension), endpoints.GetName(), err.Error()), + }, false, nil + } + + if !endpoints.Equals(s) { + return &operator.Condition{ + Status: false, + Reason: "Destination Not Found", + Message: fmt.Sprintf("Service `%s/%s` Changed", endpoints.GetNamespace(extension), endpoints.GetName()), + }, false, nil + } + + e, err := util.WithKubernetesContextTimeoutP2A2(ctx, h.kubeClient.CoreV1().Endpoints(endpoints.GetNamespace(extension)).Get, endpoints.GetName(), meta.GetOptions{}) + if err != nil { + if api.IsNotFound(err) { + return &operator.Condition{ + Status: false, + Reason: "Destination Not Found", + Message: fmt.Sprintf("Endpoints `%s/%s` Not found", endpoints.GetNamespace(extension), endpoints.GetName()), + }, false, nil + } + + return &operator.Condition{ + Status: false, + Reason: "Destination Not Found", + Message: fmt.Sprintf("Unknown error for endpoints `%s/%s`: %s", endpoints.GetNamespace(extension), endpoints.GetName(), err.Error()), + }, false, nil + } + + // Discover port name - empty names are allowed + var destPortName = "N/A" + + if port.Type == intstr.Int { + p, ok := util.PickFromList(s.Spec.Ports, func(v core.ServicePort) bool { + return v.Port == port.IntVal + }) + if !ok { + return &operator.Condition{ + Status: false, + Reason: "Destination Not Found", + Message: fmt.Sprintf("Port `%d` not defined on Service `%s/%s`", port.IntVal, endpoints.GetNamespace(extension), endpoints.GetName()), + }, false, nil + } + + destPortName = p.Name + } else if port.Type == intstr.String { + destPortName = port.StrVal + } + + if destPortName == "N/A" { + return &operator.Condition{ + Status: false, + Reason: "Destination Not Found", + Message: fmt.Sprintf("Unable to discover port on Service `%s/%s`", endpoints.GetNamespace(extension), endpoints.GetName()), + }, false, nil + } + + var target networkingApi.ArangoRouteStatusTarget + + target.Path = dest.GetPath() + target.Type = networkingApi.ArangoRouteStatusTargetEndpointsType + + // Render Auth Settings + + target.Authentication.Type = dest.GetAuthentication().GetType() + target.Authentication.PassMode = dest.GetAuthentication().GetPassMode() + + if dest.Schema.Get() == networkingApi.ArangoRouteSpecDestinationSchemaHTTPS { + target.TLS = &networkingApi.ArangoRouteStatusTargetTLS{ + Insecure: util.NewType(extension.Spec.Destination.GetTLS().GetInsecure()), + } + } + + for _, subset := range e.Subsets { + p, ok := util.PickFromList(subset.Ports, func(v core.EndpointPort) bool { + return v.Name == destPortName + }) + if !ok { + continue + } + + for _, address := range subset.Addresses { + target.Destinations = append(target.Destinations, networkingApi.ArangoRouteStatusTargetDestination{ + Host: address.IP, + Port: p.Port, + }) + } + + if s.Spec.PublishNotReadyAddresses { + for _, address := range subset.NotReadyAddresses { + target.Destinations = append(target.Destinations, networkingApi.ArangoRouteStatusTargetDestination{ + Host: address.IP, + Port: p.Port, + }) + } + } + } + + target.Destinations = util.Sort(target.Destinations, func(i, j networkingApi.ArangoRouteStatusTargetDestination) bool { + return i.Hash() < j.Hash() + }) + + if status.Target.Hash() == target.Hash() { + return &operator.Condition{ + Status: true, + Reason: "Destination Found", + Message: "Destination Found", + Hash: target.Hash(), + }, false, nil + } + + status.Target = &target + return &operator.Condition{ + Status: true, + Reason: "Destination Found", + Message: "Destination Found", + Hash: target.Hash(), + }, true, nil +} diff --git a/pkg/handlers/networking/route/handler_destination_endpoints_test.go b/pkg/handlers/networking/route/handler_destination_endpoints_test.go new file mode 100644 index 000000000..469ce724d --- /dev/null +++ b/pkg/handlers/networking/route/handler_destination_endpoints_test.go @@ -0,0 +1,330 @@ +// +// DISCLAIMER +// +// Copyright 2024 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package route + +import ( + "testing" + + "github.com/stretchr/testify/require" + core "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/intstr" + + api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" + networkingApi "github.com/arangodb/kube-arangodb/pkg/apis/networking/v1alpha1" + sharedApi "github.com/arangodb/kube-arangodb/pkg/apis/shared/v1" + "github.com/arangodb/kube-arangodb/pkg/operatorV2/operation" + "github.com/arangodb/kube-arangodb/pkg/util" + "github.com/arangodb/kube-arangodb/pkg/util/tests" +) + +func Test_Handler_Destination_Endpoints_Valid(t *testing.T) { + // Setup + handler := newFakeHandler() + + // Arrange + extension := tests.NewMetaObject[*networkingApi.ArangoRoute](t, tests.FakeNamespace, "test", + func(t *testing.T, obj *networkingApi.ArangoRoute) { + obj.Spec.Deployment = util.NewType("deployment") + }, + func(t *testing.T, obj *networkingApi.ArangoRoute) { + obj.Spec.Destination = &networkingApi.ArangoRouteSpecDestination{ + Endpoints: &networkingApi.ArangoRouteSpecDestinationEndpoints{ + Object: &sharedApi.Object{ + Name: "deployment", + }, + Port: util.NewType(intstr.FromInt32(10244)), + }, + } + }) + deployment := tests.NewMetaObject[*api.ArangoDeployment](t, tests.FakeNamespace, "deployment") + svc := tests.NewMetaObject[*core.Service](t, tests.FakeNamespace, "deployment", func(t *testing.T, obj *core.Service) { + obj.Spec.Ports = []core.ServicePort{ + { + Port: 10244, + }, + } + }) + endpoints := tests.NewMetaObject[*core.Endpoints](t, tests.FakeNamespace, "deployment", func(t *testing.T, obj *core.Endpoints) { + obj.Subsets = []core.EndpointSubset{ + { + Addresses: []core.EndpointAddress{ + { + IP: "127.0.0.1", + }, + }, + Ports: []core.EndpointPort{ + { + Name: "", + Port: 10244, + }, + }, + }, + } + }) + + refresh := tests.CreateObjects(t, handler.kubeClient, handler.client, &deployment, &extension, &svc, &endpoints) + + // Test + require.NoError(t, tests.Handle(handler, tests.NewItem(t, operation.Update, extension))) + + // Refresh + refresh(t) + + // Assert + require.True(t, extension.Status.Conditions.IsTrue(networkingApi.SpecValidCondition)) + require.True(t, extension.Status.Conditions.IsTrue(networkingApi.DestinationValidCondition)) + require.True(t, extension.Status.Conditions.IsTrue(networkingApi.ReadyCondition)) + require.Equal(t, networkingApi.ArangoRouteStatusTargetEndpointsType, extension.Status.Target.Type) + + require.Len(t, extension.Status.Target.RenderURLs(), 1) + require.EqualValues(t, "http://127.0.0.1:10244/", extension.Status.Target.RenderURLs()[0]) + + c, ok := extension.Status.Conditions.Get(networkingApi.DestinationValidCondition) + require.True(t, ok) + require.EqualValues(t, c.Reason, "Destination Found") + require.EqualValues(t, c.Reason, "Destination Found") + require.EqualValues(t, c.Hash, extension.Status.Target.Hash()) +} + +func Test_Handler_Destination_Endpoints_PortForward(t *testing.T) { + // Setup + handler := newFakeHandler() + + // Arrange + extension := tests.NewMetaObject[*networkingApi.ArangoRoute](t, tests.FakeNamespace, "test", + func(t *testing.T, obj *networkingApi.ArangoRoute) { + obj.Spec.Deployment = util.NewType("deployment") + }, + func(t *testing.T, obj *networkingApi.ArangoRoute) { + obj.Spec.Destination = &networkingApi.ArangoRouteSpecDestination{ + Endpoints: &networkingApi.ArangoRouteSpecDestinationEndpoints{ + Object: &sharedApi.Object{ + Name: "deployment", + }, + Port: util.NewType(intstr.FromInt32(10244)), + }, + } + }) + deployment := tests.NewMetaObject[*api.ArangoDeployment](t, tests.FakeNamespace, "deployment") + svc := tests.NewMetaObject[*core.Service](t, tests.FakeNamespace, "deployment", func(t *testing.T, obj *core.Service) { + obj.Spec.Ports = []core.ServicePort{ + { + Port: 10244, + }, + } + }) + endpoints := tests.NewMetaObject[*core.Endpoints](t, tests.FakeNamespace, "deployment", func(t *testing.T, obj *core.Endpoints) { + obj.Subsets = []core.EndpointSubset{ + { + Addresses: []core.EndpointAddress{ + { + IP: "127.0.0.1", + }, + }, + Ports: []core.EndpointPort{ + { + Name: "", + Port: 10245, + }, + }, + }, + } + }) + + refresh := tests.CreateObjects(t, handler.kubeClient, handler.client, &deployment, &extension, &svc, &endpoints) + + // Test + require.NoError(t, tests.Handle(handler, tests.NewItem(t, operation.Update, extension))) + + // Refresh + refresh(t) + + // Assert + require.True(t, extension.Status.Conditions.IsTrue(networkingApi.SpecValidCondition)) + require.True(t, extension.Status.Conditions.IsTrue(networkingApi.DestinationValidCondition)) + require.True(t, extension.Status.Conditions.IsTrue(networkingApi.ReadyCondition)) + require.Equal(t, networkingApi.ArangoRouteStatusTargetEndpointsType, extension.Status.Target.Type) + + require.Len(t, extension.Status.Target.RenderURLs(), 1) + require.EqualValues(t, "http://127.0.0.1:10245/", extension.Status.Target.RenderURLs()[0]) + + c, ok := extension.Status.Conditions.Get(networkingApi.DestinationValidCondition) + require.True(t, ok) + require.EqualValues(t, c.Reason, "Destination Found") + require.EqualValues(t, c.Reason, "Destination Found") + require.EqualValues(t, c.Hash, extension.Status.Target.Hash()) +} + +func Test_Handler_Destination_Endpoints_MultiTargets(t *testing.T) { + // Setup + handler := newFakeHandler() + + // Arrange + extension := tests.NewMetaObject[*networkingApi.ArangoRoute](t, tests.FakeNamespace, "test", + func(t *testing.T, obj *networkingApi.ArangoRoute) { + obj.Spec.Deployment = util.NewType("deployment") + }, + func(t *testing.T, obj *networkingApi.ArangoRoute) { + obj.Spec.Destination = &networkingApi.ArangoRouteSpecDestination{ + Endpoints: &networkingApi.ArangoRouteSpecDestinationEndpoints{ + Object: &sharedApi.Object{ + Name: "deployment", + }, + Port: util.NewType(intstr.FromInt32(10244)), + }, + } + }) + deployment := tests.NewMetaObject[*api.ArangoDeployment](t, tests.FakeNamespace, "deployment") + svc := tests.NewMetaObject[*core.Service](t, tests.FakeNamespace, "deployment", func(t *testing.T, obj *core.Service) { + obj.Spec.Ports = []core.ServicePort{ + { + Port: 10244, + }, + } + }) + endpoints := tests.NewMetaObject[*core.Endpoints](t, tests.FakeNamespace, "deployment", func(t *testing.T, obj *core.Endpoints) { + obj.Subsets = []core.EndpointSubset{ + { + Addresses: []core.EndpointAddress{ + { + IP: "127.0.0.1", + }, + }, + Ports: []core.EndpointPort{ + { + Name: "", + Port: 10245, + }, + }, + }, + { + Addresses: []core.EndpointAddress{ + { + IP: "127.0.0.2", + }, + }, + Ports: []core.EndpointPort{ + { + Name: "", + Port: 10246, + }, + }, + }, + } + }) + + refresh := tests.CreateObjects(t, handler.kubeClient, handler.client, &deployment, &extension, &svc, &endpoints) + + // Test + require.NoError(t, tests.Handle(handler, tests.NewItem(t, operation.Update, extension))) + + // Refresh + refresh(t) + + // Assert + require.True(t, extension.Status.Conditions.IsTrue(networkingApi.SpecValidCondition)) + require.True(t, extension.Status.Conditions.IsTrue(networkingApi.DestinationValidCondition)) + require.True(t, extension.Status.Conditions.IsTrue(networkingApi.ReadyCondition)) + require.Equal(t, networkingApi.ArangoRouteStatusTargetEndpointsType, extension.Status.Target.Type) + + require.Len(t, extension.Status.Target.RenderURLs(), 2) + require.EqualValues(t, "http://127.0.0.1:10245/", extension.Status.Target.RenderURLs()[0]) + require.EqualValues(t, "http://127.0.0.2:10246/", extension.Status.Target.RenderURLs()[1]) + + c, ok := extension.Status.Conditions.Get(networkingApi.DestinationValidCondition) + require.True(t, ok) + require.EqualValues(t, c.Reason, "Destination Found") + require.EqualValues(t, c.Reason, "Destination Found") + require.EqualValues(t, c.Hash, extension.Status.Target.Hash()) +} + +func Test_Handler_Destination_Endpoints_MultiDestinations(t *testing.T) { + // Setup + handler := newFakeHandler() + + // Arrange + extension := tests.NewMetaObject[*networkingApi.ArangoRoute](t, tests.FakeNamespace, "test", + func(t *testing.T, obj *networkingApi.ArangoRoute) { + obj.Spec.Deployment = util.NewType("deployment") + }, + func(t *testing.T, obj *networkingApi.ArangoRoute) { + obj.Spec.Destination = &networkingApi.ArangoRouteSpecDestination{ + Endpoints: &networkingApi.ArangoRouteSpecDestinationEndpoints{ + Object: &sharedApi.Object{ + Name: "deployment", + }, + Port: util.NewType(intstr.FromInt32(10244)), + }, + } + }) + deployment := tests.NewMetaObject[*api.ArangoDeployment](t, tests.FakeNamespace, "deployment") + svc := tests.NewMetaObject[*core.Service](t, tests.FakeNamespace, "deployment", func(t *testing.T, obj *core.Service) { + obj.Spec.Ports = []core.ServicePort{ + { + Port: 10244, + }, + } + }) + endpoints := tests.NewMetaObject[*core.Endpoints](t, tests.FakeNamespace, "deployment", func(t *testing.T, obj *core.Endpoints) { + obj.Subsets = []core.EndpointSubset{ + { + Addresses: []core.EndpointAddress{ + { + IP: "127.0.0.1", + }, + { + IP: "127.0.0.2", + }, + }, + Ports: []core.EndpointPort{ + { + Name: "", + Port: 10245, + }, + }, + }, + } + }) + + refresh := tests.CreateObjects(t, handler.kubeClient, handler.client, &deployment, &extension, &svc, &endpoints) + + // Test + require.NoError(t, tests.Handle(handler, tests.NewItem(t, operation.Update, extension))) + + // Refresh + refresh(t) + + // Assert + require.True(t, extension.Status.Conditions.IsTrue(networkingApi.SpecValidCondition)) + require.True(t, extension.Status.Conditions.IsTrue(networkingApi.DestinationValidCondition)) + require.True(t, extension.Status.Conditions.IsTrue(networkingApi.ReadyCondition)) + require.Equal(t, networkingApi.ArangoRouteStatusTargetEndpointsType, extension.Status.Target.Type) + + require.Len(t, extension.Status.Target.RenderURLs(), 2) + require.EqualValues(t, "http://127.0.0.1:10245/", extension.Status.Target.RenderURLs()[0]) + require.EqualValues(t, "http://127.0.0.2:10245/", extension.Status.Target.RenderURLs()[1]) + + c, ok := extension.Status.Conditions.Get(networkingApi.DestinationValidCondition) + require.True(t, ok) + require.EqualValues(t, c.Reason, "Destination Found") + require.EqualValues(t, c.Reason, "Destination Found") + require.EqualValues(t, c.Hash, extension.Status.Target.Hash()) +} diff --git a/pkg/handlers/networking/route/handler_destination_service.go b/pkg/handlers/networking/route/handler_destination_service.go new file mode 100644 index 000000000..de56f4efb --- /dev/null +++ b/pkg/handlers/networking/route/handler_destination_service.go @@ -0,0 +1,175 @@ +// +// DISCLAIMER +// +// Copyright 2024 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package route + +import ( + "context" + "fmt" + + core "k8s.io/api/core/v1" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + + api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" + networkingApi "github.com/arangodb/kube-arangodb/pkg/apis/networking/v1alpha1" + operator "github.com/arangodb/kube-arangodb/pkg/operatorV2" + "github.com/arangodb/kube-arangodb/pkg/operatorV2/operation" + "github.com/arangodb/kube-arangodb/pkg/util" +) + +func (h *handler) HandleArangoDestinationService(ctx context.Context, item operation.Item, extension *networkingApi.ArangoRoute, status *networkingApi.ArangoRouteStatus, deployment *api.ArangoDeployment, dest *networkingApi.ArangoRouteSpecDestination, svc *networkingApi.ArangoRouteSpecDestinationService) (*operator.Condition, bool, error) { + port := svc.Port + + if port == nil { + return &operator.Condition{ + Status: false, + Reason: "Destination Not Found", + Message: "Missing Port definition", + }, false, nil + } + + s, err := util.WithKubernetesContextTimeoutP2A2(ctx, h.kubeClient.CoreV1().Services(svc.GetNamespace(extension)).Get, svc.GetName(), meta.GetOptions{}) + if err != nil { + if api.IsNotFound(err) { + return &operator.Condition{ + Status: false, + Reason: "Destination Not Found", + Message: fmt.Sprintf("Service `%s/%s` Not found", svc.GetNamespace(extension), svc.GetName()), + }, false, nil + } + + return &operator.Condition{ + Status: false, + Reason: "Destination Not Found", + Message: fmt.Sprintf("Unknown error for service `%s/%s`: %s", svc.GetNamespace(extension), svc.GetName(), err.Error()), + }, false, nil + } + + if !svc.Equals(s) { + return &operator.Condition{ + Status: false, + Reason: "Destination Not Found", + Message: fmt.Sprintf("Service `%s/%s` Changed", svc.GetNamespace(extension), svc.GetName()), + }, false, nil + } + + var destPort int32 + + if port.Type == intstr.Int { + p, ok := util.PickFromList(s.Spec.Ports, func(v core.ServicePort) bool { + return v.Port == port.IntVal + }) + if !ok { + return &operator.Condition{ + Status: false, + Reason: "Destination Not Found", + Message: fmt.Sprintf("Port `%d` not defined on Service `%s/%s`", port.IntVal, svc.GetNamespace(extension), svc.GetName()), + }, false, nil + } + + destPort = p.Port + } else if port.Type == intstr.String && port.StrVal != "" { + p, ok := util.PickFromList(s.Spec.Ports, func(v core.ServicePort) bool { + return v.Name == port.StrVal + }) + if !ok { + return &operator.Condition{ + Status: false, + Reason: "Destination Not Found", + Message: fmt.Sprintf("Port `%s` not defined on Service `%s/%s`", port.StrVal, svc.GetNamespace(extension), svc.GetName()), + }, false, nil + } + + destPort = p.Port + } else { + return &operator.Condition{ + Status: false, + Reason: "Destination Not Found", + Message: "Unknown Port definition", + }, false, nil + } + + if destPort == -1 { + return &operator.Condition{ + Status: false, + Reason: "Destination Not Found", + Message: fmt.Sprintf("Unable to discover port on Service `%s/%s`", svc.GetNamespace(extension), svc.GetName()), + }, false, nil + } + + var target networkingApi.ArangoRouteStatusTarget + + target.Path = dest.GetPath() + target.Type = networkingApi.ArangoRouteStatusTargetServiceType + + // Render Auth Settings + + target.Authentication.Type = dest.GetAuthentication().GetType() + target.Authentication.PassMode = dest.GetAuthentication().GetPassMode() + + if dest.Schema.Get() == networkingApi.ArangoRouteSpecDestinationSchemaHTTPS { + target.TLS = &networkingApi.ArangoRouteStatusTargetTLS{ + Insecure: util.NewType(extension.Spec.Destination.GetTLS().GetInsecure()), + } + } + + if ip := s.Spec.ClusterIP; ip != "" { + target.Destinations = networkingApi.ArangoRouteStatusTargetDestinations{ + networkingApi.ArangoRouteStatusTargetDestination{ + Host: ip, + Port: destPort, + }, + } + } else { + if domain := deployment.Spec.ClusterDomain; domain != nil { + target.Destinations = networkingApi.ArangoRouteStatusTargetDestinations{ + networkingApi.ArangoRouteStatusTargetDestination{ + Host: fmt.Sprintf("%s.%s.svc.%s", s.GetName(), s.GetNamespace(), *domain), + Port: destPort, + }, + } + } else { + target.Destinations = networkingApi.ArangoRouteStatusTargetDestinations{ + networkingApi.ArangoRouteStatusTargetDestination{ + Host: fmt.Sprintf("%s.%s.svc", s.GetName(), s.GetNamespace()), + Port: destPort, + }, + } + } + } + + if status.Target.Hash() == target.Hash() { + return &operator.Condition{ + Status: true, + Reason: "Destination Found", + Message: "Destination Found", + Hash: target.Hash(), + }, false, nil + } + + status.Target = &target + return &operator.Condition{ + Status: true, + Reason: "Destination Found", + Message: "Destination Found", + Hash: target.Hash(), + }, true, nil +} diff --git a/pkg/handlers/networking/route/handler_destination_service_test.go b/pkg/handlers/networking/route/handler_destination_service_test.go new file mode 100644 index 000000000..c62b354f2 --- /dev/null +++ b/pkg/handlers/networking/route/handler_destination_service_test.go @@ -0,0 +1,598 @@ +// +// DISCLAIMER +// +// Copyright 2024 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package route + +import ( + "testing" + + "github.com/stretchr/testify/require" + core "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/intstr" + + api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" + networkingApi "github.com/arangodb/kube-arangodb/pkg/apis/networking/v1alpha1" + sharedApi "github.com/arangodb/kube-arangodb/pkg/apis/shared/v1" + "github.com/arangodb/kube-arangodb/pkg/operatorV2/operation" + "github.com/arangodb/kube-arangodb/pkg/util" + "github.com/arangodb/kube-arangodb/pkg/util/tests" +) + +func Test_Handler_Destination_Service_Missing(t *testing.T) { + // Setup + handler := newFakeHandler() + + // Arrange + extension := tests.NewMetaObject[*networkingApi.ArangoRoute](t, tests.FakeNamespace, "test", + func(t *testing.T, obj *networkingApi.ArangoRoute) { + obj.Spec.Deployment = util.NewType("deployment") + }, + func(t *testing.T, obj *networkingApi.ArangoRoute) { + obj.Spec.Destination = &networkingApi.ArangoRouteSpecDestination{ + Service: &networkingApi.ArangoRouteSpecDestinationService{ + Object: &sharedApi.Object{ + Name: "deployment", + }, + Port: util.NewType(intstr.FromInt32(10244)), + }, + } + }) + deployment := tests.NewMetaObject[*api.ArangoDeployment](t, tests.FakeNamespace, "deployment") + + refresh := tests.CreateObjects(t, handler.kubeClient, handler.client, &deployment, &extension) + + // Test + require.NoError(t, tests.Handle(handler, tests.NewItem(t, operation.Update, extension))) + + // Refresh + refresh(t) + + // Assert + require.True(t, extension.Status.Conditions.IsTrue(networkingApi.SpecValidCondition)) + require.False(t, extension.Status.Conditions.IsTrue(networkingApi.DestinationValidCondition)) + require.False(t, extension.Status.Conditions.IsTrue(networkingApi.ReadyCondition)) + + c, ok := extension.Status.Conditions.Get(networkingApi.DestinationValidCondition) + require.True(t, ok) + require.EqualValues(t, c.Reason, "Destination Not Found") + require.EqualValues(t, c.Message, "Unknown error for service `fake/deployment`: services \"deployment\" not found") +} + +func Test_Handler_Destination_Service_Valid(t *testing.T) { + // Setup + handler := newFakeHandler() + + // Arrange + extension := tests.NewMetaObject[*networkingApi.ArangoRoute](t, tests.FakeNamespace, "test", + func(t *testing.T, obj *networkingApi.ArangoRoute) { + obj.Spec.Deployment = util.NewType("deployment") + }, + func(t *testing.T, obj *networkingApi.ArangoRoute) { + obj.Spec.Destination = &networkingApi.ArangoRouteSpecDestination{ + Service: &networkingApi.ArangoRouteSpecDestinationService{ + Object: &sharedApi.Object{ + Name: "deployment", + }, + Port: util.NewType(intstr.FromInt32(10244)), + }, + } + }) + deployment := tests.NewMetaObject[*api.ArangoDeployment](t, tests.FakeNamespace, "deployment") + svc := tests.NewMetaObject[*core.Service](t, tests.FakeNamespace, "deployment", func(t *testing.T, obj *core.Service) { + obj.Spec.Ports = []core.ServicePort{ + { + Port: 10244, + }, + } + }) + + refresh := tests.CreateObjects(t, handler.kubeClient, handler.client, &deployment, &extension, &svc) + + // Test + require.NoError(t, tests.Handle(handler, tests.NewItem(t, operation.Update, extension))) + + // Refresh + refresh(t) + + // Assert + require.True(t, extension.Status.Conditions.IsTrue(networkingApi.SpecValidCondition)) + require.True(t, extension.Status.Conditions.IsTrue(networkingApi.DestinationValidCondition)) + require.True(t, extension.Status.Conditions.IsTrue(networkingApi.ReadyCondition)) + require.Equal(t, networkingApi.ArangoRouteStatusTargetServiceType, extension.Status.Target.Type) + + require.Len(t, extension.Status.Target.RenderURLs(), 1) + require.EqualValues(t, "http://deployment.fake.svc:10244/", extension.Status.Target.RenderURLs()[0]) + + c, ok := extension.Status.Conditions.Get(networkingApi.DestinationValidCondition) + require.True(t, ok) + require.EqualValues(t, c.Reason, "Destination Found") + require.EqualValues(t, c.Reason, "Destination Found") + require.EqualValues(t, c.Hash, extension.Status.Target.Hash()) +} + +func Test_Handler_Destination_Service_Valid_WithIP(t *testing.T) { + // Setup + handler := newFakeHandler() + + // Arrange + extension := tests.NewMetaObject[*networkingApi.ArangoRoute](t, tests.FakeNamespace, "test", + func(t *testing.T, obj *networkingApi.ArangoRoute) { + obj.Spec.Deployment = util.NewType("deployment") + }, + func(t *testing.T, obj *networkingApi.ArangoRoute) { + obj.Spec.Destination = &networkingApi.ArangoRouteSpecDestination{ + Service: &networkingApi.ArangoRouteSpecDestinationService{ + Object: &sharedApi.Object{ + Name: "deployment", + }, + Port: util.NewType(intstr.FromInt32(10244)), + }, + } + }) + deployment := tests.NewMetaObject[*api.ArangoDeployment](t, tests.FakeNamespace, "deployment") + svc := tests.NewMetaObject[*core.Service](t, tests.FakeNamespace, "deployment", func(t *testing.T, obj *core.Service) { + obj.Spec.Ports = []core.ServicePort{ + { + Port: 10244, + }, + } + obj.Spec.ClusterIP = "127.0.0.2" + }) + + refresh := tests.CreateObjects(t, handler.kubeClient, handler.client, &deployment, &extension, &svc) + + // Test + require.NoError(t, tests.Handle(handler, tests.NewItem(t, operation.Update, extension))) + + // Refresh + refresh(t) + + // Assert + require.True(t, extension.Status.Conditions.IsTrue(networkingApi.SpecValidCondition)) + require.True(t, extension.Status.Conditions.IsTrue(networkingApi.DestinationValidCondition)) + require.True(t, extension.Status.Conditions.IsTrue(networkingApi.ReadyCondition)) + require.Equal(t, networkingApi.ArangoRouteStatusTargetServiceType, extension.Status.Target.Type) + + require.Len(t, extension.Status.Target.RenderURLs(), 1) + require.EqualValues(t, "http://127.0.0.2:10244/", extension.Status.Target.RenderURLs()[0]) + + c, ok := extension.Status.Conditions.Get(networkingApi.DestinationValidCondition) + require.True(t, ok) + require.EqualValues(t, c.Reason, "Destination Found") + require.EqualValues(t, c.Reason, "Destination Found") + require.EqualValues(t, c.Hash, extension.Status.Target.Hash()) +} + +func Test_Handler_Destination_Service_Valid_WithPath(t *testing.T) { + // Setup + handler := newFakeHandler() + + // Arrange + extension := tests.NewMetaObject[*networkingApi.ArangoRoute](t, tests.FakeNamespace, "test", + func(t *testing.T, obj *networkingApi.ArangoRoute) { + obj.Spec.Deployment = util.NewType("deployment") + }, + func(t *testing.T, obj *networkingApi.ArangoRoute) { + obj.Spec.Destination = &networkingApi.ArangoRouteSpecDestination{ + Service: &networkingApi.ArangoRouteSpecDestinationService{ + Object: &sharedApi.Object{ + Name: "deployment", + }, + Port: util.NewType(intstr.FromInt32(10244)), + }, + Path: util.NewType("/test/path/"), + } + }) + deployment := tests.NewMetaObject[*api.ArangoDeployment](t, tests.FakeNamespace, "deployment") + svc := tests.NewMetaObject[*core.Service](t, tests.FakeNamespace, "deployment", func(t *testing.T, obj *core.Service) { + obj.Spec.Ports = []core.ServicePort{ + { + Port: 10244, + }, + } + obj.Spec.ClusterIP = "127.0.0.2" + }) + + refresh := tests.CreateObjects(t, handler.kubeClient, handler.client, &deployment, &extension, &svc) + + // Test + require.NoError(t, tests.Handle(handler, tests.NewItem(t, operation.Update, extension))) + + // Refresh + refresh(t) + + // Assert + require.True(t, extension.Status.Conditions.IsTrue(networkingApi.SpecValidCondition)) + require.True(t, extension.Status.Conditions.IsTrue(networkingApi.DestinationValidCondition)) + require.True(t, extension.Status.Conditions.IsTrue(networkingApi.ReadyCondition)) + require.Equal(t, networkingApi.ArangoRouteStatusTargetServiceType, extension.Status.Target.Type) + + require.Len(t, extension.Status.Target.RenderURLs(), 1) + require.EqualValues(t, "http://127.0.0.2:10244/test/path/", extension.Status.Target.RenderURLs()[0]) + + c, ok := extension.Status.Conditions.Get(networkingApi.DestinationValidCondition) + require.True(t, ok) + require.EqualValues(t, c.Reason, "Destination Found") + require.EqualValues(t, c.Reason, "Destination Found") + require.EqualValues(t, c.Hash, extension.Status.Target.Hash()) +} + +func Test_Handler_Destination_Service_ValidName(t *testing.T) { + // Setup + handler := newFakeHandler() + + // Arrange + extension := tests.NewMetaObject[*networkingApi.ArangoRoute](t, tests.FakeNamespace, "test", + func(t *testing.T, obj *networkingApi.ArangoRoute) { + obj.Spec.Deployment = util.NewType("deployment") + }, + func(t *testing.T, obj *networkingApi.ArangoRoute) { + obj.Spec.Destination = &networkingApi.ArangoRouteSpecDestination{ + Service: &networkingApi.ArangoRouteSpecDestinationService{ + Object: &sharedApi.Object{ + Name: "deployment", + }, + Port: util.NewType(intstr.FromString("test")), + }, + } + }) + deployment := tests.NewMetaObject[*api.ArangoDeployment](t, tests.FakeNamespace, "deployment") + svc := tests.NewMetaObject[*core.Service](t, tests.FakeNamespace, "deployment", func(t *testing.T, obj *core.Service) { + obj.Spec.Ports = []core.ServicePort{ + { + Port: 10241, + Name: "test1", + }, + { + Port: 10244, + Name: "test", + }, + } + }) + + refresh := tests.CreateObjects(t, handler.kubeClient, handler.client, &deployment, &extension, &svc) + + // Test + require.NoError(t, tests.Handle(handler, tests.NewItem(t, operation.Update, extension))) + + // Refresh + refresh(t) + + // Assert + require.True(t, extension.Status.Conditions.IsTrue(networkingApi.SpecValidCondition)) + require.True(t, extension.Status.Conditions.IsTrue(networkingApi.DestinationValidCondition)) + require.True(t, extension.Status.Conditions.IsTrue(networkingApi.ReadyCondition)) + require.Equal(t, networkingApi.ArangoRouteStatusTargetServiceType, extension.Status.Target.Type) + + c, ok := extension.Status.Conditions.Get(networkingApi.DestinationValidCondition) + require.True(t, ok) + require.EqualValues(t, c.Reason, "Destination Found") + require.EqualValues(t, c.Reason, "Destination Found") + require.EqualValues(t, c.Hash, extension.Status.Target.Hash()) +} + +func Test_Handler_Destination_Service_WrongPort(t *testing.T) { + // Setup + handler := newFakeHandler() + + // Arrange + extension := tests.NewMetaObject[*networkingApi.ArangoRoute](t, tests.FakeNamespace, "test", + func(t *testing.T, obj *networkingApi.ArangoRoute) { + obj.Spec.Deployment = util.NewType("deployment") + }, + func(t *testing.T, obj *networkingApi.ArangoRoute) { + obj.Spec.Destination = &networkingApi.ArangoRouteSpecDestination{ + Service: &networkingApi.ArangoRouteSpecDestinationService{ + Object: &sharedApi.Object{ + Name: "deployment", + }, + Port: util.NewType(intstr.FromInt32(10244)), + }, + } + }) + deployment := tests.NewMetaObject[*api.ArangoDeployment](t, tests.FakeNamespace, "deployment") + svc := tests.NewMetaObject[*core.Service](t, tests.FakeNamespace, "deployment", func(t *testing.T, obj *core.Service) { + obj.Spec.Ports = []core.ServicePort{ + { + Port: 10245, + }, + } + }) + + refresh := tests.CreateObjects(t, handler.kubeClient, handler.client, &deployment, &extension, &svc) + + // Test + require.NoError(t, tests.Handle(handler, tests.NewItem(t, operation.Update, extension))) + + // Refresh + refresh(t) + + // Assert + require.True(t, extension.Status.Conditions.IsTrue(networkingApi.SpecValidCondition)) + require.False(t, extension.Status.Conditions.IsTrue(networkingApi.DestinationValidCondition)) + require.False(t, extension.Status.Conditions.IsTrue(networkingApi.ReadyCondition)) + + c, ok := extension.Status.Conditions.Get(networkingApi.DestinationValidCondition) + require.True(t, ok) + require.EqualValues(t, c.Reason, "Destination Not Found") + require.EqualValues(t, c.Message, "Port `10244` not defined on Service `fake/deployment`") +} + +func Test_Handler_Destination_Service_WrongPortName(t *testing.T) { + // Setup + handler := newFakeHandler() + + // Arrange + extension := tests.NewMetaObject[*networkingApi.ArangoRoute](t, tests.FakeNamespace, "test", + func(t *testing.T, obj *networkingApi.ArangoRoute) { + obj.Spec.Deployment = util.NewType("deployment") + }, + func(t *testing.T, obj *networkingApi.ArangoRoute) { + obj.Spec.Destination = &networkingApi.ArangoRouteSpecDestination{ + Service: &networkingApi.ArangoRouteSpecDestinationService{ + Object: &sharedApi.Object{ + Name: "deployment", + }, + Port: util.NewType(intstr.FromString("test")), + }, + } + }) + deployment := tests.NewMetaObject[*api.ArangoDeployment](t, tests.FakeNamespace, "deployment") + svc := tests.NewMetaObject[*core.Service](t, tests.FakeNamespace, "deployment", func(t *testing.T, obj *core.Service) { + obj.Spec.Ports = []core.ServicePort{ + { + Port: 10245, + }, + } + }) + + refresh := tests.CreateObjects(t, handler.kubeClient, handler.client, &deployment, &extension, &svc) + + // Test + require.NoError(t, tests.Handle(handler, tests.NewItem(t, operation.Update, extension))) + + // Refresh + refresh(t) + + // Assert + require.True(t, extension.Status.Conditions.IsTrue(networkingApi.SpecValidCondition)) + require.False(t, extension.Status.Conditions.IsTrue(networkingApi.DestinationValidCondition)) + require.False(t, extension.Status.Conditions.IsTrue(networkingApi.ReadyCondition)) + + c, ok := extension.Status.Conditions.Get(networkingApi.DestinationValidCondition) + require.True(t, ok) + require.EqualValues(t, c.Reason, "Destination Not Found") + require.EqualValues(t, c.Message, "Port `test` not defined on Service `fake/deployment`") +} + +func Test_Handler_Destination_Service_Insecure_Default(t *testing.T) { + // Setup + handler := newFakeHandler() + + // Arrange + extension := tests.NewMetaObject[*networkingApi.ArangoRoute](t, tests.FakeNamespace, "test", + func(t *testing.T, obj *networkingApi.ArangoRoute) { + obj.Spec.Deployment = util.NewType("deployment") + }, + func(t *testing.T, obj *networkingApi.ArangoRoute) { + obj.Spec.Destination = &networkingApi.ArangoRouteSpecDestination{ + Service: &networkingApi.ArangoRouteSpecDestinationService{ + Object: &sharedApi.Object{ + Name: "deployment", + }, + Port: util.NewType(intstr.FromInt32(10244)), + }, + } + }) + deployment := tests.NewMetaObject[*api.ArangoDeployment](t, tests.FakeNamespace, "deployment") + svc := tests.NewMetaObject[*core.Service](t, tests.FakeNamespace, "deployment", func(t *testing.T, obj *core.Service) { + obj.Spec.Ports = []core.ServicePort{ + { + Port: 10244, + }, + } + }) + + refresh := tests.CreateObjects(t, handler.kubeClient, handler.client, &deployment, &extension, &svc) + + // Testcense + require.NoError(t, tests.Handle(handler, tests.NewItem(t, operation.Update, extension))) + + // Refresh + refresh(t) + + // Assert + require.True(t, extension.Status.Conditions.IsTrue(networkingApi.SpecValidCondition)) + require.True(t, extension.Status.Conditions.IsTrue(networkingApi.DestinationValidCondition)) + require.True(t, extension.Status.Conditions.IsTrue(networkingApi.ReadyCondition)) + require.Equal(t, networkingApi.ArangoRouteStatusTargetServiceType, extension.Status.Target.Type) + + c, ok := extension.Status.Conditions.Get(networkingApi.DestinationValidCondition) + require.True(t, ok) + require.EqualValues(t, c.Reason, "Destination Found") + require.EqualValues(t, c.Reason, "Destination Found") + require.EqualValues(t, c.Hash, extension.Status.Target.Hash()) + + require.False(t, extension.Status.Target.TLS.IsInsecure()) +} + +func Test_Handler_Destination_Service_Insecure_Nil(t *testing.T) { + // Setup + handler := newFakeHandler() + + // Arrange + extension := tests.NewMetaObject[*networkingApi.ArangoRoute](t, tests.FakeNamespace, "test", + func(t *testing.T, obj *networkingApi.ArangoRoute) { + obj.Spec.Deployment = util.NewType("deployment") + }, + func(t *testing.T, obj *networkingApi.ArangoRoute) { + obj.Spec.Destination = &networkingApi.ArangoRouteSpecDestination{ + Service: &networkingApi.ArangoRouteSpecDestinationService{ + Object: &sharedApi.Object{ + Name: "deployment", + }, + Port: util.NewType(intstr.FromInt32(10244)), + }, + TLS: &networkingApi.ArangoRouteSpecDestinationTLS{ + Insecure: nil, + }, + } + }) + deployment := tests.NewMetaObject[*api.ArangoDeployment](t, tests.FakeNamespace, "deployment") + svc := tests.NewMetaObject[*core.Service](t, tests.FakeNamespace, "deployment", func(t *testing.T, obj *core.Service) { + obj.Spec.Ports = []core.ServicePort{ + { + Port: 10244, + }, + } + }) + + refresh := tests.CreateObjects(t, handler.kubeClient, handler.client, &deployment, &extension, &svc) + + // Test + require.NoError(t, tests.Handle(handler, tests.NewItem(t, operation.Update, extension))) + + // Refresh + refresh(t) + + // Assert + require.True(t, extension.Status.Conditions.IsTrue(networkingApi.SpecValidCondition)) + require.True(t, extension.Status.Conditions.IsTrue(networkingApi.DestinationValidCondition)) + require.True(t, extension.Status.Conditions.IsTrue(networkingApi.ReadyCondition)) + require.Equal(t, networkingApi.ArangoRouteStatusTargetServiceType, extension.Status.Target.Type) + + c, ok := extension.Status.Conditions.Get(networkingApi.DestinationValidCondition) + require.True(t, ok) + require.EqualValues(t, c.Reason, "Destination Found") + require.EqualValues(t, c.Reason, "Destination Found") + require.EqualValues(t, c.Hash, extension.Status.Target.Hash()) + + require.False(t, extension.Status.Target.TLS.IsInsecure()) +} + +func Test_Handler_Destination_Service_Insecure_HTTPS_Override(t *testing.T) { + // Setup + handler := newFakeHandler() + + // Arrange + extension := tests.NewMetaObject[*networkingApi.ArangoRoute](t, tests.FakeNamespace, "test", + func(t *testing.T, obj *networkingApi.ArangoRoute) { + obj.Spec.Deployment = util.NewType("deployment") + }, + func(t *testing.T, obj *networkingApi.ArangoRoute) { + obj.Spec.Destination = &networkingApi.ArangoRouteSpecDestination{ + Service: &networkingApi.ArangoRouteSpecDestinationService{ + Object: &sharedApi.Object{ + Name: "deployment", + }, + Port: util.NewType(intstr.FromInt32(10244)), + }, + TLS: &networkingApi.ArangoRouteSpecDestinationTLS{ + Insecure: util.NewType(true), + }, + Schema: util.NewType(networkingApi.ArangoRouteSpecDestinationSchemaHTTPS), + } + }) + deployment := tests.NewMetaObject[*api.ArangoDeployment](t, tests.FakeNamespace, "deployment") + svc := tests.NewMetaObject[*core.Service](t, tests.FakeNamespace, "deployment", func(t *testing.T, obj *core.Service) { + obj.Spec.Ports = []core.ServicePort{ + { + Port: 10244, + }, + } + }) + + refresh := tests.CreateObjects(t, handler.kubeClient, handler.client, &deployment, &extension, &svc) + + // Test + require.NoError(t, tests.Handle(handler, tests.NewItem(t, operation.Update, extension))) + + // Refresh + refresh(t) + + // Assert + require.True(t, extension.Status.Conditions.IsTrue(networkingApi.SpecValidCondition)) + require.True(t, extension.Status.Conditions.IsTrue(networkingApi.DestinationValidCondition)) + require.True(t, extension.Status.Conditions.IsTrue(networkingApi.ReadyCondition)) + require.Equal(t, networkingApi.ArangoRouteStatusTargetServiceType, extension.Status.Target.Type) + + c, ok := extension.Status.Conditions.Get(networkingApi.DestinationValidCondition) + require.True(t, ok) + require.EqualValues(t, c.Reason, "Destination Found") + require.EqualValues(t, c.Reason, "Destination Found") + require.EqualValues(t, c.Hash, extension.Status.Target.Hash()) + + require.True(t, extension.Status.Target.TLS.IsInsecure()) +} + +func Test_Handler_Destination_Service_Insecure_HTTP_Override(t *testing.T) { + // Setup + handler := newFakeHandler() + + // Arrange + extension := tests.NewMetaObject[*networkingApi.ArangoRoute](t, tests.FakeNamespace, "test", + func(t *testing.T, obj *networkingApi.ArangoRoute) { + obj.Spec.Deployment = util.NewType("deployment") + }, + func(t *testing.T, obj *networkingApi.ArangoRoute) { + obj.Spec.Destination = &networkingApi.ArangoRouteSpecDestination{ + Service: &networkingApi.ArangoRouteSpecDestinationService{ + Object: &sharedApi.Object{ + Name: "deployment", + }, + Port: util.NewType(intstr.FromInt32(10244)), + }, + TLS: &networkingApi.ArangoRouteSpecDestinationTLS{ + Insecure: util.NewType(true), + }, + Schema: util.NewType(networkingApi.ArangoRouteSpecDestinationSchemaHTTP), + } + }) + deployment := tests.NewMetaObject[*api.ArangoDeployment](t, tests.FakeNamespace, "deployment") + svc := tests.NewMetaObject[*core.Service](t, tests.FakeNamespace, "deployment", func(t *testing.T, obj *core.Service) { + obj.Spec.Ports = []core.ServicePort{ + { + Port: 10244, + }, + } + }) + + refresh := tests.CreateObjects(t, handler.kubeClient, handler.client, &deployment, &extension, &svc) + + // Test + require.NoError(t, tests.Handle(handler, tests.NewItem(t, operation.Update, extension))) + + // Refresh + refresh(t) + + // Assert + require.True(t, extension.Status.Conditions.IsTrue(networkingApi.SpecValidCondition)) + require.True(t, extension.Status.Conditions.IsTrue(networkingApi.DestinationValidCondition)) + require.True(t, extension.Status.Conditions.IsTrue(networkingApi.ReadyCondition)) + require.Equal(t, networkingApi.ArangoRouteStatusTargetServiceType, extension.Status.Target.Type) + + c, ok := extension.Status.Conditions.Get(networkingApi.DestinationValidCondition) + require.True(t, ok) + require.EqualValues(t, c.Reason, "Destination Found") + require.EqualValues(t, c.Reason, "Destination Found") + require.EqualValues(t, c.Hash, extension.Status.Target.Hash()) + + require.False(t, extension.Status.Target.TLS.IsInsecure()) +} diff --git a/pkg/handlers/networking/route/handler_destination_test.go b/pkg/handlers/networking/route/handler_destination_test.go index e53cd0f7f..a245c63c8 100644 --- a/pkg/handlers/networking/route/handler_destination_test.go +++ b/pkg/handlers/networking/route/handler_destination_test.go @@ -35,7 +35,7 @@ import ( "github.com/arangodb/kube-arangodb/pkg/util/tests" ) -func Test_Handler_Destination_Service_Missing(t *testing.T) { +func Test_Handler_MultiDest(t *testing.T) { // Setup handler := newFakeHandler() @@ -46,342 +46,12 @@ func Test_Handler_Destination_Service_Missing(t *testing.T) { }, func(t *testing.T, obj *networkingApi.ArangoRoute) { obj.Spec.Destination = &networkingApi.ArangoRouteSpecDestination{ - Service: &networkingApi.ArangoRouteSpecDestinationService{ - Object: &sharedApi.Object{ - Name: "deployment", - }, - Port: util.NewType(intstr.FromInt32(10244)), - }, - } - }) - deployment := tests.NewMetaObject[*api.ArangoDeployment](t, tests.FakeNamespace, "deployment") - - refresh := tests.CreateObjects(t, handler.kubeClient, handler.client, &deployment, &extension) - - // Test - require.NoError(t, tests.Handle(handler, tests.NewItem(t, operation.Update, extension))) - - // Refresh - refresh(t) - - // Assert - require.False(t, extension.Status.Conditions.IsTrue(networkingApi.DestinationValidCondition)) - require.False(t, extension.Status.Conditions.IsTrue(networkingApi.ReadyCondition)) - - c, ok := extension.Status.Conditions.Get(networkingApi.DestinationValidCondition) - require.True(t, ok) - require.EqualValues(t, c.Reason, "Destination Not Found") - require.EqualValues(t, c.Message, "Unknown error for service `fake/deployment`: services \"deployment\" not found") -} - -func Test_Handler_Destination_Service_Valid(t *testing.T) { - // Setup - handler := newFakeHandler() - - // Arrange - extension := tests.NewMetaObject[*networkingApi.ArangoRoute](t, tests.FakeNamespace, "test", - func(t *testing.T, obj *networkingApi.ArangoRoute) { - obj.Spec.Deployment = util.NewType("deployment") - }, - func(t *testing.T, obj *networkingApi.ArangoRoute) { - obj.Spec.Destination = &networkingApi.ArangoRouteSpecDestination{ - Service: &networkingApi.ArangoRouteSpecDestinationService{ - Object: &sharedApi.Object{ - Name: "deployment", - }, - Port: util.NewType(intstr.FromInt32(10244)), - }, - } - }) - deployment := tests.NewMetaObject[*api.ArangoDeployment](t, tests.FakeNamespace, "deployment") - svc := tests.NewMetaObject[*core.Service](t, tests.FakeNamespace, "deployment", func(t *testing.T, obj *core.Service) { - obj.Spec.Ports = []core.ServicePort{ - { - Port: 10244, - }, - } - }) - - refresh := tests.CreateObjects(t, handler.kubeClient, handler.client, &deployment, &extension, &svc) - - // Test - require.NoError(t, tests.Handle(handler, tests.NewItem(t, operation.Update, extension))) - - // Refresh - refresh(t) - - // Assert - require.True(t, extension.Status.Conditions.IsTrue(networkingApi.DestinationValidCondition)) - require.True(t, extension.Status.Conditions.IsTrue(networkingApi.ReadyCondition)) - - require.Len(t, extension.Status.Target.RenderURLs(), 1) - require.EqualValues(t, "http://deployment.fake.svc:10244/", extension.Status.Target.RenderURLs()[0]) - - c, ok := extension.Status.Conditions.Get(networkingApi.DestinationValidCondition) - require.True(t, ok) - require.EqualValues(t, c.Reason, "Destination Found") - require.EqualValues(t, c.Reason, "Destination Found") - require.EqualValues(t, c.Hash, extension.Status.Target.Hash()) -} - -func Test_Handler_Destination_Service_Valid_WithIP(t *testing.T) { - // Setup - handler := newFakeHandler() - - // Arrange - extension := tests.NewMetaObject[*networkingApi.ArangoRoute](t, tests.FakeNamespace, "test", - func(t *testing.T, obj *networkingApi.ArangoRoute) { - obj.Spec.Deployment = util.NewType("deployment") - }, - func(t *testing.T, obj *networkingApi.ArangoRoute) { - obj.Spec.Destination = &networkingApi.ArangoRouteSpecDestination{ - Service: &networkingApi.ArangoRouteSpecDestinationService{ - Object: &sharedApi.Object{ - Name: "deployment", - }, - Port: util.NewType(intstr.FromInt32(10244)), - }, - } - }) - deployment := tests.NewMetaObject[*api.ArangoDeployment](t, tests.FakeNamespace, "deployment") - svc := tests.NewMetaObject[*core.Service](t, tests.FakeNamespace, "deployment", func(t *testing.T, obj *core.Service) { - obj.Spec.Ports = []core.ServicePort{ - { - Port: 10244, - }, - } - obj.Spec.ClusterIP = "127.0.0.2" - }) - - refresh := tests.CreateObjects(t, handler.kubeClient, handler.client, &deployment, &extension, &svc) - - // Test - require.NoError(t, tests.Handle(handler, tests.NewItem(t, operation.Update, extension))) - - // Refresh - refresh(t) - - // Assert - require.True(t, extension.Status.Conditions.IsTrue(networkingApi.DestinationValidCondition)) - require.True(t, extension.Status.Conditions.IsTrue(networkingApi.ReadyCondition)) - - require.Len(t, extension.Status.Target.RenderURLs(), 1) - require.EqualValues(t, "http://127.0.0.2:10244/", extension.Status.Target.RenderURLs()[0]) - - c, ok := extension.Status.Conditions.Get(networkingApi.DestinationValidCondition) - require.True(t, ok) - require.EqualValues(t, c.Reason, "Destination Found") - require.EqualValues(t, c.Reason, "Destination Found") - require.EqualValues(t, c.Hash, extension.Status.Target.Hash()) -} - -func Test_Handler_Destination_Service_Valid_WithPath(t *testing.T) { - // Setup - handler := newFakeHandler() - - // Arrange - extension := tests.NewMetaObject[*networkingApi.ArangoRoute](t, tests.FakeNamespace, "test", - func(t *testing.T, obj *networkingApi.ArangoRoute) { - obj.Spec.Deployment = util.NewType("deployment") - }, - func(t *testing.T, obj *networkingApi.ArangoRoute) { - obj.Spec.Destination = &networkingApi.ArangoRouteSpecDestination{ - Service: &networkingApi.ArangoRouteSpecDestinationService{ - Object: &sharedApi.Object{ - Name: "deployment", - }, - Port: util.NewType(intstr.FromInt32(10244)), - }, - Path: util.NewType("/test/path/"), - } - }) - deployment := tests.NewMetaObject[*api.ArangoDeployment](t, tests.FakeNamespace, "deployment") - svc := tests.NewMetaObject[*core.Service](t, tests.FakeNamespace, "deployment", func(t *testing.T, obj *core.Service) { - obj.Spec.Ports = []core.ServicePort{ - { - Port: 10244, - }, - } - obj.Spec.ClusterIP = "127.0.0.2" - }) - - refresh := tests.CreateObjects(t, handler.kubeClient, handler.client, &deployment, &extension, &svc) - - // Test - require.NoError(t, tests.Handle(handler, tests.NewItem(t, operation.Update, extension))) - - // Refresh - refresh(t) - - // Assert - require.True(t, extension.Status.Conditions.IsTrue(networkingApi.DestinationValidCondition)) - require.True(t, extension.Status.Conditions.IsTrue(networkingApi.ReadyCondition)) - - require.Len(t, extension.Status.Target.RenderURLs(), 1) - require.EqualValues(t, "http://127.0.0.2:10244/test/path/", extension.Status.Target.RenderURLs()[0]) - - c, ok := extension.Status.Conditions.Get(networkingApi.DestinationValidCondition) - require.True(t, ok) - require.EqualValues(t, c.Reason, "Destination Found") - require.EqualValues(t, c.Reason, "Destination Found") - require.EqualValues(t, c.Hash, extension.Status.Target.Hash()) -} - -func Test_Handler_Destination_Service_ValidName(t *testing.T) { - // Setup - handler := newFakeHandler() - - // Arrange - extension := tests.NewMetaObject[*networkingApi.ArangoRoute](t, tests.FakeNamespace, "test", - func(t *testing.T, obj *networkingApi.ArangoRoute) { - obj.Spec.Deployment = util.NewType("deployment") - }, - func(t *testing.T, obj *networkingApi.ArangoRoute) { - obj.Spec.Destination = &networkingApi.ArangoRouteSpecDestination{ - Service: &networkingApi.ArangoRouteSpecDestinationService{ - Object: &sharedApi.Object{ - Name: "deployment", - }, - Port: util.NewType(intstr.FromString("test")), - }, - } - }) - deployment := tests.NewMetaObject[*api.ArangoDeployment](t, tests.FakeNamespace, "deployment") - svc := tests.NewMetaObject[*core.Service](t, tests.FakeNamespace, "deployment", func(t *testing.T, obj *core.Service) { - obj.Spec.Ports = []core.ServicePort{ - { - Port: 10241, - Name: "test1", - }, - { - Port: 10244, - Name: "test", - }, - } - }) - - refresh := tests.CreateObjects(t, handler.kubeClient, handler.client, &deployment, &extension, &svc) - - // Test - require.NoError(t, tests.Handle(handler, tests.NewItem(t, operation.Update, extension))) - - // Refresh - refresh(t) - - // Assert - require.True(t, extension.Status.Conditions.IsTrue(networkingApi.DestinationValidCondition)) - require.True(t, extension.Status.Conditions.IsTrue(networkingApi.ReadyCondition)) - - c, ok := extension.Status.Conditions.Get(networkingApi.DestinationValidCondition) - require.True(t, ok) - require.EqualValues(t, c.Reason, "Destination Found") - require.EqualValues(t, c.Reason, "Destination Found") - require.EqualValues(t, c.Hash, extension.Status.Target.Hash()) -} - -func Test_Handler_Destination_Service_WrongPort(t *testing.T) { - // Setup - handler := newFakeHandler() - - // Arrange - extension := tests.NewMetaObject[*networkingApi.ArangoRoute](t, tests.FakeNamespace, "test", - func(t *testing.T, obj *networkingApi.ArangoRoute) { - obj.Spec.Deployment = util.NewType("deployment") - }, - func(t *testing.T, obj *networkingApi.ArangoRoute) { - obj.Spec.Destination = &networkingApi.ArangoRouteSpecDestination{ - Service: &networkingApi.ArangoRouteSpecDestinationService{ + Endpoints: &networkingApi.ArangoRouteSpecDestinationEndpoints{ Object: &sharedApi.Object{ Name: "deployment", }, Port: util.NewType(intstr.FromInt32(10244)), }, - } - }) - deployment := tests.NewMetaObject[*api.ArangoDeployment](t, tests.FakeNamespace, "deployment") - svc := tests.NewMetaObject[*core.Service](t, tests.FakeNamespace, "deployment", func(t *testing.T, obj *core.Service) { - obj.Spec.Ports = []core.ServicePort{ - { - Port: 10245, - }, - } - }) - - refresh := tests.CreateObjects(t, handler.kubeClient, handler.client, &deployment, &extension, &svc) - - // Test - require.NoError(t, tests.Handle(handler, tests.NewItem(t, operation.Update, extension))) - - // Refresh - refresh(t) - - // Assert - require.False(t, extension.Status.Conditions.IsTrue(networkingApi.DestinationValidCondition)) - require.False(t, extension.Status.Conditions.IsTrue(networkingApi.ReadyCondition)) - - c, ok := extension.Status.Conditions.Get(networkingApi.DestinationValidCondition) - require.True(t, ok) - require.EqualValues(t, c.Reason, "Destination Not Found") - require.EqualValues(t, c.Message, "Port `10244` not defined on Service `fake/deployment`") -} - -func Test_Handler_Destination_Service_WrongPortName(t *testing.T) { - // Setup - handler := newFakeHandler() - - // Arrange - extension := tests.NewMetaObject[*networkingApi.ArangoRoute](t, tests.FakeNamespace, "test", - func(t *testing.T, obj *networkingApi.ArangoRoute) { - obj.Spec.Deployment = util.NewType("deployment") - }, - func(t *testing.T, obj *networkingApi.ArangoRoute) { - obj.Spec.Destination = &networkingApi.ArangoRouteSpecDestination{ - Service: &networkingApi.ArangoRouteSpecDestinationService{ - Object: &sharedApi.Object{ - Name: "deployment", - }, - Port: util.NewType(intstr.FromString("test")), - }, - } - }) - deployment := tests.NewMetaObject[*api.ArangoDeployment](t, tests.FakeNamespace, "deployment") - svc := tests.NewMetaObject[*core.Service](t, tests.FakeNamespace, "deployment", func(t *testing.T, obj *core.Service) { - obj.Spec.Ports = []core.ServicePort{ - { - Port: 10245, - }, - } - }) - - refresh := tests.CreateObjects(t, handler.kubeClient, handler.client, &deployment, &extension, &svc) - - // Test - require.NoError(t, tests.Handle(handler, tests.NewItem(t, operation.Update, extension))) - - // Refresh - refresh(t) - - // Assert - require.False(t, extension.Status.Conditions.IsTrue(networkingApi.DestinationValidCondition)) - require.False(t, extension.Status.Conditions.IsTrue(networkingApi.ReadyCondition)) - - c, ok := extension.Status.Conditions.Get(networkingApi.DestinationValidCondition) - require.True(t, ok) - require.EqualValues(t, c.Reason, "Destination Not Found") - require.EqualValues(t, c.Message, "Port `test` not defined on Service `fake/deployment`") -} - -func Test_Handler_Destination_Service_Insecure_Default(t *testing.T) { - // Setup - handler := newFakeHandler() - - // Arrange - extension := tests.NewMetaObject[*networkingApi.ArangoRoute](t, tests.FakeNamespace, "test", - func(t *testing.T, obj *networkingApi.ArangoRoute) { - obj.Spec.Deployment = util.NewType("deployment") - }, - func(t *testing.T, obj *networkingApi.ArangoRoute) { - obj.Spec.Destination = &networkingApi.ArangoRouteSpecDestination{ Service: &networkingApi.ArangoRouteSpecDestinationService{ Object: &sharedApi.Object{ Name: "deployment", @@ -398,166 +68,25 @@ func Test_Handler_Destination_Service_Insecure_Default(t *testing.T) { }, } }) - - refresh := tests.CreateObjects(t, handler.kubeClient, handler.client, &deployment, &extension, &svc) - - // Testcense - require.NoError(t, tests.Handle(handler, tests.NewItem(t, operation.Update, extension))) - - // Refresh - refresh(t) - - // Assert - require.True(t, extension.Status.Conditions.IsTrue(networkingApi.DestinationValidCondition)) - require.True(t, extension.Status.Conditions.IsTrue(networkingApi.ReadyCondition)) - - c, ok := extension.Status.Conditions.Get(networkingApi.DestinationValidCondition) - require.True(t, ok) - require.EqualValues(t, c.Reason, "Destination Found") - require.EqualValues(t, c.Reason, "Destination Found") - require.EqualValues(t, c.Hash, extension.Status.Target.Hash()) - - require.False(t, extension.Status.Target.TLS.IsInsecure()) -} - -func Test_Handler_Destination_Service_Insecure_Nil(t *testing.T) { - // Setup - handler := newFakeHandler() - - // Arrange - extension := tests.NewMetaObject[*networkingApi.ArangoRoute](t, tests.FakeNamespace, "test", - func(t *testing.T, obj *networkingApi.ArangoRoute) { - obj.Spec.Deployment = util.NewType("deployment") - }, - func(t *testing.T, obj *networkingApi.ArangoRoute) { - obj.Spec.Destination = &networkingApi.ArangoRouteSpecDestination{ - Service: &networkingApi.ArangoRouteSpecDestinationService{ - Object: &sharedApi.Object{ - Name: "deployment", - }, - Port: util.NewType(intstr.FromInt32(10244)), - }, - TLS: &networkingApi.ArangoRouteSpecDestinationTLS{ - Insecure: nil, - }, - } - }) - deployment := tests.NewMetaObject[*api.ArangoDeployment](t, tests.FakeNamespace, "deployment") - svc := tests.NewMetaObject[*core.Service](t, tests.FakeNamespace, "deployment", func(t *testing.T, obj *core.Service) { - obj.Spec.Ports = []core.ServicePort{ + endpoints := tests.NewMetaObject[*core.Endpoints](t, tests.FakeNamespace, "deployment", func(t *testing.T, obj *core.Endpoints) { + obj.Subsets = []core.EndpointSubset{ { - Port: 10244, - }, - } - }) - - refresh := tests.CreateObjects(t, handler.kubeClient, handler.client, &deployment, &extension, &svc) - - // Test - require.NoError(t, tests.Handle(handler, tests.NewItem(t, operation.Update, extension))) - - // Refresh - refresh(t) - - // Assert - require.True(t, extension.Status.Conditions.IsTrue(networkingApi.DestinationValidCondition)) - require.True(t, extension.Status.Conditions.IsTrue(networkingApi.ReadyCondition)) - - c, ok := extension.Status.Conditions.Get(networkingApi.DestinationValidCondition) - require.True(t, ok) - require.EqualValues(t, c.Reason, "Destination Found") - require.EqualValues(t, c.Reason, "Destination Found") - require.EqualValues(t, c.Hash, extension.Status.Target.Hash()) - - require.False(t, extension.Status.Target.TLS.IsInsecure()) -} - -func Test_Handler_Destination_Service_Insecure_HTTPS_Override(t *testing.T) { - // Setup - handler := newFakeHandler() - - // Arrange - extension := tests.NewMetaObject[*networkingApi.ArangoRoute](t, tests.FakeNamespace, "test", - func(t *testing.T, obj *networkingApi.ArangoRoute) { - obj.Spec.Deployment = util.NewType("deployment") - }, - func(t *testing.T, obj *networkingApi.ArangoRoute) { - obj.Spec.Destination = &networkingApi.ArangoRouteSpecDestination{ - Service: &networkingApi.ArangoRouteSpecDestinationService{ - Object: &sharedApi.Object{ - Name: "deployment", + Addresses: []core.EndpointAddress{ + { + IP: "127.0.0.1", }, - Port: util.NewType(intstr.FromInt32(10244)), }, - TLS: &networkingApi.ArangoRouteSpecDestinationTLS{ - Insecure: util.NewType(true), - }, - Schema: util.NewType(networkingApi.ArangoRouteSpecDestinationSchemaHTTPS), - } - }) - deployment := tests.NewMetaObject[*api.ArangoDeployment](t, tests.FakeNamespace, "deployment") - svc := tests.NewMetaObject[*core.Service](t, tests.FakeNamespace, "deployment", func(t *testing.T, obj *core.Service) { - obj.Spec.Ports = []core.ServicePort{ - { - Port: 10244, - }, - } - }) - - refresh := tests.CreateObjects(t, handler.kubeClient, handler.client, &deployment, &extension, &svc) - - // Test - require.NoError(t, tests.Handle(handler, tests.NewItem(t, operation.Update, extension))) - - // Refresh - refresh(t) - - // Assert - require.True(t, extension.Status.Conditions.IsTrue(networkingApi.DestinationValidCondition)) - require.True(t, extension.Status.Conditions.IsTrue(networkingApi.ReadyCondition)) - - c, ok := extension.Status.Conditions.Get(networkingApi.DestinationValidCondition) - require.True(t, ok) - require.EqualValues(t, c.Reason, "Destination Found") - require.EqualValues(t, c.Reason, "Destination Found") - require.EqualValues(t, c.Hash, extension.Status.Target.Hash()) - - require.True(t, extension.Status.Target.TLS.IsInsecure()) -} - -func Test_Handler_Destination_Service_Insecure_HTTP_Override(t *testing.T) { - // Setup - handler := newFakeHandler() - - // Arrange - extension := tests.NewMetaObject[*networkingApi.ArangoRoute](t, tests.FakeNamespace, "test", - func(t *testing.T, obj *networkingApi.ArangoRoute) { - obj.Spec.Deployment = util.NewType("deployment") - }, - func(t *testing.T, obj *networkingApi.ArangoRoute) { - obj.Spec.Destination = &networkingApi.ArangoRouteSpecDestination{ - Service: &networkingApi.ArangoRouteSpecDestinationService{ - Object: &sharedApi.Object{ - Name: "deployment", + Ports: []core.EndpointPort{ + { + Name: "", + Port: 10244, }, - Port: util.NewType(intstr.FromInt32(10244)), - }, - TLS: &networkingApi.ArangoRouteSpecDestinationTLS{ - Insecure: util.NewType(true), }, - Schema: util.NewType(networkingApi.ArangoRouteSpecDestinationSchemaHTTP), - } - }) - deployment := tests.NewMetaObject[*api.ArangoDeployment](t, tests.FakeNamespace, "deployment") - svc := tests.NewMetaObject[*core.Service](t, tests.FakeNamespace, "deployment", func(t *testing.T, obj *core.Service) { - obj.Spec.Ports = []core.ServicePort{ - { - Port: 10244, }, } }) - refresh := tests.CreateObjects(t, handler.kubeClient, handler.client, &deployment, &extension, &svc) + refresh := tests.CreateObjects(t, handler.kubeClient, handler.client, &deployment, &extension, &svc, &endpoints) // Test require.NoError(t, tests.Handle(handler, tests.NewItem(t, operation.Update, extension))) @@ -566,14 +95,8 @@ func Test_Handler_Destination_Service_Insecure_HTTP_Override(t *testing.T) { refresh(t) // Assert - require.True(t, extension.Status.Conditions.IsTrue(networkingApi.DestinationValidCondition)) - require.True(t, extension.Status.Conditions.IsTrue(networkingApi.ReadyCondition)) - - c, ok := extension.Status.Conditions.Get(networkingApi.DestinationValidCondition) + require.False(t, extension.Status.Conditions.IsTrue(networkingApi.SpecValidCondition)) + c, ok := extension.Status.Conditions.Get(networkingApi.SpecValidCondition) require.True(t, ok) - require.EqualValues(t, c.Reason, "Destination Found") - require.EqualValues(t, c.Reason, "Destination Found") - require.EqualValues(t, c.Hash, extension.Status.Target.Hash()) - - require.False(t, extension.Status.Target.TLS.IsInsecure()) + require.EqualValues(t, "Received 1 errors: spec.destination: Too many elements provided. Expected 1, got 2. Defined: endpoints, service", c.Message) } diff --git a/pkg/replication/finalizers.go b/pkg/replication/finalizers.go index 028075a0b..618617fb4 100644 --- a/pkg/replication/finalizers.go +++ b/pkg/replication/finalizers.go @@ -32,7 +32,6 @@ import ( "github.com/arangodb/go-driver" api "github.com/arangodb/kube-arangodb/pkg/apis/replication/v1" - "github.com/arangodb/kube-arangodb/pkg/generated/clientset/versioned" "github.com/arangodb/kube-arangodb/pkg/util" "github.com/arangodb/kube-arangodb/pkg/util/constants" "github.com/arangodb/kube-arangodb/pkg/util/errors" @@ -85,7 +84,9 @@ func (dr *DeploymentReplication) runFinalizers(ctx context.Context, p *api.Arang } removalList := []string{constants.FinalizerDeplReplStopSync} - if err := removeDeploymentReplicationFinalizers(dr.deps.Client.Arango(), p, removalList, false); err != nil { + c := dr.deps.Client.Arango().ReplicationV1().ArangoDeploymentReplications(p.GetNamespace()) + + if _, err := k8sutil.RemoveSelectedFinalizers[*api.ArangoDeploymentReplication](ctx, c, c, p, removalList, false); err != nil { return true, errors.WithMessage(err, "Failed to update deployment replication (to remove finalizers)") } @@ -232,31 +233,6 @@ func (dr *DeploymentReplication) inspectFinalizerDeplReplStopSync(ctx context.Co } -// removeDeploymentReplicationFinalizers removes the given finalizers from the given DeploymentReplication. -func removeDeploymentReplicationFinalizers(crcli versioned.Interface, p *api.ArangoDeploymentReplication, finalizers []string, ignoreNotFound bool) error { - repls := crcli.ReplicationV1().ArangoDeploymentReplications(p.GetNamespace()) - getFunc := func() (meta.Object, error) { - result, err := repls.Get(context.Background(), p.GetName(), meta.GetOptions{}) - if err != nil { - return nil, errors.WithStack(err) - } - return result, nil - } - updateFunc := func(updated meta.Object) error { - updatedRepl := updated.(*api.ArangoDeploymentReplication) - result, err := repls.Update(context.Background(), updatedRepl, meta.UpdateOptions{}) - if err != nil { - return errors.WithStack(err) - } - *p = *result - return nil - } - if _, err := k8sutil.RemoveFinalizers(finalizers, getFunc, updateFunc, ignoreNotFound); err != nil { - return errors.WithStack(err) - } - return nil -} - // finalizerExists returns true if a given finalizer exists. func finalizerExists(p *api.ArangoDeploymentReplication, finalizer string) bool { for _, f := range p.ObjectMeta.GetFinalizers() { diff --git a/pkg/util/k8sutil/finalizers.go b/pkg/util/k8sutil/finalizers.go index 35100f54b..9f2180867 100644 --- a/pkg/util/k8sutil/finalizers.go +++ b/pkg/util/k8sutil/finalizers.go @@ -24,95 +24,42 @@ import ( "context" "sort" - core "k8s.io/api/core/v1" meta "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/arangodb/kube-arangodb/pkg/util/errors" - "github.com/arangodb/kube-arangodb/pkg/util/globals" - "github.com/arangodb/kube-arangodb/pkg/util/k8sutil/inspector/persistentvolumeclaim" - persistentvolumeclaimv1 "github.com/arangodb/kube-arangodb/pkg/util/k8sutil/inspector/persistentvolumeclaim/v1" - "github.com/arangodb/kube-arangodb/pkg/util/k8sutil/inspector/pod" - podv1 "github.com/arangodb/kube-arangodb/pkg/util/k8sutil/inspector/pod/v1" + "github.com/arangodb/kube-arangodb/pkg/util/k8sutil/inspector/generic" "github.com/arangodb/kube-arangodb/pkg/util/k8sutil/kerrors" + "github.com/arangodb/kube-arangodb/pkg/util/k8sutil/patcher" ) const ( maxRemoveFinalizersAttempts = 50 ) -// RemovePodFinalizers removes the given finalizers from the given pod. -func RemovePodFinalizers(ctx context.Context, cachedStatus pod.Inspector, c podv1.ModInterface, p *core.Pod, +// RemoveSelectedFinalizers removes the given finalizers from the given pod. +func RemoveSelectedFinalizers[T meta.Object](ctx context.Context, getter generic.GetInterface[T], patcher generic.PatchInterface[T], p T, finalizers []string, ignoreNotFound bool) (int, error) { - getFunc := func() (meta.Object, error) { - ctxChild, cancel := globals.GetGlobalTimeouts().Kubernetes().WithTimeout(ctx) - defer cancel() - - result, err := cachedStatus.Pod().V1().Read().Get(ctxChild, p.GetName(), meta.GetOptions{}) - if err != nil { - return nil, errors.WithStack(err) - } - return result, nil - } - updateFunc := func(updated meta.Object) error { - updatedPod := updated.(*core.Pod) - ctxChild, cancel := globals.GetGlobalTimeouts().Kubernetes().WithTimeout(ctx) - defer cancel() - - result, err := c.Update(ctxChild, updatedPod, meta.UpdateOptions{}) - if err != nil { - return errors.WithStack(err) - } - *p = *result - return nil - } - if count, err := RemoveFinalizers(finalizers, getFunc, updateFunc, ignoreNotFound); err != nil { + if count, err := RemoveFinalizers(ctx, getter, patcher, p.GetName(), finalizers, ignoreNotFound); err != nil { return 0, errors.WithStack(err) } else { return count, nil } } -// RemovePVCFinalizers removes the given finalizers from the given PVC. -func RemovePVCFinalizers(ctx context.Context, cachedStatus persistentvolumeclaim.Inspector, c persistentvolumeclaimv1.ModInterface, - p *core.PersistentVolumeClaim, finalizers []string, ignoreNotFound bool) (int, error) { - getFunc := func() (meta.Object, error) { - ctxChild, cancel := globals.GetGlobalTimeouts().Kubernetes().WithTimeout(ctx) - defer cancel() - - result, err := cachedStatus.PersistentVolumeClaim().V1().Read().Get(ctxChild, p.GetName(), meta.GetOptions{}) - if err != nil { - return nil, errors.WithStack(err) - } - return result, nil - } - updateFunc := func(updated meta.Object) error { - updatedPVC := updated.(*core.PersistentVolumeClaim) - ctxChild, cancel := globals.GetGlobalTimeouts().Kubernetes().WithTimeout(ctx) - defer cancel() - - result, err := c.Update(ctxChild, updatedPVC, meta.UpdateOptions{}) - if err != nil { - return errors.WithStack(err) - } - *p = *result - return nil - } - if count, err := RemoveFinalizers(finalizers, getFunc, updateFunc, ignoreNotFound); err != nil { - return 0, errors.WithStack(err) - } else { - return count, nil - } +type RemoveFinalizersClient[T meta.Object] interface { + generic.GetInterface[T] + generic.PatchInterface[T] } // RemoveFinalizers is a helper used to remove finalizers from an object. // The functions tries to get the object using the provided get function, // then remove the given finalizers and update the update using the given update function. // In case of an update conflict, the functions tries again. -func RemoveFinalizers(finalizers []string, getFunc func() (meta.Object, error), updateFunc func(meta.Object) error, ignoreNotFound bool) (int, error) { +func RemoveFinalizers[T meta.Object](ctx context.Context, getter generic.GetInterface[T], p generic.PatchInterface[T], name string, finalizers []string, ignoreNotFound bool) (int, error) { attempts := 0 for { attempts++ - obj, err := getFunc() + obj, err := getter.Get(ctx, name, meta.GetOptions{}) if err != nil { if kerrors.IsNotFound(err) && ignoreNotFound { // Object no longer found and we're allowed to ignore that. @@ -140,8 +87,7 @@ func RemoveFinalizers(finalizers []string, getFunc func() (meta.Object, error), } } if z := len(original) - len(newList); z > 0 { - obj.SetFinalizers(newList) - if err := updateFunc(obj); kerrors.IsConflict(err) { + if _, _, err := patcher.Patcher[T](ctx, p, obj, meta.PatchOptions{}, patcher.Finalizers[T](newList)); kerrors.IsConflict(err) { if attempts > maxRemoveFinalizersAttempts { return 0, errors.WithStack(err) } else { diff --git a/pkg/util/k8sutil/patcher/common.go b/pkg/util/k8sutil/patcher/common.go new file mode 100644 index 000000000..a5cbc3b33 --- /dev/null +++ b/pkg/util/k8sutil/patcher/common.go @@ -0,0 +1,35 @@ +// +// DISCLAIMER +// +// Copyright 2024 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +package patcher + +import ( + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/arangodb/kube-arangodb/pkg/deployment/patch" +) + +func Finalizers[T meta.Object](finalizers []string) Patch[T] { + return func(in T) []patch.Item { + return []patch.Item{ + patch.ItemReplace(patch.NewPath("metadata", "finalizers"), finalizers), + } + } +} diff --git a/pkg/util/k8sutil/patcher/patcher.go b/pkg/util/k8sutil/patcher/patcher.go index 41bbd53b7..ab95ff474 100644 --- a/pkg/util/k8sutil/patcher/patcher.go +++ b/pkg/util/k8sutil/patcher/patcher.go @@ -30,12 +30,13 @@ import ( "github.com/arangodb/kube-arangodb/pkg/deployment/patch" "github.com/arangodb/kube-arangodb/pkg/util" "github.com/arangodb/kube-arangodb/pkg/util/globals" + "github.com/arangodb/kube-arangodb/pkg/util/k8sutil/inspector/generic" ) type Patch[T meta.Object] func(in T) []patch.Item type Client[T meta.Object] interface { - Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts meta.PatchOptions, subresources ...string) (result T, err error) + generic.PatchInterface[T] } func Patcher[T meta.Object](ctx context.Context, client Client[T], in T, opts meta.PatchOptions, functions ...Patch[T]) (T, bool, error) { diff --git a/pkg/util/list.go b/pkg/util/list.go index 2046e4ee8..13409bc7f 100644 --- a/pkg/util/list.go +++ b/pkg/util/list.go @@ -107,3 +107,19 @@ func CopyList[A any](in []A) []A { copy(ret, in) return ret } + +func FlattenList[A any](in [][]A) []A { + count := 0 + + for _, v := range in { + count += len(v) + } + + res := make([]A, 0, count) + + for _, v := range in { + res = append(res, v...) + } + + return res +} diff --git a/pkg/util/tests/kubernetes.go b/pkg/util/tests/kubernetes.go index 5731a783a..ea5c520a1 100644 --- a/pkg/util/tests/kubernetes.go +++ b/pkg/util/tests/kubernetes.go @@ -143,6 +143,12 @@ func CreateObjects(t *testing.T, k8s kubernetes.Interface, arango arangoClientSe vl := *v _, err := k8s.CoreV1().Services(vl.GetNamespace()).Create(context.Background(), vl, meta.CreateOptions{}) require.NoError(t, err) + case **core.Endpoints: + require.NotNil(t, v) + + vl := *v + _, err := k8s.CoreV1().Endpoints(vl.GetNamespace()).Create(context.Background(), vl, meta.CreateOptions{}) + require.NoError(t, err) case **core.ServiceAccount: require.NotNil(t, v) @@ -310,6 +316,12 @@ func UpdateObjects(t *testing.T, k8s kubernetes.Interface, arango arangoClientSe vl := *v _, err := k8s.CoreV1().Services(vl.GetNamespace()).Update(context.Background(), vl, meta.UpdateOptions{}) require.NoError(t, err) + case **core.Endpoints: + require.NotNil(t, v) + + vl := *v + _, err := k8s.CoreV1().Endpoints(vl.GetNamespace()).Update(context.Background(), vl, meta.UpdateOptions{}) + require.NoError(t, err) case **core.ServiceAccount: require.NotNil(t, v) @@ -466,6 +478,11 @@ func DeleteObjects(t *testing.T, k8s kubernetes.Interface, arango arangoClientSe vl := *v require.NoError(t, k8s.CoreV1().Services(vl.GetNamespace()).Delete(context.Background(), vl.GetName(), meta.DeleteOptions{})) + case **core.Endpoints: + require.NotNil(t, v) + + vl := *v + require.NoError(t, k8s.CoreV1().Endpoints(vl.GetNamespace()).Delete(context.Background(), vl.GetName(), meta.DeleteOptions{})) case **core.ServiceAccount: require.NotNil(t, v) @@ -664,6 +681,21 @@ func RefreshObjects(t *testing.T, k8s kubernetes.Interface, arango arangoClientS } else { *v = vn } + case **core.Endpoints: + require.NotNil(t, v) + + vl := *v + + vn, err := k8s.CoreV1().Endpoints(vl.GetNamespace()).Get(context.Background(), vl.GetName(), meta.GetOptions{}) + if err != nil { + if kerrors.IsNotFound(err) { + *v = nil + } else { + require.NoError(t, err) + } + } else { + *v = vn + } case **core.ServiceAccount: require.NotNil(t, v) @@ -994,6 +1026,12 @@ func SetMetaBasedOnType(t *testing.T, object meta.Object) { v.SetSelfLink(fmt.Sprintf("/api/v1/services/%s/%s", object.GetNamespace(), object.GetName())) + case *core.Endpoints: + v.Kind = "Endpoints" + v.APIVersion = "v1" + v.SetSelfLink(fmt.Sprintf("/api/v1/endpoints/%s/%s", + object.GetNamespace(), + object.GetName())) case *core.ServiceAccount: v.Kind = "ServiceAccount" v.APIVersion = "v1" @@ -1214,6 +1252,12 @@ func GVK(t *testing.T, object meta.Object) schema.GroupVersionKind { Version: "v1", Kind: "Service", } + case *core.Endpoints: + return schema.GroupVersionKind{ + Group: "", + Version: "v1", + Kind: "Endpoints", + } case *core.ServiceAccount: return schema.GroupVersionKind{ Group: "", diff --git a/pkg/util/tests/kubernetes_test.go b/pkg/util/tests/kubernetes_test.go index f20f6da01..c29d42215 100644 --- a/pkg/util/tests/kubernetes_test.go +++ b/pkg/util/tests/kubernetes_test.go @@ -73,6 +73,7 @@ func Test_NewMetaObject(t *testing.T) { NewMetaObjectRun[*core.ConfigMap](t) NewMetaObjectRun[*core.ServiceAccount](t) NewMetaObjectRun[*core.Service](t) + NewMetaObjectRun[*core.Endpoints](t) NewMetaObjectRun[*apps.StatefulSet](t) NewMetaObjectRun[*rbac.Role](t) NewMetaObjectRun[*rbac.RoleBinding](t)