Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions pkg/apis/deployment/v1alpha/deployment_spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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"))
}
Expand Down Expand Up @@ -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
}
105 changes: 105 additions & 0 deletions pkg/apis/deployment/v1alpha/metrics_spec.go
Original file line number Diff line number Diff line change
@@ -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
}
10 changes: 10 additions & 0 deletions pkg/apis/deployment/v1alpha/server_group.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
49 changes: 49 additions & 0 deletions pkg/apis/deployment/v1alpha/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pkg/deployment/images.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
18 changes: 18 additions & 0 deletions pkg/deployment/reconcile/plan_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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), " ")
Expand Down
53 changes: 52 additions & 1 deletion pkg/deployment/resources/pod_creator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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")
Expand Down
32 changes: 32 additions & 0 deletions pkg/deployment/resources/secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions pkg/util/k8sutil/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const (
ArangoPort = 8529
ArangoSyncMasterPort = 8629
ArangoSyncWorkerPort = 8729
ArangoExporterPort = 9101

// K8s constants
ClusterIPNone = "None"
Expand Down
Loading