1
0
Fork 0
mirror of https://github.com/arangodb/kube-arangodb.git synced 2024-12-14 11:57:37 +00:00

[Feature] [Gateway] Dynamic Configuration (#1718)

This commit is contained in:
Adam Janikowski 2024-09-09 20:00:31 +02:00 committed by GitHub
parent 3d46436c59
commit fe97fc3cc0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 552 additions and 47 deletions

View file

@ -26,6 +26,7 @@
- (Feature) (Gateway) ArangoDB JWT Auth Integration - (Feature) (Gateway) ArangoDB JWT Auth Integration
- (Feature) Scheduler Handler - (Feature) Scheduler Handler
- (Feature) (Gateway) ArangoDB Auth Token - (Feature) (Gateway) ArangoDB Auth Token
- (Feature) (Gateway) Dynamic Configuration
## [1.2.42](https://github.com/arangodb/kube-arangodb/tree/1.2.42) (2024-07-23) ## [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 - (Maintenance) Go 1.22.4 & Kubernetes 1.29.6 libraries

View file

@ -3043,6 +3043,17 @@ Type: `boolean` <sup>[\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.
*** ***
### .spec.gateway.dynamic
Type: `boolean` <sup>[\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.42/pkg/apis/deployment/v1/deployment_spec_gateway.go#L38)</sup>
Dynamic setting enables/disables support dynamic configuration of the gateway in the cluster.
When enabled, gateway config will be reloaded by ConfigMap live updates.
Default Value: `false`
***
### .spec.gateway.enabled ### .spec.gateway.enabled
Type: `boolean` <sup>[\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.42/pkg/apis/deployment/v1/deployment_spec_gateway.go#L33)</sup> Type: `boolean` <sup>[\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.42/pkg/apis/deployment/v1/deployment_spec_gateway.go#L33)</sup>
@ -3056,7 +3067,7 @@ Default Value: `false`
### .spec.gateway.image ### .spec.gateway.image
Type: `string` <sup>[\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.42/pkg/apis/deployment/v1/deployment_spec_gateway.go#L37)</sup> Type: `string` <sup>[\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.2.42/pkg/apis/deployment/v1/deployment_spec_gateway.go#L42)</sup>
Image is the image to use for the gateway. Image is the image to use for the gateway.
By default, the image is determined by the operator. By default, the image is determined by the operator.

View file

@ -32,6 +32,11 @@ type DeploymentSpecGateway struct {
// +doc/default: false // +doc/default: false
Enabled *bool `json:"enabled,omitempty"` Enabled *bool `json:"enabled,omitempty"`
// Dynamic setting enables/disables support dynamic configuration of the gateway in the cluster.
// When enabled, gateway config will be reloaded by ConfigMap live updates.
// +doc/default: false
Dynamic *bool `json:"dynamic,omitempty"`
// Image is the image to use for the gateway. // Image is the image to use for the gateway.
// By default, the image is determined by the operator. // By default, the image is determined by the operator.
Image *string `json:"image"` Image *string `json:"image"`
@ -49,6 +54,15 @@ func (d *DeploymentSpecGateway) IsEnabled() bool {
return *d.Enabled return *d.Enabled
} }
// IsDynamic returns whether the gateway dynamic config is enabled.
func (d *DeploymentSpecGateway) IsDynamic() bool {
if d == nil || d.Dynamic == nil {
return false
}
return *d.Dynamic
}
func (d *DeploymentSpecGateway) GetSidecar() *schedulerApi.IntegrationSidecar { func (d *DeploymentSpecGateway) GetSidecar() *schedulerApi.IntegrationSidecar {
if d == nil || d.Sidecar == nil { if d == nil || d.Sidecar == nil {
return nil return nil

View file

@ -1180,6 +1180,11 @@ func (in *DeploymentSpecGateway) DeepCopyInto(out *DeploymentSpecGateway) {
*out = new(bool) *out = new(bool)
**out = **in **out = **in
} }
if in.Dynamic != nil {
in, out := &in.Dynamic, &out.Dynamic
*out = new(bool)
**out = **in
}
if in.Image != nil { if in.Image != nil {
in, out := &in.Image, &out.Image in, out := &in.Image, &out.Image
*out = new(string) *out = new(string)

View file

@ -32,6 +32,11 @@ type DeploymentSpecGateway struct {
// +doc/default: false // +doc/default: false
Enabled *bool `json:"enabled,omitempty"` Enabled *bool `json:"enabled,omitempty"`
// Dynamic setting enables/disables support dynamic configuration of the gateway in the cluster.
// When enabled, gateway config will be reloaded by ConfigMap live updates.
// +doc/default: false
Dynamic *bool `json:"dynamic,omitempty"`
// Image is the image to use for the gateway. // Image is the image to use for the gateway.
// By default, the image is determined by the operator. // By default, the image is determined by the operator.
Image *string `json:"image"` Image *string `json:"image"`
@ -49,6 +54,15 @@ func (d *DeploymentSpecGateway) IsEnabled() bool {
return *d.Enabled return *d.Enabled
} }
// IsDynamic returns whether the gateway dynamic config is enabled.
func (d *DeploymentSpecGateway) IsDynamic() bool {
if d == nil || d.Dynamic == nil {
return false
}
return *d.Dynamic
}
func (d *DeploymentSpecGateway) GetSidecar() *schedulerApi.IntegrationSidecar { func (d *DeploymentSpecGateway) GetSidecar() *schedulerApi.IntegrationSidecar {
if d == nil || d.Sidecar == nil { if d == nil || d.Sidecar == nil {
return nil return nil

View file

@ -1180,6 +1180,11 @@ func (in *DeploymentSpecGateway) DeepCopyInto(out *DeploymentSpecGateway) {
*out = new(bool) *out = new(bool)
**out = **in **out = **in
} }
if in.Dynamic != nil {
in, out := &in.Dynamic, &out.Dynamic
*out = new(bool)
**out = **in
}
if in.Image != nil { if in.Image != nil {
in, out := &in.Image, &out.Image in, out := &in.Image, &out.Image
*out = new(string) *out = new(string)

View file

@ -6567,6 +6567,11 @@ v1:
gateway: gateway:
description: Gateway defined main Gateway configuration. description: Gateway defined main Gateway configuration.
properties: properties:
dynamic:
description: |-
Dynamic setting enables/disables support dynamic configuration of the gateway in the cluster.
When enabled, gateway config will be reloaded by ConfigMap live updates.
type: boolean
enabled: enabled:
description: |- description: |-
Enabled setting enables/disables support for gateway in the cluster. Enabled setting enables/disables support for gateway in the cluster.
@ -23084,6 +23089,11 @@ v1alpha:
gateway: gateway:
description: Gateway defined main Gateway configuration. description: Gateway defined main Gateway configuration.
properties: properties:
dynamic:
description: |-
Dynamic setting enables/disables support dynamic configuration of the gateway in the cluster.
When enabled, gateway config will be reloaded by ConfigMap live updates.
type: boolean
enabled: enabled:
description: |- description: |-
Enabled setting enables/disables support for gateway in the cluster. Enabled setting enables/disables support for gateway in the cluster.

View file

@ -55,7 +55,26 @@ func (r *Resources) ensureGatewayConfig(ctx context.Context, cachedStatus inspec
return errors.WithStack(errors.Wrapf(err, "Failed to generate gateway config")) return errors.WithStack(errors.Wrapf(err, "Failed to generate gateway config"))
} }
gatewayCfgYaml, gatewayCfgChecksum, _, err := cfg.RenderYAML() gatewayCfgYaml, _, _, err := cfg.RenderYAML()
if err != nil {
return errors.WithStack(errors.Wrapf(err, "Failed to render gateway config"))
}
gatewayCfgCDSYaml, _, _, err := cfg.RenderCDSYAML()
if err != nil {
return errors.WithStack(errors.Wrapf(err, "Failed to render gateway cds config"))
}
gatewayCfgLDSYaml, _, _, err := cfg.RenderLDSYAML()
if err != nil {
return errors.WithStack(errors.Wrapf(err, "Failed to render gateway lds config"))
}
elements, err := r.renderConfigMap(map[string]string{
GatewayConfigFileName: string(gatewayCfgYaml),
GatewayCDSConfigFileName: string(gatewayCfgCDSYaml),
GatewayLDSConfigFileName: string(gatewayCfgLDSYaml),
})
if err != nil { if err != nil {
return errors.WithStack(errors.Wrapf(err, "Failed to render gateway config")) return errors.WithStack(errors.Wrapf(err, "Failed to render gateway config"))
} }
@ -66,10 +85,7 @@ func (r *Resources) ensureGatewayConfig(ctx context.Context, cachedStatus inspec
ObjectMeta: meta.ObjectMeta{ ObjectMeta: meta.ObjectMeta{
Name: configMapName, Name: configMapName,
}, },
Data: map[string]string{ Data: elements,
GatewayConfigFileName: string(gatewayCfgYaml),
GatewayConfigChecksumFileName: gatewayCfgChecksum,
},
} }
owner := r.context.GetAPIObject().AsOwner() owner := r.context.GetAPIObject().AsOwner()
@ -88,17 +104,14 @@ func (r *Resources) ensureGatewayConfig(ctx context.Context, cachedStatus inspec
return errors.Reconcile() return errors.Reconcile()
} else { } else {
// CM Exists, checks checksum - if key is not in the map we return empty string // CM Exists, checks checksum - if key is not in the map we return empty string
if existingSha, existingChecksumSha := util.SHA256FromString(cm.Data[GatewayConfigFileName]), cm.Data[GatewayConfigChecksumFileName]; existingSha != gatewayCfgChecksum || existingChecksumSha != gatewayCfgChecksum { if currentSha, expectedSha := util.Optional(cm.Data, ConfigMapChecksumKey, ""), util.Optional(elements, ConfigMapChecksumKey, ""); currentSha != expectedSha || currentSha == "" {
// We need to do the update // We need to do the update
if _, changed, err := patcher.Patcher[*core.ConfigMap](ctx, cachedStatus.ConfigMapsModInterface().V1(), cm, meta.PatchOptions{}, if _, changed, err := patcher.Patcher[*core.ConfigMap](ctx, cachedStatus.ConfigMapsModInterface().V1(), cm, meta.PatchOptions{},
patcher.PatchConfigMapData(map[string]string{ patcher.PatchConfigMapData(elements)); err != nil {
GatewayConfigFileName: string(gatewayCfgYaml),
GatewayConfigChecksumFileName: gatewayCfgChecksum,
})); err != nil {
log.Err(err).Debug("Failed to patch GatewayConfig ConfigMap") log.Err(err).Debug("Failed to patch GatewayConfig ConfigMap")
return errors.WithStack(err) return errors.WithStack(err)
} else if changed { } else if changed {
log.Str("service", cm.GetName()).Str("before", existingSha).Str("after", gatewayCfgChecksum).Info("Updated GatewayConfig") log.Str("configmap", cm.GetName()).Str("before", currentSha).Str("after", expectedSha).Info("Updated GatewayConfig")
} }
} }
} }

View file

@ -0,0 +1,50 @@
//
// 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 resources
import (
"context"
api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1"
"github.com/arangodb/kube-arangodb/pkg/deployment/resources/gateway"
inspectorInterface "github.com/arangodb/kube-arangodb/pkg/util/k8sutil/inspector"
)
func (r *Resources) ensureMemberConfigGatewayConfig(ctx context.Context, cachedStatus inspectorInterface.Inspector, member api.DeploymentStatusMemberElement) (map[string]string, error) {
if member.Group != api.ServerGroupGateways {
return nil, nil
}
data, _, _, err := gateway.NodeDynamicConfig("arangodb", member.Member.ID, &gateway.DynamicConfig{
Path: GatewayVolumeMountDir,
File: GatewayCDSConfigFileName,
}, &gateway.DynamicConfig{
Path: GatewayVolumeMountDir,
File: GatewayLDSConfigFileName,
})
if err != nil {
return nil, err
}
return map[string]string{
GatewayDynamicConfigFileName: string(data),
}, nil
}

View file

@ -54,6 +54,9 @@ func (r *Resources) EnsureConfigMaps(ctx context.Context, cachedStatus inspector
if err := reconcileRequired.WithError(r.ensureGatewayConfig(ctx, cachedStatus, configMaps)); err != nil { if err := reconcileRequired.WithError(r.ensureGatewayConfig(ctx, cachedStatus, configMaps)); err != nil {
return errors.Section(err, "Gateway ConfigMap") return errors.Section(err, "Gateway ConfigMap")
} }
if err := reconcileRequired.WithError(r.ensureMemberConfig(ctx, cachedStatus, configMaps)); err != nil {
return errors.Section(err, "Member ConfigMap")
}
} }
return reconcileRequired.Reconcile(ctx) return reconcileRequired.Reconcile(ctx)
} }

View file

@ -0,0 +1,172 @@
//
// 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 resources
import (
"context"
core "k8s.io/api/core/v1"
meta "k8s.io/apimachinery/pkg/apis/meta/v1"
api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1"
"github.com/arangodb/kube-arangodb/pkg/util"
"github.com/arangodb/kube-arangodb/pkg/util/assertion"
"github.com/arangodb/kube-arangodb/pkg/util/errors"
"github.com/arangodb/kube-arangodb/pkg/util/globals"
"github.com/arangodb/kube-arangodb/pkg/util/k8sutil"
inspectorInterface "github.com/arangodb/kube-arangodb/pkg/util/k8sutil/inspector"
configMapsV1 "github.com/arangodb/kube-arangodb/pkg/util/k8sutil/inspector/configmap/v1"
"github.com/arangodb/kube-arangodb/pkg/util/k8sutil/kerrors"
"github.com/arangodb/kube-arangodb/pkg/util/k8sutil/patcher"
)
const (
ConfigMapChecksumKey = "CHECKSUM"
MemberConfigVolumeMountDir = "/etc/member/"
MemberConfigVolumeName = "member-config"
MemberConfigChecksumENV = "MEMBER_CONFIG_CHECKSUM"
)
type memberConfigMapRenderer func(ctx context.Context, cachedStatus inspectorInterface.Inspector, member api.DeploymentStatusMemberElement) (map[string]string, error)
func (r *Resources) ensureMemberConfig(ctx context.Context, cachedStatus inspectorInterface.Inspector, configMaps configMapsV1.ModInterface) error {
status := r.context.GetStatus()
log := r.log.Str("section", "member-config-render")
reconcileRequired := k8sutil.NewReconcile(cachedStatus)
members := status.Members.AsList()
if err := reconcileRequired.ParallelAll(len(members), func(id int) error {
memberName := members[id].Member.ArangoMemberName(r.context.GetAPIObject().GetName(), members[id].Group)
am, ok := cachedStatus.ArangoMember().V1().GetSimple(memberName)
if !ok {
return errors.Errorf("ArangoMember %s not found", memberName)
}
switch members[id].Group.Type() {
case api.ServerGroupTypeGateway, api.ServerGroupTypeArangoSync, api.ServerGroupTypeArangoD:
elements, err := r.renderMemberConfigElements(ctx, cachedStatus, members[id], r.ensureMemberConfigGatewayConfig)
if err != nil {
return err
}
if len(elements) == 0 {
// CM should be gone
if obj, ok := cachedStatus.ConfigMap().V1().GetSimple(memberName); !ok {
return nil
} else {
if err := cachedStatus.ConfigMapsModInterface().V1().Delete(ctx, memberName, meta.DeleteOptions{
Preconditions: meta.NewUIDPreconditions(string(obj.GetUID())),
}); err != nil {
if !kerrors.IsNotFound(err) {
return err
}
}
}
} else {
// We expect CM
if obj, ok := cachedStatus.ConfigMap().V1().GetSimple(memberName); !ok {
// Let's Create ConfigMap
obj = &core.ConfigMap{
ObjectMeta: meta.ObjectMeta{
Name: memberName,
},
Data: elements,
}
err = globals.GetGlobalTimeouts().Kubernetes().RunWithTimeout(ctx, func(ctxChild context.Context) error {
return k8sutil.CreateConfigMap(ctxChild, configMaps, obj, util.NewType(am.AsOwner()))
})
if kerrors.IsAlreadyExists(err) {
// CM added while we tried it also
return nil
} else if err != nil {
// Failed to create
return errors.WithStack(err)
}
return errors.Reconcile()
} else {
// CM Exists, checks checksum - if key is not in the map we return empty string
if currentSha, expectedSha := util.Optional(obj.Data, ConfigMapChecksumKey, ""), util.Optional(elements, ConfigMapChecksumKey, ""); currentSha != expectedSha || currentSha == "" {
// We need to do the update
if _, changed, err := patcher.Patcher[*core.ConfigMap](ctx, cachedStatus.ConfigMapsModInterface().V1(), obj, meta.PatchOptions{},
patcher.PatchConfigMapData(elements)); err != nil {
log.Err(err).Debug("Failed to patch GatewayConfig ConfigMap")
return errors.WithStack(err)
} else if changed {
log.Str("service", obj.GetName()).Str("before", currentSha).Str("after", expectedSha).Info("Updated Member Config")
}
}
}
}
return nil
default:
assertion.InvalidGroupKey.Assert(true, "Unable to create Member ConfigMap an unknown group: %s", members[id].Group.AsRole())
return nil
}
}); err != nil {
return errors.Section(err, "Member ConfigMap")
}
return nil
}
func (r *Resources) renderConfigMap(elements ...map[string]string) (map[string]string, error) {
result := map[string]string{}
for _, r := range elements {
for k, v := range r {
if _, ok := result[k]; ok {
return nil, errors.Errorf("Key %s already defined", k)
}
result[k] = v
}
}
if len(result) == 0 {
return nil, nil
}
result[ConfigMapChecksumKey] = util.SHA256FromStringMap(result)
return result, nil
}
func (r *Resources) renderMemberConfigElements(ctx context.Context, cachedStatus inspectorInterface.Inspector, member api.DeploymentStatusMemberElement, renders ...memberConfigMapRenderer) (map[string]string, error) {
var elements = make([]map[string]string, len(renders))
for _, r := range renders {
if els, err := r(ctx, cachedStatus, member); err != nil {
return nil, errors.Wrapf(err, "Unable to render CM for %s", member.Member.ID)
} else {
elements = append(elements, els)
}
}
return r.renderConfigMap(elements...)
}

View file

@ -0,0 +1,93 @@
//
// 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 gateway
import (
"path"
bootstrapAPI "github.com/envoyproxy/go-control-plane/envoy/config/bootstrap/v3"
coreAPI "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
discoveryApi "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
proto "google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/anypb"
)
type DynamicConfig struct {
Path, File string
}
func (d *DynamicConfig) AsConfigSource() *coreAPI.ConfigSource {
if d == nil {
return nil
}
return &coreAPI.ConfigSource{
ConfigSourceSpecifier: &coreAPI.ConfigSource_PathConfigSource{
PathConfigSource: &coreAPI.PathConfigSource{
Path: path.Join(d.Path, d.File),
WatchedDirectory: &coreAPI.WatchedDirectory{
Path: d.Path,
},
},
},
}
}
func NodeDynamicConfig(cluster, id string, cds, lds *DynamicConfig) ([]byte, string, *bootstrapAPI.Bootstrap, error) {
var b = bootstrapAPI.Bootstrap{
Node: &coreAPI.Node{
Id: id,
Cluster: cluster,
},
}
if v := cds; v != nil {
if b.DynamicResources == nil {
b.DynamicResources = &bootstrapAPI.Bootstrap_DynamicResources{}
}
b.DynamicResources.CdsConfig = v.AsConfigSource()
}
if v := lds; v != nil {
if b.DynamicResources == nil {
b.DynamicResources = &bootstrapAPI.Bootstrap_DynamicResources{}
}
b.DynamicResources.LdsConfig = v.AsConfigSource()
}
return Marshal(&b)
}
func DynamicConfigResponse[T proto.Message](in ...T) (*discoveryApi.DiscoveryResponse, error) {
resources := make([]*anypb.Any, len(in))
for id := range in {
if a, err := anypb.New(in[id]); err != nil {
return nil, err
} else {
resources[id] = a
}
}
return &discoveryApi.DiscoveryResponse{
Resources: resources,
}, nil
}

View file

@ -36,11 +36,11 @@ import (
tlsInspectorApi "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/listener/tls_inspector/v3" tlsInspectorApi "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/listener/tls_inspector/v3"
httpConnectionManagerAPI "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" httpConnectionManagerAPI "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
upstreamHttpApi "github.com/envoyproxy/go-control-plane/envoy/extensions/upstreams/http/v3" upstreamHttpApi "github.com/envoyproxy/go-control-plane/envoy/extensions/upstreams/http/v3"
discoveryApi "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
"github.com/golang/protobuf/ptypes/any" "github.com/golang/protobuf/ptypes/any"
"google.golang.org/protobuf/encoding/protojson" "github.com/golang/protobuf/ptypes/wrappers"
"google.golang.org/protobuf/types/known/anypb" "google.golang.org/protobuf/types/known/anypb"
"google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/durationpb"
"sigs.k8s.io/yaml"
shared "github.com/arangodb/kube-arangodb/pkg/apis/shared" shared "github.com/arangodb/kube-arangodb/pkg/apis/shared"
"github.com/arangodb/kube-arangodb/pkg/util" "github.com/arangodb/kube-arangodb/pkg/util"
@ -74,15 +74,51 @@ func (c Config) RenderYAML() ([]byte, string, *bootstrapAPI.Bootstrap, error) {
return nil, "", nil, err return nil, "", nil, err
} }
data, err := protojson.MarshalOptions{ return Marshal(cfg)
UseProtoNames: true, }
}.Marshal(cfg)
func (c Config) RenderCDSYAML() ([]byte, string, *discoveryApi.DiscoveryResponse, error) {
cfg, err := c.RenderCDS()
if err != nil { if err != nil {
return nil, "", nil, err return nil, "", nil, err
} }
data, err = yaml.JSONToYAML(data) return Marshal(cfg)
return data, util.SHA256(data), cfg, err }
func (c Config) RenderLDSYAML() ([]byte, string, *discoveryApi.DiscoveryResponse, error) {
cfg, err := c.RenderLDS()
if err != nil {
return nil, "", nil, err
}
return Marshal(cfg)
}
func (c Config) RenderCDS() (*discoveryApi.DiscoveryResponse, error) {
if err := c.Validate(); err != nil {
return nil, errors.Wrapf(err, "Validation failed")
}
clusters, err := c.RenderClusters()
if err != nil {
return nil, errors.Wrapf(err, "Unable to render clusters")
}
return DynamicConfigResponse(clusters...)
}
func (c Config) RenderLDS() (*discoveryApi.DiscoveryResponse, error) {
if err := c.Validate(); err != nil {
return nil, errors.Wrapf(err, "Validation failed")
}
listener, err := c.RenderListener()
if err != nil {
return nil, errors.Wrapf(err, "Unable to render listener")
}
return DynamicConfigResponse(listener)
} }
func (c Config) Render() (*bootstrapAPI.Bootstrap, error) { func (c Config) Render() (*bootstrapAPI.Bootstrap, error) {
@ -268,6 +304,9 @@ func (c Config) RenderFilters() ([]*listenerAPI.Filter, error) {
Routes: routes, Routes: routes,
}, },
}, },
ValidateClusters: &wrappers.BoolValue{
Value: false,
},
}, },
}, },
HttpFilters: append(httpFilters, &httpConnectionManagerAPI.HttpFilter{ HttpFilters: append(httpFilters, &httpConnectionManagerAPI.HttpFilter{

View file

@ -0,0 +1,41 @@
//
// 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 gateway
import (
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
"sigs.k8s.io/yaml"
"github.com/arangodb/kube-arangodb/pkg/util"
)
func Marshal[T proto.Message](in T) ([]byte, string, T, error) {
data, err := protojson.MarshalOptions{
UseProtoNames: true,
}.Marshal(in)
if err != nil {
return nil, "", util.Default[T](), err
}
data, err = yaml.JSONToYAML(data)
return data, util.SHA256(data), in, err
}

View file

@ -26,6 +26,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net" "net"
"path"
"path/filepath" "path/filepath"
"strconv" "strconv"
"sync" "sync"
@ -288,7 +289,11 @@ func createArangoSyncArgs(apiObject meta.Object, spec api.DeploymentSpec, group
func createArangoGatewayArgs(input pod.Input, additionalOptions ...k8sutil.OptionPair) []string { func createArangoGatewayArgs(input pod.Input, additionalOptions ...k8sutil.OptionPair) []string {
options := k8sutil.CreateOptionPairs(64) options := k8sutil.CreateOptionPairs(64)
options.Add("--config-path", GatewayConfigFilePath) if input.Deployment.Gateway.IsDynamic() {
options.Add("--config-path", path.Join(MemberConfigVolumeMountDir, GatewayDynamicConfigFileName))
} else {
options.Add("--config-path", path.Join(GatewayVolumeMountDir, GatewayConfigFileName))
}
options.Append(additionalOptions...) options.Append(additionalOptions...)

View file

@ -30,13 +30,14 @@ import (
) )
const ( const (
ArangoGatewayExecutor string = "/usr/local/bin/envoy" ArangoGatewayExecutor = "/usr/local/bin/envoy"
GatewayVolumeMountDir = "/etc/gateway/" GatewayVolumeMountDir = "/etc/gateway/"
GatewayVolumeName = "gateway" GatewayVolumeName = "gateway"
GatewayConfigFileName = "gateway.yaml" GatewayConfigFileName = "gateway.yaml"
GatewayConfigChecksumFileName = "gateway.checksum" GatewayDynamicConfigFileName = "gateway.dynamic.yaml"
GatewayCDSConfigFileName = "gateway.dynamic.cds.yaml"
GatewayLDSConfigFileName = "gateway.dynamic.lds.yaml"
GatewayConfigChecksumENV = "GATEWAY_CONFIG_CHECKSUM" GatewayConfigChecksumENV = "GATEWAY_CONFIG_CHECKSUM"
GatewayConfigFilePath = GatewayVolumeMountDir + GatewayConfigFileName
) )
func GetGatewayConfigMapName(name string) string { func GetGatewayConfigMapName(name string) string {
@ -47,7 +48,17 @@ func createGatewayVolumes(input pod.Input) pod.Volumes {
volumes := pod.NewVolumes() volumes := pod.NewVolumes()
volumes.AddVolume(k8sutil.CreateVolumeWithConfigMap(GatewayVolumeName, GetGatewayConfigMapName(input.ApiObject.GetName()))) volumes.AddVolume(k8sutil.CreateVolumeWithConfigMap(GatewayVolumeName, GetGatewayConfigMapName(input.ApiObject.GetName())))
volumes.AddVolumeMount(GatewayVolumeMount()) volumes.AddVolume(k8sutil.CreateVolumeWithConfigMap(MemberConfigVolumeName, input.ArangoMember.GetName()))
volumes.AddVolumeMount(core.VolumeMount{
Name: GatewayVolumeName,
MountPath: GatewayVolumeMountDir,
ReadOnly: true,
})
volumes.AddVolumeMount(core.VolumeMount{
Name: MemberConfigVolumeName,
MountPath: MemberConfigVolumeMountDir,
ReadOnly: true,
})
// TLS // TLS
volumes.Append(pod.TLS(), input) volumes.Append(pod.TLS(), input)
@ -57,11 +68,3 @@ func createGatewayVolumes(input pod.Input) pod.Volumes {
return volumes return volumes
} }
func GatewayVolumeMount() core.VolumeMount {
return core.VolumeMount{
Name: GatewayVolumeName,
MountPath: GatewayVolumeMountDir,
ReadOnly: true,
}
}

View file

@ -131,18 +131,26 @@ func (a *ArangoGatewayContainer) GetEnvs() ([]core.EnvVar, []core.EnvFromSource)
envs.Add(true, k8sutil.GetLifecycleEnv()...) envs.Add(true, k8sutil.GetLifecycleEnv()...)
var cmChecksum = "" if cm, ok := a.cachedStatus.ConfigMap().V1().GetSimple(GetGatewayConfigMapName(a.input.ArangoMember.GetName())); ok {
if v, ok := cm.Data[ConfigMapChecksumKey]; ok {
envs.Add(true, core.EnvVar{
Name: MemberConfigChecksumENV,
Value: v,
})
}
}
if !a.spec.Gateway.IsDynamic() {
if cm, ok := a.cachedStatus.ConfigMap().V1().GetSimple(GetGatewayConfigMapName(a.input.ApiObject.GetName())); ok { if cm, ok := a.cachedStatus.ConfigMap().V1().GetSimple(GetGatewayConfigMapName(a.input.ApiObject.GetName())); ok {
if v, ok := cm.Data[GatewayConfigChecksumFileName]; ok { if v, ok := cm.Data[ConfigMapChecksumKey]; ok {
cmChecksum = v
}
}
envs.Add(true, core.EnvVar{ envs.Add(true, core.EnvVar{
Name: GatewayConfigChecksumENV, Name: GatewayConfigChecksumENV,
Value: cmChecksum, Value: v,
}) })
}
}
}
if len(a.groupSpec.Envs) > 0 { if len(a.groupSpec.Envs) > 0 {
for _, env := range a.groupSpec.Envs { for _, env := range a.groupSpec.Envs {

View file

@ -1,7 +1,7 @@
// //
// DISCLAIMER // DISCLAIMER
// //
// Copyright 2016-2022 ArangoDB GmbH, Cologne, Germany // Copyright 2016-2024 ArangoDB GmbH, Cologne, Germany
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -28,6 +28,10 @@ import (
) )
func Test_Timezone(t *testing.T) { func Test_Timezone(t *testing.T) {
// Ensure we use static time for comparison
testTime, err := time.Parse(time.RFC3339, "2024-09-01T00:00:00Z")
require.NoError(t, err)
for tz, tq := range timezones { for tz, tq := range timezones {
t.Run(tz, func(t *testing.T) { t.Run(tz, func(t *testing.T) {
t.Run("Check fields", func(t *testing.T) { t.Run("Check fields", func(t *testing.T) {
@ -49,7 +53,7 @@ func Test_Timezone(t *testing.T) {
l, err := time.LoadLocationFromTZData("", tz) l, err := time.LoadLocationFromTZData("", tz)
require.NoError(t, err) require.NoError(t, err)
z, offset := time.Now().In(l).Zone() z, offset := testTime.In(l).Zone()
require.Equal(t, tq.Zone, z) require.Equal(t, tq.Zone, z)
require.Equal(t, int(tq.Offset/time.Second), offset) require.Equal(t, int(tq.Offset/time.Second), offset)

View file

@ -50,6 +50,14 @@ func SHA256FromStringArray(data ...string) string {
return SHA256FromString(strings.Join(data, "|")) return SHA256FromString(strings.Join(data, "|"))
} }
func SHA256FromStringMap(data map[string]string) string {
return SHA256FromExtract(func(t KV[string, string]) string {
return fmt.Sprintf("%s:%s", t.K, SHA256FromString(t.V))
}, ExtractWithSort(data, func(i, j string) bool {
return i < j
})...)
}
func SHA256FromString(data string) string { func SHA256FromString(data string) string {
return SHA256([]byte(data)) return SHA256([]byte(data))
} }

View file

@ -43,6 +43,12 @@ func Extract[K comparable, V any](in map[K]V) []KV[K, V] {
return r return r
} }
func ExtractWithSort[K comparable, V any](in map[K]V, cmp func(i, j K) bool) []KV[K, V] {
return Sort(Extract(in), func(i, j KV[K, V]) bool {
return cmp(i.K, j.K)
})
}
func Sort[IN any](in []IN, cmp func(i, j IN) bool) []IN { func Sort[IN any](in []IN, cmp func(i, j IN) bool) []IN {
r := make([]IN, len(in)) r := make([]IN, len(in))
copy(r, in) copy(r, in)