diff --git a/CHANGELOG.md b/CHANGELOG.md index b17eda5e2..27ac61ab2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ # Change Log ## [master](https://github.com/arangodb/kube-arangodb/tree/master) (N/A) +- Added Customizable Volumes and VolumeMounts for ArangoDB server container +- Added MemoryOverride flag for ArangoDB >= 3.6.3 +- Improved Rotation discovery process - Added annotation to rotate ArangoDeployment in secure way ## [1.0.0](https://github.com/arangodb/kube-arangodb/tree/1.0.0) (2020-03-03) diff --git a/Makefile b/Makefile index 5842b4b2e..9976384cb 100644 --- a/Makefile +++ b/Makefile @@ -177,7 +177,11 @@ linter: fmt $(SOURCES_PACKAGES) .PHONY: build -build: docker docker-ubi manifests +build: docker manifests + +ifndef IGNORE_UBI +build: docker-ubi +endif .PHONY: clean clean: @@ -204,12 +208,19 @@ update-generated: @mkdir -p $(ORGDIR) @ln -s -f $(SCRIPTDIR) $(ORGDIR)/kube-arangodb GOPATH=$(GOBUILDDIR) $(VENDORDIR)/k8s.io/code-generator/generate-groups.sh \ - "all" \ - "github.com/arangodb/kube-arangodb/pkg/generated" \ - "github.com/arangodb/kube-arangodb/pkg/apis" \ - "deployment:v1 replication:v1 storage:v1alpha backup:v1" \ - --go-header-file "./tools/codegen/boilerplate.go.txt" \ - $(VERIFYARGS) + "all" \ + "github.com/arangodb/kube-arangodb/pkg/generated" \ + "github.com/arangodb/kube-arangodb/pkg/apis" \ + "deployment:v1 replication:v1 storage:v1alpha backup:v1" \ + --go-header-file "./tools/codegen/boilerplate.go.txt" \ + $(VERIFYARGS) + GOPATH=$(GOBUILDDIR) $(VENDORDIR)/k8s.io/code-generator/generate-groups.sh \ + "deepcopy" \ + "github.com/arangodb/kube-arangodb/pkg/generated" \ + "github.com/arangodb/kube-arangodb/pkg/apis" \ + "shared:v1" \ + --go-header-file "./tools/codegen/boilerplate.go.txt" \ + $(VERIFYARGS) .PHONY: verify-generated verify-generated: @@ -226,6 +237,9 @@ dashboard/assets.go: $(DASHBOARDSOURCES) $(DASHBOARDDIR)/Dockerfile.build $(DASHBOARDBUILDIMAGE) go run github.com/jessevdk/go-assets-builder -s /dashboard/build/ -o dashboard/assets.go -p dashboard dashboard/build +.PHONY: bin +bin: $(BIN) + $(BIN): $(SOURCES) dashboard/assets.go VERSION @mkdir -p $(BINDIR) CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -installsuffix netgo -ldflags "-X main.projectVersion=$(VERSION) -X main.projectBuild=$(COMMIT)" -o $(BIN) $(REPOPATH) diff --git a/go.mod b/go.mod index c0467e730..260ee5628 100644 --- a/go.mod +++ b/go.mod @@ -44,6 +44,7 @@ require ( golang.org/x/sys v0.0.0-20200116001909-b77594299b42 gopkg.in/go-playground/assert.v1 v1.2.1 // indirect gopkg.in/go-playground/validator.v8 v8.18.2 // indirect + gopkg.in/yaml.v2 v2.2.8 k8s.io/api v0.17.3 k8s.io/apiextensions-apiserver v0.17.3 k8s.io/apimachinery v0.17.3 diff --git a/pkg/apis/deployment/v1/deployment.go b/pkg/apis/deployment/v1/deployment.go index a53975a0a..b9d14d80a 100644 --- a/pkg/apis/deployment/v1/deployment.go +++ b/pkg/apis/deployment/v1/deployment.go @@ -45,8 +45,8 @@ type ArangoDeploymentList struct { type ArangoDeployment struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - Spec DeploymentSpec `json:"spec"` - Status DeploymentStatus `json:"status"` + Spec DeploymentSpec `json:"spec,omitempty"` + Status DeploymentStatus `json:"status,omitempty"` } type ServerGroupFunc func(ServerGroup, ServerGroupSpec, *MemberStatusList) error diff --git a/pkg/apis/deployment/v1/deployment_status.go b/pkg/apis/deployment/v1/deployment_status.go index ccf8ad7fc..ff64cca98 100644 --- a/pkg/apis/deployment/v1/deployment_status.go +++ b/pkg/apis/deployment/v1/deployment_status.go @@ -29,7 +29,7 @@ import ( // DeploymentStatus contains the status part of a Cluster resource. type DeploymentStatus struct { // Phase holds the current lifetime phase of the deployment - Phase DeploymentPhase `json:"phase"` + Phase DeploymentPhase `json:"phase,omitempty"` // Reason contains a human readable reason for reaching the current state (can be empty) Reason string `json:"reason,omitempty"` // Reason for current state diff --git a/pkg/apis/deployment/v1/server_group_spec.go b/pkg/apis/deployment/v1/server_group_spec.go index 6e38f6850..2875d5b49 100644 --- a/pkg/apis/deployment/v1/server_group_spec.go +++ b/pkg/apis/deployment/v1/server_group_spec.go @@ -26,6 +26,8 @@ import ( "math" "strings" + "github.com/arangodb/kube-arangodb/pkg/apis/shared" + "github.com/pkg/errors" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" @@ -72,6 +74,10 @@ type ServerGroupSpec struct { Sidecars []v1.Container `json:"sidecars,omitempty"` // SecurityContext specifies security context for group SecurityContext *ServerGroupSpecSecurityContext `json:"securityContext,omitempty"` + // Volumes define list of volumes mounted to pod + Volumes ServerGroupSpecVolumes `json:"volumes,omitempty"` + // VolumeMounts define list of volume mounts mounted into server container + VolumeMounts ServerGroupSpecVolumeMounts `json:"volumeMounts,omitempty"` } // ServerGroupSpecSecurityContext contains specification for pod security context @@ -372,12 +378,44 @@ func (s ServerGroupSpec) Validate(group ServerGroup, used bool, mode DeploymentM } } } + + if err := s.validate(); err != nil { + return maskAny(err) + } } else if s.GetCount() != 0 { return maskAny(errors.Wrapf(ValidationError, "Invalid count value %d for un-used group. Expected 0", s.GetCount())) } return nil } +func (s *ServerGroupSpec) validate() error { + if s == nil { + return nil + } + + return shared.WithErrors( + shared.PrefixResourceError("volumes", s.Volumes.Validate()), + shared.PrefixResourceError("volumeMounts", s.VolumeMounts.Validate()), + s.validateVolumes(), + ) +} + +func (s *ServerGroupSpec) validateVolumes() error { + volumes := map[string]bool{} + + for _, volume := range s.Volumes { + volumes[volume.Name] = true + } + + for _, mount := range s.VolumeMounts { + if _, ok := volumes[mount.Name]; !ok { + return errors.Errorf("Volume %s is not defined, but required by mount", mount.Name) + } + } + + return nil +} + // SetDefaults fills in missing defaults func (s *ServerGroupSpec) SetDefaults(group ServerGroup, used bool, mode DeploymentMode) { if s.GetCount() == 0 && used { diff --git a/pkg/apis/deployment/v1/server_group_volume.go b/pkg/apis/deployment/v1/server_group_volume.go new file mode 100644 index 000000000..09330da8f --- /dev/null +++ b/pkg/apis/deployment/v1/server_group_volume.go @@ -0,0 +1,176 @@ +// +// DISCLAIMER +// +// Copyright 2020 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 +// +// Author Adam Janikowski +// + +package v1 + +import ( + "fmt" + + "github.com/arangodb/kube-arangodb/pkg/apis/shared" + sharedv1 "github.com/arangodb/kube-arangodb/pkg/apis/shared/v1" + + "github.com/arangodb/kube-arangodb/pkg/util/k8sutil" + "github.com/pkg/errors" + core "k8s.io/api/core/v1" +) + +var ( + restrictedVolumeNames = []string{ + k8sutil.ArangodVolumeName, + k8sutil.TlsKeyfileVolumeName, + k8sutil.RocksdbEncryptionVolumeName, + k8sutil.ExporterJWTVolumeName, + k8sutil.ClusterJWTSecretVolumeName, + "lifecycle", + } +) + +// IsRestrictedVolumeName check of volume name is restricted, for example for originally mounted volumes +func IsRestrictedVolumeName(name string) bool { + for _, restrictedVolumeName := range restrictedVolumeNames { + if restrictedVolumeName == name { + return true + } + } + + return false +} + +// ServerGroupSpecVolumes definition of volume list which need to be mounted to Pod +type ServerGroupSpecVolumes []ServerGroupSpecVolume + +// Validate if ServerGroupSpec volumes are valid and does not collide +func (s ServerGroupSpecVolumes) Validate() error { + var validationErrors []error + + mappedVolumes := map[string]int{} + + for id, volume := range s { + if i, ok := mappedVolumes[volume.Name]; ok { + mappedVolumes[volume.Name] = i + 1 + } else { + mappedVolumes[volume.Name] = 1 + } + + if err := volume.Validate(); err != nil { + validationErrors = append(validationErrors, shared.PrefixResourceErrors(fmt.Sprintf("%d", id), err)) + } + } + + for volumeName, count := range mappedVolumes { + if IsRestrictedVolumeName(volumeName) { + validationErrors = append(validationErrors, errors.Errorf("volume with name %s is restricted", volumeName)) + } + + if count == 1 { + continue + } + + validationErrors = append(validationErrors, errors.Errorf("volume with name %s defined more than once: %d", volumeName, count)) + } + + return shared.WithErrors(validationErrors...) +} + +// Volumes create volumes +func (s ServerGroupSpecVolumes) Volumes() []core.Volume { + volumes := make([]core.Volume, len(s)) + + for id, volume := range s { + volumes[id] = volume.Volume() + } + + return volumes +} + +// ServerGroupSpecVolume definition of volume which need to be mounted to Pod +type ServerGroupSpecVolume struct { + // Name of volume + Name string `json:"name"` + + // Secret which should be mounted into pod + Secret *ServerGroupSpecVolumeSecret `json:"secret,omitempty"` + + // ConfigMap which should be mounted into pod + ConfigMap *ServerGroupSpecVolumeConfigMap `json:"configMap,omitempty"` +} + +// Validate if ServerGroupSpec volume is valid +func (s *ServerGroupSpecVolume) Validate() error { + if s == nil { + return nil + } + + return shared.WithErrors( + shared.PrefixResourceErrors("name", sharedv1.AsKubernetesResourceName(&s.Name).Validate()), + shared.PrefixResourceErrors("secret", s.Secret.Validate()), + shared.PrefixResourceErrors("configMap", s.ConfigMap.Validate()), + s.validate(), + ) +} + +// Volume create Pod Volume object +func (s ServerGroupSpecVolume) Volume() core.Volume { + return core.Volume{ + Name: s.Name, + VolumeSource: core.VolumeSource{ + ConfigMap: (*core.ConfigMapVolumeSource)(s.ConfigMap), + Secret: (*core.SecretVolumeSource)(s.Secret), + }, + } +} + +func (s *ServerGroupSpecVolume) validate() error { + if s.ConfigMap == nil && s.Secret == nil { + return errors.Errorf("at least one option need to be defined: secret or configMap") + } + + if s.ConfigMap != nil && s.Secret != nil { + return errors.Errorf("only one option can be defined: secret or configMap") + } + + return nil +} + +type ServerGroupSpecVolumeSecret core.SecretVolumeSource + +func (s *ServerGroupSpecVolumeSecret) Validate() error { + if s == nil { + return nil + } + + return shared.WithErrors( + shared.PrefixResourceError("secretName", sharedv1.AsKubernetesResourceName(&s.SecretName).Validate()), + ) +} + +type ServerGroupSpecVolumeConfigMap core.ConfigMapVolumeSource + +func (s *ServerGroupSpecVolumeConfigMap) Validate() error { + if s == nil { + return nil + } + + return shared.WithErrors( + shared.PrefixResourceError("name", sharedv1.AsKubernetesResourceName(&s.Name).Validate()), + ) +} diff --git a/pkg/apis/deployment/v1/server_group_volume_mount.go b/pkg/apis/deployment/v1/server_group_volume_mount.go new file mode 100644 index 000000000..f1b4e7c7e --- /dev/null +++ b/pkg/apis/deployment/v1/server_group_volume_mount.go @@ -0,0 +1,74 @@ +// +// DISCLAIMER +// +// Copyright 2020 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 +// +// Author Adam Janikowski +// + +package v1 + +import ( + "fmt" + + "github.com/arangodb/kube-arangodb/pkg/apis/shared" + sharedv1 "github.com/arangodb/kube-arangodb/pkg/apis/shared/v1" + + core "k8s.io/api/core/v1" +) + +type ServerGroupSpecVolumeMounts []ServerGroupSpecVolumeMount + +func (s ServerGroupSpecVolumeMounts) VolumeMounts() []core.VolumeMount { + mounts := make([]core.VolumeMount, len(s)) + + for id, mount := range s { + mounts[id] = mount.VolumeMount() + } + + return mounts +} + +func (s ServerGroupSpecVolumeMounts) Validate() error { + if s == nil { + return nil + } + + validateErrors := make([]error, len(s)) + + for id, mount := range s { + validateErrors[id] = shared.PrefixResourceErrors(fmt.Sprintf("%d", id), mount.Validate()) + } + + return shared.WithErrors(validateErrors...) +} + +type ServerGroupSpecVolumeMount core.VolumeMount + +func (s ServerGroupSpecVolumeMount) VolumeMount() core.VolumeMount { + return core.VolumeMount(s) +} + +func (s *ServerGroupSpecVolumeMount) Validate() error { + if s == nil { + return nil + } + + return shared.WithErrors( + shared.PrefixResourceError("name", sharedv1.AsKubernetesResourceName(&s.Name).Validate()), + ) +} diff --git a/pkg/apis/deployment/v1/server_group_volume_test.go b/pkg/apis/deployment/v1/server_group_volume_test.go new file mode 100644 index 000000000..cc4466e5b --- /dev/null +++ b/pkg/apis/deployment/v1/server_group_volume_test.go @@ -0,0 +1,184 @@ +// +// DISCLAIMER +// +// Copyright 2020 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 +// +// Author Adam Janikowski +// + +package v1 + +import ( + "fmt" + "testing" + + "github.com/arangodb/kube-arangodb/pkg/apis/shared" + + "github.com/stretchr/testify/require" + core "k8s.io/api/core/v1" +) + +const ( + labelValidationError = "Validation of label failed: a DNS-1123 label must consist of lower case alphanumeric characters or '-', and must start and end with an alphanumeric character (e.g. 'my-name', or '123-abc', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?')" + invalidName = "-invalid" + validName = "valid" +) + +func Test_Volume_Validation(t *testing.T) { + cases := []struct { + name string + volumes ServerGroupSpecVolumes + fail bool + failedFields map[string]string + }{ + { + name: "Nil definition", + }, + { + name: "Invalid name", + + fail: true, + failedFields: map[string]string{ + "0.name": labelValidationError, + "0.secret.secretName": labelValidationError, + }, + + volumes: []ServerGroupSpecVolume{ + { + Name: invalidName, + Secret: &ServerGroupSpecVolumeSecret{ + SecretName: invalidName, + }, + }, + }, + }, + { + name: "Restricted name", + + fail: true, + failedFields: map[string]string{ + "": fmt.Sprintf("volume with name %s is restricted", restrictedVolumeNames[0]), + }, + + volumes: []ServerGroupSpecVolume{ + { + Name: restrictedVolumeNames[0], + Secret: &ServerGroupSpecVolumeSecret{ + SecretName: validName, + }, + }, + }, + }, + { + name: "Defined multiple sources", + + fail: true, + failedFields: map[string]string{ + "0": "only one option can be defined: secret or configMap", + }, + + volumes: []ServerGroupSpecVolume{ + { + Name: validName, + Secret: &ServerGroupSpecVolumeSecret{ + SecretName: validName, + }, + ConfigMap: &ServerGroupSpecVolumeConfigMap{ + LocalObjectReference: core.LocalObjectReference{ + Name: validName, + }, + }, + }, + }, + }, + { + name: "Defined multiple volumes with same name", + + fail: true, + failedFields: map[string]string{ + "": "volume with name valid defined more than once: 2", + }, + + volumes: []ServerGroupSpecVolume{ + { + Name: validName, + Secret: &ServerGroupSpecVolumeSecret{ + SecretName: validName, + }, + }, + { + Name: validName, + Secret: &ServerGroupSpecVolumeSecret{ + SecretName: validName, + }, + }, + }, + }, + { + name: "Defined multiple volumes", + + volumes: []ServerGroupSpecVolume{ + { + Name: validName, + Secret: &ServerGroupSpecVolumeSecret{ + SecretName: validName, + }, + }, + { + Name: "valid-2", + ConfigMap: &ServerGroupSpecVolumeConfigMap{ + LocalObjectReference: core.LocalObjectReference{ + Name: validName, + }, + }, + }, + }, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + err := c.volumes.Validate() + + if c.fail { + require.Error(t, err) + + mergedErr, ok := err.(shared.MergedErrors) + require.True(t, ok, "Is not MergedError type") + + require.Equal(t, len(mergedErr.Errors()), len(c.failedFields), "Count of expected fields and merged errors does not match") + + for _, fieldError := range mergedErr.Errors() { + resourceErr, ok := fieldError.(shared.ResourceError) + if !ok { + resourceErr = shared.ResourceError{ + Prefix: "", + Err: fieldError, + } + } + + errValue, ok := c.failedFields[resourceErr.Prefix] + require.True(t, ok, "unexpected prefix %s", resourceErr.Prefix) + + require.EqualError(t, resourceErr.Err, errValue) + } + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/pkg/apis/deployment/v1/zz_generated.deepcopy.go b/pkg/apis/deployment/v1/zz_generated.deepcopy.go index 13423fe5b..fc7d7ae56 100644 --- a/pkg/apis/deployment/v1/zz_generated.deepcopy.go +++ b/pkg/apis/deployment/v1/zz_generated.deepcopy.go @@ -972,6 +972,20 @@ func (in *ServerGroupSpec) DeepCopyInto(out *ServerGroupSpec) { *out = new(ServerGroupSpecSecurityContext) (*in).DeepCopyInto(*out) } + if in.Volumes != nil { + in, out := &in.Volumes, &out.Volumes + *out = make(ServerGroupSpecVolumes, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.VolumeMounts != nil { + in, out := &in.VolumeMounts, &out.VolumeMounts + *out = make(ServerGroupSpecVolumeMounts, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } @@ -1011,6 +1025,164 @@ func (in *ServerGroupSpecSecurityContext) DeepCopy() *ServerGroupSpecSecurityCon return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServerGroupSpecVolume) DeepCopyInto(out *ServerGroupSpecVolume) { + *out = *in + if in.Secret != nil { + in, out := &in.Secret, &out.Secret + *out = new(ServerGroupSpecVolumeSecret) + (*in).DeepCopyInto(*out) + } + if in.ConfigMap != nil { + in, out := &in.ConfigMap, &out.ConfigMap + *out = new(ServerGroupSpecVolumeConfigMap) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerGroupSpecVolume. +func (in *ServerGroupSpecVolume) DeepCopy() *ServerGroupSpecVolume { + if in == nil { + return nil + } + out := new(ServerGroupSpecVolume) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServerGroupSpecVolumeConfigMap) DeepCopyInto(out *ServerGroupSpecVolumeConfigMap) { + *out = *in + out.LocalObjectReference = in.LocalObjectReference + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]corev1.KeyToPath, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.DefaultMode != nil { + in, out := &in.DefaultMode, &out.DefaultMode + *out = new(int32) + **out = **in + } + if in.Optional != nil { + in, out := &in.Optional, &out.Optional + *out = new(bool) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerGroupSpecVolumeConfigMap. +func (in *ServerGroupSpecVolumeConfigMap) DeepCopy() *ServerGroupSpecVolumeConfigMap { + if in == nil { + return nil + } + out := new(ServerGroupSpecVolumeConfigMap) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServerGroupSpecVolumeMount) DeepCopyInto(out *ServerGroupSpecVolumeMount) { + *out = *in + if in.MountPropagation != nil { + in, out := &in.MountPropagation, &out.MountPropagation + *out = new(corev1.MountPropagationMode) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerGroupSpecVolumeMount. +func (in *ServerGroupSpecVolumeMount) DeepCopy() *ServerGroupSpecVolumeMount { + if in == nil { + return nil + } + out := new(ServerGroupSpecVolumeMount) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in ServerGroupSpecVolumeMounts) DeepCopyInto(out *ServerGroupSpecVolumeMounts) { + { + in := &in + *out = make(ServerGroupSpecVolumeMounts, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + return + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerGroupSpecVolumeMounts. +func (in ServerGroupSpecVolumeMounts) DeepCopy() ServerGroupSpecVolumeMounts { + if in == nil { + return nil + } + out := new(ServerGroupSpecVolumeMounts) + in.DeepCopyInto(out) + return *out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServerGroupSpecVolumeSecret) DeepCopyInto(out *ServerGroupSpecVolumeSecret) { + *out = *in + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]corev1.KeyToPath, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.DefaultMode != nil { + in, out := &in.DefaultMode, &out.DefaultMode + *out = new(int32) + **out = **in + } + if in.Optional != nil { + in, out := &in.Optional, &out.Optional + *out = new(bool) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerGroupSpecVolumeSecret. +func (in *ServerGroupSpecVolumeSecret) DeepCopy() *ServerGroupSpecVolumeSecret { + if in == nil { + return nil + } + out := new(ServerGroupSpecVolumeSecret) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in ServerGroupSpecVolumes) DeepCopyInto(out *ServerGroupSpecVolumes) { + { + in := &in + *out = make(ServerGroupSpecVolumes, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + return + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerGroupSpecVolumes. +func (in ServerGroupSpecVolumes) DeepCopy() ServerGroupSpecVolumes { + if in == nil { + return nil + } + out := new(ServerGroupSpecVolumes) + in.DeepCopyInto(out) + return *out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SyncAuthenticationSpec) DeepCopyInto(out *SyncAuthenticationSpec) { *out = *in diff --git a/pkg/apis/shared/errors.go b/pkg/apis/shared/errors.go new file mode 100644 index 000000000..2a347ed34 --- /dev/null +++ b/pkg/apis/shared/errors.go @@ -0,0 +1,144 @@ +// +// DISCLAIMER +// +// Copyright 2020 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 +// +// Author Adam Janikowski +// + +package shared + +import ( + "fmt" + "io" + "strings" +) + +type ResourceError struct { + Prefix string + Err error +} + +// Error return string representation of error +func (p ResourceError) Error() string { + return fmt.Sprintf("%s: %s", p.Prefix, p.Err.Error()) +} + +// Format formats error with verbs +func (p *ResourceError) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + if s.Flag('+') { + fmt.Fprintf(s, "%s\n", p.Error()) + fmt.Fprintf(s, "%+v", p.Err) + return + } + fallthrough + case 's': + io.WriteString(s, p.Error()) + case 'q': + fmt.Fprintf(s, "%q", p.Error()) + } +} + +// PrefixResourceError creates new prefixed error. If error is already prefixed then current key is appended +func PrefixResourceError(prefix string, err error) error { + if err == nil { + return nil + } + + switch e := err.(type) { + case ResourceError: + return ResourceError{ + Prefix: fmt.Sprintf("%s.%s", prefix, e.Prefix), + Err: e.Err, + } + default: + return ResourceError{ + Prefix: prefix, + Err: err, + } + } +} + +// PrefixResourceErrors creates new prefixed errors. If error is already prefixed then current key is appended +func PrefixResourceErrors(prefix string, errs ...error) error { + prefixed := make([]error, 0, len(errs)) + + for _, err := range errs { + switch errType := err.(type) { + case MergedErrors: + for _, subError := range errType.errors { + prefixed = append(prefixed, PrefixResourceError(prefix, subError)) + } + default: + prefixed = append(prefixed, PrefixResourceError(prefix, err)) + } + } + + return WithErrors(prefixed...) +} + +type MergedErrors struct { + errors []error +} + +func (m MergedErrors) Error() string { + errStrings := make([]string, 0, len(m.errors)) + + for _, err := range m.errors { + if err == nil { + continue + } + + errStrings = append(errStrings, err.Error()) + } + + return fmt.Sprintf("Received %d errors: %s", len(errStrings), strings.Join(errStrings, ", ")) +} + +func (m MergedErrors) Errors() []error { + return m.errors +} + +// WithErrors filter out nil errors +func WithErrors(errs ...error) error { + filteredErrs := make([]error, 0, len(errs)) + + for _, err := range errs { + if err == nil { + continue + } + + switch errType := err.(type) { + case MergedErrors: + for _, subError := range errType.errors { + filteredErrs = append(filteredErrs, subError) + } + default: + filteredErrs = append(filteredErrs, err) + } + } + + if len(filteredErrs) == 0 { + return nil + } + + return MergedErrors{ + errors: filteredErrs, + } +} diff --git a/pkg/apis/shared/v1/doc.go b/pkg/apis/shared/v1/doc.go new file mode 100644 index 000000000..9cb7634d9 --- /dev/null +++ b/pkg/apis/shared/v1/doc.go @@ -0,0 +1,24 @@ +// +// DISCLAIMER +// +// Copyright 2020 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 +// +// Author Ewout Prangsma +// + +// +k8s:deepcopy-gen=package +package v1 diff --git a/pkg/apis/shared/v1/resource.go b/pkg/apis/shared/v1/resource.go new file mode 100644 index 000000000..3ba498e48 --- /dev/null +++ b/pkg/apis/shared/v1/resource.go @@ -0,0 +1,112 @@ +// +// DISCLAIMER +// +// Copyright 2020 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 +// +// Author Adam Janikowski +// + +package v1 + +import ( + "strings" + + "github.com/pkg/errors" + "k8s.io/apimachinery/pkg/util/validation" +) + +// KubernetesResourceName define name of kubernetes resource including validation function +type KubernetesResourceName string + +// AsKubernetesResourceName formats string into AsKubernetesResourceName for validation purposes +func AsKubernetesResourceName(s *string) *KubernetesResourceName { + if s == nil { + return nil + } + + value := KubernetesResourceName(*s) + + return &value +} + +// StringP returns string pointer to resource name +func (n *KubernetesResourceName) StringP() *string { + if n == nil { + return nil + } + + value := string(*n) + + return &value +} + +// String returns string value of name +func (n *KubernetesResourceName) String() string { + value := n.StringP() + + if value == nil { + return "" + } + + return *value +} + +// Validate validate if name is valid kubernetes DNS_LABEL +func (n *KubernetesResourceName) Validate() error { + if n == nil { + return errors.Errorf("cannot be undefined") + } + + name := *n + + if name == "" { + return errors.Errorf("cannot be empty") + } + + if err := IsValidName(name.String()); err != nil { + return err + } + + return nil +} + +// Immutable verify if field changed +func (n *KubernetesResourceName) Immutable(o *KubernetesResourceName) error { + if o == nil && n == nil { + return nil + } + + if o == nil || n == nil { + return errors.Errorf("field is immutable") + } + + if *o != *n { + return errors.Errorf("field is immutable") + } + + return nil +} + +// IsValidName validate name to be a DNS_LABEL. +// More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names +func IsValidName(name string) error { + if res := validation.IsDNS1123Label(name); len(res) > 0 { + return errors.Errorf("Validation of label failed: %s", strings.Join(res, ", ")) + } + + return nil +} diff --git a/pkg/apis/shared/v1/zz_generated.deepcopy.go b/pkg/apis/shared/v1/zz_generated.deepcopy.go new file mode 100644 index 000000000..9b0392199 --- /dev/null +++ b/pkg/apis/shared/v1/zz_generated.deepcopy.go @@ -0,0 +1,25 @@ +// +build !ignore_autogenerated + +// +// DISCLAIMER +// +// Copyright 2020 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 +// + +// Code generated by deepcopy-gen. DO NOT EDIT. + +package v1 diff --git a/pkg/deployment/deployment_test.go b/pkg/deployment/deployment_core_test.go similarity index 53% rename from pkg/deployment/deployment_test.go rename to pkg/deployment/deployment_core_test.go index cafd9fe13..fed6859f5 100644 --- a/pkg/deployment/deployment_test.go +++ b/pkg/deployment/deployment_core_test.go @@ -23,163 +23,21 @@ package deployment import ( - "fmt" - "io/ioutil" - "os" "testing" - "k8s.io/apimachinery/pkg/api/resource" - "github.com/arangodb/kube-arangodb/pkg/util/constants" - "github.com/pkg/errors" - - "github.com/arangodb/go-driver/jwt" - "github.com/arangodb/kube-arangodb/pkg/util/k8sutil" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "github.com/arangodb/kube-arangodb/pkg/util" - "github.com/rs/zerolog" - - "github.com/arangodb/kube-arangodb/pkg/deployment/resources" - - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes/fake" - recordfake "k8s.io/client-go/tools/record" - api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" - arangofake "github.com/arangodb/kube-arangodb/pkg/generated/clientset/versioned/fake" + core "k8s.io/api/core/v1" ) -const ( - testNamespace = "default" - testDeploymentName = "test" - testVersion = "3.5.2" - testImage = "arangodb/arangodb:" + testVersion - testCASecretName = "testCA" - testJWTSecretName = "testJWT" - testExporterToken = "testExporterToken" - testRocksDBEncryptionKey = "testRocksDB" - testPersistentVolumeClaimName = "testClaim" - testLicense = "testLicense" - testServiceAccountName = "testServiceAccountName" - testPriorityClassName = "testPriority" - testImageLifecycle = "arangodb/kube-arangodb:0.3.16" - testExporterImage = "arangodb/arangodb-exporter:0.1.6" - testImageAlpine = "alpine:3.7" - - testYes = "yes" -) - -type testCaseStruct struct { - Name string - ArangoDeployment *api.ArangoDeployment - Helper func(*testing.T, *Deployment, *testCaseStruct) - config Config - ExpectedError error - ExpectedEvent string - ExpectedPod v1.Pod -} - -func TestEnsurePods(t *testing.T) { - // Arange - defaultAgentTerminationTimeout := int64(api.ServerGroupAgents.DefaultTerminationGracePeriod().Seconds()) - defaultDBServerTerminationTimeout := int64(api.ServerGroupDBServers.DefaultTerminationGracePeriod().Seconds()) - defaultCoordinatorTerminationTimeout := int64(api.ServerGroupCoordinators.DefaultTerminationGracePeriod().Seconds()) - defaultSingleTerminationTimeout := int64(api.ServerGroupSingle.DefaultTerminationGracePeriod().Seconds()) - defaultSyncMasterTerminationTimeout := int64(api.ServerGroupSyncMasters.DefaultTerminationGracePeriod().Seconds()) - defaultSyncWorkerTerminationTimeout := int64(api.ServerGroupSyncWorkers.DefaultTerminationGracePeriod().Seconds()) - - var securityContext api.ServerGroupSpecSecurityContext - - nodeSelectorTest := map[string]string{ - "test": "test", - } - - firstAgentStatus := api.MemberStatus{ - ID: "agent1", - Phase: api.MemberPhaseNone, - } - - firstCoordinatorStatus := api.MemberStatus{ - ID: "coordinator1", - Phase: api.MemberPhaseNone, - } - - singleStatus := api.MemberStatus{ - ID: "single1", - Phase: api.MemberPhaseNone, - } - - firstSyncMaster := api.MemberStatus{ - ID: "syncMaster1", - Phase: api.MemberPhaseNone, - } - - firstSyncWorker := api.MemberStatus{ - ID: "syncWorker1", - Phase: api.MemberPhaseNone, - } - - firstDBServerStatus := api.MemberStatus{ - ID: "DBserver1", - Phase: api.MemberPhaseNone, - } - - noAuthentication := api.AuthenticationSpec{ - JWTSecretName: util.NewString(api.JWTSecretNameDisabled), - } - - noTLS := api.TLSSpec{ - CASecretName: util.NewString(api.CASecretNameDisabled), - } - - authenticationSpec := api.AuthenticationSpec{ - JWTSecretName: util.NewString(testJWTSecretName), - } - tlsSpec := api.TLSSpec{ - CASecretName: util.NewString(testCASecretName), - } - - rocksDBSpec := api.RocksDBSpec{ - Encryption: api.RocksDBEncryptionSpec{ - KeySecretName: util.NewString(testRocksDBEncryptionKey), - }, - } - - metricsSpec := api.MetricsSpec{ - Enabled: util.NewBool(true), - Image: util.NewString(testExporterImage), - Authentication: api.MetricsAuthenticationSpec{ - JWTTokenSecretName: util.NewString(testExporterToken), - }, - } - - resourcesUnfiltered := v1.ResourceRequirements{ - Limits: v1.ResourceList{ - v1.ResourceCPU: resource.MustParse("500m"), - v1.ResourceMemory: resource.MustParse("2Gi"), - }, - Requests: v1.ResourceList{ - v1.ResourceCPU: resource.MustParse("100m"), - v1.ResourceMemory: resource.MustParse("1Gi"), - }, - } - - emptyResources := v1.ResourceRequirements{ - Limits: make(v1.ResourceList), - Requests: make(v1.ResourceList), - } - - sidecarName1 := "sidecar1" - sidecarName2 := "sidecar2" - +func TestEnsurePod_ArangoDB_Core(t *testing.T) { testCases := []testCaseStruct{ { Name: "Agent Pod with image pull policy", @@ -188,7 +46,7 @@ func TestEnsurePods(t *testing.T) { Image: util.NewString(testImage), Authentication: noAuthentication, TLS: noTLS, - ImagePullPolicy: util.NewPullPolicy(v1.PullAlways), + ImagePullPolicy: util.NewPullPolicy(core.PullAlways), }, }, Helper: func(t *testing.T, deployment *Deployment, testCase *testCaseStruct) { @@ -203,27 +61,78 @@ func TestEnsurePods(t *testing.T) { testCase.createTestPodData(deployment, api.ServerGroupAgents, firstAgentStatus) }, ExpectedEvent: "member agent is created", - ExpectedPod: v1.Pod{ - Spec: v1.PodSpec{ - Volumes: []v1.Volume{ + ExpectedPod: core.Pod{ + Spec: core.PodSpec{ + Volumes: []core.Volume{ k8sutil.CreateVolumeEmptyDir(k8sutil.ArangodVolumeName), }, - Containers: []v1.Container{ + Containers: []core.Container{ { Name: k8sutil.ServerContainerName, Image: testImage, Command: createTestCommandForAgent(firstAgentStatus.ID, false, false, false), Ports: createTestPorts(), - VolumeMounts: []v1.VolumeMount{ + VolumeMounts: []core.VolumeMount{ k8sutil.ArangodVolumeMount(), }, Resources: emptyResources, LivenessProbe: createTestLivenessProbe(false, "", k8sutil.ArangoPort), - ImagePullPolicy: v1.PullAlways, + ImagePullPolicy: core.PullAlways, SecurityContext: securityContext.NewSecurityContext(), }, }, - RestartPolicy: v1.RestartPolicyNever, + RestartPolicy: core.RestartPolicyNever, + TerminationGracePeriodSeconds: &defaultAgentTerminationTimeout, + Hostname: testDeploymentName + "-" + api.ServerGroupAgentsString + "-" + firstAgentStatus.ID, + Subdomain: testDeploymentName + "-int", + Affinity: k8sutil.CreateAffinity(testDeploymentName, api.ServerGroupAgentsString, + false, ""), + }, + }, + }, + { + Name: "Agent Pod with image pull policy", + ArangoDeployment: &api.ArangoDeployment{ + Spec: api.DeploymentSpec{ + Image: util.NewString(testImage), + Authentication: noAuthentication, + TLS: noTLS, + ImagePullPolicy: util.NewPullPolicy(core.PullAlways), + }, + }, + Helper: func(t *testing.T, deployment *Deployment, testCase *testCaseStruct) { + deployment.status.last = api.DeploymentStatus{ + Members: api.DeploymentStatusMembers{ + Agents: api.MemberStatusList{ + firstAgentStatus, + }, + }, + Images: createTestImages(false), + } + testCase.createTestPodData(deployment, api.ServerGroupAgents, firstAgentStatus) + }, + ExpectedEvent: "member agent is created", + ExpectedPod: core.Pod{ + Spec: core.PodSpec{ + Volumes: []core.Volume{ + k8sutil.CreateVolumeEmptyDir(k8sutil.ArangodVolumeName), + }, + Containers: []core.Container{ + { + Name: k8sutil.ServerContainerName, + Image: testImage, + Command: createTestCommandForAgent(firstAgentStatus.ID, false, false, false), + Ports: createTestPorts(), + VolumeMounts: []core.VolumeMount{ + k8sutil.ArangodVolumeMount(), + }, + Resources: emptyResources, + LivenessProbe: createTestLivenessProbe(false, "", k8sutil.ArangoPort), + ImagePullPolicy: core.PullAlways, + SecurityContext: securityContext.NewSecurityContext(), + }, + }, + RestartPolicy: core.RestartPolicyNever, TerminationGracePeriodSeconds: &defaultAgentTerminationTimeout, Hostname: testDeploymentName + "-" + api.ServerGroupAgentsString + "-" + firstAgentStatus.ID, Subdomain: testDeploymentName + "-int", @@ -240,7 +149,7 @@ func TestEnsurePods(t *testing.T) { Authentication: noAuthentication, TLS: noTLS, Agents: api.ServerGroupSpec{ - Sidecars: []v1.Container{ + Sidecars: []core.Container{ { Name: sidecarName1, }, @@ -263,23 +172,23 @@ func TestEnsurePods(t *testing.T) { testCase.createTestPodData(deployment, api.ServerGroupAgents, firstAgentStatus) }, ExpectedEvent: "member agent is created", - ExpectedPod: v1.Pod{ - Spec: v1.PodSpec{ - Volumes: []v1.Volume{ + ExpectedPod: core.Pod{ + Spec: core.PodSpec{ + Volumes: []core.Volume{ k8sutil.CreateVolumeEmptyDir(k8sutil.ArangodVolumeName), }, - Containers: []v1.Container{ + Containers: []core.Container{ { Name: k8sutil.ServerContainerName, Image: testImage, Command: createTestCommandForAgent(firstAgentStatus.ID, false, false, false), Ports: createTestPorts(), - VolumeMounts: []v1.VolumeMount{ + VolumeMounts: []core.VolumeMount{ k8sutil.ArangodVolumeMount(), }, Resources: emptyResources, LivenessProbe: createTestLivenessProbe(false, "", k8sutil.ArangoPort), - ImagePullPolicy: v1.PullIfNotPresent, + ImagePullPolicy: core.PullIfNotPresent, SecurityContext: securityContext.NewSecurityContext(), }, { @@ -289,7 +198,7 @@ func TestEnsurePods(t *testing.T) { Name: sidecarName2, }, }, - RestartPolicy: v1.RestartPolicyNever, + RestartPolicy: core.RestartPolicyNever, TerminationGracePeriodSeconds: &defaultAgentTerminationTimeout, Hostname: testDeploymentName + "-" + api.ServerGroupAgentsString + "-" + firstAgentStatus.ID, Subdomain: testDeploymentName + "-int", @@ -320,27 +229,27 @@ func TestEnsurePods(t *testing.T) { testCase.createTestPodData(deployment, api.ServerGroupAgents, firstAgentStatus) }, ExpectedEvent: "member agent is created", - ExpectedPod: v1.Pod{ - Spec: v1.PodSpec{ - Volumes: []v1.Volume{ + ExpectedPod: core.Pod{ + Spec: core.PodSpec{ + Volumes: []core.Volume{ k8sutil.CreateVolumeEmptyDir(k8sutil.ArangodVolumeName), }, - Containers: []v1.Container{ + Containers: []core.Container{ { Name: k8sutil.ServerContainerName, Image: testImage, Command: createTestCommandForAgent(firstAgentStatus.ID, false, false, false), Ports: createTestPorts(), - VolumeMounts: []v1.VolumeMount{ + VolumeMounts: []core.VolumeMount{ k8sutil.ArangodVolumeMount(), }, Resources: emptyResources, LivenessProbe: createTestLivenessProbe(false, "", k8sutil.ArangoPort), - ImagePullPolicy: v1.PullIfNotPresent, + ImagePullPolicy: core.PullIfNotPresent, SecurityContext: securityContext.NewSecurityContext(), }, }, - ImagePullSecrets: []v1.LocalObjectReference{ + ImagePullSecrets: []core.LocalObjectReference{ { Name: "docker-registry", }, @@ -348,7 +257,7 @@ func TestEnsurePods(t *testing.T) { Name: "other-registry", }, }, - RestartPolicy: v1.RestartPolicyNever, + RestartPolicy: core.RestartPolicyNever, TerminationGracePeriodSeconds: &defaultAgentTerminationTimeout, Hostname: testDeploymentName + "-" + api.ServerGroupAgentsString + "-" + firstAgentStatus.ID, Subdomain: testDeploymentName + "-int", @@ -382,30 +291,30 @@ func TestEnsurePods(t *testing.T) { testCase.createTestPodData(deployment, api.ServerGroupAgents, firstAgentStatus) }, ExpectedEvent: "member agent is created", - ExpectedPod: v1.Pod{ - Spec: v1.PodSpec{ - Volumes: []v1.Volume{ + ExpectedPod: core.Pod{ + Spec: core.PodSpec{ + Volumes: []core.Volume{ k8sutil.CreateVolumeEmptyDir(k8sutil.ArangodVolumeName), }, - InitContainers: []v1.Container{ + InitContainers: []core.Container{ createTestAlpineContainer(firstAgentStatus.ID, false), }, - Containers: []v1.Container{ + Containers: []core.Container{ { Name: k8sutil.ServerContainerName, Image: testImage, Command: createTestCommandForAgent(firstAgentStatus.ID, false, false, false), Ports: createTestPorts(), - VolumeMounts: []v1.VolumeMount{ + VolumeMounts: []core.VolumeMount{ k8sutil.ArangodVolumeMount(), }, Resources: emptyResources, LivenessProbe: createTestLivenessProbe(false, "", k8sutil.ArangoPort), - ImagePullPolicy: v1.PullIfNotPresent, + ImagePullPolicy: core.PullIfNotPresent, SecurityContext: securityContext.NewSecurityContext(), }, }, - RestartPolicy: v1.RestartPolicyNever, + RestartPolicy: core.RestartPolicyNever, TerminationGracePeriodSeconds: &defaultAgentTerminationTimeout, Hostname: testDeploymentName + "-" + api.ServerGroupAgentsString + "-" + firstAgentStatus.ID, Subdomain: testDeploymentName + "-int", @@ -440,27 +349,83 @@ func TestEnsurePods(t *testing.T) { testCase.createTestPodData(deployment, api.ServerGroupDBServers, firstDBServerStatus) }, ExpectedEvent: "member dbserver is created", - ExpectedPod: v1.Pod{ - Spec: v1.PodSpec{ - Volumes: []v1.Volume{ + ExpectedPod: core.Pod{ + Spec: core.PodSpec{ + Volumes: []core.Volume{ k8sutil.CreateVolumeEmptyDir(k8sutil.ArangodVolumeName), }, - Containers: []v1.Container{ + Containers: []core.Container{ { Name: k8sutil.ServerContainerName, Image: testImage, Command: createTestCommandForDBServer(firstDBServerStatus.ID, false, false, false), Ports: createTestPorts(), Resources: k8sutil.ExtractPodResourceRequirement(resourcesUnfiltered), - VolumeMounts: []v1.VolumeMount{ + VolumeMounts: []core.VolumeMount{ k8sutil.ArangodVolumeMount(), }, LivenessProbe: createTestLivenessProbe(false, "", k8sutil.ArangoPort), - ImagePullPolicy: v1.PullIfNotPresent, + ImagePullPolicy: core.PullIfNotPresent, SecurityContext: securityContext.NewSecurityContext(), }, }, - RestartPolicy: v1.RestartPolicyNever, + RestartPolicy: core.RestartPolicyNever, + TerminationGracePeriodSeconds: &defaultDBServerTerminationTimeout, + Hostname: testDeploymentName + "-" + api.ServerGroupDBServersString + "-" + + firstDBServerStatus.ID, + Subdomain: testDeploymentName + "-int", + Affinity: k8sutil.CreateAffinity(testDeploymentName, api.ServerGroupDBServersString, + false, ""), + }, + }, + }, + { + Name: "DBserver POD with resource requirements and memory override", + ArangoDeployment: &api.ArangoDeployment{ + Spec: api.DeploymentSpec{ + Image: util.NewString(testImage), + Authentication: noAuthentication, + TLS: noTLS, + DBServers: api.ServerGroupSpec{ + Resources: resourcesUnfiltered, + }, + }, + }, + Helper: func(t *testing.T, deployment *Deployment, testCase *testCaseStruct) { + deployment.status.last = api.DeploymentStatus{ + Members: api.DeploymentStatusMembers{ + DBServers: api.MemberStatusList{ + firstDBServerStatus, + }, + }, + Images: createTestImages(false), + } + deployment.status.last.Members.DBServers[0].IsInitialized = true + + testCase.createTestPodData(deployment, api.ServerGroupDBServers, firstDBServerStatus) + }, + ExpectedEvent: "member dbserver is created", + ExpectedPod: core.Pod{ + Spec: core.PodSpec{ + Volumes: []core.Volume{ + k8sutil.CreateVolumeEmptyDir(k8sutil.ArangodVolumeName), + }, + Containers: []core.Container{ + { + Name: k8sutil.ServerContainerName, + Image: testImage, + Command: createTestCommandForDBServer(firstDBServerStatus.ID, false, false, false), + Ports: createTestPorts(), + Resources: k8sutil.ExtractPodResourceRequirement(resourcesUnfiltered), + VolumeMounts: []core.VolumeMount{ + k8sutil.ArangodVolumeMount(), + }, + LivenessProbe: createTestLivenessProbe(false, "", k8sutil.ArangoPort), + ImagePullPolicy: core.PullIfNotPresent, + SecurityContext: securityContext.NewSecurityContext(), + }, + }, + RestartPolicy: core.RestartPolicyNever, TerminationGracePeriodSeconds: &defaultDBServerTerminationTimeout, Hostname: testDeploymentName + "-" + api.ServerGroupDBServersString + "-" + firstDBServerStatus.ID, @@ -496,28 +461,28 @@ func TestEnsurePods(t *testing.T) { testCase.createTestPodData(deployment, api.ServerGroupDBServers, firstDBServerStatus) }, ExpectedEvent: "member dbserver is created", - ExpectedPod: v1.Pod{ - Spec: v1.PodSpec{ - Volumes: []v1.Volume{ + ExpectedPod: core.Pod{ + Spec: core.PodSpec{ + Volumes: []core.Volume{ k8sutil.CreateVolumeWithPersitantVolumeClaim(k8sutil.ArangodVolumeName, testPersistentVolumeClaimName), }, - Containers: []v1.Container{ + Containers: []core.Container{ { Name: k8sutil.ServerContainerName, Image: testImage, Command: createTestCommandForDBServer(firstDBServerStatus.ID, false, false, false), Ports: createTestPorts(), Resources: k8sutil.ExtractPodResourceRequirement(resourcesUnfiltered), - VolumeMounts: []v1.VolumeMount{ + VolumeMounts: []core.VolumeMount{ k8sutil.ArangodVolumeMount(), }, LivenessProbe: createTestLivenessProbe(false, "", k8sutil.ArangoPort), - ImagePullPolicy: v1.PullIfNotPresent, + ImagePullPolicy: core.PullIfNotPresent, SecurityContext: securityContext.NewSecurityContext(), }, }, - RestartPolicy: v1.RestartPolicyNever, + RestartPolicy: core.RestartPolicyNever, TerminationGracePeriodSeconds: &defaultDBServerTerminationTimeout, Hostname: testDeploymentName + "-" + api.ServerGroupDBServersString + "-" + firstDBServerStatus.ID, @@ -553,30 +518,30 @@ func TestEnsurePods(t *testing.T) { testCase.createTestPodData(deployment, api.ServerGroupDBServers, firstDBServerStatus) }, ExpectedEvent: "member dbserver is created", - ExpectedPod: v1.Pod{ - Spec: v1.PodSpec{ - Volumes: []v1.Volume{ + ExpectedPod: core.Pod{ + Spec: core.PodSpec{ + Volumes: []core.Volume{ k8sutil.CreateVolumeEmptyDir(k8sutil.ArangodVolumeName), }, - InitContainers: []v1.Container{ + InitContainers: []core.Container{ createTestAlpineContainer(firstDBServerStatus.ID, true), }, - Containers: []v1.Container{ + Containers: []core.Container{ { Name: k8sutil.ServerContainerName, Image: testImage, Command: createTestCommandForDBServer(firstDBServerStatus.ID, false, false, false), Ports: createTestPorts(), - VolumeMounts: []v1.VolumeMount{ + VolumeMounts: []core.VolumeMount{ k8sutil.ArangodVolumeMount(), }, Resources: emptyResources, LivenessProbe: createTestLivenessProbe(false, "", k8sutil.ArangoPort), - ImagePullPolicy: v1.PullIfNotPresent, + ImagePullPolicy: core.PullIfNotPresent, SecurityContext: securityContext.NewSecurityContext(), }, }, - RestartPolicy: v1.RestartPolicyNever, + RestartPolicy: core.RestartPolicyNever, TerminationGracePeriodSeconds: &defaultDBServerTerminationTimeout, Hostname: testDeploymentName + "-" + api.ServerGroupDBServersString + "-" + firstDBServerStatus.ID, @@ -608,27 +573,27 @@ func TestEnsurePods(t *testing.T) { testCase.createTestPodData(deployment, api.ServerGroupAgents, firstAgentStatus) }, ExpectedEvent: "member agent is created", - ExpectedPod: v1.Pod{ - Spec: v1.PodSpec{ - Volumes: []v1.Volume{ + ExpectedPod: core.Pod{ + Spec: core.PodSpec{ + Volumes: []core.Volume{ k8sutil.CreateVolumeEmptyDir(k8sutil.ArangodVolumeName), }, - Containers: []v1.Container{ + Containers: []core.Container{ { Name: k8sutil.ServerContainerName, Image: testImage, Command: createTestCommandForAgent(firstAgentStatus.ID, false, false, false), Ports: createTestPorts(), - VolumeMounts: []v1.VolumeMount{ + VolumeMounts: []core.VolumeMount{ k8sutil.ArangodVolumeMount(), }, Resources: emptyResources, LivenessProbe: createTestLivenessProbe(false, "", k8sutil.ArangoPort), - ImagePullPolicy: v1.PullIfNotPresent, + ImagePullPolicy: core.PullIfNotPresent, SecurityContext: securityContext.NewSecurityContext(), }, }, - RestartPolicy: v1.RestartPolicyNever, + RestartPolicy: core.RestartPolicyNever, TerminationGracePeriodSeconds: &defaultAgentTerminationTimeout, Hostname: testDeploymentName + "-" + api.ServerGroupAgentsString + "-" + firstAgentStatus.ID, Subdomain: testDeploymentName + "-int", @@ -662,28 +627,28 @@ func TestEnsurePods(t *testing.T) { testCase.createTestPodData(deployment, api.ServerGroupAgents, firstAgentStatus) }, ExpectedEvent: "member agent is created", - ExpectedPod: v1.Pod{ - Spec: v1.PodSpec{ - Volumes: []v1.Volume{ + ExpectedPod: core.Pod{ + Spec: core.PodSpec{ + Volumes: []core.Volume{ k8sutil.CreateVolumeWithPersitantVolumeClaim(k8sutil.ArangodVolumeName, testPersistentVolumeClaimName), }, - Containers: []v1.Container{ + Containers: []core.Container{ { Name: k8sutil.ServerContainerName, Image: testImage, Command: createTestCommandForAgent(firstAgentStatus.ID, false, false, false), Ports: createTestPorts(), - VolumeMounts: []v1.VolumeMount{ + VolumeMounts: []core.VolumeMount{ k8sutil.ArangodVolumeMount(), }, Resources: emptyResources, LivenessProbe: createTestLivenessProbe(false, "", k8sutil.ArangoPort), - ImagePullPolicy: v1.PullIfNotPresent, + ImagePullPolicy: core.PullIfNotPresent, SecurityContext: securityContext.NewSecurityContext(), }, }, - RestartPolicy: v1.RestartPolicyNever, + RestartPolicy: core.RestartPolicyNever, TerminationGracePeriodSeconds: &defaultAgentTerminationTimeout, Hostname: testDeploymentName + "-" + api.ServerGroupAgentsString + "-" + firstAgentStatus.ID, Subdomain: testDeploymentName + "-int", @@ -714,29 +679,29 @@ func TestEnsurePods(t *testing.T) { testCase.createTestPodData(deployment, api.ServerGroupAgents, firstAgentStatus) }, ExpectedEvent: "member agent is created", - ExpectedPod: v1.Pod{ - Spec: v1.PodSpec{ - Volumes: []v1.Volume{ + ExpectedPod: core.Pod{ + Spec: core.PodSpec{ + Volumes: []core.Volume{ k8sutil.CreateVolumeEmptyDir(k8sutil.ArangodVolumeName), createTestTLSVolume(api.ServerGroupAgentsString, firstAgentStatus.ID), }, - Containers: []v1.Container{ + Containers: []core.Container{ { Name: k8sutil.ServerContainerName, Image: testImage, Command: createTestCommandForAgent(firstAgentStatus.ID, true, false, false), Ports: createTestPorts(), - VolumeMounts: []v1.VolumeMount{ + VolumeMounts: []core.VolumeMount{ k8sutil.ArangodVolumeMount(), k8sutil.TlsKeyfileVolumeMount(), }, Resources: emptyResources, LivenessProbe: createTestLivenessProbe(true, "", k8sutil.ArangoPort), - ImagePullPolicy: v1.PullIfNotPresent, + ImagePullPolicy: core.PullIfNotPresent, SecurityContext: securityContext.NewSecurityContext(), }, }, - RestartPolicy: v1.RestartPolicyNever, + RestartPolicy: core.RestartPolicyNever, TerminationGracePeriodSeconds: &defaultAgentTerminationTimeout, Hostname: testDeploymentName + "-" + api.ServerGroupAgentsString + "-" + firstAgentStatus.ID, Subdomain: testDeploymentName + "-int", @@ -774,28 +739,28 @@ func TestEnsurePods(t *testing.T) { authorization, k8sutil.ArangoPort) }, ExpectedEvent: "member agent is created", - ExpectedPod: v1.Pod{ - Spec: v1.PodSpec{ - Volumes: []v1.Volume{ + ExpectedPod: core.Pod{ + Spec: core.PodSpec{ + Volumes: []core.Volume{ k8sutil.CreateVolumeEmptyDir(k8sutil.ArangodVolumeName), k8sutil.CreateVolumeWithSecret(k8sutil.ClusterJWTSecretVolumeName, testJWTSecretName), }, - Containers: []v1.Container{ + Containers: []core.Container{ { Name: k8sutil.ServerContainerName, Image: testImage, Command: createTestCommandForAgent(firstAgentStatus.ID, false, true, false), Ports: createTestPorts(), - VolumeMounts: []v1.VolumeMount{ + VolumeMounts: []core.VolumeMount{ k8sutil.ArangodVolumeMount(), k8sutil.ClusterJWTVolumeMount(), }, Resources: emptyResources, - ImagePullPolicy: v1.PullIfNotPresent, + ImagePullPolicy: core.PullIfNotPresent, SecurityContext: securityContext.NewSecurityContext(), }, }, - RestartPolicy: v1.RestartPolicyNever, + RestartPolicy: core.RestartPolicyNever, TerminationGracePeriodSeconds: &defaultAgentTerminationTimeout, Hostname: testDeploymentName + "-" + api.ServerGroupAgentsString + "-" + firstAgentStatus.ID, Subdomain: testDeploymentName + "-int", @@ -834,22 +799,22 @@ func TestEnsurePods(t *testing.T) { authorization, k8sutil.ArangoPort) }, ExpectedEvent: "member agent is created", - ExpectedPod: v1.Pod{ - Spec: v1.PodSpec{ - Volumes: []v1.Volume{ + ExpectedPod: core.Pod{ + Spec: core.PodSpec{ + Volumes: []core.Volume{ k8sutil.CreateVolumeEmptyDir(k8sutil.ArangodVolumeName), createTestTLSVolume(api.ServerGroupAgentsString, firstAgentStatus.ID), k8sutil.CreateVolumeWithSecret(k8sutil.ClusterJWTSecretVolumeName, testJWTSecretName), }, - Containers: []v1.Container{ + Containers: []core.Container{ { Name: k8sutil.ServerContainerName, Image: testImage, Command: createTestCommandForAgent(firstAgentStatus.ID, true, true, false), Ports: createTestPorts(), - ImagePullPolicy: v1.PullIfNotPresent, + ImagePullPolicy: core.PullIfNotPresent, SecurityContext: securityContext.NewSecurityContext(), - VolumeMounts: []v1.VolumeMount{ + VolumeMounts: []core.VolumeMount{ k8sutil.ArangodVolumeMount(), k8sutil.TlsKeyfileVolumeMount(), k8sutil.ClusterJWTVolumeMount(), @@ -857,7 +822,7 @@ func TestEnsurePods(t *testing.T) { Resources: emptyResources, }, }, - RestartPolicy: v1.RestartPolicyNever, + RestartPolicy: core.RestartPolicyNever, TerminationGracePeriodSeconds: &defaultAgentTerminationTimeout, Hostname: testDeploymentName + "-" + api.ServerGroupAgentsString + "-" + firstAgentStatus.ID, Subdomain: testDeploymentName + "-int", @@ -893,29 +858,29 @@ func TestEnsurePods(t *testing.T) { k8sutil.CreateEncryptionKeySecret(secrets, testRocksDBEncryptionKey, key) }, ExpectedEvent: "member agent is created", - ExpectedPod: v1.Pod{ - Spec: v1.PodSpec{ - Volumes: []v1.Volume{ + ExpectedPod: core.Pod{ + Spec: core.PodSpec{ + Volumes: []core.Volume{ k8sutil.CreateVolumeEmptyDir(k8sutil.ArangodVolumeName), k8sutil.CreateVolumeWithSecret(k8sutil.RocksdbEncryptionVolumeName, testRocksDBEncryptionKey), }, - Containers: []v1.Container{ + Containers: []core.Container{ { Name: k8sutil.ServerContainerName, Image: testImage, Command: createTestCommandForAgent(firstAgentStatus.ID, false, false, true), Ports: createTestPorts(), - VolumeMounts: []v1.VolumeMount{ + VolumeMounts: []core.VolumeMount{ k8sutil.ArangodVolumeMount(), k8sutil.RocksdbEncryptionVolumeMount(), }, Resources: emptyResources, LivenessProbe: createTestLivenessProbe(false, "", k8sutil.ArangoPort), - ImagePullPolicy: v1.PullIfNotPresent, + ImagePullPolicy: core.PullIfNotPresent, SecurityContext: securityContext.NewSecurityContext(), }, }, - RestartPolicy: v1.RestartPolicyNever, + RestartPolicy: core.RestartPolicyNever, TerminationGracePeriodSeconds: &defaultAgentTerminationTimeout, Hostname: testDeploymentName + "-" + api.ServerGroupAgentsString + "-" + firstAgentStatus.ID, Subdomain: testDeploymentName + "-int", @@ -947,27 +912,27 @@ func TestEnsurePods(t *testing.T) { testCase.createTestPodData(deployment, api.ServerGroupAgents, firstAgentStatus) }, ExpectedEvent: "member agent is created", - ExpectedPod: v1.Pod{ - Spec: v1.PodSpec{ - Volumes: []v1.Volume{ + ExpectedPod: core.Pod{ + Spec: core.PodSpec{ + Volumes: []core.Volume{ k8sutil.CreateVolumeEmptyDir(k8sutil.ArangodVolumeName), }, - Containers: []v1.Container{ + Containers: []core.Container{ { Name: k8sutil.ServerContainerName, Image: testImage, Command: createTestCommandForAgent(firstAgentStatus.ID, false, false, false), Ports: createTestPorts(), - VolumeMounts: []v1.VolumeMount{ + VolumeMounts: []core.VolumeMount{ k8sutil.ArangodVolumeMount(), }, Resources: emptyResources, LivenessProbe: createTestLivenessProbe(false, "", k8sutil.ArangoPort), - ImagePullPolicy: v1.PullIfNotPresent, + ImagePullPolicy: core.PullIfNotPresent, SecurityContext: securityContext.NewSecurityContext(), }, }, - RestartPolicy: v1.RestartPolicyNever, + RestartPolicy: core.RestartPolicyNever, TerminationGracePeriodSeconds: &defaultAgentTerminationTimeout, Hostname: testDeploymentName + "-" + api.ServerGroupAgentsString + "-" + firstAgentStatus.ID, Subdomain: testDeploymentName + "-int", @@ -1000,29 +965,29 @@ func TestEnsurePods(t *testing.T) { testCase.ExpectedPod.ObjectMeta.Labels[k8sutil.LabelKeyArangoExporter] = testYes }, ExpectedEvent: "member dbserver is created", - ExpectedPod: v1.Pod{ - Spec: v1.PodSpec{ - Volumes: []v1.Volume{ + ExpectedPod: core.Pod{ + Spec: core.PodSpec{ + Volumes: []core.Volume{ k8sutil.CreateVolumeEmptyDir(k8sutil.ArangodVolumeName), k8sutil.CreateVolumeWithSecret(k8sutil.ExporterJWTVolumeName, testExporterToken), }, - Containers: []v1.Container{ + Containers: []core.Container{ { Name: k8sutil.ServerContainerName, Image: testImage, Command: createTestCommandForDBServer(firstDBServerStatus.ID, false, false, false), Ports: createTestPorts(), - VolumeMounts: []v1.VolumeMount{ + VolumeMounts: []core.VolumeMount{ k8sutil.ArangodVolumeMount(), }, Resources: emptyResources, LivenessProbe: createTestLivenessProbe(false, "", k8sutil.ArangoPort), - ImagePullPolicy: v1.PullIfNotPresent, + ImagePullPolicy: core.PullIfNotPresent, SecurityContext: securityContext.NewSecurityContext(), }, testCreateExporterContainer(false, emptyResources), }, - RestartPolicy: v1.RestartPolicyNever, + RestartPolicy: core.RestartPolicyNever, TerminationGracePeriodSeconds: &defaultDBServerTerminationTimeout, Hostname: testDeploymentName + "-" + api.ServerGroupDBServersString + "-" + firstDBServerStatus.ID, Subdomain: testDeploymentName + "-int", @@ -1062,29 +1027,29 @@ func TestEnsurePods(t *testing.T) { testCase.ExpectedPod.ObjectMeta.Labels[k8sutil.LabelKeyArangoExporter] = testYes }, ExpectedEvent: "member dbserver is created", - ExpectedPod: v1.Pod{ - Spec: v1.PodSpec{ - Volumes: []v1.Volume{ + ExpectedPod: core.Pod{ + Spec: core.PodSpec{ + Volumes: []core.Volume{ k8sutil.CreateVolumeEmptyDir(k8sutil.ArangodVolumeName), k8sutil.CreateVolumeWithSecret(k8sutil.ExporterJWTVolumeName, testExporterToken), }, - Containers: []v1.Container{ + Containers: []core.Container{ { Name: k8sutil.ServerContainerName, Image: testImage, Command: createTestCommandForDBServer(firstDBServerStatus.ID, false, false, false), Ports: createTestPorts(), - VolumeMounts: []v1.VolumeMount{ + VolumeMounts: []core.VolumeMount{ k8sutil.ArangodVolumeMount(), }, Resources: emptyResources, LivenessProbe: createTestLivenessProbe(false, "", k8sutil.ArangoPort), - ImagePullPolicy: v1.PullIfNotPresent, + ImagePullPolicy: core.PullIfNotPresent, SecurityContext: securityContext.NewSecurityContext(), }, testCreateExporterContainer(false, k8sutil.ExtractPodResourceRequirement(resourcesUnfiltered)), }, - RestartPolicy: v1.RestartPolicyNever, + RestartPolicy: core.RestartPolicyNever, TerminationGracePeriodSeconds: &defaultDBServerTerminationTimeout, Hostname: testDeploymentName + "-" + api.ServerGroupDBServersString + "-" + firstDBServerStatus.ID, Subdomain: testDeploymentName + "-int", @@ -1123,41 +1088,41 @@ func TestEnsurePods(t *testing.T) { LifecycleImage: testImageLifecycle, }, ExpectedEvent: "member dbserver is created", - ExpectedPod: v1.Pod{ - Spec: v1.PodSpec{ - Volumes: []v1.Volume{ + ExpectedPod: core.Pod{ + Spec: core.PodSpec{ + Volumes: []core.Volume{ k8sutil.CreateVolumeEmptyDir(k8sutil.ArangodVolumeName), k8sutil.CreateVolumeWithSecret(k8sutil.ExporterJWTVolumeName, testExporterToken), k8sutil.LifecycleVolume(), }, - InitContainers: []v1.Container{ + InitContainers: []core.Container{ createTestLifecycleContainer(k8sutil.ExtractPodResourceRequirement(resourcesUnfiltered)), }, - Containers: []v1.Container{ + Containers: []core.Container{ { Name: k8sutil.ServerContainerName, Image: testImage, Command: createTestCommandForDBServer(firstDBServerStatus.ID, false, false, false), - Env: []v1.EnvVar{ + Env: []core.EnvVar{ k8sutil.CreateEnvFieldPath(constants.EnvOperatorPodName, "metadata.name"), k8sutil.CreateEnvFieldPath(constants.EnvOperatorPodNamespace, "metadata.namespace"), k8sutil.CreateEnvFieldPath(constants.EnvOperatorNodeName, "spec.nodeName"), k8sutil.CreateEnvFieldPath(constants.EnvOperatorNodeNameArango, "spec.nodeName"), }, Ports: createTestPorts(), - VolumeMounts: []v1.VolumeMount{ + VolumeMounts: []core.VolumeMount{ k8sutil.ArangodVolumeMount(), k8sutil.LifecycleVolumeMount(), }, Resources: emptyResources, Lifecycle: createTestLifecycle(), LivenessProbe: createTestLivenessProbe(false, "", k8sutil.ArangoPort), - ImagePullPolicy: v1.PullIfNotPresent, + ImagePullPolicy: core.PullIfNotPresent, SecurityContext: securityContext.NewSecurityContext(), }, testCreateExporterContainer(false, emptyResources), }, - RestartPolicy: v1.RestartPolicyNever, + RestartPolicy: core.RestartPolicyNever, TerminationGracePeriodSeconds: &defaultDBServerTerminationTimeout, Hostname: testDeploymentName + "-" + api.ServerGroupDBServersString + "-" + firstDBServerStatus.ID, Subdomain: testDeploymentName + "-int", @@ -1194,42 +1159,42 @@ func TestEnsurePods(t *testing.T) { AlpineImage: testImageAlpine, }, ExpectedEvent: "member dbserver is created", - ExpectedPod: v1.Pod{ - Spec: v1.PodSpec{ - Volumes: []v1.Volume{ + ExpectedPod: core.Pod{ + Spec: core.PodSpec{ + Volumes: []core.Volume{ k8sutil.CreateVolumeEmptyDir(k8sutil.ArangodVolumeName), k8sutil.CreateVolumeWithSecret(k8sutil.ExporterJWTVolumeName, testExporterToken), k8sutil.LifecycleVolume(), }, - InitContainers: []v1.Container{ + InitContainers: []core.Container{ createTestLifecycleContainer(emptyResources), createTestAlpineContainer(firstDBServerStatus.ID, false), }, - Containers: []v1.Container{ + Containers: []core.Container{ { Name: k8sutil.ServerContainerName, Image: testImage, Command: createTestCommandForDBServer(firstDBServerStatus.ID, false, false, false), - Env: []v1.EnvVar{ + Env: []core.EnvVar{ k8sutil.CreateEnvFieldPath(constants.EnvOperatorPodName, "metadata.name"), k8sutil.CreateEnvFieldPath(constants.EnvOperatorPodNamespace, "metadata.namespace"), k8sutil.CreateEnvFieldPath(constants.EnvOperatorNodeName, "spec.nodeName"), k8sutil.CreateEnvFieldPath(constants.EnvOperatorNodeNameArango, "spec.nodeName"), }, Ports: createTestPorts(), - VolumeMounts: []v1.VolumeMount{ + VolumeMounts: []core.VolumeMount{ k8sutil.ArangodVolumeMount(), k8sutil.LifecycleVolumeMount(), }, Resources: emptyResources, Lifecycle: createTestLifecycle(), LivenessProbe: createTestLivenessProbe(false, "", k8sutil.ArangoPort), - ImagePullPolicy: v1.PullIfNotPresent, + ImagePullPolicy: core.PullIfNotPresent, SecurityContext: securityContext.NewSecurityContext(), }, testCreateExporterContainer(false, emptyResources), }, - RestartPolicy: v1.RestartPolicyNever, + RestartPolicy: core.RestartPolicyNever, TerminationGracePeriodSeconds: &defaultDBServerTerminationTimeout, Hostname: testDeploymentName + "-" + api.ServerGroupDBServersString + "-" + firstDBServerStatus.ID, Subdomain: testDeploymentName + "-int", @@ -1282,9 +1247,9 @@ func TestEnsurePods(t *testing.T) { LifecycleImage: testImageLifecycle, }, ExpectedEvent: "member dbserver is created", - ExpectedPod: v1.Pod{ - Spec: v1.PodSpec{ - Volumes: []v1.Volume{ + ExpectedPod: core.Pod{ + Spec: core.PodSpec{ + Volumes: []core.Volume{ k8sutil.CreateVolumeEmptyDir(k8sutil.ArangodVolumeName), createTestTLSVolume(api.ServerGroupDBServersString, firstDBServerStatus.ID), k8sutil.CreateVolumeWithSecret(k8sutil.RocksdbEncryptionVolumeName, testRocksDBEncryptionKey), @@ -1292,15 +1257,15 @@ func TestEnsurePods(t *testing.T) { k8sutil.CreateVolumeWithSecret(k8sutil.ClusterJWTSecretVolumeName, testJWTSecretName), k8sutil.LifecycleVolume(), }, - InitContainers: []v1.Container{ + InitContainers: []core.Container{ createTestLifecycleContainer(emptyResources), }, - Containers: []v1.Container{ + Containers: []core.Container{ { Name: k8sutil.ServerContainerName, Image: testImage, Command: createTestCommandForDBServer(firstDBServerStatus.ID, true, true, true), - Env: []v1.EnvVar{ + Env: []core.EnvVar{ k8sutil.CreateEnvSecretKeySelector(constants.EnvArangoLicenseKey, testLicense, constants.SecretKeyToken), k8sutil.CreateEnvFieldPath(constants.EnvOperatorPodName, "metadata.name"), @@ -1311,9 +1276,9 @@ func TestEnsurePods(t *testing.T) { Ports: createTestPorts(), Lifecycle: createTestLifecycle(), LivenessProbe: createTestLivenessProbe(false, "", k8sutil.ArangoPort), - ImagePullPolicy: v1.PullIfNotPresent, + ImagePullPolicy: core.PullIfNotPresent, SecurityContext: securityContext.NewSecurityContext(), - VolumeMounts: []v1.VolumeMount{ + VolumeMounts: []core.VolumeMount{ k8sutil.ArangodVolumeMount(), k8sutil.LifecycleVolumeMount(), k8sutil.TlsKeyfileVolumeMount(), @@ -1324,7 +1289,7 @@ func TestEnsurePods(t *testing.T) { }, testCreateExporterContainer(true, emptyResources), }, - RestartPolicy: v1.RestartPolicyNever, + RestartPolicy: core.RestartPolicyNever, TerminationGracePeriodSeconds: &defaultDBServerTerminationTimeout, Hostname: testDeploymentName + "-" + api.ServerGroupDBServersString + "-" + firstDBServerStatus.ID, Subdomain: testDeploymentName + "-int", @@ -1363,30 +1328,30 @@ func TestEnsurePods(t *testing.T) { testCase.ExpectedPod.Spec.Containers[0].ReadinessProbe = createTestReadinessProbe(true, auth) }, ExpectedEvent: "member coordinator is created", - ExpectedPod: v1.Pod{ - Spec: v1.PodSpec{ - Volumes: []v1.Volume{ + ExpectedPod: core.Pod{ + Spec: core.PodSpec{ + Volumes: []core.Volume{ k8sutil.CreateVolumeEmptyDir(k8sutil.ArangodVolumeName), createTestTLSVolume(api.ServerGroupCoordinatorsString, firstCoordinatorStatus.ID), k8sutil.CreateVolumeWithSecret(k8sutil.ClusterJWTSecretVolumeName, testJWTSecretName), }, - Containers: []v1.Container{ + Containers: []core.Container{ { Name: k8sutil.ServerContainerName, Image: testImage, Command: createTestCommandForCoordinator(firstCoordinatorStatus.ID, true, true, false), Ports: createTestPorts(), - ImagePullPolicy: v1.PullIfNotPresent, + ImagePullPolicy: core.PullIfNotPresent, Resources: emptyResources, SecurityContext: securityContext.NewSecurityContext(), - VolumeMounts: []v1.VolumeMount{ + VolumeMounts: []core.VolumeMount{ k8sutil.ArangodVolumeMount(), k8sutil.TlsKeyfileVolumeMount(), k8sutil.ClusterJWTVolumeMount(), }, }, }, - RestartPolicy: v1.RestartPolicyNever, + RestartPolicy: core.RestartPolicyNever, TerminationGracePeriodSeconds: &defaultCoordinatorTerminationTimeout, Hostname: testDeploymentName + "-" + api.ServerGroupCoordinatorsString + "-" + firstCoordinatorStatus.ID, Subdomain: testDeploymentName + "-int", @@ -1429,22 +1394,22 @@ func TestEnsurePods(t *testing.T) { testCase.ExpectedPod.Spec.Containers[0].ReadinessProbe = createTestReadinessProbe(true, authReadiness) }, ExpectedEvent: "member single is created", - ExpectedPod: v1.Pod{ - Spec: v1.PodSpec{ - Volumes: []v1.Volume{ + ExpectedPod: core.Pod{ + Spec: core.PodSpec{ + Volumes: []core.Volume{ k8sutil.CreateVolumeEmptyDir(k8sutil.ArangodVolumeName), createTestTLSVolume(api.ServerGroupSingleString, singleStatus.ID), k8sutil.CreateVolumeWithSecret(k8sutil.ClusterJWTSecretVolumeName, testJWTSecretName), }, - Containers: []v1.Container{ + Containers: []core.Container{ { Name: k8sutil.ServerContainerName, Image: testImage, Command: createTestCommandForSingleMode(singleStatus.ID, true, true, false), Ports: createTestPorts(), - ImagePullPolicy: v1.PullIfNotPresent, + ImagePullPolicy: core.PullIfNotPresent, SecurityContext: securityContext.NewSecurityContext(), - VolumeMounts: []v1.VolumeMount{ + VolumeMounts: []core.VolumeMount{ k8sutil.ArangodVolumeMount(), k8sutil.TlsKeyfileVolumeMount(), k8sutil.ClusterJWTVolumeMount(), @@ -1452,7 +1417,7 @@ func TestEnsurePods(t *testing.T) { Resources: emptyResources, }, }, - RestartPolicy: v1.RestartPolicyNever, + RestartPolicy: core.RestartPolicyNever, TerminationGracePeriodSeconds: &defaultSingleTerminationTimeout, Hostname: testDeploymentName + "-" + api.ServerGroupSingleString + "-" + singleStatus.ID, Subdomain: testDeploymentName + "-int", @@ -1461,903 +1426,7 @@ func TestEnsurePods(t *testing.T) { }, }, }, - //ArangoD container - end - - // Arango sync master container - start - { - Name: "Sync Pod does not work for enterprise image", - ArangoDeployment: &api.ArangoDeployment{ - Spec: api.DeploymentSpec{ - Image: util.NewString(testImage), - }, - }, - Helper: func(t *testing.T, deployment *Deployment, testCase *testCaseStruct) { - deployment.status.last = api.DeploymentStatus{ - Members: api.DeploymentStatusMembers{ - SyncMasters: api.MemberStatusList{ - firstSyncMaster, - }, - }, - Images: createTestImages(false), - } - }, - ExpectedError: errors.New("Image '" + testImage + "' does not contain an Enterprise version of ArangoDB"), - }, - { - Name: "Sync Pod cannot get master JWT secret", - ArangoDeployment: &api.ArangoDeployment{ - Spec: api.DeploymentSpec{ - Image: util.NewString(testImage), - }, - }, - Helper: func(t *testing.T, deployment *Deployment, testCase *testCaseStruct) { - deployment.status.last = api.DeploymentStatus{ - Members: api.DeploymentStatusMembers{ - SyncMasters: api.MemberStatusList{ - firstSyncMaster, - }, - }, - Images: createTestImages(true), - } - }, - ExpectedError: errors.New("Master JWT secret validation failed: secrets \"" + - testDeploymentName + "-sync-jwt\" not found"), - }, - { - Name: "Sync Pod cannot get monitoring token secret", - ArangoDeployment: &api.ArangoDeployment{ - Spec: api.DeploymentSpec{ - Image: util.NewString(testImage), - Sync: api.SyncSpec{ - Enabled: util.NewBool(true), - }, - }, - }, - Helper: func(t *testing.T, deployment *Deployment, testCase *testCaseStruct) { - deployment.status.last = api.DeploymentStatus{ - Members: api.DeploymentStatusMembers{ - SyncMasters: api.MemberStatusList{ - firstSyncMaster, - }, - }, - Images: createTestImages(true), - } - - secretName := testCase.ArangoDeployment.Spec.Sync.Monitoring.GetTokenSecretName() - err := deployment.GetKubeCli().CoreV1().Secrets(testNamespace).Delete(secretName, &metav1.DeleteOptions{}) - require.NoError(t, err) - }, - ExpectedError: errors.New("Monitoring token secret validation failed: secrets \"" + - testDeploymentName + "-sync-mt\" not found"), - }, - { - Name: "Sync Master Pod cannot create TLS keyfile secret", - ArangoDeployment: &api.ArangoDeployment{ - Spec: api.DeploymentSpec{ - Image: util.NewString(testImage), - Sync: api.SyncSpec{ - Enabled: util.NewBool(true), - }, - }, - }, - Helper: func(t *testing.T, deployment *Deployment, testCase *testCaseStruct) { - deployment.status.last = api.DeploymentStatus{ - Members: api.DeploymentStatusMembers{ - SyncMasters: api.MemberStatusList{ - firstSyncMaster, - }, - }, - Images: createTestImages(true), - } - - secretName := testCase.ArangoDeployment.Spec.Sync.TLS.GetCASecretName() - err := deployment.GetKubeCli().CoreV1().Secrets(testNamespace).Delete(secretName, &metav1.DeleteOptions{}) - require.NoError(t, err) - }, - ExpectedError: errors.New("Failed to create TLS keyfile secret: secrets \"" + - testDeploymentName + "-sync-ca\" not found"), - }, - { - Name: "Sync Master Pod cannot get cluster JWT secret", - ArangoDeployment: &api.ArangoDeployment{ - Spec: api.DeploymentSpec{ - Image: util.NewString(testImage), - Authentication: authenticationSpec, - Sync: api.SyncSpec{ - Enabled: util.NewBool(true), - }, - }, - }, - Helper: func(t *testing.T, deployment *Deployment, testCase *testCaseStruct) { - deployment.status.last = api.DeploymentStatus{ - Members: api.DeploymentStatusMembers{ - SyncMasters: api.MemberStatusList{ - firstSyncMaster, - }, - }, - Images: createTestImages(true), - } - - secretName := testCase.ArangoDeployment.Spec.Authentication.GetJWTSecretName() - err := deployment.GetKubeCli().CoreV1().Secrets(testNamespace).Delete(secretName, &metav1.DeleteOptions{}) - require.NoError(t, err) - }, - ExpectedError: errors.New("Cluster JWT secret validation failed: secrets \"" + - testJWTSecretName + "\" not found"), - }, - { - Name: "Sync Master Pod cannot get authentication CA certificate", - ArangoDeployment: &api.ArangoDeployment{ - Spec: api.DeploymentSpec{ - Image: util.NewString(testImage), - Authentication: authenticationSpec, - Sync: api.SyncSpec{ - Enabled: util.NewBool(true), - }, - }, - }, - Helper: func(t *testing.T, deployment *Deployment, testCase *testCaseStruct) { - deployment.status.last = api.DeploymentStatus{ - Members: api.DeploymentStatusMembers{ - SyncMasters: api.MemberStatusList{ - firstSyncMaster, - }, - }, - Images: createTestImages(true), - } - - secretName := testCase.ArangoDeployment.Spec.Sync.Authentication.GetClientCASecretName() - err := deployment.GetKubeCli().CoreV1().Secrets(testNamespace).Delete(secretName, &metav1.DeleteOptions{}) - require.NoError(t, err) - }, - ExpectedError: errors.New("Client authentication CA certificate secret validation failed: " + - "secrets \"" + testDeploymentName + "-sync-client-auth-ca\" not found"), - }, - { - Name: "Sync Master Pod with authentication, monitoring, tls, service account, node selector, " + - "liveness probe, priority class name, resource requirements", - ArangoDeployment: &api.ArangoDeployment{ - Spec: api.DeploymentSpec{ - Image: util.NewString(testImage), - Authentication: authenticationSpec, - Sync: api.SyncSpec{ - Enabled: util.NewBool(true), - }, - SyncMasters: api.ServerGroupSpec{ - ServiceAccountName: util.NewString(testServiceAccountName), - NodeSelector: nodeSelectorTest, - PriorityClassName: testPriorityClassName, - Resources: resourcesUnfiltered, - }, - }, - }, - Helper: func(t *testing.T, deployment *Deployment, testCase *testCaseStruct) { - deployment.status.last = api.DeploymentStatus{ - Members: api.DeploymentStatusMembers{ - SyncMasters: api.MemberStatusList{ - firstSyncMaster, - }, - }, - Images: createTestImages(true), - } - - testCase.createTestPodData(deployment, api.ServerGroupSyncMasters, firstSyncMaster) - - name := testCase.ArangoDeployment.Spec.Sync.Monitoring.GetTokenSecretName() - auth, err := k8sutil.GetTokenSecret(deployment.GetKubeCli().CoreV1().Secrets(testNamespace), name) - require.NoError(t, err) - - testCase.ExpectedPod.Spec.Containers[0].LivenessProbe = createTestLivenessProbe( - true, "bearer "+auth, k8sutil.ArangoSyncMasterPort) - }, - ExpectedEvent: "member syncmaster is created", - ExpectedPod: v1.Pod{ - Spec: v1.PodSpec{ - Volumes: []v1.Volume{ - createTestTLSVolume(api.ServerGroupSyncMastersString, firstSyncMaster.ID), - k8sutil.CreateVolumeWithSecret(k8sutil.ClientAuthCAVolumeName, "test-sync-client-auth-ca"), - k8sutil.CreateVolumeWithSecret(k8sutil.MasterJWTSecretVolumeName, "test-sync-jwt"), - k8sutil.CreateVolumeWithSecret(k8sutil.ClusterJWTSecretVolumeName, testJWTSecretName), - }, - Containers: []v1.Container{ - { - Name: k8sutil.ServerContainerName, - Image: testImage, - Command: createTestCommandForSyncMaster(firstSyncMaster.ID, true, true, true), - Ports: createTestPorts(), - Env: []v1.EnvVar{ - k8sutil.CreateEnvSecretKeySelector(constants.EnvArangoSyncMonitoringToken, - testDeploymentName+"-sync-mt", constants.SecretKeyToken), - }, - ImagePullPolicy: v1.PullIfNotPresent, - Resources: resourcesUnfiltered, - SecurityContext: securityContext.NewSecurityContext(), - VolumeMounts: []v1.VolumeMount{ - k8sutil.TlsKeyfileVolumeMount(), - k8sutil.ClientAuthCACertificateVolumeMount(), - k8sutil.MasterJWTVolumeMount(), - k8sutil.ClusterJWTVolumeMount(), - }, - }, - }, - PriorityClassName: testPriorityClassName, - RestartPolicy: v1.RestartPolicyNever, - ServiceAccountName: testServiceAccountName, - NodeSelector: nodeSelectorTest, - TerminationGracePeriodSeconds: &defaultSyncMasterTerminationTimeout, - Hostname: testDeploymentName + "-" + api.ServerGroupSyncMastersString + "-" + - firstSyncMaster.ID, - Subdomain: testDeploymentName + "-int", - Affinity: k8sutil.CreateAffinity(testDeploymentName, api.ServerGroupSyncMastersString, - false, ""), - }, - }, - }, - { - Name: "Sync Master Pod with lifecycle, license, monitoring without authentication and alpine", - config: Config{ - LifecycleImage: testImageLifecycle, - AlpineImage: testImageAlpine, - }, - ArangoDeployment: &api.ArangoDeployment{ - Spec: api.DeploymentSpec{ - Image: util.NewString(testImage), - Authentication: noAuthentication, - Environment: api.NewEnvironment(api.EnvironmentProduction), - Sync: api.SyncSpec{ - Enabled: util.NewBool(true), - }, - License: api.LicenseSpec{ - SecretName: util.NewString(testLicense), - }, - }, - }, - Helper: func(t *testing.T, deployment *Deployment, testCase *testCaseStruct) { - deployment.status.last = api.DeploymentStatus{ - Members: api.DeploymentStatusMembers{ - SyncMasters: api.MemberStatusList{ - firstSyncMaster, - }, - }, - Images: createTestImages(true), - } - - testCase.createTestPodData(deployment, api.ServerGroupSyncMasters, firstSyncMaster) - name := testCase.ArangoDeployment.Spec.Sync.Monitoring.GetTokenSecretName() - auth, err := k8sutil.GetTokenSecret(deployment.GetKubeCli().CoreV1().Secrets(testNamespace), name) - require.NoError(t, err) - - testCase.ExpectedPod.Spec.Containers[0].LivenessProbe = createTestLivenessProbe( - true, "bearer "+auth, k8sutil.ArangoSyncMasterPort) - }, - ExpectedEvent: "member syncmaster is created", - ExpectedPod: v1.Pod{ - Spec: v1.PodSpec{ - Volumes: []v1.Volume{ - k8sutil.LifecycleVolume(), - createTestTLSVolume(api.ServerGroupSyncMastersString, firstSyncMaster.ID), - k8sutil.CreateVolumeWithSecret(k8sutil.ClientAuthCAVolumeName, - testDeploymentName+"-sync-client-auth-ca"), - k8sutil.CreateVolumeWithSecret(k8sutil.MasterJWTSecretVolumeName, - testDeploymentName+"-sync-jwt"), - }, - InitContainers: []v1.Container{ - createTestLifecycleContainer(emptyResources), - }, - Containers: []v1.Container{ - { - Name: k8sutil.ServerContainerName, - Image: testImage, - Command: createTestCommandForSyncMaster(firstSyncMaster.ID, true, false, true), - Ports: createTestPorts(), - Env: []v1.EnvVar{ - k8sutil.CreateEnvSecretKeySelector(constants.EnvArangoSyncMonitoringToken, - testDeploymentName+"-sync-mt", constants.SecretKeyToken), - k8sutil.CreateEnvSecretKeySelector(constants.EnvArangoLicenseKey, - testLicense, constants.SecretKeyToken), - k8sutil.CreateEnvFieldPath(constants.EnvOperatorPodName, "metadata.name"), - k8sutil.CreateEnvFieldPath(constants.EnvOperatorPodNamespace, "metadata.namespace"), - k8sutil.CreateEnvFieldPath(constants.EnvOperatorNodeName, "spec.nodeName"), - k8sutil.CreateEnvFieldPath(constants.EnvOperatorNodeNameArango, "spec.nodeName"), - }, - Resources: emptyResources, - ImagePullPolicy: v1.PullIfNotPresent, - Lifecycle: createTestLifecycle(), - SecurityContext: securityContext.NewSecurityContext(), - VolumeMounts: []v1.VolumeMount{ - k8sutil.LifecycleVolumeMount(), - k8sutil.TlsKeyfileVolumeMount(), - k8sutil.ClientAuthCACertificateVolumeMount(), - k8sutil.MasterJWTVolumeMount(), - }, - }, - }, - RestartPolicy: v1.RestartPolicyNever, - TerminationGracePeriodSeconds: &defaultSyncMasterTerminationTimeout, - Hostname: testDeploymentName + "-" + api.ServerGroupSyncMastersString + "-" + - firstSyncMaster.ID, - Subdomain: testDeploymentName + "-int", - Affinity: k8sutil.CreateAffinity(testDeploymentName, api.ServerGroupSyncMastersString, - true, ""), - }, - }, - }, - // Arango sync master container - end - - // Arango sync worker - start - { - Name: "Sync Worker Pod with monitoring, service account, node selector, lifecycle, license " + - "liveness probe, priority class name, resource requirements without alpine", - config: Config{ - LifecycleImage: testImageLifecycle, - AlpineImage: testImageAlpine, - }, - ArangoDeployment: &api.ArangoDeployment{ - Spec: api.DeploymentSpec{ - Image: util.NewString(testImage), - Authentication: noAuthentication, - Sync: api.SyncSpec{ - Enabled: util.NewBool(true), - }, - SyncWorkers: api.ServerGroupSpec{ - ServiceAccountName: util.NewString(testServiceAccountName), - NodeSelector: nodeSelectorTest, - PriorityClassName: testPriorityClassName, - Resources: resourcesUnfiltered, - }, - License: api.LicenseSpec{ - SecretName: util.NewString(testLicense), - }, - }, - }, - Helper: func(t *testing.T, deployment *Deployment, testCase *testCaseStruct) { - deployment.status.last = api.DeploymentStatus{ - Members: api.DeploymentStatusMembers{ - SyncWorkers: api.MemberStatusList{ - firstSyncWorker, - }, - }, - Images: createTestImages(true), - } - - testCase.createTestPodData(deployment, api.ServerGroupSyncWorkers, firstSyncWorker) - - name := testCase.ArangoDeployment.Spec.Sync.Monitoring.GetTokenSecretName() - auth, err := k8sutil.GetTokenSecret(deployment.GetKubeCli().CoreV1().Secrets(testNamespace), name) - require.NoError(t, err) - - testCase.ExpectedPod.Spec.Containers[0].LivenessProbe = createTestLivenessProbe( - true, "bearer "+auth, k8sutil.ArangoSyncWorkerPort) - }, - ExpectedEvent: "member syncworker is created", - ExpectedPod: v1.Pod{ - Spec: v1.PodSpec{ - Volumes: []v1.Volume{ - k8sutil.LifecycleVolume(), - k8sutil.CreateVolumeWithSecret(k8sutil.MasterJWTSecretVolumeName, testDeploymentName+"-sync-jwt"), - }, - InitContainers: []v1.Container{ - createTestLifecycleContainer(emptyResources), - }, - Containers: []v1.Container{ - { - Name: k8sutil.ServerContainerName, - Image: testImage, - Command: createTestCommandForSyncWorker(firstSyncWorker.ID, true, true), - Ports: createTestPorts(), - Env: []v1.EnvVar{ - k8sutil.CreateEnvSecretKeySelector(constants.EnvArangoSyncMonitoringToken, - testDeploymentName+"-sync-mt", constants.SecretKeyToken), - k8sutil.CreateEnvSecretKeySelector(constants.EnvArangoLicenseKey, - testLicense, constants.SecretKeyToken), - k8sutil.CreateEnvFieldPath(constants.EnvOperatorPodName, "metadata.name"), - k8sutil.CreateEnvFieldPath(constants.EnvOperatorPodNamespace, "metadata.namespace"), - k8sutil.CreateEnvFieldPath(constants.EnvOperatorNodeName, "spec.nodeName"), - k8sutil.CreateEnvFieldPath(constants.EnvOperatorNodeNameArango, "spec.nodeName"), - }, - Lifecycle: createTestLifecycle(), - ImagePullPolicy: v1.PullIfNotPresent, - Resources: k8sutil.ExtractPodResourceRequirement(resourcesUnfiltered), - SecurityContext: securityContext.NewSecurityContext(), - VolumeMounts: []v1.VolumeMount{ - k8sutil.LifecycleVolumeMount(), - k8sutil.MasterJWTVolumeMount(), - }, - }, - }, - PriorityClassName: testPriorityClassName, - RestartPolicy: v1.RestartPolicyNever, - ServiceAccountName: testServiceAccountName, - NodeSelector: nodeSelectorTest, - TerminationGracePeriodSeconds: &defaultSyncWorkerTerminationTimeout, - Hostname: testDeploymentName + "-" + api.ServerGroupSyncWorkersString + "-" + - firstSyncWorker.ID, - Subdomain: testDeploymentName + "-int", - Affinity: k8sutil.CreateAffinity(testDeploymentName, api.ServerGroupSyncWorkersString, - false, api.ServerGroupDBServersString), - }, - }, - }, - // Arango sync worker - end } - for _, testCase := range testCases { - //nolint:scopelint - t.Run(testCase.Name, func(t *testing.T) { - // Arrange - d, eventRecorder := createTestDeployment(testCase.config, testCase.ArangoDeployment) - - err := d.resources.EnsureSecrets() - require.NoError(t, err) - - if testCase.Helper != nil { - testCase.Helper(t, d, &testCase) - } - - // Create custom resource in the fake kubernetes API - _, err = d.deps.DatabaseCRCli.DatabaseV1().ArangoDeployments(testNamespace).Create(d.apiObject) - require.NoError(t, err) - - // Act - err = d.resources.EnsurePods() - - // Assert - if testCase.ExpectedError != nil { - - if !assert.EqualError(t, err, testCase.ExpectedError.Error()) { - println(fmt.Sprintf("%+v", err)) - } - return - } - - require.NoError(t, err) - pods, err := d.deps.KubeCli.CoreV1().Pods(testNamespace).List(metav1.ListOptions{}) - require.NoError(t, err) - require.Len(t, pods.Items, 1) - require.Equal(t, testCase.ExpectedPod.Spec, pods.Items[0].Spec) - require.Equal(t, testCase.ExpectedPod.ObjectMeta, pods.Items[0].ObjectMeta) - - if len(testCase.ExpectedEvent) > 0 { - select { - case msg := <-eventRecorder.Events: - assert.Contains(t, msg, testCase.ExpectedEvent) - default: - assert.Fail(t, "expected event", "expected event with message '%s'", testCase.ExpectedEvent) - } - - status, version := d.GetStatus() - assert.Equal(t, int32(1), version) - - checkEachMember := func(group api.ServerGroup, groupSpec api.ServerGroupSpec, status *api.MemberStatusList) error { - for _, m := range *status { - require.Equal(t, api.MemberPhaseCreated, m.Phase) - - _, exist := m.Conditions.Get(api.ConditionTypeReady) - require.Equal(t, false, exist) - _, exist = m.Conditions.Get(api.ConditionTypeTerminated) - require.Equal(t, false, exist) - _, exist = m.Conditions.Get(api.ConditionTypeTerminating) - require.Equal(t, false, exist) - _, exist = m.Conditions.Get(api.ConditionTypeAgentRecoveryNeeded) - require.Equal(t, false, exist) - _, exist = m.Conditions.Get(api.ConditionTypeAutoUpgrade) - require.Equal(t, false, exist) - } - return nil - } - - d.GetServerGroupIterator().ForeachServerGroup(checkEachMember, &status) - } - }) - } -} - -func createTestTLSVolume(serverGroupString, ID string) v1.Volume { - return k8sutil.CreateVolumeWithSecret(k8sutil.TlsKeyfileVolumeName, - k8sutil.CreateTLSKeyfileSecretName(testDeploymentName, serverGroupString, ID)) -} - -func createTestLifecycle() *v1.Lifecycle { - lifecycle, _ := k8sutil.NewLifecycle() - return lifecycle -} - -func createTestToken(deployment *Deployment, testCase *testCaseStruct, paths []string) (string, error) { - - name := testCase.ArangoDeployment.Spec.Authentication.GetJWTSecretName() - s, err := k8sutil.GetTokenSecret(deployment.GetKubeCli().CoreV1().Secrets(testNamespace), name) - if err != nil { - return "", err - } - - return jwt.CreateArangodJwtAuthorizationHeaderAllowedPaths(s, "kube-arangodb", paths) -} - -func createTestLivenessProbe(secure bool, authorization string, port int) *v1.Probe { - return k8sutil.HTTPProbeConfig{ - LocalPath: "/_api/version", - Secure: secure, - Authorization: authorization, - Port: port, - }.Create() -} - -func createTestReadinessProbe(secure bool, authorization string) *v1.Probe { - return k8sutil.HTTPProbeConfig{ - LocalPath: "/_admin/server/availability", - Secure: secure, - Authorization: authorization, - InitialDelaySeconds: 2, - PeriodSeconds: 2, - }.Create() -} - -func createTestCommandForDBServer(name string, tls, auth, encryptionRocksDB bool) []string { - command := []string{resources.ArangoDExecutor} - if tls { - command = append(command, "--cluster.my-address=ssl://"+testDeploymentName+"-"+ - api.ServerGroupDBServersString+"-"+name+".test-int.default.svc:8529") - } else { - command = append(command, "--cluster.my-address=tcp://"+testDeploymentName+"-"+ - api.ServerGroupDBServersString+"-"+name+".test-int.default.svc:8529") - } - - command = append(command, "--cluster.my-role=PRIMARY", "--database.directory=/data", - "--foxx.queues=false", "--log.level=INFO", "--log.output=+") - - if encryptionRocksDB { - command = append(command, "--rocksdb.encryption-keyfile=/secrets/rocksdb/encryption/key") - } - - if auth { - command = append(command, "--server.authentication=true") - } else { - command = append(command, "--server.authentication=false") - } - - if tls { - command = append(command, "--server.endpoint=ssl://[::]:8529") - } else { - command = append(command, "--server.endpoint=tcp://[::]:8529") - } - - if auth { - command = append(command, "--server.jwt-secret-keyfile=/secrets/cluster/jwt/token") - } - - command = append(command, "--server.statistics=true", "--server.storage-engine=rocksdb") - - if tls { - command = append(command, "--ssl.ecdh-curve=", "--ssl.keyfile=/secrets/tls/tls.keyfile") - } - return command -} - -func createTestCommandForCoordinator(name string, tls, auth, encryptionRocksDB bool) []string { - command := []string{resources.ArangoDExecutor} - if tls { - command = append(command, "--cluster.my-address=ssl://"+testDeploymentName+"-"+ - api.ServerGroupCoordinatorsString+"-"+name+".test-int.default.svc:8529") - } else { - command = append(command, "--cluster.my-address=tcp://"+testDeploymentName+"-"+ - api.ServerGroupCoordinatorsString+"-"+name+".test-int.default.svc:8529") - } - - command = append(command, "--cluster.my-role=COORDINATOR", "--database.directory=/data", - "--foxx.queues=true", "--log.level=INFO", "--log.output=+") - - if encryptionRocksDB { - command = append(command, "--rocksdb.encryption-keyfile=/secrets/rocksdb/encryption/key") - } - - if auth { - command = append(command, "--server.authentication=true") - } else { - command = append(command, "--server.authentication=false") - } - - if tls { - command = append(command, "--server.endpoint=ssl://[::]:8529") - } else { - command = append(command, "--server.endpoint=tcp://[::]:8529") - } - - if auth { - command = append(command, "--server.jwt-secret-keyfile=/secrets/cluster/jwt/token") - } - - command = append(command, "--server.statistics=true", "--server.storage-engine=rocksdb") - - if tls { - command = append(command, "--ssl.ecdh-curve=", "--ssl.keyfile=/secrets/tls/tls.keyfile") - } - return command -} - -func createTestCommandForSingleMode(name string, tls, auth, encryptionRocksDB bool) []string { - command := []string{resources.ArangoDExecutor} - - command = append(command, "--database.directory=/data", "--foxx.queues=true", "--log.level=INFO", - "--log.output=+") - - if encryptionRocksDB { - command = append(command, "--rocksdb.encryption-keyfile=/secrets/rocksdb/encryption/key") - } - - if auth { - command = append(command, "--server.authentication=true") - } else { - command = append(command, "--server.authentication=false") - } - - if tls { - command = append(command, "--server.endpoint=ssl://[::]:8529") - } else { - command = append(command, "--server.endpoint=tcp://[::]:8529") - } - - if auth { - command = append(command, "--server.jwt-secret-keyfile=/secrets/cluster/jwt/token") - } - - command = append(command, "--server.statistics=true", "--server.storage-engine=rocksdb") - - if tls { - command = append(command, "--ssl.ecdh-curve=", "--ssl.keyfile=/secrets/tls/tls.keyfile") - } - return command -} - -func createTestCommandForAgent(name string, tls, auth, encryptionRocksDB bool) []string { - command := []string{ - resources.ArangoDExecutor, - "--agency.activate=true", - "--agency.disaster-recovery-id=" + name} - - if tls { - command = append(command, "--agency.my-address=ssl://"+testDeploymentName+"-"+ - api.ServerGroupAgentsString+"-"+name+"."+testDeploymentName+"-int."+testNamespace+".svc:8529") - } else { - command = append(command, "--agency.my-address=tcp://"+testDeploymentName+"-"+ - api.ServerGroupAgentsString+"-"+name+"."+testDeploymentName+"-int."+testNamespace+".svc:8529") - } - - command = append(command, - "--agency.size=3", - "--agency.supervision=true", - "--database.directory=/data", - "--foxx.queues=false", - "--log.level=INFO", - "--log.output=+") - - if encryptionRocksDB { - command = append(command, "--rocksdb.encryption-keyfile=/secrets/rocksdb/encryption/key") - } - - if auth { - command = append(command, "--server.authentication=true") - } else { - command = append(command, "--server.authentication=false") - } - - if tls { - command = append(command, "--server.endpoint=ssl://[::]:8529") - } else { - command = append(command, "--server.endpoint=tcp://[::]:8529") - } - - if auth { - command = append(command, "--server.jwt-secret-keyfile=/secrets/cluster/jwt/token") - } - - command = append(command, "--server.statistics=false", "--server.storage-engine=rocksdb") - - if tls { - command = append(command, "--ssl.ecdh-curve=", "--ssl.keyfile=/secrets/tls/tls.keyfile") - } - - return command -} - -func createTestCommandForSyncMaster(name string, tls, auth, monitoring bool) []string { - command := []string{resources.ArangoSyncExecutor, "run", "master"} - - if tls { - command = append(command, "--cluster.endpoint=https://"+testDeploymentName+":8529") - } else { - command = append(command, "--cluster.endpoint=http://"+testDeploymentName+":8529") - } - - if auth { - command = append(command, "--cluster.jwt-secret=/secrets/cluster/jwt/token") - } - - command = append(command, "--master.endpoint=https://"+testDeploymentName+"-sync.default.svc:8629") - - command = append(command, "--master.jwt-secret=/secrets/master/jwt/token") - - if monitoring { - command = append(command, "--monitoring.token="+"$("+constants.EnvArangoSyncMonitoringToken+")") - } - - command = append(command, "--mq.type=direct", "--server.client-cafile=/secrets/client-auth/ca/ca.crt") - - command = append(command, "--server.endpoint=https://"+testDeploymentName+ - "-syncmaster-"+name+".test-int."+testNamespace+".svc:8629", - "--server.keyfile=/secrets/tls/tls.keyfile", "--server.port=8629") - - return command -} - -func createTestCommandForSyncWorker(name string, tls, monitoring bool) []string { - command := []string{resources.ArangoSyncExecutor, "run", "worker"} - - scheme := "http" - if tls { - scheme = "https" - } - - command = append(command, - "--master.endpoint=https://"+testDeploymentName+"-sync:8629", - "--master.jwt-secret=/secrets/master/jwt/token") - - if monitoring { - command = append(command, "--monitoring.token="+"$("+constants.EnvArangoSyncMonitoringToken+")") - } - - command = append(command, - "--server.endpoint="+scheme+"://"+testDeploymentName+"-syncworker-"+name+".test-int."+testNamespace+".svc:8729", - "--server.port=8729") - - return command -} - -func createTestDeployment(config Config, arangoDeployment *api.ArangoDeployment) (*Deployment, *recordfake.FakeRecorder) { - - eventRecorder := recordfake.NewFakeRecorder(10) - kubernetesClientSet := fake.NewSimpleClientset() - - arangoDeployment.ObjectMeta = metav1.ObjectMeta{ - Name: testDeploymentName, - Namespace: testNamespace, - } - - deps := Dependencies{ - Log: zerolog.New(ioutil.Discard), - KubeCli: kubernetesClientSet, - DatabaseCRCli: arangofake.NewSimpleClientset(&api.ArangoDeployment{}), - EventRecorder: eventRecorder, - } - - d := &Deployment{ - apiObject: arangoDeployment, - config: config, - deps: deps, - eventCh: make(chan *deploymentEvent, deploymentEventQueueSize), - stopCh: make(chan struct{}), - clientCache: newClientCache(deps.KubeCli, arangoDeployment), - } - - arangoDeployment.Spec.SetDefaults(arangoDeployment.GetName()) - d.resources = resources.NewResources(deps.Log, d) - - return d, eventRecorder -} - -func createTestPorts() []v1.ContainerPort { - return []v1.ContainerPort{ - { - Name: "server", - ContainerPort: 8529, - Protocol: "TCP", - }, - } -} - -func createTestImages(enterprise bool) api.ImageInfoList { - return api.ImageInfoList{ - { - Image: testImage, - ArangoDBVersion: testVersion, - ImageID: testImage, - Enterprise: enterprise, - }, - } -} - -func createTestExporterPorts() []v1.ContainerPort { - return []v1.ContainerPort{ - { - Name: "exporter", - ContainerPort: 9101, - Protocol: "TCP", - }, - } -} - -func createTestExporterCommand(secure bool) []string { - command := []string{ - "/app/arangodb-exporter", - } - - if secure { - command = append(command, "--arangodb.endpoint=https://localhost:8529") - } else { - command = append(command, "--arangodb.endpoint=http://localhost:8529") - } - - command = append(command, "--arangodb.jwt-file=/secrets/exporter/jwt/token") - - if secure { - command = append(command, "--ssl.keyfile=/secrets/tls/tls.keyfile") - } - return command -} - -func createTestExporterLivenessProbe(secure bool) *v1.Probe { - return k8sutil.HTTPProbeConfig{ - LocalPath: "/", - Port: k8sutil.ArangoExporterPort, - Secure: secure, - }.Create() -} - -func createTestLifecycleContainer(resources v1.ResourceRequirements) v1.Container { - binaryPath, _ := os.Executable() - var securityContext api.ServerGroupSpecSecurityContext - - return v1.Container{ - Name: "init-lifecycle", - Image: testImageLifecycle, - Command: []string{binaryPath, "lifecycle", "copy", "--target", "/lifecycle/tools"}, - VolumeMounts: []v1.VolumeMount{ - k8sutil.LifecycleVolumeMount(), - }, - ImagePullPolicy: "IfNotPresent", - Resources: resources, - SecurityContext: securityContext.NewSecurityContext(), - } -} - -func createTestAlpineContainer(name string, requireUUID bool) v1.Container { - var securityContext api.ServerGroupSpecSecurityContext - return k8sutil.ArangodInitContainer("uuid", name, "rocksdb", testImageAlpine, requireUUID, securityContext.NewSecurityContext()) -} - -func (testCase *testCaseStruct) createTestPodData(deployment *Deployment, group api.ServerGroup, - memberStatus api.MemberStatus) { - - podName := k8sutil.CreatePodName(testDeploymentName, group.AsRoleAbbreviated(), memberStatus.ID, - resources.CreatePodSuffix(testCase.ArangoDeployment.Spec)) - - testCase.ExpectedPod.ObjectMeta = metav1.ObjectMeta{ - Name: podName, - Namespace: testNamespace, - Labels: k8sutil.LabelsForDeployment(testDeploymentName, group.AsRole()), - OwnerReferences: []metav1.OwnerReference{ - testCase.ArangoDeployment.AsOwner(), - }, - Finalizers: deployment.resources.CreatePodFinalizers(group), - } - - groupSpec := testCase.ArangoDeployment.Spec.GetServerGroupSpec(group) - testCase.ExpectedPod.Spec.Tolerations = deployment.resources.CreatePodTolerations(group, groupSpec) -} - -func testCreateExporterContainer(secure bool, resources v1.ResourceRequirements) v1.Container { - var securityContext api.ServerGroupSpecSecurityContext - - return v1.Container{ - Name: k8sutil.ExporterContainerName, - Image: testExporterImage, - Command: createTestExporterCommand(secure), - Ports: createTestExporterPorts(), - VolumeMounts: []v1.VolumeMount{ - k8sutil.ExporterJWTVolumeMount(), - }, - Resources: k8sutil.ExtractPodResourceRequirement(resources), - LivenessProbe: createTestExporterLivenessProbe(secure), - ImagePullPolicy: v1.PullIfNotPresent, - SecurityContext: securityContext.NewSecurityContext(), - } + runTestCases(t, testCases...) } diff --git a/pkg/deployment/deployment_definitions_test.go b/pkg/deployment/deployment_definitions_test.go new file mode 100644 index 000000000..fa4a82d2f --- /dev/null +++ b/pkg/deployment/deployment_definitions_test.go @@ -0,0 +1,123 @@ +// +// DISCLAIMER +// +// Copyright 2020 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 +// +// Author Tomasz Mielech +// + +package deployment + +import ( + api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" + "github.com/arangodb/kube-arangodb/pkg/util" + core "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" +) + +var ( + defaultAgentTerminationTimeout = int64(api.ServerGroupAgents.DefaultTerminationGracePeriod().Seconds()) + defaultDBServerTerminationTimeout = int64(api.ServerGroupDBServers.DefaultTerminationGracePeriod().Seconds()) + defaultCoordinatorTerminationTimeout = int64(api.ServerGroupCoordinators.DefaultTerminationGracePeriod().Seconds()) + defaultSingleTerminationTimeout = int64(api.ServerGroupSingle.DefaultTerminationGracePeriod().Seconds()) + defaultSyncMasterTerminationTimeout = int64(api.ServerGroupSyncMasters.DefaultTerminationGracePeriod().Seconds()) + defaultSyncWorkerTerminationTimeout = int64(api.ServerGroupSyncWorkers.DefaultTerminationGracePeriod().Seconds()) + + securityContext api.ServerGroupSpecSecurityContext + + nodeSelectorTest = map[string]string{ + "test": "test", + } + + firstAgentStatus = api.MemberStatus{ + ID: "agent1", + Phase: api.MemberPhaseNone, + } + + firstCoordinatorStatus = api.MemberStatus{ + ID: "coordinator1", + Phase: api.MemberPhaseNone, + } + + singleStatus = api.MemberStatus{ + ID: "single1", + Phase: api.MemberPhaseNone, + } + + firstSyncMaster = api.MemberStatus{ + ID: "syncMaster1", + Phase: api.MemberPhaseNone, + } + + firstSyncWorker = api.MemberStatus{ + ID: "syncWorker1", + Phase: api.MemberPhaseNone, + } + + firstDBServerStatus = api.MemberStatus{ + ID: "DBserver1", + Phase: api.MemberPhaseNone, + } + + noAuthentication = api.AuthenticationSpec{ + JWTSecretName: util.NewString(api.JWTSecretNameDisabled), + } + + noTLS = api.TLSSpec{ + CASecretName: util.NewString(api.CASecretNameDisabled), + } + + authenticationSpec = api.AuthenticationSpec{ + JWTSecretName: util.NewString(testJWTSecretName), + } + tlsSpec = api.TLSSpec{ + CASecretName: util.NewString(testCASecretName), + } + + rocksDBSpec = api.RocksDBSpec{ + Encryption: api.RocksDBEncryptionSpec{ + KeySecretName: util.NewString(testRocksDBEncryptionKey), + }, + } + + metricsSpec = api.MetricsSpec{ + Enabled: util.NewBool(true), + Image: util.NewString(testExporterImage), + Authentication: api.MetricsAuthenticationSpec{ + JWTTokenSecretName: util.NewString(testExporterToken), + }, + } + + resourcesUnfiltered = core.ResourceRequirements{ + Limits: core.ResourceList{ + core.ResourceCPU: resource.MustParse("500m"), + core.ResourceMemory: resource.MustParse("2Gi"), + }, + Requests: core.ResourceList{ + core.ResourceCPU: resource.MustParse("100m"), + core.ResourceMemory: resource.MustParse("1Gi"), + }, + } + + emptyResources = core.ResourceRequirements{ + Limits: make(core.ResourceList), + Requests: make(core.ResourceList), + } + + sidecarName1 = "sidecar1" + sidecarName2 = "sidecar2" +) diff --git a/pkg/deployment/deployment_pod_resources_test.go b/pkg/deployment/deployment_pod_resources_test.go new file mode 100644 index 000000000..3059a1160 --- /dev/null +++ b/pkg/deployment/deployment_pod_resources_test.go @@ -0,0 +1,225 @@ +// +// DISCLAIMER +// +// Copyright 2020 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 +// +// Author Adam Janikowski +// + +package deployment + +import ( + "fmt" + "testing" + + "github.com/arangodb/kube-arangodb/pkg/deployment/resources" + "github.com/stretchr/testify/require" + + api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" + "github.com/arangodb/kube-arangodb/pkg/util" + "github.com/arangodb/kube-arangodb/pkg/util/k8sutil" + core "k8s.io/api/core/v1" +) + +func resourceLimitAsEnv(t *testing.T, requirements core.ResourceRequirements) core.EnvVar { + value, ok := requirements.Limits[core.ResourceMemory] + require.True(t, ok) + + return core.EnvVar{ + Name: resources.ArangoDBOverrideDetectedTotalMemoryEnv, + Value: fmt.Sprintf("%d", value.Value()), + } +} + +func TestEnsurePod_ArangoDB_Resources(t *testing.T) { + testCases := []testCaseStruct{ + { + Name: "DBserver POD with resource requirements", + ArangoDeployment: &api.ArangoDeployment{ + Spec: api.DeploymentSpec{ + Image: util.NewString(testImage), + Authentication: noAuthentication, + TLS: noTLS, + DBServers: api.ServerGroupSpec{ + Resources: resourcesUnfiltered, + }, + }, + }, + Helper: func(t *testing.T, deployment *Deployment, testCase *testCaseStruct) { + deployment.status.last = api.DeploymentStatus{ + Members: api.DeploymentStatusMembers{ + DBServers: api.MemberStatusList{ + firstDBServerStatus, + }, + }, + Images: createTestImages(false), + } + deployment.status.last.Members.DBServers[0].IsInitialized = true + + testCase.createTestPodData(deployment, api.ServerGroupDBServers, firstDBServerStatus) + }, + ExpectedEvent: "member dbserver is created", + ExpectedPod: core.Pod{ + Spec: core.PodSpec{ + Volumes: []core.Volume{ + k8sutil.CreateVolumeEmptyDir(k8sutil.ArangodVolumeName), + }, + Containers: []core.Container{ + { + Name: k8sutil.ServerContainerName, + Image: testImage, + Command: createTestCommandForDBServer(firstDBServerStatus.ID, false, false, false), + Ports: createTestPorts(), + Resources: k8sutil.ExtractPodResourceRequirement(resourcesUnfiltered), + VolumeMounts: []core.VolumeMount{ + k8sutil.ArangodVolumeMount(), + }, + LivenessProbe: createTestLivenessProbe(false, "", k8sutil.ArangoPort), + ImagePullPolicy: core.PullIfNotPresent, + SecurityContext: securityContext.NewSecurityContext(), + }, + }, + RestartPolicy: core.RestartPolicyNever, + TerminationGracePeriodSeconds: &defaultDBServerTerminationTimeout, + Hostname: testDeploymentName + "-" + api.ServerGroupDBServersString + "-" + + firstDBServerStatus.ID, + Subdomain: testDeploymentName + "-int", + Affinity: k8sutil.CreateAffinity(testDeploymentName, api.ServerGroupDBServersString, + false, ""), + }, + }, + }, + { + Name: "DBserver POD with resource requirements, with override flag", + ArangoDeployment: &api.ArangoDeployment{ + Spec: api.DeploymentSpec{ + Image: util.NewString(testImage), + Authentication: noAuthentication, + TLS: noTLS, + DBServers: api.ServerGroupSpec{ + Resources: resourcesUnfiltered, + OverrideDetectedTotalMemory: util.NewBool(true), + }, + }, + }, + Helper: func(t *testing.T, deployment *Deployment, testCase *testCaseStruct) { + deployment.status.last = api.DeploymentStatus{ + Members: api.DeploymentStatusMembers{ + DBServers: api.MemberStatusList{ + firstDBServerStatus, + }, + }, + Images: createTestImages(false), + } + deployment.status.last.Members.DBServers[0].IsInitialized = true + + testCase.createTestPodData(deployment, api.ServerGroupDBServers, firstDBServerStatus) + }, + ExpectedEvent: "member dbserver is created", + ExpectedPod: core.Pod{ + Spec: core.PodSpec{ + Volumes: []core.Volume{ + k8sutil.CreateVolumeEmptyDir(k8sutil.ArangodVolumeName), + }, + Containers: []core.Container{ + { + Name: k8sutil.ServerContainerName, + Image: testImage, + Command: createTestCommandForDBServer(firstDBServerStatus.ID, false, false, false), + Ports: createTestPorts(), + Resources: k8sutil.ExtractPodResourceRequirement(resourcesUnfiltered), + VolumeMounts: []core.VolumeMount{ + k8sutil.ArangodVolumeMount(), + }, + Env: []core.EnvVar{ + resourceLimitAsEnv(t, resourcesUnfiltered), + }, + LivenessProbe: createTestLivenessProbe(false, "", k8sutil.ArangoPort), + ImagePullPolicy: core.PullIfNotPresent, + SecurityContext: securityContext.NewSecurityContext(), + }, + }, + RestartPolicy: core.RestartPolicyNever, + TerminationGracePeriodSeconds: &defaultDBServerTerminationTimeout, + Hostname: testDeploymentName + "-" + api.ServerGroupDBServersString + "-" + + firstDBServerStatus.ID, + Subdomain: testDeploymentName + "-int", + Affinity: k8sutil.CreateAffinity(testDeploymentName, api.ServerGroupDBServersString, + false, ""), + }, + }, + }, + { + Name: "DBserver POD without resource requirements, with override flag", + ArangoDeployment: &api.ArangoDeployment{ + Spec: api.DeploymentSpec{ + Image: util.NewString(testImage), + Authentication: noAuthentication, + TLS: noTLS, + DBServers: api.ServerGroupSpec{ + OverrideDetectedTotalMemory: util.NewBool(true), + }, + }, + }, + Helper: func(t *testing.T, deployment *Deployment, testCase *testCaseStruct) { + deployment.status.last = api.DeploymentStatus{ + Members: api.DeploymentStatusMembers{ + DBServers: api.MemberStatusList{ + firstDBServerStatus, + }, + }, + Images: createTestImages(false), + } + deployment.status.last.Members.DBServers[0].IsInitialized = true + + testCase.createTestPodData(deployment, api.ServerGroupDBServers, firstDBServerStatus) + }, + ExpectedEvent: "member dbserver is created", + ExpectedPod: core.Pod{ + Spec: core.PodSpec{ + Volumes: []core.Volume{ + k8sutil.CreateVolumeEmptyDir(k8sutil.ArangodVolumeName), + }, + Containers: []core.Container{ + { + Name: k8sutil.ServerContainerName, + Image: testImage, + Command: createTestCommandForDBServer(firstDBServerStatus.ID, false, false, false), + Ports: createTestPorts(), + Resources: emptyResources, + VolumeMounts: []core.VolumeMount{ + k8sutil.ArangodVolumeMount(), + }, + LivenessProbe: createTestLivenessProbe(false, "", k8sutil.ArangoPort), + ImagePullPolicy: core.PullIfNotPresent, + SecurityContext: securityContext.NewSecurityContext(), + }, + }, + RestartPolicy: core.RestartPolicyNever, + TerminationGracePeriodSeconds: &defaultDBServerTerminationTimeout, + Hostname: testDeploymentName + "-" + api.ServerGroupDBServersString + "-" + + firstDBServerStatus.ID, + Subdomain: testDeploymentName + "-int", + Affinity: k8sutil.CreateAffinity(testDeploymentName, api.ServerGroupDBServersString, + false, ""), + }, + }, + }, + } + + runTestCases(t, testCases...) +} diff --git a/pkg/deployment/deployment_pod_sync_test.go b/pkg/deployment/deployment_pod_sync_test.go new file mode 100644 index 000000000..545b91125 --- /dev/null +++ b/pkg/deployment/deployment_pod_sync_test.go @@ -0,0 +1,468 @@ +// +// DISCLAIMER +// +// Copyright 2020 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 +// +// Author Tomasz Mielech +// + +package deployment + +import ( + "testing" + + api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" + "github.com/arangodb/kube-arangodb/pkg/util" + "github.com/arangodb/kube-arangodb/pkg/util/constants" + "github.com/arangodb/kube-arangodb/pkg/util/k8sutil" + "github.com/pkg/errors" + "github.com/stretchr/testify/require" + core "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestEnsurePod_Sync_Error(t *testing.T) { + testCases := []testCaseStruct{ + { + Name: "Sync Pod does not work for enterprise image", + ArangoDeployment: &api.ArangoDeployment{ + Spec: api.DeploymentSpec{ + Image: util.NewString(testImage), + }, + }, + Helper: func(t *testing.T, deployment *Deployment, testCase *testCaseStruct) { + deployment.status.last = api.DeploymentStatus{ + Members: api.DeploymentStatusMembers{ + SyncMasters: api.MemberStatusList{ + firstSyncMaster, + }, + }, + Images: createTestImages(false), + } + }, + ExpectedError: errors.New("Image '" + testImage + "' does not contain an Enterprise version of ArangoDB"), + }, + { + Name: "Sync Pod cannot get master JWT secret", + ArangoDeployment: &api.ArangoDeployment{ + Spec: api.DeploymentSpec{ + Image: util.NewString(testImage), + }, + }, + Helper: func(t *testing.T, deployment *Deployment, testCase *testCaseStruct) { + deployment.status.last = api.DeploymentStatus{ + Members: api.DeploymentStatusMembers{ + SyncMasters: api.MemberStatusList{ + firstSyncMaster, + }, + }, + Images: createTestImages(true), + } + }, + ExpectedError: errors.New("Master JWT secret validation failed: secrets \"" + + testDeploymentName + "-sync-jwt\" not found"), + }, + { + Name: "Sync Pod cannot get monitoring token secret", + ArangoDeployment: &api.ArangoDeployment{ + Spec: api.DeploymentSpec{ + Image: util.NewString(testImage), + Sync: api.SyncSpec{ + Enabled: util.NewBool(true), + }, + }, + }, + Helper: func(t *testing.T, deployment *Deployment, testCase *testCaseStruct) { + deployment.status.last = api.DeploymentStatus{ + Members: api.DeploymentStatusMembers{ + SyncMasters: api.MemberStatusList{ + firstSyncMaster, + }, + }, + Images: createTestImages(true), + } + + secretName := testCase.ArangoDeployment.Spec.Sync.Monitoring.GetTokenSecretName() + err := deployment.GetKubeCli().CoreV1().Secrets(testNamespace).Delete(secretName, &metav1.DeleteOptions{}) + require.NoError(t, err) + }, + ExpectedError: errors.New("Monitoring token secret validation failed: secrets \"" + + testDeploymentName + "-sync-mt\" not found"), + }, + } + + runTestCases(t, testCases...) +} + +func TestEnsurePod_Sync_Master(t *testing.T) { + testCases := []testCaseStruct{ + { + Name: "Sync Master Pod cannot create TLS keyfile secret", + ArangoDeployment: &api.ArangoDeployment{ + Spec: api.DeploymentSpec{ + Image: util.NewString(testImage), + Sync: api.SyncSpec{ + Enabled: util.NewBool(true), + }, + }, + }, + Helper: func(t *testing.T, deployment *Deployment, testCase *testCaseStruct) { + deployment.status.last = api.DeploymentStatus{ + Members: api.DeploymentStatusMembers{ + SyncMasters: api.MemberStatusList{ + firstSyncMaster, + }, + }, + Images: createTestImages(true), + } + + secretName := testCase.ArangoDeployment.Spec.Sync.TLS.GetCASecretName() + err := deployment.GetKubeCli().CoreV1().Secrets(testNamespace).Delete(secretName, &metav1.DeleteOptions{}) + require.NoError(t, err) + }, + ExpectedError: errors.New("Failed to create TLS keyfile secret: secrets \"" + + testDeploymentName + "-sync-ca\" not found"), + }, + { + Name: "Sync Master Pod cannot get cluster JWT secret", + ArangoDeployment: &api.ArangoDeployment{ + Spec: api.DeploymentSpec{ + Image: util.NewString(testImage), + Authentication: authenticationSpec, + Sync: api.SyncSpec{ + Enabled: util.NewBool(true), + }, + }, + }, + Helper: func(t *testing.T, deployment *Deployment, testCase *testCaseStruct) { + deployment.status.last = api.DeploymentStatus{ + Members: api.DeploymentStatusMembers{ + SyncMasters: api.MemberStatusList{ + firstSyncMaster, + }, + }, + Images: createTestImages(true), + } + + secretName := testCase.ArangoDeployment.Spec.Authentication.GetJWTSecretName() + err := deployment.GetKubeCli().CoreV1().Secrets(testNamespace).Delete(secretName, &metav1.DeleteOptions{}) + require.NoError(t, err) + }, + ExpectedError: errors.New("Cluster JWT secret validation failed: secrets \"" + + testJWTSecretName + "\" not found"), + }, + { + Name: "Sync Master Pod cannot get authentication CA certificate", + ArangoDeployment: &api.ArangoDeployment{ + Spec: api.DeploymentSpec{ + Image: util.NewString(testImage), + Authentication: authenticationSpec, + Sync: api.SyncSpec{ + Enabled: util.NewBool(true), + }, + }, + }, + Helper: func(t *testing.T, deployment *Deployment, testCase *testCaseStruct) { + deployment.status.last = api.DeploymentStatus{ + Members: api.DeploymentStatusMembers{ + SyncMasters: api.MemberStatusList{ + firstSyncMaster, + }, + }, + Images: createTestImages(true), + } + + secretName := testCase.ArangoDeployment.Spec.Sync.Authentication.GetClientCASecretName() + err := deployment.GetKubeCli().CoreV1().Secrets(testNamespace).Delete(secretName, &metav1.DeleteOptions{}) + require.NoError(t, err) + }, + ExpectedError: errors.New("Client authentication CA certificate secret validation failed: " + + "secrets \"" + testDeploymentName + "-sync-client-auth-ca\" not found"), + }, + { + Name: "Sync Master Pod with authentication, monitoring, tls, service account, node selector, " + + "liveness probe, priority class name, resource requirements", + ArangoDeployment: &api.ArangoDeployment{ + Spec: api.DeploymentSpec{ + Image: util.NewString(testImage), + Authentication: authenticationSpec, + Sync: api.SyncSpec{ + Enabled: util.NewBool(true), + }, + SyncMasters: api.ServerGroupSpec{ + ServiceAccountName: util.NewString(testServiceAccountName), + NodeSelector: nodeSelectorTest, + PriorityClassName: testPriorityClassName, + Resources: resourcesUnfiltered, + }, + }, + }, + Helper: func(t *testing.T, deployment *Deployment, testCase *testCaseStruct) { + deployment.status.last = api.DeploymentStatus{ + Members: api.DeploymentStatusMembers{ + SyncMasters: api.MemberStatusList{ + firstSyncMaster, + }, + }, + Images: createTestImages(true), + } + + testCase.createTestPodData(deployment, api.ServerGroupSyncMasters, firstSyncMaster) + + name := testCase.ArangoDeployment.Spec.Sync.Monitoring.GetTokenSecretName() + auth, err := k8sutil.GetTokenSecret(deployment.GetKubeCli().CoreV1().Secrets(testNamespace), name) + require.NoError(t, err) + + testCase.ExpectedPod.Spec.Containers[0].LivenessProbe = createTestLivenessProbe( + true, "bearer "+auth, k8sutil.ArangoSyncMasterPort) + }, + ExpectedEvent: "member syncmaster is created", + ExpectedPod: core.Pod{ + Spec: core.PodSpec{ + Volumes: []core.Volume{ + createTestTLSVolume(api.ServerGroupSyncMastersString, firstSyncMaster.ID), + k8sutil.CreateVolumeWithSecret(k8sutil.ClientAuthCAVolumeName, "test-sync-client-auth-ca"), + k8sutil.CreateVolumeWithSecret(k8sutil.MasterJWTSecretVolumeName, "test-sync-jwt"), + k8sutil.CreateVolumeWithSecret(k8sutil.ClusterJWTSecretVolumeName, testJWTSecretName), + }, + Containers: []core.Container{ + { + Name: k8sutil.ServerContainerName, + Image: testImage, + Command: createTestCommandForSyncMaster(firstSyncMaster.ID, true, true, true), + Ports: createTestPorts(), + Env: []core.EnvVar{ + k8sutil.CreateEnvSecretKeySelector(constants.EnvArangoSyncMonitoringToken, + testDeploymentName+"-sync-mt", constants.SecretKeyToken), + }, + ImagePullPolicy: core.PullIfNotPresent, + Resources: resourcesUnfiltered, + SecurityContext: securityContext.NewSecurityContext(), + VolumeMounts: []core.VolumeMount{ + k8sutil.TlsKeyfileVolumeMount(), + k8sutil.ClientAuthCACertificateVolumeMount(), + k8sutil.MasterJWTVolumeMount(), + k8sutil.ClusterJWTVolumeMount(), + }, + }, + }, + PriorityClassName: testPriorityClassName, + RestartPolicy: core.RestartPolicyNever, + ServiceAccountName: testServiceAccountName, + NodeSelector: nodeSelectorTest, + TerminationGracePeriodSeconds: &defaultSyncMasterTerminationTimeout, + Hostname: testDeploymentName + "-" + api.ServerGroupSyncMastersString + "-" + + firstSyncMaster.ID, + Subdomain: testDeploymentName + "-int", + Affinity: k8sutil.CreateAffinity(testDeploymentName, api.ServerGroupSyncMastersString, + false, ""), + }, + }, + }, + { + Name: "Sync Master Pod with lifecycle, license, monitoring without authentication and alpine", + config: Config{ + LifecycleImage: testImageLifecycle, + AlpineImage: testImageAlpine, + }, + ArangoDeployment: &api.ArangoDeployment{ + Spec: api.DeploymentSpec{ + Image: util.NewString(testImage), + Authentication: noAuthentication, + Environment: api.NewEnvironment(api.EnvironmentProduction), + Sync: api.SyncSpec{ + Enabled: util.NewBool(true), + }, + License: api.LicenseSpec{ + SecretName: util.NewString(testLicense), + }, + }, + }, + Helper: func(t *testing.T, deployment *Deployment, testCase *testCaseStruct) { + deployment.status.last = api.DeploymentStatus{ + Members: api.DeploymentStatusMembers{ + SyncMasters: api.MemberStatusList{ + firstSyncMaster, + }, + }, + Images: createTestImages(true), + } + + testCase.createTestPodData(deployment, api.ServerGroupSyncMasters, firstSyncMaster) + name := testCase.ArangoDeployment.Spec.Sync.Monitoring.GetTokenSecretName() + auth, err := k8sutil.GetTokenSecret(deployment.GetKubeCli().CoreV1().Secrets(testNamespace), name) + require.NoError(t, err) + + testCase.ExpectedPod.Spec.Containers[0].LivenessProbe = createTestLivenessProbe( + true, "bearer "+auth, k8sutil.ArangoSyncMasterPort) + }, + ExpectedEvent: "member syncmaster is created", + ExpectedPod: core.Pod{ + Spec: core.PodSpec{ + Volumes: []core.Volume{ + k8sutil.LifecycleVolume(), + createTestTLSVolume(api.ServerGroupSyncMastersString, firstSyncMaster.ID), + k8sutil.CreateVolumeWithSecret(k8sutil.ClientAuthCAVolumeName, + testDeploymentName+"-sync-client-auth-ca"), + k8sutil.CreateVolumeWithSecret(k8sutil.MasterJWTSecretVolumeName, + testDeploymentName+"-sync-jwt"), + }, + InitContainers: []core.Container{ + createTestLifecycleContainer(emptyResources), + }, + Containers: []core.Container{ + { + Name: k8sutil.ServerContainerName, + Image: testImage, + Command: createTestCommandForSyncMaster(firstSyncMaster.ID, true, false, true), + Ports: createTestPorts(), + Env: []core.EnvVar{ + k8sutil.CreateEnvSecretKeySelector(constants.EnvArangoSyncMonitoringToken, + testDeploymentName+"-sync-mt", constants.SecretKeyToken), + k8sutil.CreateEnvSecretKeySelector(constants.EnvArangoLicenseKey, + testLicense, constants.SecretKeyToken), + k8sutil.CreateEnvFieldPath(constants.EnvOperatorPodName, "metadata.name"), + k8sutil.CreateEnvFieldPath(constants.EnvOperatorPodNamespace, "metadata.namespace"), + k8sutil.CreateEnvFieldPath(constants.EnvOperatorNodeName, "spec.nodeName"), + k8sutil.CreateEnvFieldPath(constants.EnvOperatorNodeNameArango, "spec.nodeName"), + }, + Resources: emptyResources, + ImagePullPolicy: core.PullIfNotPresent, + Lifecycle: createTestLifecycle(), + SecurityContext: securityContext.NewSecurityContext(), + VolumeMounts: []core.VolumeMount{ + k8sutil.LifecycleVolumeMount(), + k8sutil.TlsKeyfileVolumeMount(), + k8sutil.ClientAuthCACertificateVolumeMount(), + k8sutil.MasterJWTVolumeMount(), + }, + }, + }, + RestartPolicy: core.RestartPolicyNever, + TerminationGracePeriodSeconds: &defaultSyncMasterTerminationTimeout, + Hostname: testDeploymentName + "-" + api.ServerGroupSyncMastersString + "-" + + firstSyncMaster.ID, + Subdomain: testDeploymentName + "-int", + Affinity: k8sutil.CreateAffinity(testDeploymentName, api.ServerGroupSyncMastersString, + true, ""), + }, + }, + }, + } + + runTestCases(t, testCases...) +} + +func TestEnsurePod_Sync_Worker(t *testing.T) { + testCases := []testCaseStruct{ + { + Name: "Sync Worker Pod with monitoring, service account, node selector, lifecycle, license " + + "liveness probe, priority class name, resource requirements without alpine", + config: Config{ + LifecycleImage: testImageLifecycle, + AlpineImage: testImageAlpine, + }, + ArangoDeployment: &api.ArangoDeployment{ + Spec: api.DeploymentSpec{ + Image: util.NewString(testImage), + Authentication: noAuthentication, + Sync: api.SyncSpec{ + Enabled: util.NewBool(true), + }, + SyncWorkers: api.ServerGroupSpec{ + ServiceAccountName: util.NewString(testServiceAccountName), + NodeSelector: nodeSelectorTest, + PriorityClassName: testPriorityClassName, + Resources: resourcesUnfiltered, + }, + License: api.LicenseSpec{ + SecretName: util.NewString(testLicense), + }, + }, + }, + Helper: func(t *testing.T, deployment *Deployment, testCase *testCaseStruct) { + deployment.status.last = api.DeploymentStatus{ + Members: api.DeploymentStatusMembers{ + SyncWorkers: api.MemberStatusList{ + firstSyncWorker, + }, + }, + Images: createTestImages(true), + } + + testCase.createTestPodData(deployment, api.ServerGroupSyncWorkers, firstSyncWorker) + + name := testCase.ArangoDeployment.Spec.Sync.Monitoring.GetTokenSecretName() + auth, err := k8sutil.GetTokenSecret(deployment.GetKubeCli().CoreV1().Secrets(testNamespace), name) + require.NoError(t, err) + + testCase.ExpectedPod.Spec.Containers[0].LivenessProbe = createTestLivenessProbe( + true, "bearer "+auth, k8sutil.ArangoSyncWorkerPort) + }, + ExpectedEvent: "member syncworker is created", + ExpectedPod: core.Pod{ + Spec: core.PodSpec{ + Volumes: []core.Volume{ + k8sutil.LifecycleVolume(), + k8sutil.CreateVolumeWithSecret(k8sutil.MasterJWTSecretVolumeName, testDeploymentName+"-sync-jwt"), + }, + InitContainers: []core.Container{ + createTestLifecycleContainer(emptyResources), + }, + Containers: []core.Container{ + { + Name: k8sutil.ServerContainerName, + Image: testImage, + Command: createTestCommandForSyncWorker(firstSyncWorker.ID, true, true), + Ports: createTestPorts(), + Env: []core.EnvVar{ + k8sutil.CreateEnvSecretKeySelector(constants.EnvArangoSyncMonitoringToken, + testDeploymentName+"-sync-mt", constants.SecretKeyToken), + k8sutil.CreateEnvSecretKeySelector(constants.EnvArangoLicenseKey, + testLicense, constants.SecretKeyToken), + k8sutil.CreateEnvFieldPath(constants.EnvOperatorPodName, "metadata.name"), + k8sutil.CreateEnvFieldPath(constants.EnvOperatorPodNamespace, "metadata.namespace"), + k8sutil.CreateEnvFieldPath(constants.EnvOperatorNodeName, "spec.nodeName"), + k8sutil.CreateEnvFieldPath(constants.EnvOperatorNodeNameArango, "spec.nodeName"), + }, + Lifecycle: createTestLifecycle(), + ImagePullPolicy: core.PullIfNotPresent, + Resources: k8sutil.ExtractPodResourceRequirement(resourcesUnfiltered), + SecurityContext: securityContext.NewSecurityContext(), + VolumeMounts: []core.VolumeMount{ + k8sutil.LifecycleVolumeMount(), + k8sutil.MasterJWTVolumeMount(), + }, + }, + }, + PriorityClassName: testPriorityClassName, + RestartPolicy: core.RestartPolicyNever, + ServiceAccountName: testServiceAccountName, + NodeSelector: nodeSelectorTest, + TerminationGracePeriodSeconds: &defaultSyncWorkerTerminationTimeout, + Hostname: testDeploymentName + "-" + api.ServerGroupSyncWorkersString + "-" + + firstSyncWorker.ID, + Subdomain: testDeploymentName + "-int", + Affinity: k8sutil.CreateAffinity(testDeploymentName, api.ServerGroupSyncWorkersString, + false, api.ServerGroupDBServersString), + }, + }, + }, + } + + runTestCases(t, testCases...) +} diff --git a/pkg/deployment/deployment_pod_volumes_test.go b/pkg/deployment/deployment_pod_volumes_test.go new file mode 100644 index 000000000..adfd7a69b --- /dev/null +++ b/pkg/deployment/deployment_pod_volumes_test.go @@ -0,0 +1,242 @@ +// +// DISCLAIMER +// +// Copyright 2020 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 +// +// Author Adam Janikowski +// + +package deployment + +import ( + "fmt" + "testing" + + api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" + "github.com/arangodb/kube-arangodb/pkg/util" + "github.com/arangodb/kube-arangodb/pkg/util/k8sutil" + core "k8s.io/api/core/v1" +) + +func createExampleVolume(name string) api.ServerGroupSpecVolume { + return api.ServerGroupSpecVolume{ + Name: name, + ConfigMap: &api.ServerGroupSpecVolumeConfigMap{ + LocalObjectReference: core.LocalObjectReference{ + Name: name, + }, + }, + } +} + +func createExampleVolumeMount(name string) api.ServerGroupSpecVolumeMount { + return api.ServerGroupSpecVolumeMount{ + Name: name, + MountPath: fmt.Sprintf("/mount/%s", name), + } +} + +func TestEnsurePod_ArangoDB_Volumes(t *testing.T) { + testCases := []testCaseStruct{ + { + Name: "DBserver POD with Volume", + ArangoDeployment: &api.ArangoDeployment{ + Spec: api.DeploymentSpec{ + Image: util.NewString(testImage), + Authentication: noAuthentication, + TLS: noTLS, + DBServers: api.ServerGroupSpec{ + Volumes: []api.ServerGroupSpecVolume{ + createExampleVolume("volume"), + }, + }, + }, + }, + Helper: func(t *testing.T, deployment *Deployment, testCase *testCaseStruct) { + deployment.status.last = api.DeploymentStatus{ + Members: api.DeploymentStatusMembers{ + DBServers: api.MemberStatusList{ + firstDBServerStatus, + }, + }, + Images: createTestImages(false), + } + deployment.status.last.Members.DBServers[0].IsInitialized = true + + testCase.createTestPodData(deployment, api.ServerGroupDBServers, firstDBServerStatus) + }, + ExpectedEvent: "member dbserver is created", + ExpectedPod: core.Pod{ + Spec: core.PodSpec{ + Volumes: []core.Volume{ + k8sutil.CreateVolumeEmptyDir(k8sutil.ArangodVolumeName), + createExampleVolume("volume").Volume(), + }, + Containers: []core.Container{ + { + Name: k8sutil.ServerContainerName, + Image: testImage, + Command: createTestCommandForDBServer(firstDBServerStatus.ID, false, false, false), + Ports: createTestPorts(), + Resources: emptyResources, + VolumeMounts: []core.VolumeMount{ + k8sutil.ArangodVolumeMount(), + }, + LivenessProbe: createTestLivenessProbe(false, "", k8sutil.ArangoPort), + ImagePullPolicy: core.PullIfNotPresent, + SecurityContext: securityContext.NewSecurityContext(), + }, + }, + RestartPolicy: core.RestartPolicyNever, + TerminationGracePeriodSeconds: &defaultDBServerTerminationTimeout, + Hostname: testDeploymentName + "-" + api.ServerGroupDBServersString + "-" + + firstDBServerStatus.ID, + Subdomain: testDeploymentName + "-int", + Affinity: k8sutil.CreateAffinity(testDeploymentName, api.ServerGroupDBServersString, + false, ""), + }, + }, + }, + { + Name: "DBserver POD with Volumes", + ArangoDeployment: &api.ArangoDeployment{ + Spec: api.DeploymentSpec{ + Image: util.NewString(testImage), + Authentication: noAuthentication, + TLS: noTLS, + DBServers: api.ServerGroupSpec{ + Volumes: []api.ServerGroupSpecVolume{ + createExampleVolume("volume"), + createExampleVolume("volume2"), + }, + }, + }, + }, + Helper: func(t *testing.T, deployment *Deployment, testCase *testCaseStruct) { + deployment.status.last = api.DeploymentStatus{ + Members: api.DeploymentStatusMembers{ + DBServers: api.MemberStatusList{ + firstDBServerStatus, + }, + }, + Images: createTestImages(false), + } + deployment.status.last.Members.DBServers[0].IsInitialized = true + + testCase.createTestPodData(deployment, api.ServerGroupDBServers, firstDBServerStatus) + }, + ExpectedEvent: "member dbserver is created", + ExpectedPod: core.Pod{ + Spec: core.PodSpec{ + Volumes: []core.Volume{ + k8sutil.CreateVolumeEmptyDir(k8sutil.ArangodVolumeName), + createExampleVolume("volume").Volume(), + createExampleVolume("volume2").Volume(), + }, + Containers: []core.Container{ + { + Name: k8sutil.ServerContainerName, + Image: testImage, + Command: createTestCommandForDBServer(firstDBServerStatus.ID, false, false, false), + Ports: createTestPorts(), + Resources: emptyResources, + VolumeMounts: []core.VolumeMount{ + k8sutil.ArangodVolumeMount(), + }, + LivenessProbe: createTestLivenessProbe(false, "", k8sutil.ArangoPort), + ImagePullPolicy: core.PullIfNotPresent, + SecurityContext: securityContext.NewSecurityContext(), + }, + }, + RestartPolicy: core.RestartPolicyNever, + TerminationGracePeriodSeconds: &defaultDBServerTerminationTimeout, + Hostname: testDeploymentName + "-" + api.ServerGroupDBServersString + "-" + + firstDBServerStatus.ID, + Subdomain: testDeploymentName + "-int", + Affinity: k8sutil.CreateAffinity(testDeploymentName, api.ServerGroupDBServersString, + false, ""), + }, + }, + }, + + { + Name: "DBserver POD with Volume Mount", + ArangoDeployment: &api.ArangoDeployment{ + Spec: api.DeploymentSpec{ + Image: util.NewString(testImage), + Authentication: noAuthentication, + TLS: noTLS, + DBServers: api.ServerGroupSpec{ + Volumes: []api.ServerGroupSpecVolume{ + createExampleVolume("volume"), + }, + VolumeMounts: []api.ServerGroupSpecVolumeMount{ + createExampleVolumeMount("volume"), + }, + }, + }, + }, + Helper: func(t *testing.T, deployment *Deployment, testCase *testCaseStruct) { + deployment.status.last = api.DeploymentStatus{ + Members: api.DeploymentStatusMembers{ + DBServers: api.MemberStatusList{ + firstDBServerStatus, + }, + }, + Images: createTestImages(false), + } + deployment.status.last.Members.DBServers[0].IsInitialized = true + + testCase.createTestPodData(deployment, api.ServerGroupDBServers, firstDBServerStatus) + }, + ExpectedEvent: "member dbserver is created", + ExpectedPod: core.Pod{ + Spec: core.PodSpec{ + Volumes: []core.Volume{ + k8sutil.CreateVolumeEmptyDir(k8sutil.ArangodVolumeName), + createExampleVolume("volume").Volume(), + }, + Containers: []core.Container{ + { + Name: k8sutil.ServerContainerName, + Image: testImage, + Command: createTestCommandForDBServer(firstDBServerStatus.ID, false, false, false), + Ports: createTestPorts(), + Resources: emptyResources, + VolumeMounts: []core.VolumeMount{ + k8sutil.ArangodVolumeMount(), + createExampleVolumeMount("volume").VolumeMount(), + }, + LivenessProbe: createTestLivenessProbe(false, "", k8sutil.ArangoPort), + ImagePullPolicy: core.PullIfNotPresent, + SecurityContext: securityContext.NewSecurityContext(), + }, + }, + RestartPolicy: core.RestartPolicyNever, + TerminationGracePeriodSeconds: &defaultDBServerTerminationTimeout, + Hostname: testDeploymentName + "-" + api.ServerGroupDBServersString + "-" + + firstDBServerStatus.ID, + Subdomain: testDeploymentName + "-int", + Affinity: k8sutil.CreateAffinity(testDeploymentName, api.ServerGroupDBServersString, + false, ""), + }, + }, + }, + } + + runTestCases(t, testCases...) +} diff --git a/pkg/deployment/deployment_run_test.go b/pkg/deployment/deployment_run_test.go new file mode 100644 index 000000000..5797dc5f9 --- /dev/null +++ b/pkg/deployment/deployment_run_test.go @@ -0,0 +1,108 @@ +// +// DISCLAIMER +// +// Copyright 2020 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 +// +// Author Adam Janikowski +// + +package deployment + +import ( + "fmt" + "testing" + + api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func runTestCases(t *testing.T, testCases ...testCaseStruct) { + for _, testCase := range testCases { + runTestCase(t, testCase) + } +} + +func runTestCase(t *testing.T, testCase testCaseStruct) { + t.Run(testCase.Name, func(t *testing.T) { + // Arrange + d, eventRecorder := createTestDeployment(testCase.config, testCase.ArangoDeployment) + + err := d.resources.EnsureSecrets() + require.NoError(t, err) + + if testCase.Helper != nil { + testCase.Helper(t, d, &testCase) + } + + // Create custom resource in the fake kubernetes API + _, err = d.deps.DatabaseCRCli.DatabaseV1().ArangoDeployments(testNamespace).Create(d.apiObject) + require.NoError(t, err) + + // Act + err = d.resources.EnsurePods() + + // Assert + if testCase.ExpectedError != nil { + + if !assert.EqualError(t, err, testCase.ExpectedError.Error()) { + println(fmt.Sprintf("%+v", err)) + } + return + } + + require.NoError(t, err) + pods, err := d.deps.KubeCli.CoreV1().Pods(testNamespace).List(metav1.ListOptions{}) + require.NoError(t, err) + require.Len(t, pods.Items, 1) + require.Equal(t, testCase.ExpectedPod.Spec, pods.Items[0].Spec) + require.Equal(t, testCase.ExpectedPod.ObjectMeta, pods.Items[0].ObjectMeta) + + if len(testCase.ExpectedEvent) > 0 { + select { + case msg := <-eventRecorder.Events: + assert.Contains(t, msg, testCase.ExpectedEvent) + default: + assert.Fail(t, "expected event", "expected event with message '%s'", testCase.ExpectedEvent) + } + + status, version := d.GetStatus() + assert.Equal(t, int32(1), version) + + checkEachMember := func(group api.ServerGroup, groupSpec api.ServerGroupSpec, status *api.MemberStatusList) error { + for _, m := range *status { + require.Equal(t, api.MemberPhaseCreated, m.Phase) + + _, exist := m.Conditions.Get(api.ConditionTypeReady) + require.Equal(t, false, exist) + _, exist = m.Conditions.Get(api.ConditionTypeTerminated) + require.Equal(t, false, exist) + _, exist = m.Conditions.Get(api.ConditionTypeTerminating) + require.Equal(t, false, exist) + _, exist = m.Conditions.Get(api.ConditionTypeAgentRecoveryNeeded) + require.Equal(t, false, exist) + _, exist = m.Conditions.Get(api.ConditionTypeAutoUpgrade) + require.Equal(t, false, exist) + } + return nil + } + + d.GetServerGroupIterator().ForeachServerGroup(checkEachMember, &status) + } + }) +} diff --git a/pkg/deployment/deployment_suite_test.go b/pkg/deployment/deployment_suite_test.go new file mode 100644 index 000000000..0b67cd5cd --- /dev/null +++ b/pkg/deployment/deployment_suite_test.go @@ -0,0 +1,481 @@ +// +// DISCLAIMER +// +// Copyright 2020 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 +// +// Author Adam Janikowski +// + +package deployment + +import ( + "io/ioutil" + "os" + "testing" + + "github.com/arangodb/go-driver/jwt" + api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" + "github.com/arangodb/kube-arangodb/pkg/deployment/resources" + arangofake "github.com/arangodb/kube-arangodb/pkg/generated/clientset/versioned/fake" + "github.com/arangodb/kube-arangodb/pkg/util/constants" + "github.com/arangodb/kube-arangodb/pkg/util/k8sutil" + "github.com/rs/zerolog" + core "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" + recordfake "k8s.io/client-go/tools/record" +) + +const ( + testNamespace = "default" + testDeploymentName = "test" + testVersion = "3.5.2" + testImage = "arangodb/arangodb:" + testVersion + testCASecretName = "testCA" + testJWTSecretName = "testJWT" + testExporterToken = "testExporterToken" + testRocksDBEncryptionKey = "testRocksDB" + testPersistentVolumeClaimName = "testClaim" + testLicense = "testLicense" + testServiceAccountName = "testServiceAccountName" + testPriorityClassName = "testPriority" + testImageLifecycle = "arangodb/kube-arangodb:0.3.16" + testExporterImage = "arangodb/arangodb-exporter:0.1.6" + testImageAlpine = "alpine:3.7" + + testYes = "yes" +) + +type testCaseStruct struct { + Name string + ArangoDeployment *api.ArangoDeployment + Helper func(*testing.T, *Deployment, *testCaseStruct) + config Config + ExpectedError error + ExpectedEvent string + ExpectedPod core.Pod +} + +func createTestTLSVolume(serverGroupString, ID string) core.Volume { + return k8sutil.CreateVolumeWithSecret(k8sutil.TlsKeyfileVolumeName, + k8sutil.CreateTLSKeyfileSecretName(testDeploymentName, serverGroupString, ID)) +} + +func createTestLifecycle() *core.Lifecycle { + lifecycle, _ := k8sutil.NewLifecycle() + return lifecycle +} + +func createTestToken(deployment *Deployment, testCase *testCaseStruct, paths []string) (string, error) { + + name := testCase.ArangoDeployment.Spec.Authentication.GetJWTSecretName() + s, err := k8sutil.GetTokenSecret(deployment.GetKubeCli().CoreV1().Secrets(testNamespace), name) + if err != nil { + return "", err + } + + return jwt.CreateArangodJwtAuthorizationHeaderAllowedPaths(s, "kube-arangodb", paths) +} + +func createTestLivenessProbe(secure bool, authorization string, port int) *core.Probe { + return k8sutil.HTTPProbeConfig{ + LocalPath: "/_api/version", + Secure: secure, + Authorization: authorization, + Port: port, + }.Create() +} + +func createTestReadinessProbe(secure bool, authorization string) *core.Probe { + return k8sutil.HTTPProbeConfig{ + LocalPath: "/_admin/server/availability", + Secure: secure, + Authorization: authorization, + InitialDelaySeconds: 2, + PeriodSeconds: 2, + }.Create() +} + +func createTestCommandForDBServer(name string, tls, auth, encryptionRocksDB bool) []string { + command := []string{resources.ArangoDExecutor} + if tls { + command = append(command, "--cluster.my-address=ssl://"+testDeploymentName+"-"+ + api.ServerGroupDBServersString+"-"+name+".test-int.default.svc:8529") + } else { + command = append(command, "--cluster.my-address=tcp://"+testDeploymentName+"-"+ + api.ServerGroupDBServersString+"-"+name+".test-int.default.svc:8529") + } + + command = append(command, "--cluster.my-role=PRIMARY", "--database.directory=/data", + "--foxx.queues=false", "--log.level=INFO", "--log.output=+") + + if encryptionRocksDB { + command = append(command, "--rocksdb.encryption-keyfile=/secrets/rocksdb/encryption/key") + } + + if auth { + command = append(command, "--server.authentication=true") + } else { + command = append(command, "--server.authentication=false") + } + + if tls { + command = append(command, "--server.endpoint=ssl://[::]:8529") + } else { + command = append(command, "--server.endpoint=tcp://[::]:8529") + } + + if auth { + command = append(command, "--server.jwt-secret-keyfile=/secrets/cluster/jwt/token") + } + + command = append(command, "--server.statistics=true", "--server.storage-engine=rocksdb") + + if tls { + command = append(command, "--ssl.ecdh-curve=", "--ssl.keyfile=/secrets/tls/tls.keyfile") + } + return command +} + +func createTestCommandForCoordinator(name string, tls, auth, encryptionRocksDB bool) []string { + command := []string{resources.ArangoDExecutor} + if tls { + command = append(command, "--cluster.my-address=ssl://"+testDeploymentName+"-"+ + api.ServerGroupCoordinatorsString+"-"+name+".test-int.default.svc:8529") + } else { + command = append(command, "--cluster.my-address=tcp://"+testDeploymentName+"-"+ + api.ServerGroupCoordinatorsString+"-"+name+".test-int.default.svc:8529") + } + + command = append(command, "--cluster.my-role=COORDINATOR", "--database.directory=/data", + "--foxx.queues=true", "--log.level=INFO", "--log.output=+") + + if encryptionRocksDB { + command = append(command, "--rocksdb.encryption-keyfile=/secrets/rocksdb/encryption/key") + } + + if auth { + command = append(command, "--server.authentication=true") + } else { + command = append(command, "--server.authentication=false") + } + + if tls { + command = append(command, "--server.endpoint=ssl://[::]:8529") + } else { + command = append(command, "--server.endpoint=tcp://[::]:8529") + } + + if auth { + command = append(command, "--server.jwt-secret-keyfile=/secrets/cluster/jwt/token") + } + + command = append(command, "--server.statistics=true", "--server.storage-engine=rocksdb") + + if tls { + command = append(command, "--ssl.ecdh-curve=", "--ssl.keyfile=/secrets/tls/tls.keyfile") + } + return command +} + +func createTestCommandForSingleMode(name string, tls, auth, encryptionRocksDB bool) []string { + command := []string{resources.ArangoDExecutor} + + command = append(command, "--database.directory=/data", "--foxx.queues=true", "--log.level=INFO", + "--log.output=+") + + if encryptionRocksDB { + command = append(command, "--rocksdb.encryption-keyfile=/secrets/rocksdb/encryption/key") + } + + if auth { + command = append(command, "--server.authentication=true") + } else { + command = append(command, "--server.authentication=false") + } + + if tls { + command = append(command, "--server.endpoint=ssl://[::]:8529") + } else { + command = append(command, "--server.endpoint=tcp://[::]:8529") + } + + if auth { + command = append(command, "--server.jwt-secret-keyfile=/secrets/cluster/jwt/token") + } + + command = append(command, "--server.statistics=true", "--server.storage-engine=rocksdb") + + if tls { + command = append(command, "--ssl.ecdh-curve=", "--ssl.keyfile=/secrets/tls/tls.keyfile") + } + return command +} + +func createTestCommandForAgent(name string, tls, auth, encryptionRocksDB bool) []string { + command := []string{ + resources.ArangoDExecutor, + "--agency.activate=true", + "--agency.disaster-recovery-id=" + name} + + if tls { + command = append(command, "--agency.my-address=ssl://"+testDeploymentName+"-"+ + api.ServerGroupAgentsString+"-"+name+"."+testDeploymentName+"-int."+testNamespace+".svc:8529") + } else { + command = append(command, "--agency.my-address=tcp://"+testDeploymentName+"-"+ + api.ServerGroupAgentsString+"-"+name+"."+testDeploymentName+"-int."+testNamespace+".svc:8529") + } + + command = append(command, + "--agency.size=3", + "--agency.supervision=true", + "--database.directory=/data", + "--foxx.queues=false", + "--log.level=INFO", + "--log.output=+") + + if encryptionRocksDB { + command = append(command, "--rocksdb.encryption-keyfile=/secrets/rocksdb/encryption/key") + } + + if auth { + command = append(command, "--server.authentication=true") + } else { + command = append(command, "--server.authentication=false") + } + + if tls { + command = append(command, "--server.endpoint=ssl://[::]:8529") + } else { + command = append(command, "--server.endpoint=tcp://[::]:8529") + } + + if auth { + command = append(command, "--server.jwt-secret-keyfile=/secrets/cluster/jwt/token") + } + + command = append(command, "--server.statistics=false", "--server.storage-engine=rocksdb") + + if tls { + command = append(command, "--ssl.ecdh-curve=", "--ssl.keyfile=/secrets/tls/tls.keyfile") + } + + return command +} + +func createTestCommandForSyncMaster(name string, tls, auth, monitoring bool) []string { + command := []string{resources.ArangoSyncExecutor, "run", "master"} + + if tls { + command = append(command, "--cluster.endpoint=https://"+testDeploymentName+":8529") + } else { + command = append(command, "--cluster.endpoint=http://"+testDeploymentName+":8529") + } + + if auth { + command = append(command, "--cluster.jwt-secret=/secrets/cluster/jwt/token") + } + + command = append(command, "--master.endpoint=https://"+testDeploymentName+"-sync.default.svc:8629") + + command = append(command, "--master.jwt-secret=/secrets/master/jwt/token") + + if monitoring { + command = append(command, "--monitoring.token="+"$("+constants.EnvArangoSyncMonitoringToken+")") + } + + command = append(command, "--mq.type=direct", "--server.client-cafile=/secrets/client-auth/ca/ca.crt") + + command = append(command, "--server.endpoint=https://"+testDeploymentName+ + "-syncmaster-"+name+".test-int."+testNamespace+".svc:8629", + "--server.keyfile=/secrets/tls/tls.keyfile", "--server.port=8629") + + return command +} + +func createTestCommandForSyncWorker(name string, tls, monitoring bool) []string { + command := []string{resources.ArangoSyncExecutor, "run", "worker"} + + scheme := "http" + if tls { + scheme = "https" + } + + command = append(command, + "--master.endpoint=https://"+testDeploymentName+"-sync:8629", + "--master.jwt-secret=/secrets/master/jwt/token") + + if monitoring { + command = append(command, "--monitoring.token="+"$("+constants.EnvArangoSyncMonitoringToken+")") + } + + command = append(command, + "--server.endpoint="+scheme+"://"+testDeploymentName+"-syncworker-"+name+".test-int."+testNamespace+".svc:8729", + "--server.port=8729") + + return command +} + +func createTestDeployment(config Config, arangoDeployment *api.ArangoDeployment) (*Deployment, *recordfake.FakeRecorder) { + + eventRecorder := recordfake.NewFakeRecorder(10) + kubernetesClientSet := fake.NewSimpleClientset() + + arangoDeployment.ObjectMeta = metav1.ObjectMeta{ + Name: testDeploymentName, + Namespace: testNamespace, + } + + deps := Dependencies{ + Log: zerolog.New(ioutil.Discard), + KubeCli: kubernetesClientSet, + DatabaseCRCli: arangofake.NewSimpleClientset(&api.ArangoDeployment{}), + EventRecorder: eventRecorder, + } + + d := &Deployment{ + apiObject: arangoDeployment, + config: config, + deps: deps, + eventCh: make(chan *deploymentEvent, deploymentEventQueueSize), + stopCh: make(chan struct{}), + clientCache: newClientCache(deps.KubeCli, arangoDeployment), + } + + arangoDeployment.Spec.SetDefaults(arangoDeployment.GetName()) + d.resources = resources.NewResources(deps.Log, d) + + return d, eventRecorder +} + +func createTestPorts() []core.ContainerPort { + return []core.ContainerPort{ + { + Name: "server", + ContainerPort: 8529, + Protocol: "TCP", + }, + } +} + +func createTestImages(enterprise bool) api.ImageInfoList { + return api.ImageInfoList{ + { + Image: testImage, + ArangoDBVersion: testVersion, + ImageID: testImage, + Enterprise: enterprise, + }, + } +} + +func createTestExporterPorts() []core.ContainerPort { + return []core.ContainerPort{ + { + Name: "exporter", + ContainerPort: 9101, + Protocol: "TCP", + }, + } +} + +func createTestExporterCommand(secure bool) []string { + command := []string{ + "/app/arangodb-exporter", + } + + if secure { + command = append(command, "--arangodb.endpoint=https://localhost:8529") + } else { + command = append(command, "--arangodb.endpoint=http://localhost:8529") + } + + command = append(command, "--arangodb.jwt-file=/secrets/exporter/jwt/token") + + if secure { + command = append(command, "--ssl.keyfile=/secrets/tls/tls.keyfile") + } + return command +} + +func createTestExporterLivenessProbe(secure bool) *core.Probe { + return k8sutil.HTTPProbeConfig{ + LocalPath: "/", + Port: k8sutil.ArangoExporterPort, + Secure: secure, + }.Create() +} + +func createTestLifecycleContainer(resources core.ResourceRequirements) core.Container { + binaryPath, _ := os.Executable() + var securityContext api.ServerGroupSpecSecurityContext + + return core.Container{ + Name: "init-lifecycle", + Image: testImageLifecycle, + Command: []string{binaryPath, "lifecycle", "copy", "--target", "/lifecycle/tools"}, + VolumeMounts: []core.VolumeMount{ + k8sutil.LifecycleVolumeMount(), + }, + ImagePullPolicy: "IfNotPresent", + Resources: resources, + SecurityContext: securityContext.NewSecurityContext(), + } +} + +func createTestAlpineContainer(name string, requireUUID bool) core.Container { + var securityContext api.ServerGroupSpecSecurityContext + return k8sutil.ArangodInitContainer("uuid", name, "rocksdb", testImageAlpine, requireUUID, securityContext.NewSecurityContext()) +} + +func (testCase *testCaseStruct) createTestPodData(deployment *Deployment, group api.ServerGroup, + memberStatus api.MemberStatus) { + + podName := k8sutil.CreatePodName(testDeploymentName, group.AsRoleAbbreviated(), memberStatus.ID, + resources.CreatePodSuffix(testCase.ArangoDeployment.Spec)) + + testCase.ExpectedPod.ObjectMeta = metav1.ObjectMeta{ + Name: podName, + Namespace: testNamespace, + Labels: k8sutil.LabelsForDeployment(testDeploymentName, group.AsRole()), + OwnerReferences: []metav1.OwnerReference{ + testCase.ArangoDeployment.AsOwner(), + }, + Finalizers: deployment.resources.CreatePodFinalizers(group), + } + + groupSpec := testCase.ArangoDeployment.Spec.GetServerGroupSpec(group) + testCase.ExpectedPod.Spec.Tolerations = deployment.resources.CreatePodTolerations(group, groupSpec) +} + +func testCreateExporterContainer(secure bool, resources core.ResourceRequirements) core.Container { + var securityContext api.ServerGroupSpecSecurityContext + + return core.Container{ + Name: k8sutil.ExporterContainerName, + Image: testExporterImage, + Command: createTestExporterCommand(secure), + Ports: createTestExporterPorts(), + VolumeMounts: []core.VolumeMount{ + k8sutil.ExporterJWTVolumeMount(), + }, + Resources: k8sutil.ExtractPodResourceRequirement(resources), + LivenessProbe: createTestExporterLivenessProbe(secure), + ImagePullPolicy: core.PullIfNotPresent, + SecurityContext: securityContext.NewSecurityContext(), + } +} diff --git a/pkg/deployment/reconcile/plan_builder_rotate_upgrade.go b/pkg/deployment/reconcile/plan_builder_rotate_upgrade.go index 572fa3034..e79340ac0 100644 --- a/pkg/deployment/reconcile/plan_builder_rotate_upgrade.go +++ b/pkg/deployment/reconcile/plan_builder_rotate_upgrade.go @@ -23,9 +23,12 @@ package reconcile import ( + "fmt" "reflect" "strings" + "k8s.io/apimachinery/pkg/api/equality" + "github.com/arangodb/kube-arangodb/pkg/apis/deployment" "github.com/arangodb/kube-arangodb/pkg/deployment/pod" @@ -358,6 +361,15 @@ func podNeedsRotation(log zerolog.Logger, p core.Pod, apiObject metav1.Object, s return rotate, reason } + // Volumes + if rotate, reason := compareVolumes(groupSpec, p); rotate { + return rotate, reason + } + + if rotate, reason := compareVolumeMounts(groupSpec, c); rotate { + return rotate, reason + } + return false, "" } @@ -417,6 +429,63 @@ func compareProbes(probe pod.Probe, groupProbeDisabled *bool, groupProbeSpec *ap return false, "" } +func compareVolumes(spec api.ServerGroupSpec, pod core.Pod) (bool, string) { + if len(spec.Volumes) == 0 { + return false, "" + } + + currentVolumes := map[string]core.Volume{} + + for _, volume := range pod.Spec.Volumes { + currentVolumes[volume.Name] = volume + } + + for _, expectedVolumeTemplate := range spec.Volumes { + expectedVolume := expectedVolumeTemplate.Volume() + + currentVolume, ok := currentVolumes[expectedVolume.Name] + if !ok { + return true, fmt.Sprintf("Volume %s is not mount. Rotating", expectedVolume.Name) + } + + if !equality.Semantic.DeepDerivative(expectedVolume, currentVolume) { + return true, fmt.Sprintf("Volume %s needs to be updated", expectedVolume.Name) + } + } + + return false, "" +} + +func compareVolumeMounts(spec api.ServerGroupSpec, container core.Container) (bool, string) { + if len(container.VolumeMounts) < len(spec.VolumeMounts) { + return true, "Missing volume mounts in container" + } + + // Get compared mounts + mounts := container.VolumeMounts + if len(mounts) > 0 { + if container.VolumeMounts[len(mounts)-1].MountPath == "/var/run/secrets/kubernetes.io/serviceaccount" { + // Remove last mount added by ServiceAccount from compare + mounts = mounts[0 : len(mounts)-1] + } + } + + if len(spec.VolumeMounts) > len(mounts) { + return true, "Missing volume mounts in container" + } + + mounts = mounts[len(mounts)-len(spec.VolumeMounts):] + + // Now we can compare lists + for id, mount := range mounts { + if !equality.Semantic.DeepDerivative(spec.VolumeMounts[id].VolumeMount(), mount) { + return true, fmt.Sprintf("Mount with if %d does not match - got %s, expected %s", id, mount.Name, spec.VolumeMounts[id].Name) + } + } + + return false, "" +} + // clusterReadyForUpgrade returns true if the cluster is ready for the next update, that is: // - all shards are in sync // - all members are ready and fine diff --git a/pkg/deployment/reconcile/plan_builder_security.go b/pkg/deployment/reconcile/plan_builder_security.go index f78e599fa..96f650c3f 100644 --- a/pkg/deployment/reconcile/plan_builder_security.go +++ b/pkg/deployment/reconcile/plan_builder_security.go @@ -23,6 +23,8 @@ package reconcile import ( + "strings" + "github.com/rs/zerolog" core "k8s.io/api/core/v1" @@ -141,6 +143,11 @@ func compareCapabilityLists(a, b []core.Capability) bool { return false } + // If we got ALL on list and expect ALL to be present then it is equal for us + if strings.EqualFold(string(capability), "ALL") { + return true + } + checked[capability] = true } diff --git a/pkg/deployment/reconcile/plan_builder_storage.go b/pkg/deployment/reconcile/plan_builder_storage.go index 801a3013d..9d69b7ddd 100644 --- a/pkg/deployment/reconcile/plan_builder_storage.go +++ b/pkg/deployment/reconcile/plan_builder_storage.go @@ -121,6 +121,7 @@ func createRotateServerStoragePlan(log zerolog.Logger, apiObject k8sutil.APIObje } return nil }) + return plan } diff --git a/pkg/deployment/resources/pod_creator_arangod.go b/pkg/deployment/resources/pod_creator_arangod.go index 97f215995..9435295db 100644 --- a/pkg/deployment/resources/pod_creator_arangod.go +++ b/pkg/deployment/resources/pod_creator_arangod.go @@ -253,6 +253,14 @@ func (m *MemberArangoDPod) GetVolumes() ([]core.Volume, []core.VolumeMount) { volumes = append(volumes, k8sutil.LifecycleVolume()) } + if len(m.groupSpec.Volumes) > 0 { + volumes = append(volumes, m.groupSpec.Volumes.Volumes()...) + } + + if len(m.groupSpec.VolumeMounts) > 0 { + volumeMounts = append(volumeMounts, m.groupSpec.VolumeMounts.VolumeMounts()...) + } + return volumes, volumeMounts }