diff --git a/CHANGELOG.md b/CHANGELOG.md index a0a0a8be5..9990e6248 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Add Kustomize support - Improve Helm 3 support - Allow to customize ID Pod selectors +- Add Label and Envs Pod customization ## [1.0.3](https://github.com/arangodb/kube-arangodb/tree/1.0.3) (2020-05-25) - Prevent deletion of not known PVC's diff --git a/pkg/apis/deployment/v1/deployment_spec.go b/pkg/apis/deployment/v1/deployment_spec.go index 8c36f0d97..a0acc00dc 100644 --- a/pkg/apis/deployment/v1/deployment_spec.go +++ b/pkg/apis/deployment/v1/deployment_spec.go @@ -64,6 +64,8 @@ type DeploymentSpec struct { // Annotations specified the annotations added to all resources Annotations map[string]string `json:"annotations,omitempty"` + // Labels specified the labels added to all resources + Labels map[string]string `json:"labels,omitempty"` RestoreFrom *string `json:"restoreFrom,omitempty"` diff --git a/pkg/apis/deployment/v1/server_group_env_var.go b/pkg/apis/deployment/v1/server_group_env_var.go new file mode 100644 index 000000000..e6059188b --- /dev/null +++ b/pkg/apis/deployment/v1/server_group_env_var.go @@ -0,0 +1,30 @@ +// +// 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 + +type ServerGroupEnvVars []ServerGroupEnvVar + +type ServerGroupEnvVar struct { + Name string `json:"name" protobuf:"bytes,1,opt,name=name"` + Value string `json:"value,omitempty"` +} diff --git a/pkg/apis/deployment/v1/server_group_spec.go b/pkg/apis/deployment/v1/server_group_spec.go index 010388152..37a6a6876 100644 --- a/pkg/apis/deployment/v1/server_group_spec.go +++ b/pkg/apis/deployment/v1/server_group_spec.go @@ -58,6 +58,10 @@ type ServerGroupSpec struct { Tolerations []core.Toleration `json:"tolerations,omitempty"` // Annotations specified the annotations added to Pods in this group. Annotations map[string]string `json:"annotations,omitempty"` + // Labels specified the labels added to Pods in this group. + Labels map[string]string `json:"labels,omitempty"` + // Envs allow to specify additional envs in this group. + Envs ServerGroupEnvVars `json:"envs,omitempty"` // ServiceAccountName specifies the name of the service account used for Pods in this group. ServiceAccountName *string `json:"serviceAccountName,omitempty"` // NodeSelector speficies a set of selectors for nodes diff --git a/pkg/apis/deployment/v1/zz_generated.deepcopy.go b/pkg/apis/deployment/v1/zz_generated.deepcopy.go index d3580594e..aebb49788 100644 --- a/pkg/apis/deployment/v1/zz_generated.deepcopy.go +++ b/pkg/apis/deployment/v1/zz_generated.deepcopy.go @@ -312,6 +312,13 @@ func (in *DeploymentSpec) DeepCopyInto(out *DeploymentSpec) { (*out)[key] = val } } + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } if in.RestoreFrom != nil { in, out := &in.RestoreFrom, &out.RestoreFrom *out = new(string) @@ -905,6 +912,42 @@ func (in *SecretHashes) DeepCopy() *SecretHashes { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServerGroupEnvVar) DeepCopyInto(out *ServerGroupEnvVar) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerGroupEnvVar. +func (in *ServerGroupEnvVar) DeepCopy() *ServerGroupEnvVar { + if in == nil { + return nil + } + out := new(ServerGroupEnvVar) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in ServerGroupEnvVars) DeepCopyInto(out *ServerGroupEnvVars) { + { + in := &in + *out = make(ServerGroupEnvVars, len(*in)) + copy(*out, *in) + return + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServerGroupEnvVars. +func (in ServerGroupEnvVars) DeepCopy() ServerGroupEnvVars { + if in == nil { + return nil + } + out := new(ServerGroupEnvVars) + in.DeepCopyInto(out) + return *out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServerGroupProbeSpec) DeepCopyInto(out *ServerGroupProbeSpec) { *out = *in @@ -1035,6 +1078,18 @@ func (in *ServerGroupSpec) DeepCopyInto(out *ServerGroupSpec) { (*out)[key] = val } } + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Envs != nil { + in, out := &in.Envs, &out.Envs + *out = make(ServerGroupEnvVars, len(*in)) + copy(*out, *in) + } if in.ServiceAccountName != nil { in, out := &in.ServiceAccountName, &out.ServiceAccountName *out = new(string) diff --git a/pkg/deployment/deployment_inspector.go b/pkg/deployment/deployment_inspector.go index e373f1c74..6aec21e01 100644 --- a/pkg/deployment/deployment_inspector.go +++ b/pkg/deployment/deployment_inspector.go @@ -301,6 +301,10 @@ func (d *Deployment) ensureResources(lastInterval util.Interval, cachedStatus in return minInspectionInterval, errors.Wrapf(err, "Annotation update failed") } + if err := d.resources.EnsureLabels(cachedStatus); err != nil { + return minInspectionInterval, errors.Wrapf(err, "Labels update failed") + } + return lastInterval, nil } diff --git a/pkg/deployment/patch/item.go b/pkg/deployment/patch/item.go index e09ad38a9..3058d6ba1 100644 --- a/pkg/deployment/patch/item.go +++ b/pkg/deployment/patch/item.go @@ -38,8 +38,18 @@ const ( var _ json.Marshaler = &Path{} +func EscapePatchElement(element string) string { + return strings.ReplaceAll(element, "/", "~1") // https://tools.ietf.org/html/rfc6901#section-3 +} + func NewPath(items ...string) Path { - return items + i := make([]string, len(items)) + + for id, item := range items { + i[id] = EscapePatchElement(item) + } + + return i } type Path []string diff --git a/pkg/deployment/reconcile/plan_builder_test.go b/pkg/deployment/reconcile/plan_builder_test.go index 2b472f7e7..419dd1be2 100644 --- a/pkg/deployment/reconcile/plan_builder_test.go +++ b/pkg/deployment/reconcile/plan_builder_test.go @@ -28,6 +28,8 @@ import ( "io/ioutil" "testing" + policy "k8s.io/api/policy/v1beta1" + "github.com/arangodb/kube-arangodb/pkg/deployment/resources/inspector" backupApi "github.com/arangodb/kube-arangodb/pkg/apis/backup/v1" @@ -533,10 +535,12 @@ func TestCreatePlan(t *testing.T) { ExpectedLog string ExpectedEvent *k8sutil.Event - Pods map[string]*core.Pod - Secrets map[string]*core.Secret - Services map[string]*core.Service - PVCS map[string]*core.PersistentVolumeClaim + Pods map[string]*core.Pod + Secrets map[string]*core.Secret + Services map[string]*core.Service + PVCS map[string]*core.PersistentVolumeClaim + ServiceAccounts map[string]*core.ServiceAccount + PDBS map[string]*policy.PodDisruptionBudget }{ { Name: "Can not create plan for single deployment", @@ -796,7 +800,7 @@ func TestCreatePlan(t *testing.T) { if testCase.Helper != nil { testCase.Helper(testCase.context.ArangoDeployment) } - err, _ := r.CreatePlan(ctx, inspector.NewInspectorFromData(testCase.Pods, testCase.Secrets, testCase.PVCS, testCase.Services)) + err, _ := r.CreatePlan(ctx, inspector.NewInspectorFromData(testCase.Pods, testCase.Secrets, testCase.PVCS, testCase.Services, testCase.ServiceAccounts, testCase.PDBS)) // Assert if testCase.ExpectedEvent != nil { diff --git a/pkg/deployment/resources/annotations.go b/pkg/deployment/resources/annotations.go index 7fd06b809..02cb454bf 100644 --- a/pkg/deployment/resources/annotations.go +++ b/pkg/deployment/resources/annotations.go @@ -54,6 +54,7 @@ func (r *Resources) EnsureAnnotations(cachedStatus inspector.Inspector) error { } if err := ensureServiceAccountsAnnotations(kubecli.CoreV1().ServiceAccounts(r.context.GetNamespace()), + cachedStatus, deployment.ArangoDeploymentResourceKind, r.context.GetAPIObject().GetName(), r.context.GetAPIObject().GetNamespace(), @@ -71,6 +72,7 @@ func (r *Resources) EnsureAnnotations(cachedStatus inspector.Inspector) error { } if err := ensurePdbsAnnotations(kubecli.PolicyV1beta1().PodDisruptionBudgets(r.context.GetNamespace()), + cachedStatus, deployment.ArangoDeploymentResourceKind, r.context.GetAPIObject().GetName(), r.context.GetAPIObject().GetNamespace(), @@ -137,22 +139,20 @@ func setSecretAnnotations(client typedCore.SecretInterface, secret *core.Secret, }) } -func ensureServiceAccountsAnnotations(client typedCore.ServiceAccountInterface, kind, name, namespace string, annotations map[string]string) error { - serviceAccounts, err := k8sutil.GetServiceAccountsForParent(client, - kind, - name, - namespace) - if err != nil { - return err - } - - for _, serviceAccount := range serviceAccounts { +func ensureServiceAccountsAnnotations(client typedCore.ServiceAccountInterface, cachedStatus inspector.Inspector, kind, name, namespace string, annotations map[string]string) error { + if err := cachedStatus.IterateServiceAccounts(func(serviceAccount *core.ServiceAccount) error { if !k8sutil.CompareAnnotations(serviceAccount.GetAnnotations(), annotations) { log.Info().Msgf("Replacing annotations for ServiceAccount %s", serviceAccount.Name) - if err = setServiceAccountAnnotations(client, serviceAccount, annotations); err != nil { + if err := setServiceAccountAnnotations(client, serviceAccount, annotations); err != nil { return err } } + + return nil + }, func(serviceAccount *core.ServiceAccount) bool { + return k8sutil.IsChildResource(kind, name, namespace, serviceAccount) + }); err != nil { + return err } return nil @@ -213,22 +213,20 @@ func setServiceAnnotations(client typedCore.ServiceInterface, service *core.Serv }) } -func ensurePdbsAnnotations(client policyTyped.PodDisruptionBudgetInterface, kind, name, namespace string, annotations map[string]string) error { - podDisruptionBudgets, err := k8sutil.GetPDBForParent(client, - kind, - name, - namespace) - if err != nil { - return err - } - - for _, podDisruptionBudget := range podDisruptionBudgets { +func ensurePdbsAnnotations(client policyTyped.PodDisruptionBudgetInterface, cachedStatus inspector.Inspector, kind, name, namespace string, annotations map[string]string) error { + if err := cachedStatus.IteratePodDisruptionBudgets(func(podDisruptionBudget *policy.PodDisruptionBudget) error { if !k8sutil.CompareAnnotations(podDisruptionBudget.GetAnnotations(), annotations) { - log.Info().Msgf("Replacing annotations for PDB %s", podDisruptionBudget.Name) - if err = setPdbAnnotations(client, podDisruptionBudget, annotations); err != nil { + log.Info().Msgf("Replacing annotations for PodDisruptionBudget %s", podDisruptionBudget.Name) + if err := setPdbAnnotations(client, podDisruptionBudget, annotations); err != nil { return err } } + + return nil + }, func(podDisruptionBudget *policy.PodDisruptionBudget) bool { + return k8sutil.IsChildResource(kind, name, namespace, podDisruptionBudget) + }); err != nil { + return err } return nil @@ -289,17 +287,23 @@ func setPvcAnnotations(client typedCore.PersistentVolumeClaimInterface, persiste }) } -func getPodGroup(pod *core.Pod) api.ServerGroup { - if pod.Labels == nil { +func getObjectGroup(obj meta.Object) api.ServerGroup { + l := obj.GetLabels() + if len(l) == 0 { return api.ServerGroupUnknown } - return api.ServerGroupFromRole(pod.Labels[k8sutil.LabelKeyRole]) + group, ok := l[k8sutil.LabelKeyRole] + if !ok { + return api.ServerGroupUnknown + } + + return api.ServerGroupFromRole(group) } func ensurePodsAnnotations(client typedCore.PodInterface, cachedStatus inspector.Inspector, kind, name, namespace string, annotations map[string]string, spec api.DeploymentSpec) error { if err := cachedStatus.IteratePods(func(pod *core.Pod) error { - group := getPodGroup(pod) + group := getObjectGroup(pod) mergedAnnotations := k8sutil.MergeAnnotations(annotations, spec.GetServerGroupSpec(group).Annotations) if !k8sutil.CompareAnnotations(pod.GetAnnotations(), mergedAnnotations) { @@ -336,3 +340,10 @@ func setPodAnnotations(client typedCore.PodInterface, pod *core.Pod, annotations return nil }) } + +func (r *Resources) isChildResource(obj meta.Object) bool { + return k8sutil.IsChildResource(deployment.ArangoDeploymentResourceKind, + r.context.GetAPIObject().GetName(), + r.context.GetAPIObject().GetNamespace(), + obj) +} diff --git a/pkg/deployment/resources/inspector/inspector.go b/pkg/deployment/resources/inspector/inspector.go index f0b2c261c..88e10b8f9 100644 --- a/pkg/deployment/resources/inspector/inspector.go +++ b/pkg/deployment/resources/inspector/inspector.go @@ -24,6 +24,7 @@ package inspector import ( core "k8s.io/api/core/v1" + policy "k8s.io/api/policy/v1beta1" "k8s.io/client-go/kubernetes" ) @@ -48,19 +49,36 @@ func NewInspector(k kubernetes.Interface, namespace string) (Inspector, error) { return nil, err } - return NewInspectorFromData(pods, secrets, pvcs, services), nil + serviceAccounts, err := serviceAccountsToMap(k, namespace) + if err != nil { + return nil, err + } + + podDisruptionBudgets, err := podDisruptionBudgetsToMap(k, namespace) + if err != nil { + return nil, err + } + + return NewInspectorFromData(pods, secrets, pvcs, services, serviceAccounts, podDisruptionBudgets), nil } func NewEmptyInspector() Inspector { - return NewInspectorFromData(nil, nil, nil, nil) + return NewInspectorFromData(nil, nil, nil, nil, nil, nil) } -func NewInspectorFromData(pods map[string]*core.Pod, secrets map[string]*core.Secret, pvcs map[string]*core.PersistentVolumeClaim, services map[string]*core.Service) Inspector { +func NewInspectorFromData(pods map[string]*core.Pod, + secrets map[string]*core.Secret, + pvcs map[string]*core.PersistentVolumeClaim, + services map[string]*core.Service, + serviceAccounts map[string]*core.ServiceAccount, + podDisruptionBudgets map[string]*policy.PodDisruptionBudget) Inspector { return &inspector{ - pods: pods, - secrets: secrets, - pvcs: pvcs, - services: services, + pods: pods, + secrets: secrets, + pvcs: pvcs, + services: services, + serviceAccounts: serviceAccounts, + podDisruptionBudgets: podDisruptionBudgets, } } @@ -76,11 +94,19 @@ type Inspector interface { Service(name string) (*core.Service, bool) IterateServices(action ServiceAction, filters ...ServiceFilter) error + + ServiceAccount(name string) (*core.ServiceAccount, bool) + IterateServiceAccounts(action ServiceAccountAction, filters ...ServiceAccountFilter) error + + PodDisruptionBudget(name string) (*policy.PodDisruptionBudget, bool) + IteratePodDisruptionBudgets(action PodDisruptionBudgetAction, filters ...PodDisruptionBudgetFilter) error } type inspector struct { - pods map[string]*core.Pod - secrets map[string]*core.Secret - pvcs map[string]*core.PersistentVolumeClaim - services map[string]*core.Service + pods map[string]*core.Pod + secrets map[string]*core.Secret + pvcs map[string]*core.PersistentVolumeClaim + services map[string]*core.Service + serviceAccounts map[string]*core.ServiceAccount + podDisruptionBudgets map[string]*policy.PodDisruptionBudget } diff --git a/pkg/deployment/resources/inspector/pdbs.go b/pkg/deployment/resources/inspector/pdbs.go new file mode 100644 index 000000000..a3945e80e --- /dev/null +++ b/pkg/deployment/resources/inspector/pdbs.go @@ -0,0 +1,124 @@ +// +// 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 inspector + +import ( + "github.com/pkg/errors" + policy "k8s.io/api/policy/v1beta1" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +type PodDisruptionBudgetFilter func(podDisruptionBudget *policy.PodDisruptionBudget) bool +type PodDisruptionBudgetAction func(podDisruptionBudget *policy.PodDisruptionBudget) error + +func (i *inspector) IteratePodDisruptionBudgets(action PodDisruptionBudgetAction, filters ...PodDisruptionBudgetFilter) error { + for _, podDisruptionBudget := range i.podDisruptionBudgets { + if err := i.iteratePodDisruptionBudget(podDisruptionBudget, action, filters...); err != nil { + return err + } + } + return nil +} + +func (i *inspector) iteratePodDisruptionBudget(podDisruptionBudget *policy.PodDisruptionBudget, action PodDisruptionBudgetAction, filters ...PodDisruptionBudgetFilter) error { + for _, filter := range filters { + if !filter(podDisruptionBudget) { + return nil + } + } + + return action(podDisruptionBudget) +} + +func (i *inspector) PodDisruptionBudget(name string) (*policy.PodDisruptionBudget, bool) { + podDisruptionBudget, ok := i.podDisruptionBudgets[name] + if !ok { + return nil, false + } + + return podDisruptionBudget, true +} + +func podDisruptionBudgetsToMap(k kubernetes.Interface, namespace string) (map[string]*policy.PodDisruptionBudget, error) { + podDisruptionBudgets, err := getPodDisruptionBudgets(k, namespace, "") + if err != nil { + return nil, err + } + + podDisruptionBudgetMap := map[string]*policy.PodDisruptionBudget{} + + for _, podDisruptionBudget := range podDisruptionBudgets { + _, exists := podDisruptionBudgetMap[podDisruptionBudget.GetName()] + if exists { + return nil, errors.Errorf("PodDisruptionBudget %s already exists in map, error received", podDisruptionBudget.GetName()) + } + + podDisruptionBudgetMap[podDisruptionBudget.GetName()] = podDisruptionBudgetPointer(podDisruptionBudget) + } + + return podDisruptionBudgetMap, nil +} + +func podDisruptionBudgetPointer(podDisruptionBudget policy.PodDisruptionBudget) *policy.PodDisruptionBudget { + return &podDisruptionBudget +} + +func getPodDisruptionBudgets(k kubernetes.Interface, namespace, cont string) ([]policy.PodDisruptionBudget, error) { + podDisruptionBudgets, err := k.PolicyV1beta1().PodDisruptionBudgets(namespace).List(meta.ListOptions{ + Limit: 128, + Continue: cont, + }) + + if err != nil { + return nil, err + } + + if podDisruptionBudgets.Continue != "" { + nextPodDisruptionBudgetsLayer, err := getPodDisruptionBudgets(k, namespace, podDisruptionBudgets.Continue) + if err != nil { + return nil, err + } + + return append(podDisruptionBudgets.Items, nextPodDisruptionBudgetsLayer...), nil + } + + return podDisruptionBudgets.Items, nil +} + +func FilterPodDisruptionBudgetsByLabels(labels map[string]string) PodDisruptionBudgetFilter { + return func(podDisruptionBudget *policy.PodDisruptionBudget) bool { + for key, value := range labels { + v, ok := podDisruptionBudget.Labels[key] + if !ok { + return false + } + + if v != value { + return false + } + } + + return true + } +} diff --git a/pkg/deployment/resources/inspector/sa.go b/pkg/deployment/resources/inspector/sa.go new file mode 100644 index 000000000..59038eeef --- /dev/null +++ b/pkg/deployment/resources/inspector/sa.go @@ -0,0 +1,124 @@ +// +// 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 inspector + +import ( + "github.com/pkg/errors" + core "k8s.io/api/core/v1" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +type ServiceAccountFilter func(serviceAccount *core.ServiceAccount) bool +type ServiceAccountAction func(serviceAccount *core.ServiceAccount) error + +func (i *inspector) IterateServiceAccounts(action ServiceAccountAction, filters ...ServiceAccountFilter) error { + for _, serviceAccount := range i.serviceAccounts { + if err := i.iterateServiceAccount(serviceAccount, action, filters...); err != nil { + return err + } + } + return nil +} + +func (i *inspector) iterateServiceAccount(serviceAccount *core.ServiceAccount, action ServiceAccountAction, filters ...ServiceAccountFilter) error { + for _, filter := range filters { + if !filter(serviceAccount) { + return nil + } + } + + return action(serviceAccount) +} + +func (i *inspector) ServiceAccount(name string) (*core.ServiceAccount, bool) { + serviceAccount, ok := i.serviceAccounts[name] + if !ok { + return nil, false + } + + return serviceAccount, true +} + +func serviceAccountsToMap(k kubernetes.Interface, namespace string) (map[string]*core.ServiceAccount, error) { + serviceAccounts, err := getServiceAccounts(k, namespace, "") + if err != nil { + return nil, err + } + + serviceAccountMap := map[string]*core.ServiceAccount{} + + for _, serviceAccount := range serviceAccounts { + _, exists := serviceAccountMap[serviceAccount.GetName()] + if exists { + return nil, errors.Errorf("ServiceAccount %s already exists in map, error received", serviceAccount.GetName()) + } + + serviceAccountMap[serviceAccount.GetName()] = serviceAccountPointer(serviceAccount) + } + + return serviceAccountMap, nil +} + +func serviceAccountPointer(serviceAccount core.ServiceAccount) *core.ServiceAccount { + return &serviceAccount +} + +func getServiceAccounts(k kubernetes.Interface, namespace, cont string) ([]core.ServiceAccount, error) { + serviceAccounts, err := k.CoreV1().ServiceAccounts(namespace).List(meta.ListOptions{ + Limit: 128, + Continue: cont, + }) + + if err != nil { + return nil, err + } + + if serviceAccounts.Continue != "" { + nextServiceAccountsLayer, err := getServiceAccounts(k, namespace, serviceAccounts.Continue) + if err != nil { + return nil, err + } + + return append(serviceAccounts.Items, nextServiceAccountsLayer...), nil + } + + return serviceAccounts.Items, nil +} + +func FilterServiceAccountsByLabels(labels map[string]string) ServiceAccountFilter { + return func(serviceAccount *core.ServiceAccount) bool { + for key, value := range labels { + v, ok := serviceAccount.Labels[key] + if !ok { + return false + } + + if v != value { + return false + } + } + + return true + } +} diff --git a/pkg/deployment/resources/labels.go b/pkg/deployment/resources/labels.go new file mode 100644 index 000000000..2e5774fcb --- /dev/null +++ b/pkg/deployment/resources/labels.go @@ -0,0 +1,350 @@ +// +// 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 resources + +import ( + "encoding/json" + + "github.com/arangodb/kube-arangodb/pkg/deployment/patch" + "github.com/arangodb/kube-arangodb/pkg/deployment/resources/inspector" + "github.com/arangodb/kube-arangodb/pkg/util/errors" + "github.com/arangodb/kube-arangodb/pkg/util/k8sutil" + core "k8s.io/api/core/v1" + policy "k8s.io/api/policy/v1beta1" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +var ( + reservedLabels = RestrictedList{ + k8sutil.LabelKeyArangoDeployment, + k8sutil.LabelKeyArangoLocalStorage, + k8sutil.LabelKeyApp, + k8sutil.LabelKeyRole, + k8sutil.LabelKeyArangoExporter, + } +) + +func (r *Resources) EnsureLabels(cachedStatus inspector.Inspector) error { + r.log.Info().Msgf("Ensuring labels") + + if err := r.EnsureSecretLabels(cachedStatus); err != nil { + return err + } + + if err := r.EnsureServiceAccountsLabels(cachedStatus); err != nil { + return err + } + + if err := r.EnsureServicesLabels(cachedStatus); err != nil { + return err + } + + if err := r.EnsurePodsLabels(cachedStatus); err != nil { + return err + } + + if err := r.EnsurePersistentVolumeClaimsLabels(cachedStatus); err != nil { + return err + } + + if err := r.EnsurePodDisruptionBudgetsLabels(cachedStatus); err != nil { + return err + } + + return nil +} + +func (r *Resources) EnsureSecretLabels(cachedStatus inspector.Inspector) error { + changed := false + if err := cachedStatus.IterateSecrets(func(secret *core.Secret) error { + if p := ensureLabelsFromMaps(secret, r.context.GetSpec().Labels, r.context.GetSpec().GetServerGroupSpec(getObjectGroup(secret)).Labels); len(p) != 0 { + patch, err := json.Marshal(p) + if err != nil { + return err + } + r.log.Info().Int("changes", len(p)).Msgf("Updating labels for secret %s", secret.GetName()) + + if _, err = r.context.GetKubeCli().CoreV1().Secrets(r.context.GetAPIObject().GetNamespace()).Patch(secret.GetName(), types.JSONPatchType, patch); err != nil { + return err + } + + changed = true + return nil + } + + return nil + }, func(secret *core.Secret) bool { + return r.isChildResource(secret) + }); err != nil { + return err + } + + if changed { + return errors.Reconcile() + } + + return nil +} + +func (r *Resources) EnsureServiceAccountsLabels(cachedStatus inspector.Inspector) error { + changed := false + if err := cachedStatus.IterateServiceAccounts(func(serviceAccount *core.ServiceAccount) error { + if p := ensureLabelsFromMaps(serviceAccount, r.context.GetSpec().Labels, r.context.GetSpec().GetServerGroupSpec(getObjectGroup(serviceAccount)).Labels); len(p) != 0 { + patch, err := json.Marshal(p) + if err != nil { + return err + } + r.log.Info().Int("changes", len(p)).Msgf("Updating labels for ServiceAccount %s", serviceAccount.GetName()) + + if _, err = r.context.GetKubeCli().CoreV1().ServiceAccounts(r.context.GetAPIObject().GetNamespace()).Patch(serviceAccount.GetName(), types.JSONPatchType, patch); err != nil { + return err + } + + changed = true + return nil + } + + return nil + }, func(serviceAccount *core.ServiceAccount) bool { + return r.isChildResource(serviceAccount) + }); err != nil { + return err + } + + if changed { + return errors.Reconcile() + } + + return nil +} + +func (r *Resources) EnsureServicesLabels(cachedStatus inspector.Inspector) error { + changed := false + if err := cachedStatus.IterateServices(func(service *core.Service) error { + if p := ensureLabelsFromMaps(service, r.context.GetSpec().Labels, r.context.GetSpec().GetServerGroupSpec(getObjectGroup(service)).Labels); len(p) != 0 { + patch, err := json.Marshal(p) + if err != nil { + return err + } + r.log.Info().Int("changes", len(p)).Msgf("Updating labels for Service %s", service.GetName()) + + if _, err = r.context.GetKubeCli().CoreV1().Services(r.context.GetAPIObject().GetNamespace()).Patch(service.GetName(), types.JSONPatchType, patch); err != nil { + return err + } + + changed = true + return nil + } + + return nil + }, func(service *core.Service) bool { + return r.isChildResource(service) + }); err != nil { + return err + } + + if changed { + return errors.Reconcile() + } + + return nil +} + +func (r *Resources) EnsurePodsLabels(cachedStatus inspector.Inspector) error { + changed := false + if err := cachedStatus.IteratePods(func(pod *core.Pod) error { + if p := ensureLabelsFromMaps(pod, r.context.GetSpec().Labels, r.context.GetSpec().GetServerGroupSpec(getObjectGroup(pod)).Labels); len(p) != 0 { + patch, err := json.Marshal(p) + if err != nil { + return err + } + r.log.Info().Int("changes", len(p)).Msgf("Updating labels for Pod %s", pod.GetName()) + + if _, err = r.context.GetKubeCli().CoreV1().Pods(r.context.GetAPIObject().GetNamespace()).Patch(pod.GetName(), types.JSONPatchType, patch); err != nil { + return err + } + + changed = true + return nil + } + + return nil + }, func(pod *core.Pod) bool { + return r.isChildResource(pod) + }); err != nil { + return err + } + + if changed { + return errors.Reconcile() + } + + return nil +} + +func (r *Resources) EnsurePersistentVolumeClaimsLabels(cachedStatus inspector.Inspector) error { + changed := false + if err := cachedStatus.IteratePersistentVolumeClaims(func(persistentVolumeClaim *core.PersistentVolumeClaim) error { + if p := ensureLabelsFromMaps(persistentVolumeClaim, r.context.GetSpec().Labels, r.context.GetSpec().GetServerGroupSpec(getObjectGroup(persistentVolumeClaim)).Labels); len(p) != 0 { + patch, err := json.Marshal(p) + if err != nil { + return err + } + r.log.Info().Int("changes", len(p)).Msgf("Updating labels for PersistentVolumeClaim %s", persistentVolumeClaim.GetName()) + + if _, err = r.context.GetKubeCli().CoreV1().PersistentVolumeClaims(r.context.GetAPIObject().GetNamespace()).Patch(persistentVolumeClaim.GetName(), types.JSONPatchType, patch); err != nil { + return err + } + + changed = true + return nil + } + + return nil + }, func(persistentVolumeClaim *core.PersistentVolumeClaim) bool { + return r.isChildResource(persistentVolumeClaim) + }); err != nil { + return err + } + + if changed { + return errors.Reconcile() + } + + return nil +} + +func (r *Resources) EnsurePodDisruptionBudgetsLabels(cachedStatus inspector.Inspector) error { + changed := false + if err := cachedStatus.IteratePodDisruptionBudgets(func(budget *policy.PodDisruptionBudget) error { + if p := ensureLabelsFromMaps(budget, r.context.GetSpec().Labels, r.context.GetSpec().GetServerGroupSpec(getObjectGroup(budget)).Labels); len(p) != 0 { + patch, err := json.Marshal(p) + if err != nil { + return err + } + r.log.Info().Int("changes", len(p)).Msgf("Updating labels for PodDisruptionBudget %s", budget.GetName()) + + if _, err = r.context.GetKubeCli().PolicyV1beta1().PodDisruptionBudgets(r.context.GetAPIObject().GetNamespace()).Patch(budget.GetName(), types.JSONPatchType, patch); err != nil { + return err + } + + changed = true + return nil + } + + return nil + }, func(budget *policy.PodDisruptionBudget) bool { + return r.isChildResource(budget) + }); err != nil { + return err + } + + if changed { + return errors.Reconcile() + } + + return nil +} + +type RestrictedList []string + +func (r RestrictedList) IsRestricted(s string) bool { + for _, i := range r { + if i == s { + return true + } + } + + return false +} + +func ensureLabelsFromMaps(obj meta.Object, labels ...map[string]string) patch.Patch { + m := map[string]string{} + for _, labelMap := range labels { + if len(labelMap) == 0 { + continue + } + for k, v := range labelMap { + m[k] = v + } + } + + return ensureLabels(obj, m) +} + +func ensureLabels(obj meta.Object, labels map[string]string) patch.Patch { + objLabels := obj.GetLabels() + if objLabels == nil { + objLabels = map[string]string{} + } + + if labels == nil { + labels = map[string]string{} + } + + m := ensureMap(objLabels, labels, reservedLabels) + if len(m) == 0 { + return m + } + + // Map not present, we need to fix this + if obj.GetLabels() == nil { + p := patch.NewPatch() + p.ItemAdd(patch.NewPath("metadata", "labels"), map[string]string{}) + p.Add(m...) + return p + } + + return m +} + +func ensureMap(obj map[string]string, labels map[string]string, restricted RestrictedList) patch.Patch { + p := patch.Patch{} + + for k := range obj { + if restricted.IsRestricted(k) { + continue + } + + if _, ok := labels[k]; !ok { + p.ItemRemove(patch.NewPath("metadata", "labels", k)) + } + } + + for k, v := range labels { + if restricted.IsRestricted(k) { + continue + } + + if objV, ok := obj[k]; !ok { + p.ItemAdd(patch.NewPath("metadata", "labels", k), v) + continue + } else if objV != v { + p.ItemReplace(patch.NewPath("metadata", "labels", k), v) + continue + } + } + + return p +} diff --git a/pkg/deployment/resources/pod_creator_arangod.go b/pkg/deployment/resources/pod_creator_arangod.go index b509b24b6..893146a4b 100644 --- a/pkg/deployment/resources/pod_creator_arangod.go +++ b/pkg/deployment/resources/pod_creator_arangod.go @@ -165,6 +165,16 @@ func (a *ArangoDContainer) GetEnvs() []core.EnvVar { } } + if len(a.groupSpec.Envs) > 0 { + for _, env := range a.groupSpec.Envs { + // Do not override preset envs + envs.Add(false, core.EnvVar{ + Name: env.Name, + Value: env.Value, + }) + } + } + return envs.GetEnvList() } diff --git a/pkg/deployment/resources/pod_creator_sync.go b/pkg/deployment/resources/pod_creator_sync.go index 6675c1e6c..0ec3075d7 100644 --- a/pkg/deployment/resources/pod_creator_sync.go +++ b/pkg/deployment/resources/pod_creator_sync.go @@ -126,31 +126,37 @@ func (a *ArangoSyncContainer) GetImage() string { } func (a *ArangoSyncContainer) GetEnvs() []core.EnvVar { - envs := make([]core.EnvVar, 0) + envs := NewEnvBuilder() if a.spec.Sync.Monitoring.GetTokenSecretName() != "" { env := k8sutil.CreateEnvSecretKeySelector(constants.EnvArangoSyncMonitoringToken, a.spec.Sync.Monitoring.GetTokenSecretName(), constants.SecretKeyToken) - envs = append(envs, env) + envs.Add(true, env) } if a.spec.License.HasSecretName() { env := k8sutil.CreateEnvSecretKeySelector(constants.EnvArangoLicenseKey, a.spec.License.GetSecretName(), constants.SecretKeyToken) - envs = append(envs, env) + envs.Add(true, env) } if a.resources.context.GetLifecycleImage() != "" { - envs = append(envs, k8sutil.GetLifecycleEnv()...) + envs.Add(true, k8sutil.GetLifecycleEnv()...) } - if len(envs) > 0 { - return envs + if len(a.groupSpec.Envs) > 0 { + for _, env := range a.groupSpec.Envs { + // Do not override preset envs + envs.Add(false, core.EnvVar{ + Name: env.Name, + Value: env.Value, + }) + } } - return nil + return envs.GetEnvList() } func (m *MemberSyncPod) GetName() string {