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/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/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)