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

[Feature] Timezone (#1088)

This commit is contained in:
Adam Janikowski 2022-08-18 09:13:03 +02:00 committed by GitHub
parent 583532e665
commit ef4c3c6161
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 1343 additions and 132 deletions

View file

@ -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

6
go.mod
View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -200,6 +200,7 @@ const (
// Resources
ActionTypeResourceSync ActionType = "ResourceSync"
ActionTypeTimezoneSecretSet ActionType = "TimezoneSecretSet"
)
const (

View file

@ -41,6 +41,7 @@ var (
shared.LifecycleVolumeName,
shared.FoxxAppEphemeralVolumeName,
shared.TMPEphemeralVolumeName,
shared.ArangoDTimezoneVolumeName,
}
)

View file

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

View file

@ -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

View file

@ -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

View file

@ -200,6 +200,7 @@ const (
// Resources
ActionTypeResourceSync ActionType = "ResourceSync"
ActionTypeTimezoneSecretSet ActionType = "TimezoneSecretSet"
)
const (

View file

@ -41,6 +41,7 @@ var (
shared.LifecycleVolumeName,
shared.FoxxAppEphemeralVolumeName,
shared.TMPEphemeralVolumeName,
shared.ArangoDTimezoneVolumeName,
}
)

View file

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

View file

@ -56,6 +56,7 @@ const (
LifecycleVolumeName = "lifecycle"
FoxxAppEphemeralVolumeName = "ephemeral-apps"
TMPEphemeralVolumeName = "ephemeral-tmp"
ArangoDTimezoneVolumeName = "arangod-timezone"
RocksdbEncryptionVolumeName = "rocksdb-encryption"
ExporterJWTVolumeName = "exporter-jwt"
ArangodVolumeMountDir = "/data"

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -34,6 +34,7 @@ const (
const (
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

View file

@ -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

View file

@ -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")}
}

View file

@ -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
}

View file

@ -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)
}

View file

@ -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)...)
}

View file

@ -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

View file

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

View file

@ -35,24 +35,28 @@ 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")),
TestCaseOverride: TestCaseOverride{
expectedMode: InPlaceRotation,
expectedPlan: api.Plan{
actions.NewClusterAction(api.ActionTypeRuntimeContainerImageUpdate),
},
},
},
{
name: "Sidecar Image Update with more than one sidecar",
spec: buildPodSpec(addSidecarWithImage("sidecar1", "local:1.0"), addSidecarWithImage("sidecar", "local:1.0")),
status: buildPodSpec(addSidecarWithImage("sidecar1", "local:1.0"), addSidecarWithImage("sidecar", "local:2.0")),
TestCaseOverride: TestCaseOverride{
expectedMode: InPlaceRotation,
expectedPlan: api.Plan{
actions.NewClusterAction(api.ActionTypeRuntimeContainerImageUpdate),
},
},
},
}
runTestCases(t)(testCases...)
@ -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"
})),
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"
})),
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"
})),
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"
})),
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"
})),
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"
})),
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"
})),
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(),
}
}),
@ -264,8 +282,10 @@ func Test_Container_Args(t *testing.T) {
spec: buildPodSpec(addContainerWithCommand("sidecar",
[]string{"--log.level=INFO", "--log.level=requests=error"})),
status: buildPodSpec(addContainerWithCommand("sidecar", []string{"--log.level=INFO"})),
TestCaseOverride: TestCaseOverride{
expectedMode: GracefulRotation,
},
},
}
runTestCases(t)(testCases...)
@ -293,8 +313,10 @@ func Test_Container_Ports(t *testing.T) {
},
}
})),
TestCaseOverride: TestCaseOverride{
expectedMode: SilentRotation,
},
},
{
name: "Ports of sidecar pod changed",
spec: buildPodSpec(addContainer("sidecar", func(c *core.Container) {
@ -315,8 +337,10 @@ func Test_Container_Ports(t *testing.T) {
},
}
})),
TestCaseOverride: TestCaseOverride{
expectedMode: GracefulRotation,
},
},
}
runTestCases(t)(testCases...)

