diff --git a/Makefile b/Makefile index ca873c065..09d43e3b0 100644 --- a/Makefile +++ b/Makefile @@ -187,6 +187,11 @@ fmt: @echo ">> Ensuring style of files" @go run golang.org/x/tools/cmd/goimports -w $(SOURCES) +.PHONY: license +license: + @echo ">> Ensuring license of files" + @go run github.com/google/addlicense -f "./tools/codegen/boilerplate.go.txt" $(SOURCES) + .PHONY: fmt-verify fmt-verify: license-verify @echo ">> Verify files style" diff --git a/chart/kube-arangodb/templates/deployment.yaml b/chart/kube-arangodb/templates/deployment.yaml index e6f79faba..5bc61ab01 100644 --- a/chart/kube-arangodb/templates/deployment.yaml +++ b/chart/kube-arangodb/templates/deployment.yaml @@ -85,6 +85,11 @@ spec: - --operator.backup {{- end }} - --chaos.allowed={{ .Values.operator.allowChaos }} +{{- if .Values.operator.args }} +{{- range .Values.operator.args }} + - {{ . | quote }} +{{- end }} +{{- end }} env: - name: MY_POD_NAMESPACE valueFrom: diff --git a/chart/kube-arangodb/values.yaml b/chart/kube-arangodb/values.yaml index 765494f77..2e809bff4 100644 --- a/chart/kube-arangodb/values.yaml +++ b/chart/kube-arangodb/values.yaml @@ -5,6 +5,8 @@ operator: imagePullPolicy: IfNotPresent imagePullSecrets: [] + args: [] + service: type: ClusterIP diff --git a/main.go b/main.go index b06bcdb5e..95b6cfafb 100644 --- a/main.go +++ b/main.go @@ -31,6 +31,8 @@ import ( "strings" "time" + "github.com/arangodb/kube-arangodb/pkg/deployment/features" + "github.com/rs/zerolog/log" deploymentApi "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" @@ -105,6 +107,8 @@ var ( enableBackup bool // Run backup operator alpineImage, metricsExporterImage, arangoImage string + + singleMode bool } chaosOptions struct { allowed bool @@ -133,7 +137,9 @@ func init() { f.StringVar(&operatorOptions.metricsExporterImage, "operator.metrics-exporter-image", MetricsExporterImageEnv.GetOrDefault(defaultMetricsExporterImage), "Docker image used for metrics containers by default") f.StringVar(&operatorOptions.arangoImage, "operator.arango-image", ArangoImageEnv.GetOrDefault(defaultArangoImage), "Docker image used for arango by default") f.BoolVar(&chaosOptions.allowed, "chaos.allowed", false, "Set to allow chaos in deployments. Only activated when allowed and enabled in deployment") + f.BoolVar(&operatorOptions.singleMode, "mode.single", false, "Enable single mode in Operator. WARNING: There should be only one replica of Operator, otherwise Operator can take unexpected actions") + features.Init(&cmdMain) } func main() { @@ -300,6 +306,7 @@ func newOperatorConfigAndDeps(id, namespace, name string) (operator.Config, oper AlpineImage: operatorOptions.alpineImage, MetricsExporterImage: operatorOptions.metricsExporterImage, ArangoImage: operatorOptions.arangoImage, + SingleMode: operatorOptions.singleMode, } deps := operator.Dependencies{ LogService: logService, diff --git a/pkg/deployment/context_impl.go b/pkg/deployment/context_impl.go index 46b30c6e1..83adff7c8 100644 --- a/pkg/deployment/context_impl.go +++ b/pkg/deployment/context_impl.go @@ -31,6 +31,8 @@ import ( "strconv" "time" + "github.com/arangodb/kube-arangodb/pkg/deployment/features" + "github.com/arangodb/go-driver/http" "github.com/arangodb/go-driver/jwt" "github.com/arangodb/kube-arangodb/pkg/deployment/pod" @@ -245,7 +247,7 @@ func (d *Deployment) getAuth() (driver.Authentication, error) { secrets := d.GetKubeCli().CoreV1().Secrets(d.apiObject.GetNamespace()) var secret string - if i := d.apiObject.Status.CurrentImage; i == nil || i.ArangoDBVersion.CompareTo("3.7.0") < 0 || !i.Enterprise { + if i := d.apiObject.Status.CurrentImage; i == nil || !features.JWTRotation().Supported(i.ArangoDBVersion, i.Enterprise) { s, err := secrets.Get(d.apiObject.Spec.Authentication.GetJWTSecretName(), meta.GetOptions{}) if err != nil { return nil, goErrors.Errorf("JWT Secret is missing") diff --git a/pkg/deployment/deployment_encryption_test.go b/pkg/deployment/deployment_encryption_test.go index 74fda1fe4..d9fbf099c 100644 --- a/pkg/deployment/deployment_encryption_test.go +++ b/pkg/deployment/deployment_encryption_test.go @@ -199,7 +199,7 @@ func TestEnsurePod_ArangoDB_Encryption(t *testing.T) { }, }, { - Name: "Agent EE 3.7.0 Pod with encrypted rocksdb", + Name: "Agent EE 3.7.0 Pod with encrypted rocksdb, disabled feature", ArangoDeployment: &api.ArangoDeployment{ Spec: api.DeploymentSpec{ Image: util.NewString(testImage), @@ -225,6 +225,70 @@ func TestEnsurePod_ArangoDB_Encryption(t *testing.T) { k8sutil.CreateEncryptionKeySecret(secrets, testRocksDBEncryptionKey, key) }, ExpectedEvent: "member agent is created", + ExpectedPod: core.Pod{ + Spec: core.PodSpec{ + Volumes: []core.Volume{ + k8sutil.CreateVolumeEmptyDir(k8sutil.ArangodVolumeName), + k8sutil.CreateVolumeWithSecret(k8sutil.RocksdbEncryptionVolumeName, testRocksDBEncryptionKey), + }, + Containers: []core.Container{ + { + Name: k8sutil.ServerContainerName, + Image: testImage, + Command: BuildTestAgentArgs(t, firstAgentStatus.ID, + AgentArgsWithTLS(firstAgentStatus.ID, false), + ArgsWithAuth(false), + ArgsWithEncryptionKey()), + Ports: createTestPorts(), + VolumeMounts: []core.VolumeMount{ + k8sutil.ArangodVolumeMount(), + k8sutil.RocksdbEncryptionVolumeMount(), + }, + Resources: emptyResources, + LivenessProbe: createTestLivenessProbe(cmd, false, "", k8sutil.ArangoPort), + ImagePullPolicy: core.PullIfNotPresent, + SecurityContext: securityContext.NewSecurityContext(), + }, + }, + RestartPolicy: core.RestartPolicyNever, + TerminationGracePeriodSeconds: &defaultAgentTerminationTimeout, + Hostname: testDeploymentName + "-" + api.ServerGroupAgentsString + "-" + firstAgentStatus.ID, + Subdomain: testDeploymentName + "-int", + Affinity: k8sutil.CreateAffinity(testDeploymentName, api.ServerGroupAgentsString, + false, ""), + }, + }, + }, + { + Name: "Agent EE 3.7.0 Pod with encrypted rocksdb, enabled feature", + ArangoDeployment: &api.ArangoDeployment{ + Spec: api.DeploymentSpec{ + Image: util.NewString(testImage), + Authentication: noAuthentication, + TLS: noTLS, + RocksDB: rocksDBSpec, + }, + }, + Features: testCaseFeatures{ + EncryptionRotation: true, + }, + Helper: func(t *testing.T, deployment *Deployment, testCase *testCaseStruct) { + deployment.status.last = api.DeploymentStatus{ + Members: api.DeploymentStatusMembers{ + Agents: api.MemberStatusList{ + firstAgentStatus, + }, + }, + Images: createTestImagesWithVersion(true, "3.7.0"), + } + + testCase.createTestPodData(deployment, api.ServerGroupAgents, firstAgentStatus) + + secrets := deployment.GetKubeCli().CoreV1().Secrets(testNamespace) + key := make([]byte, 32) + k8sutil.CreateEncryptionKeySecret(secrets, testRocksDBEncryptionKey, key) + }, + ExpectedEvent: "member agent is created", ExpectedPod: core.Pod{ Spec: core.PodSpec{ Volumes: []core.Volume{ diff --git a/pkg/deployment/deployment_pod_tls_sni_test.go b/pkg/deployment/deployment_pod_tls_sni_test.go index 8d789689f..ce9338a9a 100644 --- a/pkg/deployment/deployment_pod_tls_sni_test.go +++ b/pkg/deployment/deployment_pod_tls_sni_test.go @@ -83,6 +83,9 @@ func TestEnsurePod_ArangoDB_TLS_SNI(t *testing.T) { }(), }, }, + Features: testCaseFeatures{ + TLSSNI: true, + }, Resources: func(t *testing.T, deployment *Deployment) { createTLSSNISecret(t, deployment.GetKubeCli(), "sni1", deployment.Namespace(), constants.SecretTLSKeyfile, "") createTLSSNISecret(t, deployment.GetKubeCli(), "sni2", deployment.Namespace(), constants.SecretTLSKeyfile, "") @@ -155,6 +158,9 @@ func TestEnsurePod_ArangoDB_TLS_SNI(t *testing.T) { }(), }, }, + Features: testCaseFeatures{ + TLSSNI: true, + }, Resources: func(t *testing.T, deployment *Deployment) { createTLSSNISecret(t, deployment.GetKubeCli(), "sni1", deployment.Namespace(), constants.SecretTLSKeyfile, "") createTLSSNISecret(t, deployment.GetKubeCli(), "sni2", deployment.Namespace(), constants.SecretTLSKeyfile, "") @@ -227,6 +233,9 @@ func TestEnsurePod_ArangoDB_TLS_SNI(t *testing.T) { }(), }, }, + Features: testCaseFeatures{ + TLSSNI: true, + }, Resources: func(t *testing.T, deployment *Deployment) { createTLSSNISecret(t, deployment.GetKubeCli(), "sni1", deployment.Namespace(), constants.SecretTLSKeyfile, "") createTLSSNISecret(t, deployment.GetKubeCli(), "sni2", deployment.Namespace(), constants.SecretTLSKeyfile, "") @@ -299,6 +308,9 @@ func TestEnsurePod_ArangoDB_TLS_SNI(t *testing.T) { }(), }, }, + Features: testCaseFeatures{ + TLSSNI: true, + }, Resources: func(t *testing.T, deployment *Deployment) { createTLSSNISecret(t, deployment.GetKubeCli(), "sni1", deployment.Namespace(), constants.SecretTLSKeyfile, "") createTLSSNISecret(t, deployment.GetKubeCli(), "sni2", deployment.Namespace(), constants.SecretTLSKeyfile, "") @@ -404,6 +416,9 @@ func TestEnsurePod_ArangoDB_TLS_SNI(t *testing.T) { }(), }, }, + Features: testCaseFeatures{ + TLSSNI: true, + }, Resources: func(t *testing.T, deployment *Deployment) { createTLSSNISecret(t, deployment.GetKubeCli(), "sni1", deployment.Namespace(), constants.SecretTLSKeyfile, "") createTLSSNISecret(t, deployment.GetKubeCli(), "sni2", deployment.Namespace(), constants.SecretTLSKeyfile, "") diff --git a/pkg/deployment/deployment_run_test.go b/pkg/deployment/deployment_run_test.go index 040850c75..f357b46d7 100644 --- a/pkg/deployment/deployment_run_test.go +++ b/pkg/deployment/deployment_run_test.go @@ -27,6 +27,8 @@ import ( "fmt" "testing" + "github.com/arangodb/kube-arangodb/pkg/deployment/features" + "github.com/rs/zerolog/log" "github.com/arangodb/kube-arangodb/pkg/deployment/resources/inspector" @@ -92,6 +94,15 @@ func runTestCase(t *testing.T, testCase testCaseStruct) { testCase.Resources(t, d) } + // Set features + { + *features.EncryptionRotation().EnabledPointer() = testCase.Features.EncryptionRotation + require.Equal(t, testCase.Features.EncryptionRotation, *features.EncryptionRotation().EnabledPointer()) + *features.JWTRotation().EnabledPointer() = testCase.Features.JWTRotation + *features.TLSSNI().EnabledPointer() = testCase.Features.TLSSNI + *features.TLSRotation().EnabledPointer() = testCase.Features.TLSRotation + } + // Act cache, err := inspector.NewInspector(d.GetKubeCli(), d.GetNamespace()) require.NoError(t, err) diff --git a/pkg/deployment/deployment_suite_test.go b/pkg/deployment/deployment_suite_test.go index 3be9a3973..d60af5cb5 100644 --- a/pkg/deployment/deployment_suite_test.go +++ b/pkg/deployment/deployment_suite_test.go @@ -68,6 +68,10 @@ const ( testYes = "yes" ) +type testCaseFeatures struct { + TLSSNI, TLSRotation, JWTRotation, EncryptionRotation bool +} + type testCaseStruct struct { Name string ArangoDeployment *api.ArangoDeployment @@ -78,6 +82,7 @@ type testCaseStruct struct { ExpectedError error ExpectedEvent string ExpectedPod core.Pod + Features testCaseFeatures } func createTestTLSVolume(serverGroupString, ID string) core.Volume { diff --git a/pkg/deployment/features/encryption.go b/pkg/deployment/features/encryption.go new file mode 100644 index 000000000..97a9882f2 --- /dev/null +++ b/pkg/deployment/features/encryption.go @@ -0,0 +1,37 @@ +// +// 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 +// + +package features + +func init() { + registerFeature(encryptionRotation) +} + +var encryptionRotation = &feature{ + name: "encryption-rotation", + description: "Encryption Key rotation in runtime", + version: "3.7.0", + enterpriseRequired: true, + enabledByDefault: false, +} + +func EncryptionRotation() Feature { + return encryptionRotation +} diff --git a/pkg/deployment/features/features.go b/pkg/deployment/features/features.go new file mode 100644 index 000000000..726dff2ec --- /dev/null +++ b/pkg/deployment/features/features.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 +// + +package features + +import "github.com/arangodb/go-driver" + +var _ Feature = &feature{} + +type Feature interface { + Name() string + Description() string + Version() driver.Version + EnterpriseRequired() bool + EnabledByDefault() bool + Enabled() bool + EnabledPointer() *bool + Supported(v driver.Version, enterprise bool) bool +} + +type feature struct { + name, description string + version driver.Version + enterpriseRequired, enabledByDefault, enabled bool +} + +func (f feature) Supported(v driver.Version, enterprise bool) bool { + return Supported(&f, v, enterprise) +} + +func (f feature) Enabled() bool { + return f.enabled +} + +func (f *feature) EnabledPointer() *bool { + return &f.enabled +} + +func (f feature) Version() driver.Version { + return f.version +} + +func (f feature) EnterpriseRequired() bool { + return f.enterpriseRequired +} + +func (f feature) EnabledByDefault() bool { + return f.enabledByDefault +} + +func (f feature) Name() string { + return f.name +} + +func (f feature) Description() string { + return f.description +} diff --git a/pkg/deployment/features/jwt.go b/pkg/deployment/features/jwt.go new file mode 100644 index 000000000..df6d32431 --- /dev/null +++ b/pkg/deployment/features/jwt.go @@ -0,0 +1,37 @@ +// +// 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 +// + +package features + +func init() { + registerFeature(jwtRotation) +} + +var jwtRotation = &feature{ + name: "jwt-rotation", + description: "JWT Token rotation in runtime", + version: "3.7.0", + enterpriseRequired: true, + enabledByDefault: false, +} + +func JWTRotation() Feature { + return jwtRotation +} diff --git a/pkg/deployment/features/local.go b/pkg/deployment/features/local.go new file mode 100644 index 000000000..d20abd183 --- /dev/null +++ b/pkg/deployment/features/local.go @@ -0,0 +1,113 @@ +// +// 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 +// + +package features + +import ( + "fmt" + "sync" + + "github.com/arangodb/go-driver" + "github.com/spf13/cobra" +) + +var features = map[string]Feature{} +var featuresLock sync.Mutex +var enableAll = false + +func registerFeature(f Feature) { + featuresLock.Lock() + defer featuresLock.Unlock() + + if f == nil { + panic("Feature cannot be nil") + } + + if _, ok := features[f.Name()]; ok { + panic("Feature already registered") + } + + features[f.Name()] = f +} + +var internalCMD = &cobra.Command{ + Use: "features", + Short: "Describe all operator features", + Run: cmdRun, +} + +func Init(cmd *cobra.Command) { + featuresLock.Lock() + defer featuresLock.Unlock() + + cmd.AddCommand(internalCMD) + + f := cmd.Flags() + + f.BoolVar(&enableAll, "deployment.feature.all", false, "Enable ALL Features") + + for _, feature := range features { + z := "" + + if v := feature.Version(); v != "" || feature.EnterpriseRequired() { + if v != "" && feature.EnterpriseRequired() { + z = fmt.Sprintf("%s - Required version %s and Enterprise Edition", feature.Description(), v) + } else if v != "" { + z = fmt.Sprintf("%s. Required version %s", feature.Description(), v) + } else if feature.EnterpriseRequired() { + z = fmt.Sprintf("%s - Required Enterprise Edition", feature.Description()) + } else { + z = feature.Description() + } + } + + f.BoolVar(feature.EnabledPointer(), fmt.Sprintf("deployment.feature.%s", feature.Name()), feature.EnabledByDefault(), z) + } +} + +func cmdRun(cmd *cobra.Command, args []string) { + featuresLock.Lock() + defer featuresLock.Unlock() + + for _, feature := range features { + println(fmt.Sprintf("Feature: %s", feature.Name())) + println(fmt.Sprintf("Description: %s", feature.Description())) + if feature.EnabledByDefault() { + println("Enabled: true") + } else { + println("Enabled: false") + } + if v := feature.Version(); v != "" { + println(fmt.Sprintf("ArangoDB Version Required: >= %s", v)) + } + + if feature.EnterpriseRequired() { + println(fmt.Sprintf("ArangoDB Edition Required: Enterprise")) + } else { + println(fmt.Sprintf("ArangoDB Edition Required: Community, Enterprise")) + } + + println() + } +} + +func Supported(f Feature, v driver.Version, enterprise bool) bool { + return f.Enabled() && ((f.EnterpriseRequired() && enterprise) || !f.EnterpriseRequired()) && v.CompareTo(f.Version()) >= 0 +} diff --git a/pkg/deployment/features/tls.go b/pkg/deployment/features/tls.go new file mode 100644 index 000000000..3a17a5d7a --- /dev/null +++ b/pkg/deployment/features/tls.go @@ -0,0 +1,50 @@ +// +// 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 +// + +package features + +func init() { + registerFeature(tlsRotation) + registerFeature(tlsSNI) +} + +var tlsRotation Feature = &feature{ + name: "tls-rotation", + description: "TLS Keyfile rotation in runtime", + version: "3.7.0", + enterpriseRequired: true, + enabledByDefault: false, +} + +func TLSRotation() Feature { + return tlsRotation +} + +var tlsSNI Feature = &feature{ + name: "tls-sni", + description: "TLS SNI Support", + version: "3.7.0", + enterpriseRequired: true, + enabledByDefault: true, +} + +func TLSSNI() Feature { + return tlsSNI +} diff --git a/pkg/deployment/pod/encryption.go b/pkg/deployment/pod/encryption.go index 67e2abb5b..2f1ab092b 100644 --- a/pkg/deployment/pod/encryption.go +++ b/pkg/deployment/pod/encryption.go @@ -27,6 +27,8 @@ import ( "fmt" "path/filepath" + "github.com/arangodb/kube-arangodb/pkg/deployment/features" + "github.com/arangodb/kube-arangodb/pkg/deployment/resources/inspector" api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" @@ -106,7 +108,7 @@ func IsEncryptionEnabled(i Input) bool { } func MultiFileMode(i Input) bool { - return i.Enterprise && i.Version.CompareTo("3.7.0") >= 0 + return features.EncryptionRotation().Supported(i.Version, i.Enterprise) } func Encryption() Builder { diff --git a/pkg/deployment/pod/jwt.go b/pkg/deployment/pod/jwt.go index bcc31e81f..26739f62d 100644 --- a/pkg/deployment/pod/jwt.go +++ b/pkg/deployment/pod/jwt.go @@ -26,6 +26,8 @@ import ( "fmt" "path/filepath" + "github.com/arangodb/kube-arangodb/pkg/deployment/features" + "github.com/arangodb/go-driver" "github.com/arangodb/kube-arangodb/pkg/deployment/resources/inspector" "github.com/arangodb/kube-arangodb/pkg/util/constants" @@ -56,7 +58,7 @@ func JWTSecretFolder(name string) string { } func VersionHasJWTSecretKeyfolder(v driver.Version, enterprise bool) bool { - return enterprise && v.CompareTo("3.7.0") > 0 + return features.JWTRotation().Supported(v, enterprise) } func JWT() Builder { diff --git a/pkg/deployment/pod/sni.go b/pkg/deployment/pod/sni.go index a0b67840c..3d59fd242 100644 --- a/pkg/deployment/pod/sni.go +++ b/pkg/deployment/pod/sni.go @@ -26,6 +26,8 @@ import ( "crypto/sha256" "fmt" + "github.com/arangodb/kube-arangodb/pkg/deployment/features" + "github.com/arangodb/kube-arangodb/pkg/deployment/resources/inspector" api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" @@ -66,7 +68,7 @@ func (s sni) isSupported(i Input) bool { return false } - if i.Version.CompareTo("3.7.0") < 0 || !i.Enterprise { + if !features.TLSSNI().Supported(i.Version, i.Enterprise) { // We need 3.7.0+ and Enterprise to support this return false } diff --git a/pkg/deployment/pod/tls.go b/pkg/deployment/pod/tls.go index 978e420e7..738daebc9 100644 --- a/pkg/deployment/pod/tls.go +++ b/pkg/deployment/pod/tls.go @@ -25,6 +25,8 @@ package pod import ( "path/filepath" + "github.com/arangodb/kube-arangodb/pkg/deployment/features" + api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" "github.com/arangodb/kube-arangodb/pkg/deployment/resources/inspector" @@ -35,8 +37,7 @@ import ( ) func IsRuntimeTLSKeyfileUpdateSupported(i Input) bool { - return IsTLSEnabled(i) && i.Enterprise && - i.Version.CompareTo("3.7.0") >= 0 && + return IsTLSEnabled(i) && features.TLSRotation().Supported(i.Version, i.Enterprise) && i.Deployment.TLS.Mode.Get() == api.TLSRotateModeInPlace } diff --git a/pkg/deployment/reconcile/action_encryption_add.go b/pkg/deployment/reconcile/action_encryption_add.go index 027daf908..486609efa 100644 --- a/pkg/deployment/reconcile/action_encryption_add.go +++ b/pkg/deployment/reconcile/action_encryption_add.go @@ -26,6 +26,8 @@ import ( "context" "encoding/base64" + "github.com/arangodb/kube-arangodb/pkg/deployment/features" + "github.com/arangodb/kube-arangodb/pkg/deployment/patch" "github.com/arangodb/kube-arangodb/pkg/deployment/pod" "github.com/pkg/errors" @@ -43,11 +45,8 @@ func ensureEncryptionSupport(actionCtx ActionContext) error { if image, ok := actionCtx.GetCurrentImageInfo(); !ok { return errors.Errorf("Missing image info") } else { - if !image.Enterprise { - return errors.Errorf("Supported only in enterprise") - } - if image.ArangoDBVersion.CompareTo("3.7.0") < 0 { - return errors.Errorf("Supported only in 3.7.0+") + if !features.EncryptionRotation().Supported(image.ArangoDBVersion, image.Enterprise) { + return errors.Errorf("Supported only in Enterprise Edition 3.7.0+") } } return nil diff --git a/pkg/deployment/reconcile/action_jwt_status_update.go b/pkg/deployment/reconcile/action_jwt_status_update.go index 11d02918e..b229168c3 100644 --- a/pkg/deployment/reconcile/action_jwt_status_update.go +++ b/pkg/deployment/reconcile/action_jwt_status_update.go @@ -27,6 +27,8 @@ import ( "fmt" "sort" + "github.com/arangodb/kube-arangodb/pkg/deployment/features" + "github.com/arangodb/kube-arangodb/pkg/util/constants" "github.com/pkg/errors" @@ -56,10 +58,7 @@ func ensureJWTFolderSupport(spec api.DeploymentSpec, status api.DeploymentStatus if image := status.CurrentImage; image == nil { return false, errors.Errorf("Missing image info") } else { - if !image.Enterprise { - return false, nil - } - if image.ArangoDBVersion.CompareTo("3.7.0") < 0 { + if !features.JWTRotation().Supported(image.ArangoDBVersion, image.Enterprise) { return false, nil } } diff --git a/pkg/deployment/reconcile/plan_builder_encryption.go b/pkg/deployment/reconcile/plan_builder_encryption.go index e6d468dc3..57be02cf1 100644 --- a/pkg/deployment/reconcile/plan_builder_encryption.go +++ b/pkg/deployment/reconcile/plan_builder_encryption.go @@ -25,6 +25,8 @@ package reconcile import ( "context" + "github.com/arangodb/kube-arangodb/pkg/deployment/features" + core "k8s.io/api/core/v1" "github.com/arangodb/kube-arangodb/pkg/util/k8sutil" @@ -45,7 +47,7 @@ func skipEncryptionPlan(spec api.DeploymentSpec, status api.DeploymentStatus) bo return true } - if i := status.CurrentImage; i == nil || !i.Enterprise || i.ArangoDBVersion.CompareTo("3.7.0") < 0 { + if i := status.CurrentImage; i == nil || !features.EncryptionRotation().Supported(i.ArangoDBVersion, i.Enterprise) { return true } @@ -275,7 +277,7 @@ func isEncryptionKeyUpToDate(ctx context.Context, return false, true } - if m.ArangoVersion.CompareTo("3.7.0") < 0 { + if i, ok := status.Images.GetByImageID(m.ImageID); !ok || !features.EncryptionRotation().Supported(i.ArangoDBVersion, i.Enterprise) { return false, false } diff --git a/pkg/deployment/reconcile/plan_builder_jwt.go b/pkg/deployment/reconcile/plan_builder_jwt.go index 24258f74e..834f8b40d 100644 --- a/pkg/deployment/reconcile/plan_builder_jwt.go +++ b/pkg/deployment/reconcile/plan_builder_jwt.go @@ -27,6 +27,8 @@ import ( "fmt" "sort" + "github.com/arangodb/kube-arangodb/pkg/deployment/features" + "github.com/arangodb/kube-arangodb/pkg/deployment/client" "github.com/pkg/errors" "github.com/rs/zerolog/log" @@ -70,7 +72,7 @@ func createJWTKeyUpdate(ctx context.Context, jwtSha := util.SHA256(jwt) if _, ok := folder.Data[jwtSha]; !ok { - return addJWTPropagatedPlanAction(status, api.NewAction(api.ActionTypeJWTAdd, api.ServerGroupUnknown, "", "Add JWT key").AddParam(checksum, jwtSha)) + return addJWTPropagatedPlanAction(status, api.NewAction(api.ActionTypeJWTAdd, api.ServerGroupUnknown, "", "Add JWTRotation key").AddParam(checksum, jwtSha)) } activeKey, ok := folder.Data[pod.ActiveJWTKey] @@ -246,7 +248,7 @@ func isJWTTokenUpToDate(ctx context.Context, return false, true } - if m.ArangoVersion.CompareTo("3.7.0") < 0 { + if i, ok := status.Images.GetByImageID(m.ImageID); !ok || !features.JWTRotation().Supported(i.ArangoDBVersion, i.Enterprise) { return false, false } @@ -259,7 +261,7 @@ func isJWTTokenUpToDate(ctx context.Context, } if updateRequired, err := isMemberJWTTokenInvalid(ctx, client.NewClient(c.Connection()), folder.Data, false); err != nil { - mlog.Warn().Err(err).Msg("JET UpToDate Check failed") + mlog.Warn().Err(err).Msg("JWT UpToDate Check failed") return false, true } else if updateRequired { return true, false diff --git a/pkg/deployment/reconcile/plan_builder_restore.go b/pkg/deployment/reconcile/plan_builder_restore.go index 101bd967e..28e5e4cac 100644 --- a/pkg/deployment/reconcile/plan_builder_restore.go +++ b/pkg/deployment/reconcile/plan_builder_restore.go @@ -25,6 +25,8 @@ package reconcile import ( "context" + "github.com/arangodb/kube-arangodb/pkg/deployment/features" + "github.com/arangodb/kube-arangodb/pkg/deployment/resources/inspector" "github.com/arangodb/kube-arangodb/pkg/util/k8sutil" @@ -65,9 +67,11 @@ func createRestorePlan(ctx context.Context, return p } - if !status.Hashes.Encryption.Propagated { - log.Warn().Msg("Backup not able to be restored in non propagated state") - return nil + if i := status.CurrentImage; i != nil && features.EncryptionRotation().Supported(i.ArangoDBVersion, i.Enterprise) { + if !status.Hashes.Encryption.Propagated { + log.Warn().Msg("Backup not able to be restored in non propagated state") + return nil + } } } @@ -85,8 +89,8 @@ func createRestorePlanEncryption(ctx context.Context, log zerolog.Logger, spec a return true, nil } - if i := status.CurrentImage; i == nil || !i.Enterprise || i.ArangoDBVersion.CompareTo("3.7.0") < 0 { - return true, nil + if i := status.CurrentImage; i == nil || !features.EncryptionRotation().Supported(i.ArangoDBVersion, i.Enterprise) { + return false, nil } if !status.Hashes.Encryption.Propagated { diff --git a/pkg/deployment/reconcile/plan_builder_tls.go b/pkg/deployment/reconcile/plan_builder_tls.go index 54748fc7c..107c2f11d 100644 --- a/pkg/deployment/reconcile/plan_builder_tls.go +++ b/pkg/deployment/reconcile/plan_builder_tls.go @@ -32,6 +32,8 @@ import ( "reflect" "time" + "github.com/arangodb/kube-arangodb/pkg/deployment/features" + "github.com/arangodb/kube-arangodb/pkg/deployment/client" "github.com/arangodb/kube-arangodb/pkg/util/constants" @@ -303,7 +305,7 @@ func createKeyfileRenewalPlanDefault(ctx context.Context, return nil } if renew, recreate := keyfileRenewalRequired(ctx, log, apiObject, spec, status, cachedStatus, context, group, member, api.TLSRotateModeRecreate); renew { - log.Info().Msg("Renewal of keyfile required") + log.Info().Msg("Renewal of keyfile required - Recreate") if recreate { plan = append(plan, api.NewAction(api.ActionTypeCleanTLSKeyfileCertificate, group, member.ID, "Remove server keyfile and enforce renewal")) } @@ -334,7 +336,7 @@ func createKeyfileRenewalPlanInPlace(ctx context.Context, for _, member := range members { if renew, recreate := keyfileRenewalRequired(ctx, log, apiObject, spec, status, cachedStatus, context, group, member, api.TLSRotateModeInPlace); renew { - log.Info().Msg("Renewal of keyfile required") + log.Info().Msg("Renewal of keyfile required - InPlace") if recreate { plan = append(plan, api.NewAction(api.ActionTypeCleanTLSKeyfileCertificate, group, member.ID, "Remove server keyfile and enforce renewal")) } @@ -382,10 +384,10 @@ func createKeyfileRenewalPlanMode( return nil } - if i := status.CurrentImage; i == nil { + if i, ok := status.Images.GetByImageID(member.ImageID); !ok { mode = api.TLSRotateModeRecreate } else { - if !i.Enterprise || i.ArangoDBVersion.CompareTo("3.7.0") < 0 || i.ImageID != member.ImageID { + if !features.TLSRotation().Supported(i.ArangoDBVersion, i.Enterprise) { mode = api.TLSRotateModeRecreate } } diff --git a/pkg/deployment/reconcile/plan_builder_tls_sni.go b/pkg/deployment/reconcile/plan_builder_tls_sni.go index 72e5e078e..3ce48f8dd 100644 --- a/pkg/deployment/reconcile/plan_builder_tls_sni.go +++ b/pkg/deployment/reconcile/plan_builder_tls_sni.go @@ -25,6 +25,8 @@ package reconcile import ( "context" + "github.com/arangodb/kube-arangodb/pkg/deployment/features" + "github.com/arangodb/kube-arangodb/pkg/util/k8sutil" "github.com/arangodb/kube-arangodb/pkg/deployment/resources/inspector" @@ -75,7 +77,7 @@ func createRotateTLSServerSNIPlan(ctx context.Context, continue } - if m.ArangoVersion.CompareTo("3.7.0") < 0 { + if i, ok := status.Images.GetByImageID(m.ImageID); !ok || !features.EncryptionRotation().Supported(i.ArangoDBVersion, i.Enterprise) { continue } diff --git a/pkg/deployment/resources/secret_hashes.go b/pkg/deployment/resources/secret_hashes.go index 9c97283b7..df3cec8aa 100644 --- a/pkg/deployment/resources/secret_hashes.go +++ b/pkg/deployment/resources/secret_hashes.go @@ -30,6 +30,8 @@ import ( "sort" "strings" + "github.com/arangodb/kube-arangodb/pkg/deployment/features" + "github.com/arangodb/kube-arangodb/pkg/deployment/pod" "github.com/arangodb/kube-arangodb/pkg/deployment/resources/inspector" @@ -138,7 +140,7 @@ func (r *Resources) ValidateSecretHashes(cachedStatus inspector.Inspector) error } if spec.IsAuthenticated() { - if image == nil || image.ArangoDBVersion.CompareTo("3.7.0") < 0 { + if image == nil || !features.JWTRotation().Supported(image.ArangoDBVersion, image.Enterprise) { secretName := spec.Authentication.GetJWTSecretName() getExpectedHash := func() string { return getHashes().AuthJWT } setExpectedHash := func(h string) error { @@ -165,7 +167,7 @@ func (r *Resources) ValidateSecretHashes(cachedStatus inspector.Inspector) error } } if spec.RocksDB.IsEncrypted() { - if image == nil || image.ArangoDBVersion.CompareTo("3.7.0") < 0 { + if image == nil || !features.EncryptionRotation().Supported(image.ArangoDBVersion, image.Enterprise) { secretName := spec.RocksDB.Encryption.GetKeySecretName() getExpectedHash := func() string { return getHashes().RocksDBEncryptionKey } setExpectedHash := func(h string) error { diff --git a/pkg/deployment/resources/secrets.go b/pkg/deployment/resources/secrets.go index edf38267a..653011526 100644 --- a/pkg/deployment/resources/secrets.go +++ b/pkg/deployment/resources/secrets.go @@ -29,6 +29,7 @@ import ( "fmt" "time" + "github.com/arangodb/kube-arangodb/pkg/deployment/features" "github.com/arangodb/kube-arangodb/pkg/deployment/patch" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/json" @@ -152,7 +153,7 @@ func (r *Resources) EnsureSecrets(log zerolog.Logger, cachedStatus inspector.Ins } } if spec.RocksDB.IsEncrypted() { - if i := status.CurrentImage; i != nil && i.Enterprise && i.ArangoDBVersion.CompareTo("3.7.0") >= 0 { + if i := status.CurrentImage; i != nil && features.EncryptionRotation().Supported(i.ArangoDBVersion, i.Enterprise) { if err := r.refreshCache(cachedStatus, r.ensureEncryptionKeyfolderSecret(cachedStatus, secrets, spec.RocksDB.Encryption.GetKeySecretName(), pod.GetEncryptionFolderSecretName(deploymentName))); err != nil { return maskAny(err) } diff --git a/pkg/operator/operator.go b/pkg/operator/operator.go index 01990a119..33f7c4679 100644 --- a/pkg/operator/operator.go +++ b/pkg/operator/operator.go @@ -92,6 +92,7 @@ type Config struct { EnableStorage bool EnableBackup bool AllowChaos bool + SingleMode bool } type Dependencies struct { @@ -123,16 +124,32 @@ func NewOperator(config Config, deps Dependencies) (*Operator, error) { // Run the operator func (o *Operator) Run() { if o.Config.EnableDeployment { - go o.runLeaderElection("arango-deployment-operator", constants.LabelRole, o.onStartDeployment, o.Dependencies.DeploymentProbe) + if !o.Config.SingleMode { + go o.runLeaderElection("arango-deployment-operator", constants.LabelRole, o.onStartDeployment, o.Dependencies.DeploymentProbe) + } else { + go o.runWithoutLeaderElection("arango-deployment-operator", constants.LabelRole, o.onStartDeployment, o.Dependencies.DeploymentProbe) + } } if o.Config.EnableDeploymentReplication { - go o.runLeaderElection("arango-deployment-replication-operator", constants.LabelRole, o.onStartDeploymentReplication, o.Dependencies.DeploymentReplicationProbe) + if !o.Config.SingleMode { + go o.runLeaderElection("arango-deployment-replication-operator", constants.LabelRole, o.onStartDeploymentReplication, o.Dependencies.DeploymentReplicationProbe) + } else { + go o.runWithoutLeaderElection("arango-deployment-replication-operator", constants.LabelRole, o.onStartDeploymentReplication, o.Dependencies.DeploymentReplicationProbe) + } } if o.Config.EnableStorage { - go o.runLeaderElection("arango-storage-operator", constants.LabelRole, o.onStartStorage, o.Dependencies.StorageProbe) + if !o.Config.SingleMode { + go o.runLeaderElection("arango-storage-operator", constants.LabelRole, o.onStartStorage, o.Dependencies.StorageProbe) + } else { + go o.runWithoutLeaderElection("arango-storage-operator", constants.LabelRole, o.onStartStorage, o.Dependencies.StorageProbe) + } } if o.Config.EnableBackup { - go o.runLeaderElection("arango-backup-operator", constants.BackupLabelRole, o.onStartBackup, o.Dependencies.BackupProbe) + if !o.Config.SingleMode { + go o.runLeaderElection("arango-backup-operator", constants.BackupLabelRole, o.onStartBackup, o.Dependencies.BackupProbe) + } else { + go o.runWithoutLeaderElection("arango-backup-operator", constants.BackupLabelRole, o.onStartBackup, o.Dependencies.BackupProbe) + } } // Wait until process terminates <-context.TODO().Done() diff --git a/pkg/operator/operator_leader.go b/pkg/operator/operator_leader.go index fe7db19c7..ed0cda8d2 100644 --- a/pkg/operator/operator_leader.go +++ b/pkg/operator/operator_leader.go @@ -99,6 +99,25 @@ func (o *Operator) runLeaderElection(lockName, label string, onStart func(stop < }) } +func (o *Operator) runWithoutLeaderElection(lockName, label string, onStart func(stop <-chan struct{}), readyProbe *probe.ReadyProbe) { + log := o.log.With().Str("lock-name", lockName).Logger() + eventTarget := o.getLeaderElectionEventTarget(log) + recordEvent := func(reason, message string) { + if eventTarget != nil { + o.Dependencies.EventRecorder.Event(eventTarget, v1.EventTypeNormal, reason, message) + } + } + ctx := context.Background() + + recordEvent("Leader Election Skipped", fmt.Sprintf("Pod %s is running as leader", o.Config.PodName)) + readyProbe.SetReady() + if err := o.setRoleLabel(log, label, constants.LabelRoleLeader); err != nil { + log.Error().Msg("Cannot set leader role on Pod. Terminating process") + os.Exit(2) + } + onStart(ctx.Done()) +} + // getLeaderElectionEventTarget returns the object that leader election related // events will be added to. func (o *Operator) getLeaderElectionEventTarget(log zerolog.Logger) runtime.Object {