diff --git a/pkg/apis/deployment/v1alpha/deployment_spec.go b/pkg/apis/deployment/v1alpha/deployment_spec.go index b37cd36c1..31f9a6f4c 100644 --- a/pkg/apis/deployment/v1alpha/deployment_spec.go +++ b/pkg/apis/deployment/v1alpha/deployment_spec.go @@ -61,6 +61,7 @@ type DeploymentSpec struct { TLS TLSSpec `json:"tls"` Sync SyncSpec `json:"sync"` License LicenseSpec `json:"license"` + Metrics MetricsSpec `json:"metrics"` Single ServerGroupSpec `json:"single"` Agents ServerGroupSpec `json:"agents"` @@ -189,6 +190,7 @@ func (s *DeploymentSpec) SetDefaults(deploymentName string) { s.Coordinators.SetDefaults(ServerGroupCoordinators, s.GetMode().HasCoordinators(), s.GetMode()) s.SyncMasters.SetDefaults(ServerGroupSyncMasters, s.Sync.IsEnabled(), s.GetMode()) s.SyncWorkers.SetDefaults(ServerGroupSyncWorkers, s.Sync.IsEnabled(), s.GetMode()) + s.Metrics.SetDefaults(deploymentName+"-exporter-jwt-token", s.Authentication.IsAuthenticated()) s.Chaos.SetDefaults() s.Bootstrap.SetDefaults(deploymentName) } @@ -228,6 +230,7 @@ func (s *DeploymentSpec) SetDefaultsFrom(source DeploymentSpec) { s.Coordinators.SetDefaultsFrom(source.Coordinators) s.SyncMasters.SetDefaultsFrom(source.SyncMasters) s.SyncWorkers.SetDefaultsFrom(source.SyncWorkers) + s.Metrics.SetDefaultsFrom(source.Metrics) s.Chaos.SetDefaultsFrom(source.Chaos) s.Bootstrap.SetDefaultsFrom(source.Bootstrap) } @@ -283,6 +286,9 @@ func (s *DeploymentSpec) Validate() error { if err := s.SyncWorkers.Validate(ServerGroupSyncWorkers, s.Sync.IsEnabled(), s.GetMode(), s.GetEnvironment()); err != nil { return maskAny(err) } + if err := s.Metrics.Validate(); err != nil { + return maskAny(errors.Wrap(err, "spec.metrics")) + } if err := s.Chaos.Validate(); err != nil { return maskAny(errors.Wrap(err, "spec.chaos")) } @@ -352,5 +358,8 @@ func (s DeploymentSpec) ResetImmutableFields(target *DeploymentSpec) []string { if l := s.SyncWorkers.ResetImmutableFields(ServerGroupSyncWorkers, "syncworkers", &target.SyncWorkers); l != nil { resetFields = append(resetFields, l...) } + if l := s.Metrics.ResetImmutableFields("metrics", &target.Metrics); l != nil { + resetFields = append(resetFields, l...) + } return resetFields } diff --git a/pkg/apis/deployment/v1alpha/metrics_spec.go b/pkg/apis/deployment/v1alpha/metrics_spec.go new file mode 100644 index 000000000..41c316bb5 --- /dev/null +++ b/pkg/apis/deployment/v1alpha/metrics_spec.go @@ -0,0 +1,105 @@ +// +// DISCLAIMER +// +// Copyright 2018 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 v1alpha + +import ( + "github.com/arangodb/kube-arangodb/pkg/util" + "github.com/arangodb/kube-arangodb/pkg/util/k8sutil" +) + +// MetricsAuthenticationSpec contains spec for authentication with arangodb +type MetricsAuthenticationSpec struct { + // JWTTokenSecretName contains the name of the JWT kubernetes secret used for authentication + JWTTokenSecretName *string `json:"jwtTokenSecretName,omitempty"` +} + +// MetricsSpec contains spec for arangodb exporter +type MetricsSpec struct { + Enabled *bool `json:"enabled,omitempty"` + Image *string `json:"image,omitempty"` + Authentication MetricsAuthenticationSpec `json:"authentication,omitempty"` +} + +// IsEnabled returns whether metrics are enabled or not +func (s *MetricsSpec) IsEnabled() bool { + return util.BoolOrDefault(s.Enabled, false) +} + +// HasImage returns whether a image was specified or not +func (s *MetricsSpec) HasImage() bool { + return s.Image != nil +} + +// GetImage returns the Image or empty string +func (s *MetricsSpec) GetImage() string { + return util.StringOrDefault(s.Image) +} + +// SetDefaults sets default values +func (s *MetricsSpec) SetDefaults(defaultTokenName string, isAuthenticated bool) { + if s.Enabled == nil { + s.Enabled = util.NewBool(false) + } + if s.GetJWTTokenSecretName() == "" { + s.Authentication.JWTTokenSecretName = util.NewString(defaultTokenName) + } +} + +// GetJWTTokenSecretName returns the token secret name or empty string +func (s *MetricsSpec) GetJWTTokenSecretName() string { + return util.StringOrDefault(s.Authentication.JWTTokenSecretName) +} + +// HasJWTTokenSecretName returns true if a secret name was specified +func (s *MetricsSpec) HasJWTTokenSecretName() bool { + return s.Authentication.JWTTokenSecretName != nil +} + +// SetDefaultsFrom fills unspecified fields with a value from given source spec. +func (s *MetricsSpec) SetDefaultsFrom(source MetricsSpec) { + if s.Enabled == nil { + s.Enabled = util.NewBoolOrNil(source.Enabled) + } + if s.Image == nil { + s.Image = util.NewStringOrNil(source.Image) + } + if s.Authentication.JWTTokenSecretName == nil { + s.Authentication.JWTTokenSecretName = util.NewStringOrNil(source.Authentication.JWTTokenSecretName) + } +} + +// Validate the given spec +func (s *MetricsSpec) Validate() error { + + if s.HasJWTTokenSecretName() { + if err := k8sutil.ValidateResourceName(s.GetJWTTokenSecretName()); err != nil { + return err + } + } + + return nil +} + +// ResetImmutableFields replaces all immutable fields in the given target with values from the source spec. +func (s MetricsSpec) ResetImmutableFields(fieldPrefix string, target *MetricsSpec) []string { + return nil +} diff --git a/pkg/apis/deployment/v1alpha/server_group.go b/pkg/apis/deployment/v1alpha/server_group.go index 4ccb7f21b..d6ceefe16 100644 --- a/pkg/apis/deployment/v1alpha/server_group.go +++ b/pkg/apis/deployment/v1alpha/server_group.go @@ -130,3 +130,13 @@ func (g ServerGroup) IsArangosync() bool { return false } } + +// IsExportMetrics return true when the group can be used with the arangodbexporter +func (g ServerGroup) IsExportMetrics() bool { + switch g { + case ServerGroupCoordinators, ServerGroupDBServers, ServerGroupSingle: + return true + default: + return false + } +} diff --git a/pkg/apis/deployment/v1alpha/zz_generated.deepcopy.go b/pkg/apis/deployment/v1alpha/zz_generated.deepcopy.go index b626125cd..eec1f9beb 100644 --- a/pkg/apis/deployment/v1alpha/zz_generated.deepcopy.go +++ b/pkg/apis/deployment/v1alpha/zz_generated.deepcopy.go @@ -273,6 +273,7 @@ func (in *DeploymentSpec) DeepCopyInto(out *DeploymentSpec) { in.TLS.DeepCopyInto(&out.TLS) in.Sync.DeepCopyInto(&out.Sync) in.License.DeepCopyInto(&out.License) + in.Metrics.DeepCopyInto(&out.Metrics) in.Single.DeepCopyInto(&out.Single) in.Agents.DeepCopyInto(&out.Agents) in.DBServers.DeepCopyInto(&out.DBServers) @@ -554,6 +555,54 @@ func (in MemberStatusList) DeepCopy() MemberStatusList { return *out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MetricsAuthenticationSpec) DeepCopyInto(out *MetricsAuthenticationSpec) { + *out = *in + if in.JWTTokenSecretName != nil { + in, out := &in.JWTTokenSecretName, &out.JWTTokenSecretName + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MetricsAuthenticationSpec. +func (in *MetricsAuthenticationSpec) DeepCopy() *MetricsAuthenticationSpec { + if in == nil { + return nil + } + out := new(MetricsAuthenticationSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MetricsSpec) DeepCopyInto(out *MetricsSpec) { + *out = *in + if in.Enabled != nil { + in, out := &in.Enabled, &out.Enabled + *out = new(bool) + **out = **in + } + if in.Image != nil { + in, out := &in.Image, &out.Image + *out = new(string) + **out = **in + } + in.Authentication.DeepCopyInto(&out.Authentication) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MetricsSpec. +func (in *MetricsSpec) DeepCopy() *MetricsSpec { + if in == nil { + return nil + } + out := new(MetricsSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *MonitoringSpec) DeepCopyInto(out *MonitoringSpec) { *out = *in diff --git a/pkg/deployment/images.go b/pkg/deployment/images.go index d89267a9c..82e5cceec 100644 --- a/pkg/deployment/images.go +++ b/pkg/deployment/images.go @@ -198,7 +198,7 @@ func (ib *imagesBuilder) fetchArangoDBImageIDAndVersion(ctx context.Context, ima } } if err := k8sutil.CreateArangodPod(ib.KubeCli, true, ib.APIObject, role, id, podName, "", image, "", "", ib.Spec.GetImagePullPolicy(), "", false, terminationGracePeriod, args, env, nil, nil, nil, - tolerations, serviceAccountName, "", "", "", nil, "", v1.ResourceRequirements{}); err != nil { + tolerations, serviceAccountName, "", "", "", nil, "", v1.ResourceRequirements{}, nil); err != nil { log.Debug().Err(err).Msg("Failed to create image ID pod") return true, maskAny(err) } diff --git a/pkg/deployment/reconcile/plan_builder.go b/pkg/deployment/reconcile/plan_builder.go index 4e1dd34b0..7d41cff7e 100644 --- a/pkg/deployment/reconcile/plan_builder.go +++ b/pkg/deployment/reconcile/plan_builder.go @@ -343,6 +343,24 @@ func podNeedsRotation(log zerolog.Logger, p v1.Pod, apiObject metav1.Object, spe return false, "Server Image not found" } + if group.IsExportMetrics() { + e, hasExporter := k8sutil.GetContainerByName(&p, k8sutil.ExporterContainerName) + + if spec.Metrics.IsEnabled() { + if !hasExporter { + return true, "Exporter configuration changed" + } + + if spec.Metrics.HasImage() { + if e.Image != spec.Metrics.GetImage() { + return true, "Exporter image changed" + } + } + } else if hasExporter { + return true, "Exporter was disabled" + } + } + // Check arguments expectedArgs := strings.Join(context.GetExpectedPodArguments(apiObject, spec, group, status.Members.Agents, id, podImageInfo.ArangoDBVersion), " ") actualArgs := strings.Join(getContainerArgs(c), " ") diff --git a/pkg/deployment/resources/pod_creator.go b/pkg/deployment/resources/pod_creator.go index e48a8e802..cc5919ace 100644 --- a/pkg/deployment/resources/pod_creator.go +++ b/pkg/deployment/resources/pod_creator.go @@ -329,6 +329,30 @@ func createArangoSyncArgs(apiObject metav1.Object, spec api.DeploymentSpec, grou return args } +func createExporterArgs(isSecure bool) []string { + tokenpath := filepath.Join(k8sutil.ExporterJWTVolumeMountDir, constants.SecretKeyToken) + options := make([]optionPair, 0, 64) + options = append(options, + optionPair{"--arangodb.jwt-file", tokenpath}, + optionPair{"--arangodb.endpoint", "http://localhost:" + strconv.Itoa(k8sutil.ArangoPort)}, + ) + keyPath := filepath.Join(k8sutil.TLSKeyfileVolumeMountDir, constants.SecretTLSKeyfile) + if isSecure { + options = append(options, + optionPair{"--ssl.keyfile", keyPath}, + ) + } + args := make([]string, 0, 2+len(options)) + sort.Slice(options, func(i, j int) bool { + return options[i].CompareTo(options[j]) < 0 + }) + for _, o := range options { + args = append(args, o.Key+"="+o.Value) + } + + return args +} + // createLivenessProbe creates configuration for a liveness probe of a server in the given group. func (r *Resources) createLivenessProbe(spec api.DeploymentSpec, group api.ServerGroup) (*k8sutil.HTTPProbeConfig, error) { groupspec := spec.GetServerGroupSpec(group) @@ -497,6 +521,16 @@ func (r *Resources) createPodTolerations(group api.ServerGroup, groupSpec api.Se return tolerations } +func createExporterLivenessProbe(isSecure bool) *k8sutil.HTTPProbeConfig { + probeCfg := &k8sutil.HTTPProbeConfig{ + LocalPath: "/", + Port: k8sutil.ArangoExporterPort, + Secure: isSecure, + } + + return probeCfg +} + // createPodForMember creates all Pods listed in member status func (r *Resources) createPodForMember(spec api.DeploymentSpec, memberID string, imageNotFoundOnce *sync.Once) error { kubecli := r.context.GetKubeCli() @@ -604,12 +638,29 @@ func (r *Resources) createPodForMember(spec api.DeploymentSpec, memberID string, } } + var exporter *k8sutil.ArangodbExporterContainerConf + + if spec.Metrics.IsEnabled() { + if group.IsExportMetrics() { + image := spec.GetImage() + if spec.Metrics.HasImage() { + image = spec.Metrics.GetImage() + } + exporter = &k8sutil.ArangodbExporterContainerConf{ + Args: createExporterArgs(spec.IsSecure()), + JWTTokenSecretName: spec.Metrics.GetJWTTokenSecretName(), + LivenessProbe: createExporterLivenessProbe(spec.IsSecure()), + Image: image, + } + } + } + engine := spec.GetStorageEngine().AsArangoArgument() requireUUID := group == api.ServerGroupDBServers && m.IsInitialized finalizers := r.createPodFinalizers(group) if err := k8sutil.CreateArangodPod(kubecli, spec.IsDevelopment(), apiObject, role, m.ID, m.PodName, m.PersistentVolumeClaimName, imageInfo.ImageID, lifecycleImage, alpineImage, spec.GetImagePullPolicy(), engine, requireUUID, terminationGracePeriod, args, env, finalizers, livenessProbe, readinessProbe, tolerations, serviceAccountName, tlsKeyfileSecretName, rocksdbEncryptionSecretName, - clusterJWTSecretName, groupSpec.GetNodeSelector(), groupSpec.PriorityClassName, groupSpec.Resources); err != nil { + clusterJWTSecretName, groupSpec.GetNodeSelector(), groupSpec.PriorityClassName, groupSpec.Resources, exporter); err != nil { return maskAny(err) } log.Debug().Str("pod-name", m.PodName).Msg("Created pod") diff --git a/pkg/deployment/resources/secrets.go b/pkg/deployment/resources/secrets.go index 001669130..392cbbc95 100644 --- a/pkg/deployment/resources/secrets.go +++ b/pkg/deployment/resources/secrets.go @@ -55,6 +55,12 @@ func (r *Resources) EnsureSecrets() error { if err := r.ensureTokenSecret(secrets, spec.Authentication.GetJWTSecretName()); err != nil { return maskAny(err) } + + if spec.Metrics.IsEnabled() { + if err := r.ensureExporterTokenSecret(secrets, spec.Metrics.GetJWTTokenSecretName(), spec.Authentication.GetJWTSecretName()); err != nil { + return maskAny(err) + } + } } if spec.IsSecure() { counterMetric.Inc() @@ -110,6 +116,32 @@ func (r *Resources) ensureTokenSecret(secrets k8sutil.SecretInterface, secretNam return nil } +// ensureExporterTokenSecret checks if a secret with given name exists in the namespace +// of the deployment. If not, it will add such a secret with correct access. +func (r *Resources) ensureExporterTokenSecret(secrets k8sutil.SecretInterface, tokenSecretName, secretSecretName string) error { + if _, err := secrets.Get(tokenSecretName, metav1.GetOptions{}); k8sutil.IsNotFound(err) { + // Secret not found, create it + claims := map[string]interface{}{ + "iss": "arangodb", + "server_id": "exporter", + "allowed_paths": []string{"/_admin/statistics", "/_admin/statistics-description"}, + } + // Create secret + owner := r.context.GetAPIObject().AsOwner() + if err := k8sutil.CreateJWTFromSecret(secrets, tokenSecretName, secretSecretName, claims, &owner); k8sutil.IsAlreadyExists(err) { + // Secret added while we tried it also + return nil + } else if err != nil { + // Failed to create secret + return maskAny(err) + } + } else if err != nil { + // Failed to get secret for other reasons + return maskAny(err) + } + return nil +} + // ensureTLSCACertificateSecret checks if a secret with given name exists in the namespace // of the deployment. If not, it will add such a secret with a generated CA certificate. func (r *Resources) ensureTLSCACertificateSecret(secrets k8sutil.SecretInterface, spec api.TLSSpec) error { diff --git a/pkg/util/k8sutil/constants.go b/pkg/util/k8sutil/constants.go index d4524d201..0ae48e1dd 100644 --- a/pkg/util/k8sutil/constants.go +++ b/pkg/util/k8sutil/constants.go @@ -27,6 +27,7 @@ const ( ArangoPort = 8529 ArangoSyncMasterPort = 8629 ArangoSyncWorkerPort = 8729 + ArangoExporterPort = 9101 // K8s constants ClusterIPNone = "None" diff --git a/pkg/util/k8sutil/pods.go b/pkg/util/k8sutil/pods.go index a450be61a..da52661fd 100644 --- a/pkg/util/k8sutil/pods.go +++ b/pkg/util/k8sutil/pods.go @@ -40,6 +40,7 @@ const ( InitDataContainerName = "init-data" InitLifecycleContainerName = "init-lifecycle" ServerContainerName = "server" + ExporterContainerName = "exporter" arangodVolumeName = "arangod-data" tlsKeyfileVolumeName = "tls-keyfile" lifecycleVolumeName = "lifecycle" @@ -47,6 +48,7 @@ const ( clusterJWTSecretVolumeName = "cluster-jwt" masterJWTSecretVolumeName = "master-jwt" rocksdbEncryptionVolumeName = "rocksdb-encryption" + exporterJWTVolumeName = "exporter-jwt" ArangodVolumeMountDir = "/data" RocksDBEncryptionVolumeMountDir = "/secrets/rocksdb/encryption" JWTSecretFileVolumeMountDir = "/secrets/jwt" @@ -54,6 +56,7 @@ const ( LifecycleVolumeMountDir = "/lifecycle/tools" ClientAuthCAVolumeMountDir = "/secrets/client-auth/ca" ClusterJWTSecretVolumeMountDir = "/secrets/cluster/jwt" + ExporterJWTVolumeMountDir = "/secrets/exporter/jwt" MasterJWTSecretVolumeMountDir = "/secrets/master/jwt" ) @@ -91,16 +94,45 @@ func IsPodReady(pod *v1.Pod) bool { return condition != nil && condition.Status == v1.ConditionTrue } -// IsPodSucceeded returns true if all containers of the pod -// have terminated with exit code 0. +// IsPodSucceeded returns true if the arangodb container of the pod +// has terminated with exit code 0. func IsPodSucceeded(pod *v1.Pod) bool { - return pod.Status.Phase == v1.PodSucceeded + if pod.Status.Phase == v1.PodSucceeded { + return true + } else { + for _, c := range pod.Status.ContainerStatuses { + if c.Name != ServerContainerName { + continue + } + + t := c.State.Terminated + if t != nil { + return t.ExitCode == 0 + } + } + return false + } } -// IsPodFailed returns true if all containers of the pod -// have terminated and at least one of them wih a non-zero exit code. +// IsPodFailed returns true if the arangodb container of the pod +// has terminated wih a non-zero exit code. func IsPodFailed(pod *v1.Pod) bool { - return pod.Status.Phase == v1.PodFailed + if pod.Status.Phase == v1.PodFailed { + return true + } else { + for _, c := range pod.Status.ContainerStatuses { + if c.Name != ServerContainerName { + continue + } + + t := c.State.Terminated + if t != nil { + return t.ExitCode != 0 + } + } + + return false + } } // IsPodScheduled returns true if the pod has been scheduled. @@ -221,6 +253,15 @@ func clusterJWTVolumeMounts() []v1.VolumeMount { } } +func exporterJWTVolumeMounts() []v1.VolumeMount { + return []v1.VolumeMount{ + { + Name: exporterJWTVolumeName, + MountPath: ExporterJWTVolumeMountDir, + }, + } +} + // rocksdbEncryptionVolumeMounts creates a volume mount structure for a RocksDB encryption key. func rocksdbEncryptionVolumeMounts() []v1.VolumeMount { return []v1.VolumeMount{ @@ -351,6 +392,29 @@ func arangosyncContainer(image string, imagePullPolicy v1.PullPolicy, args []str return c } +func arangodbexporterContainer(image string, imagePullPolicy v1.PullPolicy, args []string, env map[string]EnvValue, livenessProbe *HTTPProbeConfig) v1.Container { + c := v1.Container{ + Command: append([]string{"/app/arangodb-exporter"}, args...), + Name: ExporterContainerName, + Image: image, + ImagePullPolicy: v1.PullIfNotPresent, + Ports: []v1.ContainerPort{ + { + Name: "exporter", + ContainerPort: int32(ArangoExporterPort), + Protocol: v1.ProtocolTCP, + }, + }, + } + for k, v := range env { + c.Env = append(c.Env, v.CreateEnvVar(k)) + } + if livenessProbe != nil { + c.LivenessProbe = livenessProbe.Create() + } + return c +} + // newLifecycle creates a lifecycle structure with preStop handler. func newLifecycle() (*v1.Lifecycle, []v1.EnvVar, []v1.Volume, error) { binaryPath, err := os.Executable() @@ -448,6 +512,15 @@ func newPod(deploymentName, ns, role, id, podName string, finalizers []string, t return p } +// ArangodbExporterContainerConf contains configuration of the exporter container +type ArangodbExporterContainerConf struct { + Args []string + Env map[string]EnvValue + JWTTokenSecretName string + LivenessProbe *HTTPProbeConfig + Image string +} + // CreateArangodPod creates a Pod that runs `arangod`. // If the pod already exists, nil is returned. // If another error occurs, that error is returned. @@ -457,8 +530,7 @@ func CreateArangodPod(kubecli kubernetes.Interface, developmentMode bool, deploy args []string, env map[string]EnvValue, finalizers []string, livenessProbe *HTTPProbeConfig, readinessProbe *HTTPProbeConfig, tolerations []v1.Toleration, serviceAccountName string, tlsKeyfileSecretName, rocksdbEncryptionSecretName string, clusterJWTSecretName string, nodeSelector map[string]string, - podPriorityClassName string, resources v1.ResourceRequirements) error { - + podPriorityClassName string, resources v1.ResourceRequirements, exporter *ArangodbExporterContainerConf) error { // Prepare basic pod p := newPod(deployment.GetName(), deployment.GetNamespace(), role, id, podName, finalizers, tolerations, serviceAccountName, nodeSelector) terminationGracePeriodSeconds := int64(math.Ceil(terminationGracePeriod.Seconds())) @@ -491,8 +563,22 @@ func CreateArangodPod(kubecli kubernetes.Interface, developmentMode bool, deploy if clusterJWTSecretName != "" { c.VolumeMounts = append(c.VolumeMounts, clusterJWTVolumeMounts()...) } + p.Spec.Containers = append(p.Spec.Containers, c) + // Add arangodb exporter container + if exporter != nil { + c = arangodbexporterContainer(exporter.Image, imagePullPolicy, exporter.Args, exporter.Env, exporter.LivenessProbe) + if exporter.JWTTokenSecretName != "" { + c.VolumeMounts = append(c.VolumeMounts, exporterJWTVolumeMounts()...) + } + if tlsKeyfileSecretName != "" { + c.VolumeMounts = append(c.VolumeMounts, tlsKeyfileVolumeMounts()...) + } + p.Spec.Containers = append(p.Spec.Containers, c) + p.Labels[LabelKeyArangoExporter] = "yes" + } + // Add priorityClassName p.Spec.PriorityClassName = podPriorityClassName @@ -550,6 +636,19 @@ func CreateArangodPod(kubecli kubernetes.Interface, developmentMode bool, deploy p.Spec.Volumes = append(p.Spec.Volumes, vol) } + // Exporter Token Mount + if exporter != nil && exporter.JWTTokenSecretName != "" { + vol := v1.Volume{ + Name: exporterJWTVolumeName, + VolumeSource: v1.VolumeSource{ + Secret: &v1.SecretVolumeSource{ + SecretName: exporter.JWTTokenSecretName, + }, + }, + } + p.Spec.Volumes = append(p.Spec.Volumes, vol) + } + // Cluster JWT secret mount (if any) if clusterJWTSecretName != "" { vol := v1.Volume{ diff --git a/pkg/util/k8sutil/secrets.go b/pkg/util/k8sutil/secrets.go index 2c8b94769..0a510dd61 100644 --- a/pkg/util/k8sutil/secrets.go +++ b/pkg/util/k8sutil/secrets.go @@ -29,6 +29,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/arangodb/kube-arangodb/pkg/util/constants" + jg "github.com/dgrijalva/jwt-go" ) // SecretInterface has methods to work with Secret resources. @@ -248,6 +249,27 @@ func CreateTokenSecret(secrets SecretInterface, secretName, token string, ownerR return nil } +// CreateJWTFromSecret creates a JWT using the secret stored in secretSecretName and stores the +// result in a new secret called tokenSecretName +func CreateJWTFromSecret(secrets SecretInterface, tokenSecretName, secretSecretName string, claims map[string]interface{}, ownerRef *metav1.OwnerReference) error { + + secret, err := GetTokenSecret(secrets, secretSecretName) + if err != nil { + return maskAny(err) + } + // Create a new token object, specifying signing method and the claims + // you would like it to contain. + token := jg.NewWithClaims(jg.SigningMethodHS256, jg.MapClaims(claims)) + + // Sign and get the complete encoded token as a string using the secret + signedToken, err := token.SignedString([]byte(secret)) + if err != nil { + return maskAny(err) + } + + return CreateTokenSecret(secrets, tokenSecretName, signedToken, ownerRef) +} + // CreateBasicAuthSecret creates a secret with given name in given namespace // with a given username and password as value. func CreateBasicAuthSecret(secrets SecretInterface, secretName, username, password string, ownerRef *metav1.OwnerReference) error { diff --git a/pkg/util/k8sutil/util.go b/pkg/util/k8sutil/util.go index ac1273fab..0e6f94576 100644 --- a/pkg/util/k8sutil/util.go +++ b/pkg/util/k8sutil/util.go @@ -36,6 +36,8 @@ const ( LabelKeyApp = "app" // LabelKeyRole is the key of the label used to store the role of the resource in LabelKeyRole = "role" + // LabelKeyArangoExporter is the key of the label used to indicate that a exporter is present + LabelKeyArangoExporter = "arango_exporter" // AppName is the fixed value for the "app" label AppName = "arangodb"