View file

@ -40,8 +40,10 @@ func Test_ArangoD_SchedulerName(t *testing.T) {
pod.Spec.SchedulerName = "new"
}),
TestCaseOverride: TestCaseOverride{
expectedMode: SilentRotation,
},
},
{
name: "Change SchedulerName into Empty",
spec: buildPodSpec(func(pod *core.PodTemplateSpec) {
@ -51,8 +53,10 @@ func Test_ArangoD_SchedulerName(t *testing.T) {
pod.Spec.SchedulerName = ""
}),
TestCaseOverride: TestCaseOverride{
expectedMode: SilentRotation,
},
},
{
name: "SchedulerName equals",
spec: buildPodSpec(func(pod *core.PodTemplateSpec) {
@ -62,8 +66,10 @@ func Test_ArangoD_SchedulerName(t *testing.T) {
pod.Spec.SchedulerName = ""
}),
TestCaseOverride: TestCaseOverride{
expectedMode: SkippedRotation,
},
},
}
runTestCases(t)(testCases...)
@ -80,8 +86,10 @@ func Test_ArangoD_TerminationGracePeriodSeconds(t *testing.T) {
pod.Spec.TerminationGracePeriodSeconds = util.NewInt64(30)
}),
TestCaseOverride: TestCaseOverride{
expectedMode: SilentRotation,
},
},
{
name: "Remove",
spec: buildPodSpec(func(pod *core.PodTemplateSpec) {
@ -91,8 +99,10 @@ func Test_ArangoD_TerminationGracePeriodSeconds(t *testing.T) {
pod.Spec.TerminationGracePeriodSeconds = nil
}),
TestCaseOverride: TestCaseOverride{
expectedMode: SilentRotation,
},
},
{
name: "Update",
spec: buildPodSpec(func(pod *core.PodTemplateSpec) {
@ -102,8 +112,10 @@ func Test_ArangoD_TerminationGracePeriodSeconds(t *testing.T) {
pod.Spec.TerminationGracePeriodSeconds = util.NewInt64(30)
}),
TestCaseOverride: TestCaseOverride{
expectedMode: SilentRotation,
},
},
}
runTestCases(t)(testCases...)
@ -137,8 +149,10 @@ func Test_ArangoD_Affinity(t *testing.T) {
status: buildPodSpec(func(pod *core.PodTemplateSpec) {
}),
TestCaseOverride: TestCaseOverride{
expectedMode: GracefulRotation,
},
},
{
name: "Add affinity",
spec: buildPodSpec(func(pod *core.PodTemplateSpec) {
@ -165,8 +179,10 @@ func Test_ArangoD_Affinity(t *testing.T) {
}
}),
TestCaseOverride: TestCaseOverride{
expectedMode: GracefulRotation,
},
},
{
name: "Change affinity",
spec: buildPodSpec(func(pod *core.PodTemplateSpec) {
@ -212,8 +228,10 @@ func Test_ArangoD_Affinity(t *testing.T) {
}
}),
TestCaseOverride: TestCaseOverride{
expectedMode: SilentRotation,
},
},
{
name: "Change affinity archs",
spec: buildPodSpec(func(pod *core.PodTemplateSpec) {
@ -259,8 +277,10 @@ func Test_ArangoD_Affinity(t *testing.T) {
}
}),
TestCaseOverride: TestCaseOverride{
expectedMode: GracefulRotation,
},
},
{
name: "Change affinity archs - swap arch order",
spec: buildPodSpec(func(pod *core.PodTemplateSpec) {
@ -306,8 +326,10 @@ func Test_ArangoD_Affinity(t *testing.T) {
}
}),
TestCaseOverride: TestCaseOverride{
expectedMode: SilentRotation,
},
},
}
runTestCases(t)(testCases...)
@ -328,8 +350,10 @@ func Test_ArangoD_Labels(t *testing.T) {
}
}),
TestCaseOverride: TestCaseOverride{
expectedMode: SkippedRotation,
},
},
{
name: "Remove label",
@ -343,8 +367,10 @@ func Test_ArangoD_Labels(t *testing.T) {
pod.Labels = map[string]string{}
}),
TestCaseOverride: TestCaseOverride{
expectedMode: SkippedRotation,
},
},
{
name: "Change label",
@ -360,8 +386,10 @@ func Test_ArangoD_Labels(t *testing.T) {
}
}),
TestCaseOverride: TestCaseOverride{
expectedMode: SkippedRotation,
},
},
}
runTestCases(t)(testCases...)
@ -385,8 +413,10 @@ func Test_ArangoD_Envs_Zone(t *testing.T) {
}
})),
TestCaseOverride: TestCaseOverride{
expectedMode: SilentRotation,
},
},
{
name: "Remove Zone env",
@ -403,8 +433,10 @@ func Test_ArangoD_Envs_Zone(t *testing.T) {
c.Env = []core.EnvVar{}
})),
TestCaseOverride: TestCaseOverride{
expectedMode: SilentRotation,
},
},
{
name: "Update Zone env",
@ -426,8 +458,10 @@ func Test_ArangoD_Envs_Zone(t *testing.T) {
}
})),
TestCaseOverride: TestCaseOverride{
expectedMode: SilentRotation,
},
},
{
name: "Update other env",
@ -453,8 +487,10 @@ func Test_ArangoD_Envs_Zone(t *testing.T) {
}
})),
TestCaseOverride: TestCaseOverride{
expectedMode: GracefulRotation,
},
},
}
runTestCases(t)(testCases...)

