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:
parent
3d46436c59
commit
fe97fc3cc0
20 changed files with 552 additions and 47 deletions
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
5
pkg/apis/deployment/v1/zz_generated.deepcopy.go
generated
5
pkg/apis/deployment/v1/zz_generated.deepcopy.go
generated
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
50
pkg/deployment/resources/config_map_gateway_member.go
Normal file
50
pkg/deployment/resources/config_map_gateway_member.go
Normal 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
|
||||||
|
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
172
pkg/deployment/resources/config_maps_member.go
Normal file
172
pkg/deployment/resources/config_maps_member.go
Normal 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...)
|
||||||
|
}
|
93
pkg/deployment/resources/gateway/dynamic.go
Normal file
93
pkg/deployment/resources/gateway/dynamic.go
Normal 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
|
||||||
|
}
|
|
@ -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{
|
||||||
|
|
41
pkg/deployment/resources/gateway/marshal.go
Normal file
41
pkg/deployment/resources/gateway/marshal.go
Normal 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
|
||||||
|
}
|
|
@ -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...)
|
||||||
|
|
||||||
|
|
|
@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
8
pkg/generated/timezones/timezones_test.go
generated
8
pkg/generated/timezones/timezones_test.go
generated
|
@ -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)
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue