diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8e2df76e3..a9abb42b0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,6 +18,7 @@
- (Bugfix) (Platform) Installer move to OCI
- (Bugfix) (Platform) Fix Monitoring RBAC
- (Feature) (Platform) Do not require LM during install commands
+- (Feature) (Platform) ArangoRoute Redirect
## [1.3.1](https://github.com/arangodb/kube-arangodb/tree/1.3.1) (2025-10-07)
- (Documentation) Add ArangoPlatformStorage Docs & Examples
diff --git a/docs/api/ArangoRoute.V1Beta1.md b/docs/api/ArangoRoute.V1Beta1.md
index 0402ede37..d272fe459 100644
--- a/docs/api/ArangoRoute.V1Beta1.md
+++ b/docs/api/ArangoRoute.V1Beta1.md
@@ -73,7 +73,7 @@ Port defines Port or Port Name used as destination
### .spec.destination.path
-Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.3.1/pkg/apis/networking/v1beta1/route_spec_destination.go#L52)
+Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.3.1/pkg/apis/networking/v1beta1/route_spec_destination.go#L57)
Path defines service path used for overrides
@@ -81,7 +81,7 @@ Path defines service path used for overrides
### .spec.destination.protocol
-Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.3.1/pkg/apis/networking/v1beta1/route_spec_destination.go#L46)
+Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.3.1/pkg/apis/networking/v1beta1/route_spec_destination.go#L51)
Protocol defines http protocol used for the route
@@ -91,9 +91,19 @@ Possible Values:
***
+### .spec.destination.redirect.code
+
+Type: `integer` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.3.1/pkg/apis/networking/v1beta1/route_spec_destination_redirect.go#L33)
+
+Code the redirection response status code
+
+Default Value: `307`
+
+***
+
### .spec.destination.schema
-Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.3.1/pkg/apis/networking/v1beta1/route_spec_destination.go#L41)
+Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.3.1/pkg/apis/networking/v1beta1/route_spec_destination.go#L45)
Schema defines HTTP/S schema used for connection
@@ -133,7 +143,7 @@ Port defines Port or Port Name used as destination
### .spec.destination.timeout
-Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.3.1/pkg/apis/networking/v1beta1/route_spec_destination.go#L60)
+Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.3.1/pkg/apis/networking/v1beta1/route_spec_destination.go#L65)
Timeout specify the upstream request timeout
diff --git a/pkg/apis/networking/v1beta1/route_spec_destination.go b/pkg/apis/networking/v1beta1/route_spec_destination.go
index 8ab1412a1..bded8e539 100644
--- a/pkg/apis/networking/v1beta1/route_spec_destination.go
+++ b/pkg/apis/networking/v1beta1/route_spec_destination.go
@@ -35,12 +35,17 @@ type ArangoRouteSpecDestination struct {
// Endpoints defines service upstream reference - which is used to find endpoints
Endpoints *ArangoRouteSpecDestinationEndpoints `json:"endpoints,omitempty"`
+ // Redirect defines redirect instruction
+ Redirect *ArangoRouteSpecDestinationRedirect `json:"redirect,omitempty"`
+
// Schema defines HTTP/S schema used for connection
+ // +doc/default: http
// +doc/enum: http|HTTP Connection
// +doc/enum: https|HTTPS Connection (HTTP with TLS)
Schema *ArangoRouteSpecDestinationSchema `json:"schema,omitempty"`
// Protocol defines http protocol used for the route
+ // +doc/default: http1
// +doc/enum: http1|HTTP 1.1 Protocol
// +doc/enum: http2|HTTP 2 Protocol
Protocol *ArangoRouteDestinationProtocol `json:"protocol,omitempty"`
@@ -68,6 +73,14 @@ func (a *ArangoRouteSpecDestination) GetService() *ArangoRouteSpecDestinationSer
return a.Service
}
+func (a *ArangoRouteSpecDestination) GetRedirect() *ArangoRouteSpecDestinationRedirect {
+ if a == nil || a.Redirect == nil {
+ return nil
+ }
+
+ return a.Redirect
+}
+
func (a *ArangoRouteSpecDestination) GetEndpoints() *ArangoRouteSpecDestinationEndpoints {
if a == nil || a.Endpoints == nil {
return nil
@@ -132,9 +145,10 @@ func (a *ArangoRouteSpecDestination) Validate() error {
}
if err := shared.WithErrors(
- shared.ValidateExclusiveFields(a, 1, "Service", "Endpoints"),
+ shared.ValidateExclusiveFields(a, 1, "Service", "Endpoints", "Redirect"),
shared.ValidateOptionalInterfacePath("service", a.Service),
shared.ValidateOptionalInterfacePath("endpoints", a.Endpoints),
+ shared.ValidateOptionalInterfacePath("redirect", a.Redirect),
shared.ValidateOptionalInterfacePath("schema", a.Schema),
shared.ValidateOptionalInterfacePath("protocol", a.Protocol),
shared.ValidateOptionalInterfacePath("tls", a.TLS),
diff --git a/pkg/apis/networking/v1beta1/route_spec_destination_redirect.go b/pkg/apis/networking/v1beta1/route_spec_destination_redirect.go
new file mode 100644
index 000000000..803700bde
--- /dev/null
+++ b/pkg/apis/networking/v1beta1/route_spec_destination_redirect.go
@@ -0,0 +1,71 @@
+//
+// DISCLAIMER
+//
+// Copyright 2025 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 v1beta1
+
+import (
+ goHttp "net/http"
+
+ shared "github.com/arangodb/kube-arangodb/pkg/apis/shared"
+ "github.com/arangodb/kube-arangodb/pkg/util/errors"
+)
+
+type ArangoRouteSpecDestinationRedirect struct {
+ // Code the redirection response status code
+ // +doc/default: 307
+ Code *int `json:"code,omitempty"`
+}
+
+func (a *ArangoRouteSpecDestinationRedirect) GetCode() int {
+ if a == nil || a.Code == nil {
+ return goHttp.StatusTemporaryRedirect
+ }
+ return *a.Code
+}
+
+func (a *ArangoRouteSpecDestinationRedirect) Validate() error {
+ if a == nil {
+ a = &ArangoRouteSpecDestinationRedirect{}
+ }
+
+ if err := shared.WithErrors(
+ shared.ValidateOptionalPath("code", a.Code, func(i int) error {
+ if i == goHttp.StatusTemporaryRedirect || i == goHttp.StatusMovedPermanently {
+ return nil
+ }
+
+ return errors.Errorf("Invalid code. Got %d, allowed 301 & 307", i)
+ }),
+ ); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (a *ArangoRouteSpecDestinationRedirect) AsStatus() ArangoRouteStatusTargetRedirect {
+ if a == nil {
+ return ArangoRouteStatusTargetRedirect{}
+ }
+
+ return ArangoRouteStatusTargetRedirect{
+ Code: a.GetCode(),
+ }
+}
diff --git a/pkg/apis/networking/v1beta1/route_status_target.go b/pkg/apis/networking/v1beta1/route_status_target.go
index 31d4207f0..6cf5fb7bb 100644
--- a/pkg/apis/networking/v1beta1/route_status_target.go
+++ b/pkg/apis/networking/v1beta1/route_status_target.go
@@ -55,6 +55,9 @@ type ArangoRouteStatusTarget struct {
// Timeout specify the upstream request timeout
Timeout meta.Duration `json:"timeout,omitempty"`
+
+ // Redirect defines the route status
+ Redirect ArangoRouteStatusTargetRedirect `json:"redirect,omitempty"`
}
func (a *ArangoRouteStatusTarget) RenderURLs() []string {
@@ -81,5 +84,5 @@ func (a *ArangoRouteStatusTarget) Hash() string {
if a == nil {
return ""
}
- return util.SHA256FromStringArray(a.Destinations.Hash(), a.Type.Hash(), a.TLS.Hash(), a.Protocol.String(), a.Path, a.Authentication.Hash(), a.Options.Hash(), a.Timeout.String(), a.Route.Hash())
+ return util.SHA256FromNonEmptyStringArray(a.Destinations.Hash(), a.Type.Hash(), a.TLS.Hash(), a.Protocol.String(), a.Path, a.Authentication.Hash(), a.Options.Hash(), a.Timeout.String(), a.Route.Hash(), a.Redirect.Hash())
}
diff --git a/pkg/apis/networking/v1beta1/route_status_target_redirect.go b/pkg/apis/networking/v1beta1/route_status_target_redirect.go
new file mode 100644
index 000000000..b0ba0d32c
--- /dev/null
+++ b/pkg/apis/networking/v1beta1/route_status_target_redirect.go
@@ -0,0 +1,35 @@
+//
+// DISCLAIMER
+//
+// Copyright 2025 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 v1beta1
+
+import (
+ "fmt"
+
+ "github.com/arangodb/kube-arangodb/pkg/util"
+)
+
+type ArangoRouteStatusTargetRedirect struct {
+ Code int `json:"code,omitempty"`
+}
+
+func (a ArangoRouteStatusTargetRedirect) Hash() string {
+ return util.SHA256FromStringArray(fmt.Sprintf("%d", a.Code))
+}
diff --git a/pkg/apis/networking/v1beta1/route_status_target_type.go b/pkg/apis/networking/v1beta1/route_status_target_type.go
index 587375ff8..916aa8f09 100644
--- a/pkg/apis/networking/v1beta1/route_status_target_type.go
+++ b/pkg/apis/networking/v1beta1/route_status_target_type.go
@@ -31,4 +31,5 @@ func (a ArangoRouteStatusTargetType) Hash() string {
const (
ArangoRouteStatusTargetServiceType ArangoRouteStatusTargetType = "service"
ArangoRouteStatusTargetEndpointsType ArangoRouteStatusTargetType = "endpoints"
+ ArangoRouteStatusTargetRedirectType ArangoRouteStatusTargetType = "redirect"
)
diff --git a/pkg/apis/networking/v1beta1/zz_generated.deepcopy.go b/pkg/apis/networking/v1beta1/zz_generated.deepcopy.go
index 1b2493f93..b1d116dae 100644
--- a/pkg/apis/networking/v1beta1/zz_generated.deepcopy.go
+++ b/pkg/apis/networking/v1beta1/zz_generated.deepcopy.go
@@ -143,6 +143,11 @@ func (in *ArangoRouteSpecDestination) DeepCopyInto(out *ArangoRouteSpecDestinati
*out = new(ArangoRouteSpecDestinationEndpoints)
(*in).DeepCopyInto(*out)
}
+ if in.Redirect != nil {
+ in, out := &in.Redirect, &out.Redirect
+ *out = new(ArangoRouteSpecDestinationRedirect)
+ (*in).DeepCopyInto(*out)
+ }
if in.Schema != nil {
in, out := &in.Schema, &out.Schema
*out = new(ArangoRouteSpecDestinationSchema)
@@ -238,6 +243,27 @@ func (in *ArangoRouteSpecDestinationEndpoints) DeepCopy() *ArangoRouteSpecDestin
return out
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ArangoRouteSpecDestinationRedirect) DeepCopyInto(out *ArangoRouteSpecDestinationRedirect) {
+ *out = *in
+ if in.Code != nil {
+ in, out := &in.Code, &out.Code
+ *out = new(int)
+ **out = **in
+ }
+ return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ArangoRouteSpecDestinationRedirect.
+func (in *ArangoRouteSpecDestinationRedirect) DeepCopy() *ArangoRouteSpecDestinationRedirect {
+ if in == nil {
+ return nil
+ }
+ out := new(ArangoRouteSpecDestinationRedirect)
+ 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
@@ -426,6 +452,7 @@ func (in *ArangoRouteStatusTarget) DeepCopyInto(out *ArangoRouteStatusTarget) {
}
out.Route = in.Route
out.Timeout = in.Timeout
+ out.Redirect = in.Redirect
return
}
@@ -557,6 +584,22 @@ func (in ArangoRouteStatusTargetOptionsUpgrade) DeepCopy() ArangoRouteStatusTarg
return *out
}
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ArangoRouteStatusTargetRedirect) DeepCopyInto(out *ArangoRouteStatusTargetRedirect) {
+ *out = *in
+ return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ArangoRouteStatusTargetRedirect.
+func (in *ArangoRouteStatusTargetRedirect) DeepCopy() *ArangoRouteStatusTargetRedirect {
+ if in == nil {
+ return nil
+ }
+ out := new(ArangoRouteStatusTargetRedirect)
+ in.DeepCopyInto(out)
+ return out
+}
+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ArangoRouteStatusTargetRoute) DeepCopyInto(out *ArangoRouteStatusTargetRoute) {
*out = *in
diff --git a/pkg/apis/shared/validate.go b/pkg/apis/shared/validate.go
index 420e76a2a..548169165 100644
--- a/pkg/apis/shared/validate.go
+++ b/pkg/apis/shared/validate.go
@@ -205,6 +205,13 @@ func ValidatePath[T any](path string, in T, validator func(T) error) error {
return PrefixResourceErrors(path, validator(in))
}
+// ValidateMultiPath Validates object
+func ValidateMultiPath[T any](path string, in T, validators ...func(T) error) error {
+ return PrefixResourceErrors(path, util.FormatList(validators, func(a func(T) error) error {
+ return a(in)
+ })...)
+}
+
// ValidateRequiredPath Validates object and required not nil value
func ValidateRequiredPath[T any](path string, in *T, validator func(T) error) error {
return PrefixResourceErrors(path, ValidateRequired(in, validator))
diff --git a/pkg/crd/crds/networking-route.schema.generated.yaml b/pkg/crd/crds/networking-route.schema.generated.yaml
index c5ed2539b..09d1ed325 100644
--- a/pkg/crd/crds/networking-route.schema.generated.yaml
+++ b/pkg/crd/crds/networking-route.schema.generated.yaml
@@ -174,6 +174,14 @@ v1beta1:
- http1
- http2
type: string
+ redirect:
+ description: Redirect defines redirect instruction
+ properties:
+ code:
+ description: Code the redirection response status code
+ format: int32
+ type: integer
+ type: object
schema:
description: Schema defines HTTP/S schema used for connection
enum:
diff --git a/pkg/deployment/resources/config_map_gateway.go b/pkg/deployment/resources/config_map_gateway.go
index 4b3840d36..b4ecd5506 100644
--- a/pkg/deployment/resources/config_map_gateway.go
+++ b/pkg/deployment/resources/config_map_gateway.go
@@ -347,50 +347,76 @@ func (r *Resources) renderGatewayConfig(cachedStatus inspectorInterface.Inspecto
}
if target := at.Status.Target; target != nil {
+ var dest gateway.ConfigDestination
+
if target.Route.Path == "" {
log.Warn("ArangoRoute Route Path not defined")
return nil
}
- var dest gateway.ConfigDestination
- if destinations := target.Destinations; len(destinations) > 0 {
- for _, destination := range destinations {
- var t gateway.ConfigDestinationTarget
- t.Host = destination.Host
- t.Port = destination.Port
+ switch target.Type {
+ case networkingApi.ArangoRouteStatusTargetRedirectType:
+ dest.Redirect = &gateway.ConfigDestinationRedirect{
+ Code: target.Redirect.Code,
+ }
+ dest.Path = util.NewType(target.Path)
+ dest.Match = util.NewType(gateway.ConfigMatchPath)
+ dest.Type = util.NewType(gateway.ConfigDestinationTypeRedirect)
+ dest.ResponseHeaders = map[string]string{
+ utilConstants.EnvoyRouteHeader: at.GetName(),
+ }
+ dest.AuthExtension = &gateway.ConfigAuthZExtension{
+ AuthZExtension: map[string]string{
+ pbImplEnvoyAuthV3Shared.AuthConfigAuthRequiredKey: pbImplEnvoyAuthV3Shared.AuthConfigKeywordFalse,
+ pbImplEnvoyAuthV3Shared.AuthConfigAuthPassModeKey: string(networkingApi.ArangoRouteSpecAuthenticationPassModeRemove),
+ },
+ }
+ case networkingApi.ArangoRouteStatusTargetEndpointsType, networkingApi.ArangoRouteStatusTargetServiceType:
+ if destinations := target.Destinations; len(destinations) > 0 {
+ for _, destination := range destinations {
+ var t gateway.ConfigDestinationTarget
+
+ t.Host = destination.Host
+ t.Port = destination.Port
- dest.Targets = append(dest.Targets, t)
+ dest.Targets = append(dest.Targets, t)
+ }
}
- }
- if tls := target.TLS; tls != nil {
- dest.Type = util.NewType(gateway.ConfigDestinationTypeHTTPS)
- dest.TLS.Insecure = util.NewType(tls.IsInsecure())
- }
- switch target.Protocol {
- case networkingApi.ArangoRouteDestinationProtocolHTTP1:
- dest.Protocol = util.NewType(gateway.ConfigDestinationProtocolHTTP1)
- case networkingApi.ArangoRouteDestinationProtocolHTTP2:
- dest.Protocol = util.NewType(gateway.ConfigDestinationProtocolHTTP2)
- }
- if opts := target.Options; opts != nil {
- for _, upgrade := range opts.Upgrade {
- dest.UpgradeConfigs = append(dest.UpgradeConfigs, gateway.ConfigDestinationUpgrade{
- Type: string(upgrade.Type),
- Enabled: util.NewType(util.WithDefault(upgrade.Enabled)),
- })
+ dest.Match = util.NewType(gateway.ConfigMatchPrefix)
+ dest.Type = util.NewType(gateway.ConfigDestinationTypeHTTP)
+ if tls := target.TLS; tls != nil {
+ dest.Type = util.NewType(gateway.ConfigDestinationTypeHTTPS)
+ dest.TLS.Insecure = util.NewType(tls.IsInsecure())
}
+ switch target.Protocol {
+ case networkingApi.ArangoRouteDestinationProtocolHTTP1:
+ dest.Protocol = util.NewType(gateway.ConfigDestinationProtocolHTTP1)
+ case networkingApi.ArangoRouteDestinationProtocolHTTP2:
+ dest.Protocol = util.NewType(gateway.ConfigDestinationProtocolHTTP2)
+ }
+ if opts := target.Options; opts != nil {
+ for _, upgrade := range opts.Upgrade {
+ dest.UpgradeConfigs = append(dest.UpgradeConfigs, gateway.ConfigDestinationUpgrade{
+ Type: string(upgrade.Type),
+ Enabled: util.NewType(util.WithDefault(upgrade.Enabled)),
+ })
+ }
+ }
+ dest.Path = util.NewType(target.Path)
+ dest.Timeout = target.Timeout.DeepCopy()
+ dest.AuthExtension = &gateway.ConfigAuthZExtension{
+ AuthZExtension: map[string]string{
+ pbImplEnvoyAuthV3Shared.AuthConfigAuthRequiredKey: util.BoolSwitch[string](target.Authentication.Type.Get() == networkingApi.ArangoRouteSpecAuthenticationTypeRequired, pbImplEnvoyAuthV3Shared.AuthConfigKeywordTrue, pbImplEnvoyAuthV3Shared.AuthConfigKeywordFalse),
+ pbImplEnvoyAuthV3Shared.AuthConfigAuthPassModeKey: string(target.Authentication.PassMode),
+ },
+ }
+ dest.ResponseHeaders = map[string]string{
+ utilConstants.EnvoyRouteHeader: at.GetName(),
+ }
+ default:
+ return errors.Errorf("Unknown route destination type %s", target.Type)
}
- dest.Path = util.NewType(target.Path)
- dest.Timeout = target.Timeout.DeepCopy()
- dest.AuthExtension = &gateway.ConfigAuthZExtension{
- AuthZExtension: map[string]string{
- pbImplEnvoyAuthV3Shared.AuthConfigAuthRequiredKey: util.BoolSwitch[string](target.Authentication.Type.Get() == networkingApi.ArangoRouteSpecAuthenticationTypeRequired, pbImplEnvoyAuthV3Shared.AuthConfigKeywordTrue, pbImplEnvoyAuthV3Shared.AuthConfigKeywordFalse),
- pbImplEnvoyAuthV3Shared.AuthConfigAuthPassModeKey: string(target.Authentication.PassMode),
- },
- }
- dest.ResponseHeaders = map[string]string{
- utilConstants.EnvoyRouteHeader: at.GetName(),
- }
+
cfg.Destinations[target.Route.Path] = dest
routes[at.GetName()] = &pbInventoryV1.InventoryNetworkingRoute{
diff --git a/pkg/deployment/resources/gateway/gateway_config_destination.go b/pkg/deployment/resources/gateway/gateway_config_destination.go
index ffc39da61..1d64ce123 100644
--- a/pkg/deployment/resources/gateway/gateway_config_destination.go
+++ b/pkg/deployment/resources/gateway/gateway_config_destination.go
@@ -21,6 +21,7 @@
package gateway
import (
+ goHttp "net/http"
"time"
pbEnvoyClusterV3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
@@ -45,7 +46,7 @@ func (c ConfigDestinations) Validate() error {
return shared.WithErrors(
shared.ValidateMap(c, func(k string, destination ConfigDestination) error {
var errs []error
- if k == "/" {
+ if k == "/" && destination.Type.Get() != ConfigDestinationTypeRedirect {
errs = append(errs, errors.Errorf("Route for `/` is reserved"))
}
if err := shared.ValidateAPIPath(k); err != nil {
@@ -85,6 +86,8 @@ type ConfigDestination struct {
Static ConfigDestinationStaticInterface `json:"static,omitempty"`
File ConfigDestinationFileInterface `json:"file,omitempty"`
+
+ Redirect *ConfigDestinationRedirect `json:"redirect,omitempty"`
}
func (c *ConfigDestination) Validate() error {
@@ -100,6 +103,13 @@ func (c *ConfigDestination) Validate() error {
shared.PrefixResourceError("pathType", shared.ValidateOptionalInterface(c.Match)),
shared.PrefixResourceError("authExtension", c.AuthExtension.Validate()),
)
+ case ConfigDestinationTypeRedirect:
+ return shared.WithErrors(
+ shared.PrefixResourceError("type", c.Type.Validate()),
+ shared.PrefixResourceError("path", shared.ValidateAPIPath(c.GetPath())),
+ shared.PrefixResourceError("pathType", shared.ValidateOptionalInterface(c.Match)),
+ shared.PrefixResourceError("redirect", c.Redirect.Validate()),
+ )
case ConfigDestinationTypeStatic:
return shared.WithErrors(
shared.PrefixResourceError("type", c.Type.Validate()),
@@ -230,6 +240,36 @@ func (c *ConfigDestination) appendRouteAction(route *pbEnvoyRouteV3.Route, name
}
return nil
}
+ if c.Type.Get() == ConfigDestinationTypeRedirect {
+ if c.Redirect == nil {
+ return errors.Errorf("Redirect response is not defined!")
+ }
+ switch c.Redirect.Code {
+ case goHttp.StatusTemporaryRedirect:
+ route.Action = &pbEnvoyRouteV3.Route_Redirect{
+ Redirect: &pbEnvoyRouteV3.RedirectAction{
+ PathRewriteSpecifier: &pbEnvoyRouteV3.RedirectAction_PathRedirect{
+ PathRedirect: c.GetPath(),
+ },
+ ResponseCode: pbEnvoyRouteV3.RedirectAction_TEMPORARY_REDIRECT,
+ StripQuery: false,
+ },
+ }
+ return nil
+ case goHttp.StatusMovedPermanently:
+ route.Action = &pbEnvoyRouteV3.Route_Redirect{
+ Redirect: &pbEnvoyRouteV3.RedirectAction{
+ PathRewriteSpecifier: &pbEnvoyRouteV3.RedirectAction_PathRedirect{
+ PathRedirect: c.GetPath(),
+ },
+ ResponseCode: pbEnvoyRouteV3.RedirectAction_MOVED_PERMANENTLY,
+ StripQuery: false,
+ },
+ }
+ return nil
+ }
+ return errors.Errorf("Unable to render redirection action")
+ }
route.Action = &pbEnvoyRouteV3.Route_Route{
Route: &pbEnvoyRouteV3.RouteAction{
diff --git a/pkg/deployment/resources/gateway/gateway_config_destination_redirect.go b/pkg/deployment/resources/gateway/gateway_config_destination_redirect.go
new file mode 100644
index 000000000..28119d131
--- /dev/null
+++ b/pkg/deployment/resources/gateway/gateway_config_destination_redirect.go
@@ -0,0 +1,47 @@
+//
+// DISCLAIMER
+//
+// Copyright 2025 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 gateway
+
+import (
+ goHttp "net/http"
+
+ shared "github.com/arangodb/kube-arangodb/pkg/apis/shared"
+ "github.com/arangodb/kube-arangodb/pkg/util/errors"
+)
+
+type ConfigDestinationRedirect struct {
+ Code int `json:"code"`
+}
+
+func (c *ConfigDestinationRedirect) Validate() error {
+ if c == nil {
+ c = &ConfigDestinationRedirect{}
+ }
+
+ return shared.WithErrors(
+ shared.PrefixResourceErrorFunc("code", func() error {
+ if c.Code == goHttp.StatusTemporaryRedirect || c.Code == goHttp.StatusMovedPermanently {
+ return nil
+ }
+ return errors.Errorf("Invalid code. Got %d, allowed 301 & 307", c.Code)
+ }),
+ )
+}
diff --git a/pkg/deployment/resources/gateway/gateway_config_destination_type.go b/pkg/deployment/resources/gateway/gateway_config_destination_type.go
index 82b64f3a2..441dd701a 100644
--- a/pkg/deployment/resources/gateway/gateway_config_destination_type.go
+++ b/pkg/deployment/resources/gateway/gateway_config_destination_type.go
@@ -36,6 +36,7 @@ const (
ConfigDestinationTypeHTTPS
ConfigDestinationTypeStatic
ConfigDestinationTypeFile
+ ConfigDestinationTypeRedirect
)
func (c *ConfigDestinationType) Get() ConfigDestinationType {
@@ -44,7 +45,7 @@ func (c *ConfigDestinationType) Get() ConfigDestinationType {
}
switch v := *c; v {
- case ConfigDestinationTypeHTTP, ConfigDestinationTypeHTTPS, ConfigDestinationTypeStatic, ConfigDestinationTypeFile:
+ case ConfigDestinationTypeHTTP, ConfigDestinationTypeHTTPS, ConfigDestinationTypeStatic, ConfigDestinationTypeFile, ConfigDestinationTypeRedirect:
return v
default:
return ConfigDestinationTypeHTTP
@@ -80,7 +81,7 @@ func (c *ConfigDestinationType) RenderUpstreamTransportSocket(protocol *ConfigDe
func (c *ConfigDestinationType) Validate() error {
switch c.Get() {
- case ConfigDestinationTypeHTTP, ConfigDestinationTypeHTTPS, ConfigDestinationTypeStatic, ConfigDestinationTypeFile:
+ case ConfigDestinationTypeHTTP, ConfigDestinationTypeHTTPS, ConfigDestinationTypeStatic, ConfigDestinationTypeFile, ConfigDestinationTypeRedirect:
return nil
default:
return errors.Errorf("Invalid destination type")
diff --git a/pkg/deployment/resources/gateway/gateway_config_test.go b/pkg/deployment/resources/gateway/gateway_config_test.go
index e682a6cf8..6ea76e3ff 100644
--- a/pkg/deployment/resources/gateway/gateway_config_test.go
+++ b/pkg/deployment/resources/gateway/gateway_config_test.go
@@ -22,6 +22,7 @@ package gateway
import (
"fmt"
+ goHttp "net/http"
"testing"
pbEnvoyBootstrapV3 "github.com/envoyproxy/go-control-plane/envoy/config/bootstrap/v3"
@@ -464,4 +465,28 @@ func Test_GatewayConfig(t *testing.T) {
},
})
})
+
+ t.Run("Default", func(t *testing.T) {
+ renderAndPrintGatewayConfig(t, Config{
+ DefaultDestination: ConfigDestination{
+ Targets: []ConfigDestinationTarget{
+ {
+ Host: "127.0.0.1",
+ Port: 12345,
+ },
+ },
+ Path: util.NewType("/test/path/"),
+ Type: util.NewType(ConfigDestinationTypeHTTPS),
+ },
+ Destinations: ConfigDestinations{
+ "/_test/": {
+ Redirect: &ConfigDestinationRedirect{
+ Code: goHttp.StatusTemporaryRedirect,
+ },
+ Path: util.NewType("/test/path/"),
+ Type: util.NewType(ConfigDestinationTypeRedirect),
+ },
+ },
+ })
+ })
}
diff --git a/pkg/handlers/networking/route/handler_destination.go b/pkg/handlers/networking/route/handler_destination.go
index 0cd456bc0..b16b3bbcd 100644
--- a/pkg/handlers/networking/route/handler_destination.go
+++ b/pkg/handlers/networking/route/handler_destination.go
@@ -37,6 +37,9 @@ func (h *handler) HandleArangoDestination(ctx context.Context, item operation.It
if endpoints := dest.GetEndpoints(); endpoints != nil {
return h.HandleArangoDestinationEndpoints(ctx, item, extension, status, deployment, dest, endpoints)
}
+ if redirect := dest.GetRedirect(); redirect != nil {
+ return h.HandleArangoDestinationRedirect(ctx, item, extension, status, deployment, dest, redirect)
+ }
}
return &operator.Condition{
diff --git a/pkg/handlers/networking/route/handler_destination_redirect.go b/pkg/handlers/networking/route/handler_destination_redirect.go
new file mode 100644
index 000000000..beace47a2
--- /dev/null
+++ b/pkg/handlers/networking/route/handler_destination_redirect.go
@@ -0,0 +1,56 @@
+//
+// DISCLAIMER
+//
+// Copyright 2025 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"
+
+ api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1"
+ networkingApi "github.com/arangodb/kube-arangodb/pkg/apis/networking/v1beta1"
+ operator "github.com/arangodb/kube-arangodb/pkg/operatorV2"
+ "github.com/arangodb/kube-arangodb/pkg/operatorV2/operation"
+)
+
+func (h *handler) HandleArangoDestinationRedirect(ctx context.Context, item operation.Item, extension *networkingApi.ArangoRoute, status *networkingApi.ArangoRouteStatus, deployment *api.ArangoDeployment, dest *networkingApi.ArangoRouteSpecDestination, redirect *networkingApi.ArangoRouteSpecDestinationRedirect) (*operator.Condition, bool, error) {
+ var target networkingApi.ArangoRouteStatusTarget
+
+ target.Path = dest.GetPath()
+ target.Type = networkingApi.ArangoRouteStatusTargetRedirectType
+ target.Redirect = redirect.AsStatus()
+ target.Route = extension.Spec.Route.AsStatus()
+
+ 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_redirect_test.go b/pkg/handlers/networking/route/handler_destination_redirect_test.go
new file mode 100644
index 000000000..6d31aa207
--- /dev/null
+++ b/pkg/handlers/networking/route/handler_destination_redirect_test.go
@@ -0,0 +1,265 @@
+//
+// DISCLAIMER
+//
+// Copyright 2025 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 (
+ goHttp "net/http"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1"
+ networkingApi "github.com/arangodb/kube-arangodb/pkg/apis/networking/v1beta1"
+ "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_Redirect_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{
+ Redirect: &networkingApi.ArangoRouteSpecDestinationRedirect{},
+ Path: util.NewType("/ui/"),
+ }
+ })
+ 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.True(t, extension.Status.Conditions.IsTrue(networkingApi.DestinationValidCondition))
+ require.True(t, extension.Status.Conditions.IsTrue(networkingApi.ReadyCondition))
+ require.Equal(t, networkingApi.ArangoRouteStatusTargetRedirectType, extension.Status.Target.Type)
+
+ require.EqualValues(t, extension.Status.Target.Path, "/ui/")
+ require.EqualValues(t, extension.Status.Target.Redirect.Code, goHttp.StatusTemporaryRedirect)
+
+ c, ok := extension.Status.Conditions.Get(networkingApi.DestinationValidCondition)
+ require.True(t, ok)
+ require.EqualValues(t, c.Reason, "Destination Found")
+ require.EqualValues(t, c.Message, "Destination Found")
+ require.EqualValues(t, c.Hash, extension.Status.Target.Hash())
+}
+
+func Test_Handler_Redirect_Invalid_Code(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{
+ Redirect: &networkingApi.ArangoRouteSpecDestinationRedirect{
+ Code: util.NewType(goHttp.StatusNotFound),
+ },
+ Path: util.NewType("/ui/"),
+ }
+ })
+ 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.SpecValidCondition))
+ require.False(t, extension.Status.Conditions.IsTrue(networkingApi.ReadyCondition))
+ require.Nil(t, extension.Status.Target)
+
+ c, ok := extension.Status.Conditions.Get(networkingApi.SpecValidCondition)
+ require.True(t, ok)
+ require.EqualValues(t, c.Reason, "Spec is invalid")
+ require.EqualValues(t, c.Message, "Received 1 errors: spec.destination.redirect.code: Invalid code. Got 404, allowed 301 & 307")
+}
+
+func Test_Handler_Redirect_Invalid_Target(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{
+ Redirect: &networkingApi.ArangoRouteSpecDestinationRedirect{},
+ Path: util.NewType("&643/ui/"),
+ }
+ })
+ 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.SpecValidCondition))
+ require.False(t, extension.Status.Conditions.IsTrue(networkingApi.ReadyCondition))
+ require.Nil(t, extension.Status.Target)
+
+ c, ok := extension.Status.Conditions.Get(networkingApi.SpecValidCondition)
+ require.True(t, ok)
+ require.EqualValues(t, c.Reason, "Spec is invalid")
+ require.EqualValues(t, c.Message, "Received 1 errors: spec.destination.path: String '&643/ui/' is not a valid api path")
+}
+
+func Test_Handler_Redirect_Invalid_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{}
+ })
+ 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.SpecValidCondition))
+ require.False(t, extension.Status.Conditions.IsTrue(networkingApi.ReadyCondition))
+ require.Nil(t, extension.Status.Target)
+
+ c, ok := extension.Status.Conditions.Get(networkingApi.SpecValidCondition)
+ require.True(t, ok)
+ require.EqualValues(t, c.Reason, "Spec is invalid")
+ require.EqualValues(t, c.Message, "Received 1 errors: spec.destination: Elements not provided. Expected 1. Possible: endpoints, redirect, service")
+}
+
+func Test_Handler_Redirect_Empty(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{
+ Redirect: &networkingApi.ArangoRouteSpecDestinationRedirect{},
+ }
+ })
+ 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.True(t, extension.Status.Conditions.IsTrue(networkingApi.DestinationValidCondition))
+ require.True(t, extension.Status.Conditions.IsTrue(networkingApi.ReadyCondition))
+ require.Equal(t, networkingApi.ArangoRouteStatusTargetRedirectType, extension.Status.Target.Type)
+
+ require.EqualValues(t, extension.Status.Target.Path, "/")
+ require.EqualValues(t, extension.Status.Target.Redirect.Code, goHttp.StatusTemporaryRedirect)
+
+ c, ok := extension.Status.Conditions.Get(networkingApi.DestinationValidCondition)
+ require.True(t, ok)
+ require.EqualValues(t, c.Reason, "Destination Found")
+ require.EqualValues(t, c.Message, "Destination Found")
+ require.EqualValues(t, c.Hash, extension.Status.Target.Hash())
+}
+
+func Test_Handler_Redirect_Valid_CustomCode(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{
+ Redirect: &networkingApi.ArangoRouteSpecDestinationRedirect{
+ Code: util.NewType(goHttp.StatusMovedPermanently),
+ },
+ Path: util.NewType("/ui/"),
+ }
+ })
+ 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.True(t, extension.Status.Conditions.IsTrue(networkingApi.DestinationValidCondition))
+ require.True(t, extension.Status.Conditions.IsTrue(networkingApi.ReadyCondition))
+ require.Equal(t, networkingApi.ArangoRouteStatusTargetRedirectType, extension.Status.Target.Type)
+
+ require.EqualValues(t, extension.Status.Target.Path, "/ui/")
+ require.EqualValues(t, extension.Status.Target.Redirect.Code, goHttp.StatusMovedPermanently)
+
+ c, ok := extension.Status.Conditions.Get(networkingApi.DestinationValidCondition)
+ require.True(t, ok)
+ require.EqualValues(t, c.Reason, "Destination Found")
+ require.EqualValues(t, c.Message, "Destination Found")
+ require.EqualValues(t, c.Hash, extension.Status.Target.Hash())
+}
diff --git a/pkg/util/checksum.sha256.go b/pkg/util/checksum.sha256.go
index 92f5f5ad1..f6777ce7f 100644
--- a/pkg/util/checksum.sha256.go
+++ b/pkg/util/checksum.sha256.go
@@ -38,6 +38,15 @@ func SHA256FromHashArray[T Hash](data []T) string {
return t.Hash()
}, data...)
}
+func SHA256FromNonEmptyStringArray(data ...string) string {
+ return SHA256FromFilteredStringArray(func(in string) bool {
+ return in != ""
+ }, data...)
+}
+
+func SHA256FromFilteredStringArray(filter func(in string) bool, data ...string) string {
+ return SHA256FromStringArray(FilterList(data, filter)...)
+}
func SHA256FromStringArray(data ...string) string {
return SHA256FromString(goStrings.Join(data, "|"))