View file

@ -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
}

View file

@ -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...)
}

View file

@ -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)
}
}

View file

@ -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()
}
}

View file

@ -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 {

View file

@ -30,44 +30,98 @@ import (
"github.com/arangodb/kube-arangodb/pkg/deployment/resources"
)
type TestCase struct {
name string
spec, status *core.PodTemplateSpec
deploymentSpec api.DeploymentSpec
type TestCaseOverride struct {
expectedMode Mode
expectedPlan api.Plan
expectedErr string
}
type TestCase struct {
name string
spec, status *core.PodTemplateSpec
deploymentSpec api.DeploymentSpec
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) {
runTestCasesForMode(t, api.DeploymentModeSingle, tc)
runTestCasesForMode(t, api.DeploymentModeActiveFailover, tc)
runTestCasesForMode(t, api.DeploymentModeCluster, tc)
})
}
}
}
pspec := newTemplateFromSpec(t, tc.spec, api.ServerGroupAgents, tc.deploymentSpec)
pstatus := newTemplateFromSpec(t, tc.status, api.ServerGroupAgents, tc.deploymentSpec)
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)
}
})
}
mode, plan, err := compare(tc.deploymentSpec, api.MemberStatus{ID: "id"}, api.ServerGroupAgents, pspec, pstatus)
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, tc.expectedErr)
require.EqualError(t, err, q.expectedErr)
} else {
require.Equal(t, tc.expectedMode, mode)
require.Equal(t, q.expectedMode, mode)
switch mode {
case InPlaceRotation:
require.Len(t, plan, len(tc.expectedPlan))
require.Len(t, plan, len(q.expectedPlan))
for i := range plan {
require.Equal(t, tc.expectedPlan[i].Type, plan[i].Type)
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 {
@ -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
}

View file

@ -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
}

View file

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

View file

@ -55,6 +55,8 @@ func NewFactory(root zerolog.Logger) Factory {
return &factory{
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, v := range in {
z[k] = v
}
f.levels = z
for k := range f.loggers {
if ov, ok := in[k]; ok {
// Override in place
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[k] = &l
f.loggers[name] = &l
} else {
// Override in place
// override on global level
l := f.root.Level(zerolog.Level(def))
f.loggers[k] = &l
}
f.loggers[name] = &l
}
} else {
for k := range f.loggers {
if ov, ok := in[k]; ok {
// Override in place
if ov, ok := f.levels[name]; ok {
// override on logger level
l := f.root.Level(zerolog.Level(ov))
f.loggers[k] = &l
}
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
}

View file

@ -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)
}