diff --git a/CHANGELOG.md b/CHANGELOG.md index c979a4659..32df5ad9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - (Logging) Internal client trace - (QA) Member maintenance feature - (Feature) Extract Pod Details +- (Feature) Add Timezone management ## [1.2.15](https://github.com/arangodb/kube-arangodb/tree/1.2.15) (2022-07-20) - (Bugfix) Ensure pod names not too long diff --git a/go.mod b/go.mod index b5b5af695..795fee57d 100644 --- a/go.mod +++ b/go.mod @@ -47,6 +47,7 @@ require ( github.com/stretchr/testify v1.7.0 golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c + google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad // indirect google.golang.org/grpc v1.47.0 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b k8s.io/api v0.21.10 @@ -56,11 +57,6 @@ require ( k8s.io/klog v1.0.0 ) -require ( - github.com/arangodb/rebalancer v0.1.1 // indirect - google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad // indirect -) - require ( github.com/arangodb/go-velocypack v0.0.0-20200318135517-5af53c29c67e // indirect github.com/beorn7/perks v1.0.1 // indirect diff --git a/pkg/apis/deployment/v1/deployment_spec.go b/pkg/apis/deployment/v1/deployment_spec.go index d9040e81e..a69143098 100644 --- a/pkg/apis/deployment/v1/deployment_spec.go +++ b/pkg/apis/deployment/v1/deployment_spec.go @@ -171,6 +171,8 @@ type DeploymentSpec struct { // Architecture definition of supported architectures Architecture ArangoDeploymentArchitecture `json:"architecture,omitempty"` + + Timezone *string `json:"timezone,omitempty"` } // GetAllowMemberRecreation returns member recreation policy based on group and settings diff --git a/pkg/apis/deployment/v1/deployment_status.go b/pkg/apis/deployment/v1/deployment_status.go index 074afc155..94d23206b 100644 --- a/pkg/apis/deployment/v1/deployment_status.go +++ b/pkg/apis/deployment/v1/deployment_status.go @@ -91,6 +91,8 @@ type DeploymentStatus struct { Version *Version `json:"version,omitempty"` + Timezone *string `json:"timezone,omitempty"` + Single *ServerGroupStatus `json:"single,omitempty"` Agents *ServerGroupStatus `json:"agents,omitempty"` DBServers *ServerGroupStatus `json:"dbservers,omitempty"` @@ -126,7 +128,8 @@ func (ds *DeploymentStatus) Equal(other DeploymentStatus) bool { ds.DBServers.Equal(other.DBServers) && ds.Coordinators.Equal(other.Coordinators) && ds.SyncMasters.Equal(other.SyncMasters) && - ds.SyncWorkers.Equal(other.SyncWorkers) + ds.SyncWorkers.Equal(other.SyncWorkers) && + util.CompareStringPointers(ds.Timezone, other.Timezone) } // IsForceReload returns true if ForceStatusReload is set to true diff --git a/pkg/apis/deployment/v1/plan.go b/pkg/apis/deployment/v1/plan.go index 1de0757ba..bd56c9885 100644 --- a/pkg/apis/deployment/v1/plan.go +++ b/pkg/apis/deployment/v1/plan.go @@ -199,7 +199,8 @@ const ( ActionTypeRebalancerClean ActionType = "RebalancerClean" // Resources - ActionTypeResourceSync ActionType = "ResourceSync" + ActionTypeResourceSync ActionType = "ResourceSync" + ActionTypeTimezoneSecretSet ActionType = "TimezoneSecretSet" ) const ( diff --git a/pkg/apis/deployment/v1/server_group_volume.go b/pkg/apis/deployment/v1/server_group_volume.go index 3d0460040..a37cdde90 100644 --- a/pkg/apis/deployment/v1/server_group_volume.go +++ b/pkg/apis/deployment/v1/server_group_volume.go @@ -41,6 +41,7 @@ var ( shared.LifecycleVolumeName, shared.FoxxAppEphemeralVolumeName, shared.TMPEphemeralVolumeName, + shared.ArangoDTimezoneVolumeName, } ) diff --git a/pkg/apis/deployment/v1/zz_generated.deepcopy.go b/pkg/apis/deployment/v1/zz_generated.deepcopy.go index 3eb34dfb6..604f9fe84 100644 --- a/pkg/apis/deployment/v1/zz_generated.deepcopy.go +++ b/pkg/apis/deployment/v1/zz_generated.deepcopy.go @@ -1080,6 +1080,11 @@ func (in *DeploymentSpec) DeepCopyInto(out *DeploymentSpec) { *out = make(ArangoDeploymentArchitecture, len(*in)) copy(*out, *in) } + if in.Timezone != nil { + in, out := &in.Timezone, &out.Timezone + *out = new(string) + **out = **in + } return } @@ -1183,6 +1188,11 @@ func (in *DeploymentStatus) DeepCopyInto(out *DeploymentStatus) { *out = new(Version) **out = **in } + if in.Timezone != nil { + in, out := &in.Timezone, &out.Timezone + *out = new(string) + **out = **in + } if in.Single != nil { in, out := &in.Single, &out.Single *out = new(ServerGroupStatus) diff --git a/pkg/apis/deployment/v2alpha1/deployment_spec.go b/pkg/apis/deployment/v2alpha1/deployment_spec.go index 19bb9395f..4fda05c62 100644 --- a/pkg/apis/deployment/v2alpha1/deployment_spec.go +++ b/pkg/apis/deployment/v2alpha1/deployment_spec.go @@ -171,6 +171,8 @@ type DeploymentSpec struct { // Architecture definition of supported architectures Architecture ArangoDeploymentArchitecture `json:"architecture,omitempty"` + + Timezone *string `json:"timezone,omitempty"` } // GetAllowMemberRecreation returns member recreation policy based on group and settings diff --git a/pkg/apis/deployment/v2alpha1/deployment_status.go b/pkg/apis/deployment/v2alpha1/deployment_status.go index c99a46ed2..67569def3 100644 --- a/pkg/apis/deployment/v2alpha1/deployment_status.go +++ b/pkg/apis/deployment/v2alpha1/deployment_status.go @@ -91,6 +91,8 @@ type DeploymentStatus struct { Version *Version `json:"version,omitempty"` + Timezone *string `json:"timezone,omitempty"` + Single *ServerGroupStatus `json:"single,omitempty"` Agents *ServerGroupStatus `json:"agents,omitempty"` DBServers *ServerGroupStatus `json:"dbservers,omitempty"` @@ -126,7 +128,8 @@ func (ds *DeploymentStatus) Equal(other DeploymentStatus) bool { ds.DBServers.Equal(other.DBServers) && ds.Coordinators.Equal(other.Coordinators) && ds.SyncMasters.Equal(other.SyncMasters) && - ds.SyncWorkers.Equal(other.SyncWorkers) + ds.SyncWorkers.Equal(other.SyncWorkers) && + util.CompareStringPointers(ds.Timezone, other.Timezone) } // IsForceReload returns true if ForceStatusReload is set to true diff --git a/pkg/apis/deployment/v2alpha1/plan.go b/pkg/apis/deployment/v2alpha1/plan.go index 2c31eb3f2..caaca7bef 100644 --- a/pkg/apis/deployment/v2alpha1/plan.go +++ b/pkg/apis/deployment/v2alpha1/plan.go @@ -199,7 +199,8 @@ const ( ActionTypeRebalancerClean ActionType = "RebalancerClean" // Resources - ActionTypeResourceSync ActionType = "ResourceSync" + ActionTypeResourceSync ActionType = "ResourceSync" + ActionTypeTimezoneSecretSet ActionType = "TimezoneSecretSet" ) const ( diff --git a/pkg/apis/deployment/v2alpha1/server_group_volume.go b/pkg/apis/deployment/v2alpha1/server_group_volume.go index 9a36fa9f0..e2bdec4ec 100644 --- a/pkg/apis/deployment/v2alpha1/server_group_volume.go +++ b/pkg/apis/deployment/v2alpha1/server_group_volume.go @@ -41,6 +41,7 @@ var ( shared.LifecycleVolumeName, shared.FoxxAppEphemeralVolumeName, shared.TMPEphemeralVolumeName, + shared.ArangoDTimezoneVolumeName, } ) diff --git a/pkg/apis/deployment/v2alpha1/zz_generated.deepcopy.go b/pkg/apis/deployment/v2alpha1/zz_generated.deepcopy.go index 5bf7b8760..e2db2cda9 100644 --- a/pkg/apis/deployment/v2alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/deployment/v2alpha1/zz_generated.deepcopy.go @@ -1080,6 +1080,11 @@ func (in *DeploymentSpec) DeepCopyInto(out *DeploymentSpec) { *out = make(ArangoDeploymentArchitecture, len(*in)) copy(*out, *in) } + if in.Timezone != nil { + in, out := &in.Timezone, &out.Timezone + *out = new(string) + **out = **in + } return } @@ -1183,6 +1188,11 @@ func (in *DeploymentStatus) DeepCopyInto(out *DeploymentStatus) { *out = new(Version) **out = **in } + if in.Timezone != nil { + in, out := &in.Timezone, &out.Timezone + *out = new(string) + **out = **in + } if in.Single != nil { in, out := &in.Single, &out.Single *out = new(ServerGroupStatus) diff --git a/pkg/apis/shared/constants.go b/pkg/apis/shared/constants.go index c8535adcc..0c8f6e7e5 100644 --- a/pkg/apis/shared/constants.go +++ b/pkg/apis/shared/constants.go @@ -56,6 +56,7 @@ const ( LifecycleVolumeName = "lifecycle" FoxxAppEphemeralVolumeName = "ephemeral-apps" TMPEphemeralVolumeName = "ephemeral-tmp" + ArangoDTimezoneVolumeName = "arangod-timezone" RocksdbEncryptionVolumeName = "rocksdb-encryption" ExporterJWTVolumeName = "exporter-jwt" ArangodVolumeMountDir = "/data" diff --git a/pkg/deployment/features/timezone.go b/pkg/deployment/features/timezone.go new file mode 100644 index 000000000..c137a3f3d --- /dev/null +++ b/pkg/deployment/features/timezone.go @@ -0,0 +1,37 @@ +// +// DISCLAIMER +// +// Copyright 2016-2022 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 features + +func init() { + registerFeature(timezone) +} + +var timezone = &feature{ + name: "timezone-management", + description: "Enable timezone management for pods", + version: "3.6.0", + enterpriseRequired: false, + enabledByDefault: false, +} + +func Timezone() Feature { + return timezone +} diff --git a/pkg/deployment/pod/timezone.go b/pkg/deployment/pod/timezone.go new file mode 100644 index 000000000..57ec60127 --- /dev/null +++ b/pkg/deployment/pod/timezone.go @@ -0,0 +1,91 @@ +// +// DISCLAIMER +// +// Copyright 2016-2022 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 pod + +import ( + "fmt" + + core "k8s.io/api/core/v1" + + "github.com/arangodb/kube-arangodb/pkg/apis/shared" + "github.com/arangodb/kube-arangodb/pkg/deployment/features" + "github.com/arangodb/kube-arangodb/pkg/util/k8sutil" + "github.com/arangodb/kube-arangodb/pkg/util/k8sutil/interfaces" +) + +const ( + TimezoneNameKey string = "name" + TimezoneDataKey string = "data" + TimezoneTZKey string = "timezone" +) + +func TimezoneSecret(name string) string { + return fmt.Sprintf("%s-timezone", name) +} + +func Timezone() Builder { + return timezone{} +} + +type timezone struct { +} + +func (t timezone) Args(i Input) k8sutil.OptionPairs { + return nil +} + +func (t timezone) Volumes(i Input) ([]core.Volume, []core.VolumeMount) { + if !features.Timezone().Enabled() { + return nil, nil + } + + return []core.Volume{ + { + Name: shared.ArangoDTimezoneVolumeName, + VolumeSource: core.VolumeSource{ + Secret: &core.SecretVolumeSource{ + SecretName: TimezoneSecret(i.ApiObject.GetName()), + }, + }, + }, + }, []core.VolumeMount{ + { + Name: shared.ArangoDTimezoneVolumeName, + ReadOnly: true, + MountPath: "/etc/localtime", + SubPath: TimezoneDataKey, + }, + { + Name: shared.ArangoDTimezoneVolumeName, + ReadOnly: true, + MountPath: "/etc/timezone", + SubPath: TimezoneTZKey, + }, + } +} + +func (t timezone) Envs(i Input) []core.EnvVar { + return nil +} + +func (t timezone) Verify(i Input, cachedStatus interfaces.Inspector) error { + return nil +} diff --git a/pkg/deployment/reconcile/action_timezone_secret_set.go b/pkg/deployment/reconcile/action_timezone_secret_set.go new file mode 100644 index 000000000..96e267b7e --- /dev/null +++ b/pkg/deployment/reconcile/action_timezone_secret_set.go @@ -0,0 +1,135 @@ +// +// DISCLAIMER +// +// Copyright 2016-2022 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 reconcile + +import ( + "context" + "encoding/base64" + + core "k8s.io/api/core/v1" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + + api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" + "github.com/arangodb/kube-arangodb/pkg/deployment/features" + "github.com/arangodb/kube-arangodb/pkg/deployment/patch" + "github.com/arangodb/kube-arangodb/pkg/deployment/pod" + "github.com/arangodb/kube-arangodb/pkg/util/errors" + "github.com/arangodb/kube-arangodb/pkg/util/globals" + "github.com/arangodb/kube-arangodb/pkg/util/k8sutil" +) + +func init() { + registerAction(api.ActionTypeTimezoneSecretSet, newTimezoneCMSetAction, operationTLSCACertificateTimeout) +} + +func newTimezoneCMSetAction(action api.Action, actionCtx ActionContext) Action { + a := &timezoneCMSetAction{} + + a.actionImpl = newActionImplDefRef(action, actionCtx) + + return a +} + +type timezoneCMSetAction struct { + actionImpl + + actionEmptyCheckProgress +} + +func (a *timezoneCMSetAction) Start(ctx context.Context) (bool, error) { + if !features.Timezone().Enabled() { + return true, nil + } + + secrets := a.actionCtx.ACS().CurrentClusterCache().Secret().V1() + + tz, ok := GetTimezone(a.actionCtx.GetSpec().Timezone) + if !ok { + return true, nil + } + + tzd, ok := tz.GetData() + if !ok { + return true, nil + } + + if IsTimezoneValid(secrets, a.actionCtx.GetName(), tz) { + return true, nil + } + + if s, ok := secrets.GetSimple(pod.TimezoneSecret(a.actionCtx.GetName())); ok { + // Exists + // We need to prepare patch + data := map[string]string{} + + data[pod.TimezoneNameKey] = base64.StdEncoding.EncodeToString([]byte(tz.Name)) + data[pod.TimezoneDataKey] = base64.StdEncoding.EncodeToString(tzd) + data[pod.TimezoneTZKey] = base64.StdEncoding.EncodeToString([]byte(tz.Name)) + + p := patch.NewPatch() + p.ItemReplace(patch.NewPath("data"), data) + + patch, err := p.Marshal() + if err != nil { + a.log.Err(err).Error("Unable to encrypt patch") + return true, nil + } + + err = globals.GetGlobalTimeouts().Kubernetes().RunWithTimeout(ctx, func(ctxChild context.Context) error { + _, err := a.actionCtx.ACS().CurrentClusterCache().SecretsModInterface().V1().Patch(ctxChild, s.GetName(), types.JSONPatchType, patch, meta.PatchOptions{}) + return err + }) + if err != nil { + if !k8sutil.IsInvalid(err) { + return false, errors.Wrapf(err, "Unable to update secret: %s", s.GetName()) + } + } + + return true, nil + } else { + s = &core.Secret{ + ObjectMeta: meta.ObjectMeta{ + Name: pod.TimezoneSecret(a.actionCtx.GetName()), + Namespace: a.actionCtx.GetNamespace(), + OwnerReferences: []meta.OwnerReference{ + a.actionCtx.GetAPIObject().AsOwner(), + }, + }, + Data: map[string][]byte{ + pod.TimezoneNameKey: []byte(tz.Name), + pod.TimezoneDataKey: tzd, + pod.TimezoneTZKey: []byte(tz.Zone), + }, + Type: core.SecretTypeOpaque, + } + + if err := globals.GetGlobalTimeouts().Kubernetes().RunWithTimeout(ctx, func(ctxChild context.Context) error { + _, err := a.actionCtx.ACS().CurrentClusterCache().SecretsModInterface().V1().Create(ctxChild, s, meta.CreateOptions{}) + return err + }); err != nil { + a.log.Err(err).Error("Unable to create cm secret") + return true, nil + } + } + + return true, nil +} diff --git a/pkg/deployment/reconcile/plan_builder.go b/pkg/deployment/reconcile/plan_builder.go index 691265380..b5c500dd1 100644 --- a/pkg/deployment/reconcile/plan_builder.go +++ b/pkg/deployment/reconcile/plan_builder.go @@ -32,8 +32,9 @@ const ( ) const ( - BackOffCheck api.BackOffKey = "check" - LicenseCheck api.BackOffKey = "license" + BackOffCheck api.BackOffKey = "check" + LicenseCheck api.BackOffKey = "license" + TimezoneCheck api.BackOffKey = "timezone" ) // CreatePlan considers the current specification & status of the deployment creates a plan to diff --git a/pkg/deployment/reconcile/plan_builder_high.go b/pkg/deployment/reconcile/plan_builder_high.go index 26b1717c9..3af8cbbc8 100644 --- a/pkg/deployment/reconcile/plan_builder_high.go +++ b/pkg/deployment/reconcile/plan_builder_high.go @@ -59,6 +59,7 @@ func (r *Reconciler) createHighPlan(ctx context.Context, apiObject k8sutil.APIOb ApplyIfEmpty(r.createRebalancerCheckPlan). ApplyIfEmpty(r.createMemberFailedRestoreHighPlan). ApplyWithBackOff(BackOffCheck, time.Minute, r.emptyPlanBuilder)). + ApplyIfEmptyWithBackOff(TimezoneCheck, time.Minute, r.createTimezoneUpdatePlan). Apply(r.createBackupInProgressConditionPlan). // Discover backups always Apply(r.createMaintenanceConditionPlan). // Discover maintenance always Apply(r.cleanupConditions) // Cleanup Conditions diff --git a/pkg/deployment/reconcile/plan_builder_timezone.go b/pkg/deployment/reconcile/plan_builder_timezone.go new file mode 100644 index 000000000..62ceb3011 --- /dev/null +++ b/pkg/deployment/reconcile/plan_builder_timezone.go @@ -0,0 +1,51 @@ +// +// DISCLAIMER +// +// Copyright 2016-2022 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 reconcile + +import ( + "context" + + api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" + "github.com/arangodb/kube-arangodb/pkg/deployment/actions" + "github.com/arangodb/kube-arangodb/pkg/deployment/features" + "github.com/arangodb/kube-arangodb/pkg/util/k8sutil" +) + +func (r *Reconciler) createTimezoneUpdatePlan(ctx context.Context, apiObject k8sutil.APIObject, + spec api.DeploymentSpec, status api.DeploymentStatus, + context PlanBuilderContext) api.Plan { + if !features.Timezone().Enabled() { + return nil + } + + secrets := context.ACS().CurrentClusterCache().Secret().V1() + + tz, ok := GetTimezone(context.GetSpec().Timezone) + if !ok { + return nil + } + + if IsTimezoneValid(secrets, context.GetName(), tz) { + return nil + } + + return api.Plan{actions.NewClusterAction(api.ActionTypeTimezoneSecretSet, "Update timezone")} +} diff --git a/pkg/deployment/reconcile/utils_timezone.go b/pkg/deployment/reconcile/utils_timezone.go new file mode 100644 index 000000000..be5ee21b2 --- /dev/null +++ b/pkg/deployment/reconcile/utils_timezone.go @@ -0,0 +1,76 @@ +// +// DISCLAIMER +// +// Copyright 2016-2022 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 reconcile + +import ( + "github.com/arangodb/kube-arangodb/pkg/deployment/pod" + "github.com/arangodb/kube-arangodb/pkg/generated/timezones" + "github.com/arangodb/kube-arangodb/pkg/util" + secretv1 "github.com/arangodb/kube-arangodb/pkg/util/k8sutil/inspector/secret/v1" +) + +const defaultTimezone = "UTC" + +func GetTimezone(tz *string) (timezones.Timezone, bool) { + if tz == nil { + return timezones.GetTimezone(defaultTimezone) + } + return timezones.GetTimezone(*tz) +} + +func IsTimezoneValid(cache secretv1.Inspector, name string, timezone timezones.Timezone) bool { + sn := pod.TimezoneSecret(name) + + tzd, ok := timezone.GetData() + if !ok { + // Unable to get TZ Data, so ignoring + return true + } + + if s, ok := cache.GetSimple(sn); ok { + // Secret exists, verify + if v, ok := s.Data[pod.TimezoneNameKey]; ok { + if string(v) != timezone.Name { + return false + } + } else { + return false + } + if v, ok := s.Data[pod.TimezoneDataKey]; ok { + if util.SHA256(v) != util.SHA256(tzd) { + return false + } + } else { + return false + } + if v, ok := s.Data[pod.TimezoneTZKey]; ok { + if string(v) != timezone.Zone { + return false + } + } else { + return false + } + } else { + return false + } + + return true +} diff --git a/pkg/deployment/reconcile/utils_timezone_test.go b/pkg/deployment/reconcile/utils_timezone_test.go new file mode 100644 index 000000000..500e1d223 --- /dev/null +++ b/pkg/deployment/reconcile/utils_timezone_test.go @@ -0,0 +1,37 @@ +// +// DISCLAIMER +// +// Copyright 2016-2022 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 reconcile + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/arangodb/kube-arangodb/pkg/generated/timezones" +) + +func Test_Timezone_Default(t *testing.T) { + tz, ok := timezones.GetTimezone(defaultTimezone) + require.True(t, ok) + + _, ok = tz.GetData() + require.True(t, ok) +} diff --git a/pkg/deployment/resources/pod_creator_arangod.go b/pkg/deployment/resources/pod_creator_arangod.go index 836ef748f..c707c6efa 100644 --- a/pkg/deployment/resources/pod_creator_arangod.go +++ b/pkg/deployment/resources/pod_creator_arangod.go @@ -590,6 +590,8 @@ func CreateArangoDVolumes(status api.MemberStatus, input pod.Input, spec api.Dep // SNI volumes.Append(pod.SNI(), input) + volumes.Append(pod.Timezone(), input) + if len(groupSpec.Volumes) > 0 { volumes.AddVolume(groupSpec.Volumes.RenderVolumes(input.ApiObject, input.Group, status)...) } diff --git a/pkg/deployment/rotation/arangod.go b/pkg/deployment/rotation/arangod.go index 2e7dd8b8e..26601d31a 100644 --- a/pkg/deployment/rotation/arangod.go +++ b/pkg/deployment/rotation/arangod.go @@ -29,7 +29,7 @@ import ( "github.com/arangodb/kube-arangodb/pkg/util" ) -func podCompare(_ api.DeploymentSpec, _ api.ServerGroup, spec, status *core.PodSpec) compareFunc { +func podCompare(_ api.DeploymentSpec, _ api.ServerGroup, spec, status *core.PodSpec) comparePodFunc { return func(builder api.ActionBuilder) (mode Mode, plan api.Plan, err error) { if spec.SchedulerName != status.SchedulerName { status.SchedulerName = spec.SchedulerName @@ -45,7 +45,7 @@ func podCompare(_ api.DeploymentSpec, _ api.ServerGroup, spec, status *core.PodS } } -func affinityCompare(_ api.DeploymentSpec, _ api.ServerGroup, spec, status *core.PodSpec) compareFunc { +func affinityCompare(_ api.DeploymentSpec, _ api.ServerGroup, spec, status *core.PodSpec) comparePodFunc { return func(builder api.ActionBuilder) (mode Mode, plan api.Plan, e error) { if specC, err := util.SHA256FromJSON(spec.Affinity); err != nil { e = err diff --git a/pkg/deployment/rotation/arangod_containers.go b/pkg/deployment/rotation/arangod_containers.go index 262950595..0661d4430 100644 --- a/pkg/deployment/rotation/arangod_containers.go +++ b/pkg/deployment/rotation/arangod_containers.go @@ -23,6 +23,7 @@ package rotation import ( "strings" + "github.com/rs/zerolog/log" core "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/equality" @@ -36,7 +37,7 @@ const ( ContainerImage = "image" ) -func containersCompare(_ api.DeploymentSpec, _ api.ServerGroup, spec, status *core.PodSpec) compareFunc { +func containersCompare(ds api.DeploymentSpec, g api.ServerGroup, spec, status *core.PodSpec) comparePodFunc { return func(builder api.ActionBuilder) (mode Mode, plan api.Plan, err error) { a, b := spec.Containers, status.Containers @@ -56,6 +57,16 @@ func containersCompare(_ api.DeploymentSpec, _ api.ServerGroup, spec, status *co mode = mode.And(InPlaceRotation) } + g := podContainerFuncGenerator(ds, g, ac, bc) + + if m, p, err := comparePodContainer(builder, g(compareServerContainerVolumeMounts)); err != nil { + log.Err(err).Msg("Error while getting pod diff") + return SkippedRotation, nil, err + } else { + mode = mode.And(m) + plan = append(plan, p...) + } + if !equality.Semantic.DeepEqual(ac.Env, bc.Env) { if areEnvsEqual(ac.Env, bc.Env, func(a, b map[string]core.EnvVar) (map[string]core.EnvVar, map[string]core.EnvVar) { delete(a, topology.ArangoDBZone) @@ -97,7 +108,7 @@ func containersCompare(_ api.DeploymentSpec, _ api.ServerGroup, spec, status *co } } -func initContainersCompare(deploymentSpec api.DeploymentSpec, group api.ServerGroup, spec, status *core.PodSpec) compareFunc { +func initContainersCompare(deploymentSpec api.DeploymentSpec, group api.ServerGroup, spec, status *core.PodSpec) comparePodFunc { return func(builder api.ActionBuilder) (Mode, api.Plan, error) { gs := deploymentSpec.GetServerGroupSpec(group) diff --git a/pkg/deployment/rotation/arangod_containers_test.go b/pkg/deployment/rotation/arangod_containers_test.go index 3eb74d41a..19511757d 100644 --- a/pkg/deployment/rotation/arangod_containers_test.go +++ b/pkg/deployment/rotation/arangod_containers_test.go @@ -35,12 +35,14 @@ func Test_ArangoDContainers_SidecarImages(t *testing.T) { testCases := []TestCase{ { name: "Sidecar Image Update", - spec: buildPodSpec(addContainer(shared.ServerContainerName, nil), addSidecarWithImage("sidecar", "local:1.0")), - status: buildPodSpec(addContainer(shared.ServerContainerName, nil), addSidecarWithImage("sidecar", "local:2.0")), + spec: buildPodSpec(addContainer(shared.ServerContainerName), addSidecarWithImage("sidecar", "local:1.0")), + status: buildPodSpec(addContainer(shared.ServerContainerName), addSidecarWithImage("sidecar", "local:2.0")), - expectedMode: InPlaceRotation, - expectedPlan: api.Plan{ - actions.NewClusterAction(api.ActionTypeRuntimeContainerImageUpdate), + TestCaseOverride: TestCaseOverride{ + expectedMode: InPlaceRotation, + expectedPlan: api.Plan{ + actions.NewClusterAction(api.ActionTypeRuntimeContainerImageUpdate), + }, }, }, { @@ -48,9 +50,11 @@ func Test_ArangoDContainers_SidecarImages(t *testing.T) { spec: buildPodSpec(addSidecarWithImage("sidecar1", "local:1.0"), addSidecarWithImage("sidecar", "local:1.0")), status: buildPodSpec(addSidecarWithImage("sidecar1", "local:1.0"), addSidecarWithImage("sidecar", "local:2.0")), - expectedMode: InPlaceRotation, - expectedPlan: api.Plan{ - actions.NewClusterAction(api.ActionTypeRuntimeContainerImageUpdate), + TestCaseOverride: TestCaseOverride{ + expectedMode: InPlaceRotation, + expectedPlan: api.Plan{ + actions.NewClusterAction(api.ActionTypeRuntimeContainerImageUpdate), + }, }, }, } @@ -63,34 +67,38 @@ func Test_InitContainers(t *testing.T) { testCases := []TestCase{ { name: "Same containers", - spec: buildPodSpec(addInitContainer(api.ServerGroupReservedInitContainerNameUUID, nil), addInitContainer("sidecar", func(c *core.Container) { + spec: buildPodSpec(addInitContainer(api.ServerGroupReservedInitContainerNameUUID), addInitContainer("sidecar", func(c *core.Container) { c.Image = "local:1.0" })), - status: buildPodSpec(addInitContainer(api.ServerGroupReservedInitContainerNameUUID, nil), addInitContainer("sidecar", func(c *core.Container) { + status: buildPodSpec(addInitContainer(api.ServerGroupReservedInitContainerNameUUID), addInitContainer("sidecar", func(c *core.Container) { c.Image = "local:1.0" })), - expectedMode: SkippedRotation, + TestCaseOverride: TestCaseOverride{ + expectedMode: SkippedRotation, + }, - deploymentSpec: buildDeployment(func(depl *api.DeploymentSpec) { - depl.Agents.InitContainers = &api.ServerGroupInitContainers{ + groupSpec: buildGroupSpec(func(depl *api.ServerGroupSpec) { + depl.InitContainers = &api.ServerGroupInitContainers{ Mode: api.ServerGroupInitContainerIgnoreMode.New(), } }), }, { name: "Containers with different image", - spec: buildPodSpec(addInitContainer(api.ServerGroupReservedInitContainerNameUUID, nil), addInitContainer("sidecar", func(c *core.Container) { + spec: buildPodSpec(addInitContainer(api.ServerGroupReservedInitContainerNameUUID), addInitContainer("sidecar", func(c *core.Container) { c.Image = "local:1.0" })), - status: buildPodSpec(addInitContainer(api.ServerGroupReservedInitContainerNameUUID, nil), addInitContainer("sidecar", func(c *core.Container) { + status: buildPodSpec(addInitContainer(api.ServerGroupReservedInitContainerNameUUID), addInitContainer("sidecar", func(c *core.Container) { c.Image = "local:2.0" })), - expectedMode: SilentRotation, + TestCaseOverride: TestCaseOverride{ + expectedMode: SilentRotation, + }, - deploymentSpec: buildDeployment(func(depl *api.DeploymentSpec) { - depl.Agents.InitContainers = &api.ServerGroupInitContainers{ + groupSpec: buildGroupSpec(func(depl *api.ServerGroupSpec) { + depl.InitContainers = &api.ServerGroupInitContainers{ Mode: api.ServerGroupInitContainerIgnoreMode.New(), } }), @@ -104,17 +112,19 @@ func Test_InitContainers(t *testing.T) { testCases := []TestCase{ { name: "Containers with different image but init rotation enforced", - spec: buildPodSpec(addInitContainer(api.ServerGroupReservedInitContainerNameUUID, nil), addInitContainer("sidecar", func(c *core.Container) { + spec: buildPodSpec(addInitContainer(api.ServerGroupReservedInitContainerNameUUID), addInitContainer("sidecar", func(c *core.Container) { c.Image = "local:1.0" })), - status: buildPodSpec(addInitContainer(api.ServerGroupReservedInitContainerNameUUID, nil), addInitContainer("sidecar", func(c *core.Container) { + status: buildPodSpec(addInitContainer(api.ServerGroupReservedInitContainerNameUUID), addInitContainer("sidecar", func(c *core.Container) { c.Image = "local:2.0" })), - expectedMode: GracefulRotation, + TestCaseOverride: TestCaseOverride{ + expectedMode: GracefulRotation, + }, - deploymentSpec: buildDeployment(func(depl *api.DeploymentSpec) { - depl.Agents.InitContainers = &api.ServerGroupInitContainers{ + groupSpec: buildGroupSpec(func(depl *api.ServerGroupSpec) { + depl.InitContainers = &api.ServerGroupInitContainers{ Mode: api.ServerGroupInitContainerUpdateMode.New(), } }), @@ -132,10 +142,12 @@ func Test_InitContainers(t *testing.T) { c.Image = "local:1.0" })), - expectedMode: SilentRotation, + TestCaseOverride: TestCaseOverride{ + expectedMode: SilentRotation, + }, - deploymentSpec: buildDeployment(func(depl *api.DeploymentSpec) { - depl.Agents.InitContainers = &api.ServerGroupInitContainers{ + groupSpec: buildGroupSpec(func(depl *api.ServerGroupSpec) { + depl.InitContainers = &api.ServerGroupInitContainers{ Mode: api.ServerGroupInitContainerUpdateMode.New(), } }), @@ -151,10 +163,12 @@ func Test_InitContainers(t *testing.T) { c.Image = "local:2.0" })), - expectedMode: SilentRotation, + TestCaseOverride: TestCaseOverride{ + expectedMode: SilentRotation, + }, - deploymentSpec: buildDeployment(func(depl *api.DeploymentSpec) { - depl.Agents.InitContainers = &api.ServerGroupInitContainers{ + groupSpec: buildGroupSpec(func(depl *api.ServerGroupSpec) { + depl.InitContainers = &api.ServerGroupInitContainers{ Mode: api.ServerGroupInitContainerUpdateMode.New(), } }), @@ -174,10 +188,12 @@ func Test_InitContainers(t *testing.T) { c.Image = "local:1.0" })), - expectedMode: SilentRotation, + TestCaseOverride: TestCaseOverride{ + expectedMode: SilentRotation, + }, - deploymentSpec: buildDeployment(func(depl *api.DeploymentSpec) { - depl.Agents.InitContainers = &api.ServerGroupInitContainers{ + groupSpec: buildGroupSpec(func(depl *api.ServerGroupSpec) { + depl.InitContainers = &api.ServerGroupInitContainers{ Mode: api.ServerGroupInitContainerUpdateMode.New(), } }), @@ -197,10 +213,12 @@ func Test_InitContainers(t *testing.T) { c.Image = "local:2.0" })), - expectedMode: GracefulRotation, + TestCaseOverride: TestCaseOverride{ + expectedMode: GracefulRotation, + }, - deploymentSpec: buildDeployment(func(depl *api.DeploymentSpec) { - depl.Agents.InitContainers = &api.ServerGroupInitContainers{ + groupSpec: buildGroupSpec(func(depl *api.ServerGroupSpec) { + depl.InitContainers = &api.ServerGroupInitContainers{ Mode: api.ServerGroupInitContainerUpdateMode.New(), } }), @@ -263,8 +281,10 @@ func Test_Container_Args(t *testing.T) { name: "Only log level arguments of the Sidecar have been changed", spec: buildPodSpec(addContainerWithCommand("sidecar", []string{"--log.level=INFO", "--log.level=requests=error"})), - status: buildPodSpec(addContainerWithCommand("sidecar", []string{"--log.level=INFO"})), - expectedMode: GracefulRotation, + status: buildPodSpec(addContainerWithCommand("sidecar", []string{"--log.level=INFO"})), + TestCaseOverride: TestCaseOverride{ + expectedMode: GracefulRotation, + }, }, } @@ -293,7 +313,9 @@ func Test_Container_Ports(t *testing.T) { }, } })), - expectedMode: SilentRotation, + TestCaseOverride: TestCaseOverride{ + expectedMode: SilentRotation, + }, }, { name: "Ports of sidecar pod changed", @@ -315,7 +337,9 @@ func Test_Container_Ports(t *testing.T) { }, } })), - expectedMode: GracefulRotation, + TestCaseOverride: TestCaseOverride{ + expectedMode: GracefulRotation, + }, }, } diff --git a/pkg/deployment/rotation/arangod_test.go b/pkg/deployment/rotation/arangod_test.go index 59ffc1052..8cbe4d06a 100644 --- a/pkg/deployment/rotation/arangod_test.go +++ b/pkg/deployment/rotation/arangod_test.go @@ -40,7 +40,9 @@ func Test_ArangoD_SchedulerName(t *testing.T) { pod.Spec.SchedulerName = "new" }), - expectedMode: SilentRotation, + TestCaseOverride: TestCaseOverride{ + expectedMode: SilentRotation, + }, }, { name: "Change SchedulerName into Empty", @@ -51,7 +53,9 @@ func Test_ArangoD_SchedulerName(t *testing.T) { pod.Spec.SchedulerName = "" }), - expectedMode: SilentRotation, + TestCaseOverride: TestCaseOverride{ + expectedMode: SilentRotation, + }, }, { name: "SchedulerName equals", @@ -62,7 +66,9 @@ func Test_ArangoD_SchedulerName(t *testing.T) { pod.Spec.SchedulerName = "" }), - expectedMode: SkippedRotation, + TestCaseOverride: TestCaseOverride{ + expectedMode: SkippedRotation, + }, }, } @@ -80,7 +86,9 @@ func Test_ArangoD_TerminationGracePeriodSeconds(t *testing.T) { pod.Spec.TerminationGracePeriodSeconds = util.NewInt64(30) }), - expectedMode: SilentRotation, + TestCaseOverride: TestCaseOverride{ + expectedMode: SilentRotation, + }, }, { name: "Remove", @@ -91,7 +99,9 @@ func Test_ArangoD_TerminationGracePeriodSeconds(t *testing.T) { pod.Spec.TerminationGracePeriodSeconds = nil }), - expectedMode: SilentRotation, + TestCaseOverride: TestCaseOverride{ + expectedMode: SilentRotation, + }, }, { name: "Update", @@ -102,7 +112,9 @@ func Test_ArangoD_TerminationGracePeriodSeconds(t *testing.T) { pod.Spec.TerminationGracePeriodSeconds = util.NewInt64(30) }), - expectedMode: SilentRotation, + TestCaseOverride: TestCaseOverride{ + expectedMode: SilentRotation, + }, }, } @@ -137,7 +149,9 @@ func Test_ArangoD_Affinity(t *testing.T) { status: buildPodSpec(func(pod *core.PodTemplateSpec) { }), - expectedMode: GracefulRotation, + TestCaseOverride: TestCaseOverride{ + expectedMode: GracefulRotation, + }, }, { name: "Add affinity", @@ -165,7 +179,9 @@ func Test_ArangoD_Affinity(t *testing.T) { } }), - expectedMode: GracefulRotation, + TestCaseOverride: TestCaseOverride{ + expectedMode: GracefulRotation, + }, }, { name: "Change affinity", @@ -212,7 +228,9 @@ func Test_ArangoD_Affinity(t *testing.T) { } }), - expectedMode: SilentRotation, + TestCaseOverride: TestCaseOverride{ + expectedMode: SilentRotation, + }, }, { name: "Change affinity archs", @@ -259,7 +277,9 @@ func Test_ArangoD_Affinity(t *testing.T) { } }), - expectedMode: GracefulRotation, + TestCaseOverride: TestCaseOverride{ + expectedMode: GracefulRotation, + }, }, { name: "Change affinity archs - swap arch order", @@ -306,7 +326,9 @@ func Test_ArangoD_Affinity(t *testing.T) { } }), - expectedMode: SilentRotation, + TestCaseOverride: TestCaseOverride{ + expectedMode: SilentRotation, + }, }, } @@ -328,7 +350,9 @@ func Test_ArangoD_Labels(t *testing.T) { } }), - expectedMode: SkippedRotation, + TestCaseOverride: TestCaseOverride{ + expectedMode: SkippedRotation, + }, }, { name: "Remove label", @@ -343,7 +367,9 @@ func Test_ArangoD_Labels(t *testing.T) { pod.Labels = map[string]string{} }), - expectedMode: SkippedRotation, + TestCaseOverride: TestCaseOverride{ + expectedMode: SkippedRotation, + }, }, { name: "Change label", @@ -360,7 +386,9 @@ func Test_ArangoD_Labels(t *testing.T) { } }), - expectedMode: SkippedRotation, + TestCaseOverride: TestCaseOverride{ + expectedMode: SkippedRotation, + }, }, } @@ -385,7 +413,9 @@ func Test_ArangoD_Envs_Zone(t *testing.T) { } })), - expectedMode: SilentRotation, + TestCaseOverride: TestCaseOverride{ + expectedMode: SilentRotation, + }, }, { name: "Remove Zone env", @@ -403,7 +433,9 @@ func Test_ArangoD_Envs_Zone(t *testing.T) { c.Env = []core.EnvVar{} })), - expectedMode: SilentRotation, + TestCaseOverride: TestCaseOverride{ + expectedMode: SilentRotation, + }, }, { name: "Update Zone env", @@ -426,7 +458,9 @@ func Test_ArangoD_Envs_Zone(t *testing.T) { } })), - expectedMode: SilentRotation, + TestCaseOverride: TestCaseOverride{ + expectedMode: SilentRotation, + }, }, { name: "Update other env", @@ -453,7 +487,9 @@ func Test_ArangoD_Envs_Zone(t *testing.T) { } })), - expectedMode: GracefulRotation, + TestCaseOverride: TestCaseOverride{ + expectedMode: GracefulRotation, + }, }, } diff --git a/pkg/deployment/rotation/arangod_volumes.go b/pkg/deployment/rotation/arangod_volumes.go new file mode 100644 index 000000000..eb53fda32 --- /dev/null +++ b/pkg/deployment/rotation/arangod_volumes.go @@ -0,0 +1,190 @@ +// +// DISCLAIMER +// +// Copyright 2016-2022 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 rotation + +import ( + "reflect" + + core "k8s.io/api/core/v1" + + api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" + "github.com/arangodb/kube-arangodb/pkg/apis/shared" +) + +type volumeDiff struct { + a, b *core.Volume +} + +func comparePodVolumes(ds api.DeploymentSpec, g api.ServerGroup, spec, status *core.PodSpec) comparePodFunc { + return func(builder api.ActionBuilder) (mode Mode, plan api.Plan, err error) { + specV := mapVolumes(spec) + statusV := mapVolumes(status) + + diff := getVolumesDiffFromPods(specV, statusV) + + if len(diff) == 0 { + return SkippedRotation, nil, nil + } + + for k, v := range diff { + switch k { + case shared.ArangoDTimezoneVolumeName: + // We are fine, should be just replaced + if v.a == nil { + // we remove volume + return GracefulRotation, nil, nil + } + + if ds.Mode.Get().ServingGroup() == g { + // Always enforce on serving group + return GracefulRotation, nil, nil + } + default: + return GracefulRotation, nil, nil + } + } + + status.Volumes = spec.Volumes + return SilentRotation, nil, nil + } +} + +func getVolumesDiffFromPods(a, b map[string]*core.Volume) map[string]volumeDiff { + d := map[string]volumeDiff{} + + for k := range a { + if z, ok := b[k]; ok { + if !reflect.DeepEqual(a[k], z) { + d[k] = volumeDiff{ + a: a[k], + b: z, + } + } + } else { + d[k] = volumeDiff{ + a: a[k], + b: nil, + } + } + } + for k := range b { + if _, ok := a[k]; !ok { + d[k] = volumeDiff{ + a: nil, + b: b[k], + } + } + } + + return d +} + +func mapVolumes(a *core.PodSpec) map[string]*core.Volume { + n := make(map[string]*core.Volume, len(a.Volumes)) + + for id := range a.Volumes { + v := &a.Volumes[id] + + n[v.Name] = v + } + + return n +} + +func compareServerContainerVolumeMounts(ds api.DeploymentSpec, g api.ServerGroup, spec, status *core.Container) comparePodContainerFunc { + return func(builder api.ActionBuilder) (mode Mode, plan api.Plan, err error) { + specV := mapVolumeMounts(spec) + statusV := mapVolumeMounts(status) + + diff := getVolumeMountsDiffFromPods(specV, statusV) + + if len(diff) == 0 { + return SkippedRotation, nil, nil + } + + for k, v := range diff { + switch k { + case shared.ArangoDTimezoneVolumeName: + // We are fine, should be just replaced + if v.a == nil { + // we remove volume + return GracefulRotation, nil, nil + } + + if ds.Mode.Get().ServingGroup() == g { + // Always enforce on serving group + return GracefulRotation, nil, nil + } + default: + return GracefulRotation, nil, nil + } + } + + status.VolumeMounts = spec.VolumeMounts + return SilentRotation, nil, nil + } +} + +type volumeMountDiff struct { + a, b []*core.VolumeMount +} + +func getVolumeMountsDiffFromPods(a, b map[string][]*core.VolumeMount) map[string]volumeMountDiff { + d := map[string]volumeMountDiff{} + + for k := range a { + if z, ok := b[k]; ok { + if !reflect.DeepEqual(a[k], z) { + d[k] = volumeMountDiff{ + a: a[k], + b: z, + } + } + } else { + d[k] = volumeMountDiff{ + a: a[k], + b: nil, + } + } + } + for k := range b { + if _, ok := a[k]; !ok { + d[k] = volumeMountDiff{ + a: nil, + b: a[k], + } + } + } + + return d +} + +func mapVolumeMounts(a *core.Container) map[string][]*core.VolumeMount { + n := make(map[string][]*core.VolumeMount, len(a.VolumeMounts)) + + for id := range a.VolumeMounts { + v := &a.VolumeMounts[id] + + n[v.Name] = append(n[v.Name], v) + } + + return n +} diff --git a/pkg/deployment/rotation/arangod_volumes_test.go b/pkg/deployment/rotation/arangod_volumes_test.go new file mode 100644 index 000000000..51b556af0 --- /dev/null +++ b/pkg/deployment/rotation/arangod_volumes_test.go @@ -0,0 +1,264 @@ +// +// DISCLAIMER +// +// Copyright 2016-2022 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 rotation + +import ( + "testing" + + core "k8s.io/api/core/v1" + + api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" + "github.com/arangodb/kube-arangodb/pkg/apis/shared" +) + +func Test_ArangoD_Volumes(t *testing.T) { + testCases := []TestCase{ + { + name: "Empty volumes", + spec: buildPodSpec(), + status: buildPodSpec(), + + deploymentSpec: buildDeployment(), + + TestCaseOverride: TestCaseOverride{ + expectedMode: SkippedRotation, + }, + }, + { + name: "Same volumes", + spec: buildPodSpec(addVolume("data", addVolumeConfigMapSource(&core.ConfigMapVolumeSource{}))), + status: buildPodSpec(addVolume("data", addVolumeConfigMapSource(&core.ConfigMapVolumeSource{}))), + + TestCaseOverride: TestCaseOverride{ + expectedMode: SkippedRotation, + }, + }, + { + name: "Different volumes", + spec: buildPodSpec(addVolume("data", addVolumeConfigMapSource(&core.ConfigMapVolumeSource{}))), + status: buildPodSpec(addVolume("data", addVolumeConfigMapSource(&core.ConfigMapVolumeSource{ + LocalObjectReference: core.LocalObjectReference{ + Name: "test", + }, + }))), + + TestCaseOverride: TestCaseOverride{ + expectedMode: GracefulRotation, + }, + }, + { + name: "Missing volumes", + spec: buildPodSpec(addVolume("data", addVolumeConfigMapSource(&core.ConfigMapVolumeSource{}))), + status: buildPodSpec(), + + TestCaseOverride: TestCaseOverride{ + expectedMode: GracefulRotation, + }, + }, + { + name: "Added volumes", + spec: buildPodSpec(), + status: buildPodSpec(addVolume("data", addVolumeConfigMapSource(&core.ConfigMapVolumeSource{ + LocalObjectReference: core.LocalObjectReference{ + Name: "test", + }, + }))), + + TestCaseOverride: TestCaseOverride{ + expectedMode: GracefulRotation, + }, + }, + { + name: "Timezone: Different volumes", + spec: buildPodSpec(addVolume(shared.ArangoDTimezoneVolumeName, addVolumeConfigMapSource(&core.ConfigMapVolumeSource{}))), + status: buildPodSpec(addVolume(shared.ArangoDTimezoneVolumeName, addVolumeConfigMapSource(&core.ConfigMapVolumeSource{ + LocalObjectReference: core.LocalObjectReference{ + Name: "test", + }, + }))), + + overrides: map[api.DeploymentMode]map[api.ServerGroup]TestCaseOverride{ + api.DeploymentModeSingle: { + api.ServerGroupSingle: { + expectedMode: GracefulRotation, + }, + }, + api.DeploymentModeActiveFailover: { + api.ServerGroupSingle: { + expectedMode: GracefulRotation, + }, + }, + api.DeploymentModeCluster: { + api.ServerGroupCoordinators: { + expectedMode: GracefulRotation, + }, + }, + }, + + TestCaseOverride: TestCaseOverride{ + expectedMode: SilentRotation, + }, + }, + { + name: "Timezone: Missing volumes", + spec: buildPodSpec(addVolume(shared.ArangoDTimezoneVolumeName, addVolumeConfigMapSource(&core.ConfigMapVolumeSource{}))), + status: buildPodSpec(), + + overrides: map[api.DeploymentMode]map[api.ServerGroup]TestCaseOverride{ + api.DeploymentModeSingle: { + api.ServerGroupSingle: { + expectedMode: GracefulRotation, + }, + }, + api.DeploymentModeActiveFailover: { + api.ServerGroupSingle: { + expectedMode: GracefulRotation, + }, + }, + api.DeploymentModeCluster: { + api.ServerGroupCoordinators: { + expectedMode: GracefulRotation, + }, + }, + }, + + TestCaseOverride: TestCaseOverride{ + expectedMode: SilentRotation, + }, + }, + { + name: "Timezone: Added volumes", + spec: buildPodSpec(), + status: buildPodSpec(addVolume(shared.ArangoDTimezoneVolumeName, addVolumeConfigMapSource(&core.ConfigMapVolumeSource{ + LocalObjectReference: core.LocalObjectReference{ + Name: "test", + }, + }))), + + TestCaseOverride: TestCaseOverride{ + expectedMode: GracefulRotation, + }, + }, + } + + runTestCases(t)(testCases...) +} + +func Test_ArangoD_VolumeMounts(t *testing.T) { + testCases := []TestCase{ + { + name: "Empty volume mounts", + spec: buildPodSpec(addContainer("server")), + status: buildPodSpec(addContainer("server")), + + TestCaseOverride: TestCaseOverride{ + expectedMode: SkippedRotation, + }, + }, + { + name: "Same volumes", + spec: buildPodSpec(addContainer("server", addVolumeMount("mount", func(in *core.VolumeMount) { + + }))), + status: buildPodSpec(addContainer("server", addVolumeMount("mount", func(in *core.VolumeMount) { + + }))), + + TestCaseOverride: TestCaseOverride{ + expectedMode: SkippedRotation, + }, + }, + { + name: "Different volumes", + spec: buildPodSpec(addContainer("server", addVolumeMount("mount"))), + status: buildPodSpec(addContainer("server", addVolumeMount("mount2"))), + + TestCaseOverride: TestCaseOverride{ + expectedMode: GracefulRotation, + }, + }, + { + name: "Missing volumes", + spec: buildPodSpec(addContainer("server", addVolumeMount("mount"))), + status: buildPodSpec(addContainer("server")), + + TestCaseOverride: TestCaseOverride{ + expectedMode: GracefulRotation, + }, + }, + { + name: "Added volumes", + spec: buildPodSpec(addContainer("server")), + status: buildPodSpec(addContainer("server", addVolumeMount("mount"))), + + TestCaseOverride: TestCaseOverride{ + expectedMode: GracefulRotation, + }, + }, + { + name: "Timezone: Different volumes", + spec: buildPodSpec(addContainer("server", addVolumeMount(shared.ArangoDTimezoneVolumeName))), + status: buildPodSpec(addContainer("server", addVolumeMount("mount"))), + + TestCaseOverride: TestCaseOverride{ + expectedMode: GracefulRotation, + }, + }, + { + name: "Timezone: Missing volumes", + spec: buildPodSpec(addContainer("server")), + status: buildPodSpec(addContainer("server", addVolumeMount(shared.ArangoDTimezoneVolumeName))), + + TestCaseOverride: TestCaseOverride{ + expectedMode: GracefulRotation, + }, + }, + { + name: "Timezone: Added volumes", + spec: buildPodSpec(addContainer("server", addVolumeMount(shared.ArangoDTimezoneVolumeName))), + status: buildPodSpec(addContainer("server")), + + overrides: map[api.DeploymentMode]map[api.ServerGroup]TestCaseOverride{ + api.DeploymentModeSingle: { + api.ServerGroupSingle: { + expectedMode: GracefulRotation, + }, + }, + api.DeploymentModeActiveFailover: { + api.ServerGroupSingle: { + expectedMode: GracefulRotation, + }, + }, + api.DeploymentModeCluster: { + api.ServerGroupCoordinators: { + expectedMode: GracefulRotation, + }, + }, + }, + + TestCaseOverride: TestCaseOverride{ + expectedMode: SilentRotation, + }, + }, + } + + runTestCases(t)(testCases...) +} diff --git a/pkg/deployment/rotation/builder_utils_volume_mounts_test.go b/pkg/deployment/rotation/builder_utils_volume_mounts_test.go new file mode 100644 index 000000000..6e36873e4 --- /dev/null +++ b/pkg/deployment/rotation/builder_utils_volume_mounts_test.go @@ -0,0 +1,38 @@ +// +// DISCLAIMER +// +// Copyright 2016-2022 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 rotation + +import core "k8s.io/api/core/v1" + +type podVolumeMountSpecBuilder func(in *core.VolumeMount) + +func addVolumeMount(name string, builders ...podVolumeMountSpecBuilder) podContainerBuilder { + return func(c *core.Container) { + var v core.VolumeMount + v.Name = name + + for _, b := range builders { + b(&v) + } + + c.VolumeMounts = append(c.VolumeMounts, v) + } +} diff --git a/pkg/deployment/rotation/builder_utils_volume_test.go b/pkg/deployment/rotation/builder_utils_volume_test.go new file mode 100644 index 000000000..45155487d --- /dev/null +++ b/pkg/deployment/rotation/builder_utils_volume_test.go @@ -0,0 +1,44 @@ +// +// DISCLAIMER +// +// Copyright 2016-2022 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 rotation + +import core "k8s.io/api/core/v1" + +type podVolumeSpecBuilder func(in *core.Volume) + +func addVolume(name string, builders ...podVolumeSpecBuilder) podSpecBuilder { + return func(pod *core.PodTemplateSpec) { + var v core.Volume + v.Name = name + + for _, b := range builders { + b(&v) + } + + pod.Spec.Volumes = append(pod.Spec.Volumes, v) + } +} + +func addVolumeConfigMapSource(cm *core.ConfigMapVolumeSource) podVolumeSpecBuilder { + return func(in *core.Volume) { + in.ConfigMap = cm.DeepCopy() + } +} diff --git a/pkg/deployment/rotation/compare.go b/pkg/deployment/rotation/compare.go index e2346cf33..a5d0ffcb5 100644 --- a/pkg/deployment/rotation/compare.go +++ b/pkg/deployment/rotation/compare.go @@ -31,16 +31,38 @@ import ( "github.com/arangodb/kube-arangodb/pkg/deployment/resources" ) -type compareFuncGen func(deploymentSpec api.DeploymentSpec, group api.ServerGroup, spec, status *core.PodSpec) compareFunc -type compareFunc func(builder api.ActionBuilder) (mode Mode, plan api.Plan, err error) +type comparePodFuncGen func(deploymentSpec api.DeploymentSpec, group api.ServerGroup, spec, status *core.PodSpec) comparePodFunc +type comparePodFunc func(builder api.ActionBuilder) (mode Mode, plan api.Plan, err error) -func generator(deploymentSpec api.DeploymentSpec, group api.ServerGroup, spec, status *core.PodSpec) func(c compareFuncGen) compareFunc { - return func(c compareFuncGen) compareFunc { +func podFuncGenerator(deploymentSpec api.DeploymentSpec, group api.ServerGroup, spec, status *core.PodSpec) func(c comparePodFuncGen) comparePodFunc { + return func(c comparePodFuncGen) comparePodFunc { return c(deploymentSpec, group, spec, status) } } -func compareFuncs(builder api.ActionBuilder, f ...compareFunc) (mode Mode, plan api.Plan, err error) { +type comparePodContainerFuncGen func(deploymentSpec api.DeploymentSpec, group api.ServerGroup, spec, status *core.Container) comparePodContainerFunc +type comparePodContainerFunc func(builder api.ActionBuilder) (mode Mode, plan api.Plan, err error) + +func podContainerFuncGenerator(deploymentSpec api.DeploymentSpec, group api.ServerGroup, spec, status *core.Container) func(c comparePodContainerFuncGen) comparePodContainerFunc { + return func(c comparePodContainerFuncGen) comparePodContainerFunc { + return c(deploymentSpec, group, spec, status) + } +} + +func comparePodContainer(builder api.ActionBuilder, f ...comparePodContainerFunc) (mode Mode, plan api.Plan, err error) { + for _, q := range f { + if m, p, err := q(builder); err != nil { + return 0, nil, err + } else { + mode = mode.And(m) + plan = append(plan, p...) + } + } + + return +} + +func comparePod(builder api.ActionBuilder, f ...comparePodFunc) (mode Mode, plan api.Plan, err error) { for _, q := range f { if m, p, err := q(builder); err != nil { return 0, nil, err @@ -69,9 +91,9 @@ func compare(deploymentSpec api.DeploymentSpec, member api.MemberStatus, group a // Try to fill fields b := actions.NewActionBuilderWrap(group, member) - g := generator(deploymentSpec, group, &spec.PodSpec.Spec, &podStatus.Spec) + g := podFuncGenerator(deploymentSpec, group, &spec.PodSpec.Spec, &podStatus.Spec) - if m, p, err := compareFuncs(b, g(podCompare), g(affinityCompare), g(containersCompare), g(initContainersCompare)); err != nil { + if m, p, err := comparePod(b, g(podCompare), g(affinityCompare), g(comparePodVolumes), g(containersCompare), g(initContainersCompare)); err != nil { log.Err(err).Msg("Error while getting pod diff") return SkippedRotation, nil, err } else { diff --git a/pkg/deployment/rotation/utils_test.go b/pkg/deployment/rotation/utils_test.go index 5aadb98ea..7bdcc8232 100644 --- a/pkg/deployment/rotation/utils_test.go +++ b/pkg/deployment/rotation/utils_test.go @@ -30,46 +30,100 @@ import ( "github.com/arangodb/kube-arangodb/pkg/deployment/resources" ) +type TestCaseOverride struct { + expectedMode Mode + expectedPlan api.Plan + expectedErr string +} + type TestCase struct { name string spec, status *core.PodTemplateSpec deploymentSpec api.DeploymentSpec - expectedMode Mode - expectedPlan api.Plan - expectedErr string + groupSpec api.ServerGroupSpec + + TestCaseOverride + + overrides map[api.DeploymentMode]map[api.ServerGroup]TestCaseOverride } func runTestCases(t *testing.T) func(tcs ...TestCase) { + return func(tcs ...TestCase) { for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { - - pspec := newTemplateFromSpec(t, tc.spec, api.ServerGroupAgents, tc.deploymentSpec) - pstatus := newTemplateFromSpec(t, tc.status, api.ServerGroupAgents, tc.deploymentSpec) - - mode, plan, err := compare(tc.deploymentSpec, api.MemberStatus{ID: "id"}, api.ServerGroupAgents, pspec, pstatus) - - if tc.expectedErr != "" { - require.Error(t, err) - require.EqualError(t, err, tc.expectedErr) - } else { - require.Equal(t, tc.expectedMode, mode) - - switch mode { - case InPlaceRotation: - require.Len(t, plan, len(tc.expectedPlan)) - - for i := range plan { - require.Equal(t, tc.expectedPlan[i].Type, plan[i].Type) - } - } - } + runTestCasesForMode(t, api.DeploymentModeSingle, tc) + runTestCasesForMode(t, api.DeploymentModeActiveFailover, tc) + runTestCasesForMode(t, api.DeploymentModeCluster, tc) }) } } } +func runTestCasesForMode(t *testing.T, m api.DeploymentMode, tc TestCase) { + t.Run(m.String(), func(t *testing.T) { + switch m { + case api.DeploymentModeSingle: + runTestCasesForModeAndGroup(t, m, api.ServerGroupSingle, tc) + case api.DeploymentModeCluster: + runTestCasesForModeAndGroup(t, m, api.ServerGroupAgents, tc) + runTestCasesForModeAndGroup(t, m, api.ServerGroupDBServers, tc) + runTestCasesForModeAndGroup(t, m, api.ServerGroupCoordinators, tc) + case api.DeploymentModeActiveFailover: + runTestCasesForModeAndGroup(t, m, api.ServerGroupAgents, tc) + runTestCasesForModeAndGroup(t, m, api.ServerGroupSingle, tc) + } + }) +} + +func runTestCasesForModeAndGroup(t *testing.T, m api.DeploymentMode, g api.ServerGroup, tc TestCase) { + t.Run(g.AsRole(), func(t *testing.T) { + ds := tc.deploymentSpec.DeepCopy() + if ds == nil { + ds = &api.DeploymentSpec{} + } + + ds.Mode = m.New() + + ds.UpdateServerGroupSpec(g, tc.groupSpec) + + if tc.spec == nil { + tc.spec = buildPodSpec() + } + if tc.status == nil { + tc.status = buildPodSpec() + } + + pspec := newTemplateFromSpec(t, tc.spec, g, *ds) + pstatus := newTemplateFromSpec(t, tc.status, g, *ds) + + mode, plan, err := compare(*ds, api.MemberStatus{ID: "id"}, g, pspec, pstatus) + + q := tc.TestCaseOverride + + if v, ok := tc.overrides[m][g]; ok { + q = v + } + + if tc.expectedErr != "" { + require.Error(t, err) + require.EqualError(t, err, q.expectedErr) + } else { + require.Equal(t, q.expectedMode, mode) + + switch mode { + case InPlaceRotation: + require.Len(t, plan, len(q.expectedPlan)) + + for i := range plan { + require.Equal(t, q.expectedPlan[i].Type, plan[i].Type) + } + } + } + }) +} + func newTemplateFromSpec(t *testing.T, podSpec *core.PodTemplateSpec, group api.ServerGroup, deploymentSpec api.DeploymentSpec) *api.ArangoMemberPodTemplate { checksum, err := resources.ChecksumArangoPod(deploymentSpec.GetServerGroupSpec(group), resources.CreatePodFromTemplate(podSpec)) require.NoError(t, err) @@ -82,6 +136,8 @@ func newTemplateFromSpec(t *testing.T, podSpec *core.PodTemplateSpec, group api. type podSpecBuilder func(pod *core.PodTemplateSpec) +type podContainerBuilder func(c *core.Container) + func buildPodSpec(b ...podSpecBuilder) *core.PodTemplateSpec { p := &core.PodTemplateSpec{} @@ -92,28 +148,28 @@ func buildPodSpec(b ...podSpecBuilder) *core.PodTemplateSpec { return p } -func addContainer(name string, f func(c *core.Container)) podSpecBuilder { +func addContainer(name string, f ...podContainerBuilder) podSpecBuilder { return func(pod *core.PodTemplateSpec) { var c core.Container c.Name = name - if f != nil { - f(&c) + for _, q := range f { + q(&c) } pod.Spec.Containers = append(pod.Spec.Containers, c) } } -func addInitContainer(name string, f func(c *core.Container)) podSpecBuilder { +func addInitContainer(name string, f ...podContainerBuilder) podSpecBuilder { return func(pod *core.PodTemplateSpec) { var c core.Container c.Name = name - if f != nil { - f(&c) + for _, q := range f { + q(&c) } pod.Spec.InitContainers = append(pod.Spec.InitContainers, c) @@ -143,3 +199,15 @@ func buildDeployment(b ...deploymentBuilder) api.DeploymentSpec { return p } + +type groupSpecBuilder func(depl *api.ServerGroupSpec) + +func buildGroupSpec(b ...groupSpecBuilder) api.ServerGroupSpec { + p := api.ServerGroupSpec{} + + for _, i := range b { + i(&p) + } + + return p +} diff --git a/pkg/generated/timezones/timezones.go b/pkg/generated/timezones/timezones.go index d6d2955bc..0a49f70e9 100644 --- a/pkg/generated/timezones/timezones.go +++ b/pkg/generated/timezones/timezones.go @@ -34,7 +34,7 @@ type Timezone struct { } func (t Timezone) GetData() ([]byte, bool) { - if d, ok := timezonesData[t.Name]; ok { + if d, ok := timezonesData[t.Parent]; ok { if d, err := base64.StdEncoding.DecodeString(d); err == nil { return d, true } diff --git a/pkg/logging/level.go b/pkg/logging/level.go index 4d483edc5..6853b5779 100644 --- a/pkg/logging/level.go +++ b/pkg/logging/level.go @@ -24,6 +24,10 @@ import "github.com/rs/zerolog" type Level zerolog.Level +func (l Level) New() *Level { + return &l +} + const ( Trace = Level(zerolog.TraceLevel) Debug = Level(zerolog.DebugLevel) diff --git a/pkg/logging/logger.go b/pkg/logging/logger.go index b399c35de..c4f1dcba5 100644 --- a/pkg/logging/logger.go +++ b/pkg/logging/logger.go @@ -53,8 +53,10 @@ func NewDefaultFactory() Factory { func NewFactory(root zerolog.Logger) Factory { return &factory{ - root: root, - loggers: map[string]*zerolog.Logger{}, + root: root, + loggers: map[string]*zerolog.Logger{}, + defaults: map[string]Level{}, + levels: map[string]Level{}, } } @@ -66,6 +68,9 @@ type factory struct { wrappers []Wrap loggers map[string]*zerolog.Logger + + defaults map[string]Level + levels map[string]Level } func (f *factory) Names() []string { @@ -110,27 +115,39 @@ func (f *factory) ApplyLogLevels(in map[string]Level) { f.lock.Lock() defer f.lock.Unlock() - if def, ok := in[AllLevels]; ok { - // Apply with default log level + z := make(map[string]Level, len(in)) - for k := range f.loggers { - if ov, ok := in[k]; ok { - // Override in place - l := f.root.Level(zerolog.Level(ov)) - f.loggers[k] = &l - } else { - // Override in place - l := f.root.Level(zerolog.Level(def)) - f.loggers[k] = &l - } + for k, v := range in { + z[k] = v + } + + f.levels = z + + for k := range f.loggers { + f.applyForLogger(k) + } +} + +func (f *factory) applyForLogger(name string) { + if def, ok := f.levels[AllLevels]; ok { + if ov, ok := f.levels[name]; ok { + // override on logger level + l := f.root.Level(zerolog.Level(ov)) + f.loggers[name] = &l + } else { + // override on global level + l := f.root.Level(zerolog.Level(def)) + f.loggers[name] = &l } } else { - for k := range f.loggers { - if ov, ok := in[k]; ok { - // Override in place - l := f.root.Level(zerolog.Level(ov)) - f.loggers[k] = &l - } + if ov, ok := f.levels[name]; ok { + // override on logger level + l := f.root.Level(zerolog.Level(ov)) + f.loggers[name] = &l + } else { + // override on global level + l := f.root.Level(zerolog.Level(f.defaults[name])) + f.loggers[name] = &l } } } @@ -143,8 +160,8 @@ func (f *factory) RegisterLogger(name string, level Level) bool { return false } - l := f.root.Level(zerolog.Level(level)) - f.loggers[name] = &l + f.defaults[name] = level + f.applyForLogger(name) return true } diff --git a/pkg/util/timer/after_test.go b/pkg/util/timer/after_test.go new file mode 100644 index 000000000..3eb22b63b --- /dev/null +++ b/pkg/util/timer/after_test.go @@ -0,0 +1,30 @@ +// +// DISCLAIMER +// +// Copyright 2016-2022 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 timer + +import ( + "testing" + "time" +) + +func Test_After(t *testing.T) { + <-After(250 * time.Millisecond) +}