From 4650dec94d4ab348be75a3caa8ca6cafaaeb03e9 Mon Sep 17 00:00:00 2001 From: ajanikow <12255597+ajanikow@users.noreply.github.com> Date: Wed, 22 Oct 2025 14:43:45 +0000 Subject: [PATCH 1/9] [Feature] [License] Master Management --- CHANGELOG.md | 1 + docs/api/ArangoDeployment.V1.md | 36 ++++- docs/generated/actions.md | 2 + internal/actions.yaml | 2 + pkg/apis/deployment/v1/actions.generated.go | 12 ++ pkg/apis/deployment/v1/deployment_status.go | 11 +- pkg/apis/deployment/v1/image_info.go | 11 +- pkg/apis/deployment/v1/license_spec.go | 92 +++++++++++- .../deployment/v1/zz_generated.deepcopy.go | 15 ++ .../deployment/v2alpha1/actions.generated.go | 12 ++ .../deployment/v2alpha1/deployment_status.go | 11 +- pkg/apis/deployment/v2alpha1/image_info.go | 11 +- pkg/apis/deployment/v2alpha1/license_spec.go | 92 +++++++++++- .../v2alpha1/zz_generated.deepcopy.go | 15 ++ .../database-deployment.schema.generated.yaml | 28 +++- pkg/deployment/client/license.go | 14 ++ .../reconcile/action.register.generated.go | 17 +++ .../action.register.generated_test.go | 10 ++ .../reconcile/action_license_generate.go | 119 +++++++++++++++ ...n_set_license.go => action_license_set.go} | 2 +- .../reconcile/plan_builder_license.go | 70 ++++++++- pkg/license/license.community.go | 45 ------ pkg/license/license.go | 141 ------------------ pkg/license/loader.go | 29 ---- pkg/license/loader_arangodeployment.go | 87 ----------- pkg/license/loader_test.go | 35 ----- .../manager => license_manager}/client.go | 15 +- .../manager => license_manager}/registry.go | 2 +- .../manager => license_manager}/stage.go | 2 +- .../inventory/fetcher.deployment.id.go | 4 +- pkg/platform/inventory/inventory.go | 42 ++++++ pkg/platform/license.go | 40 +---- pkg/platform/license_activate.go | 6 +- pkg/platform/license_generate.go | 4 +- pkg/platform/license_secret.go | 4 +- pkg/util/cli/lm.go | 10 +- pkg/util/constants/constants.go | 10 +- pkg/util/k8sutil/license.go | 25 +++- 38 files changed, 653 insertions(+), 431 deletions(-) create mode 100644 pkg/deployment/reconcile/action_license_generate.go rename pkg/deployment/reconcile/{action_set_license.go => action_license_set.go} (97%) delete mode 100644 pkg/license/license.community.go delete mode 100644 pkg/license/license.go delete mode 100644 pkg/license/loader.go delete mode 100644 pkg/license/loader_arangodeployment.go delete mode 100644 pkg/license/loader_test.go rename pkg/{license/manager => license_manager}/client.go (87%) rename pkg/{license/manager => license_manager}/registry.go (98%) rename pkg/{license/manager => license_manager}/stage.go (98%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e796f494..29a344689 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - (Feature) (Platform) EventsV1 Integration - (Feature) (Platform) Allows to opt out in the Inventory Telemetry - (Feature) Simplify Operator ID Process +- (Feature) (License) Activation API Integration ## [1.3.1](https://github.com/arangodb/kube-arangodb/tree/1.3.1) (2025-10-07) - (Documentation) Add ArangoPlatformStorage Docs & Examples diff --git a/docs/api/ArangoDeployment.V1.md b/docs/api/ArangoDeployment.V1.md index f88555cef..9d1a25f92 100644 --- a/docs/api/ArangoDeployment.V1.md +++ b/docs/api/ArangoDeployment.V1.md @@ -4791,16 +4791,48 @@ Possible Values: *** +### .spec.license.expirationGracePeriod + +Type: `integer` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.3.1/pkg/apis/deployment/v1/license_spec.go#L69) + +ExpirationGracePeriod defines the expiration grace period for the license + +Default Value: `72h` + +*** + +### .spec.license.mode + +Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.3.1/pkg/apis/deployment/v1/license_spec.go#L61) + +Mode Defines the mode of license + +Possible Values: +* `"key"` (default) - Use License Key mechanism +* `"master"` - Use License Master Key mechanism + +*** + ### .spec.license.secretName -Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.3.1/pkg/apis/deployment/v1/license_spec.go#L33) +Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.3.1/pkg/apis/deployment/v1/license_spec.go#L55) SecretName setting specifies the name of a kubernetes `Secret` that contains -the license key token used for enterprise images. This value is not used for +the license key token or master key used for enterprise images. This value is not used for the Community Edition. *** +### .spec.license.ttl + +Type: `integer` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.3.1/pkg/apis/deployment/v1/license_spec.go#L65) + +TTL Sets the requested License TTL + +Default Value: `336h` + +*** + ### .spec.lifecycle.resources Type: `core.ResourceRequirements` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.3.1/pkg/apis/deployment/v1/lifecycle_spec.go#L31) diff --git a/docs/generated/actions.md b/docs/generated/actions.md index 3a1445a21..d860e7d13 100644 --- a/docs/generated/actions.md +++ b/docs/generated/actions.md @@ -47,6 +47,7 @@ nav_order: 11 | JWTSetActive | no | 10m0s | no | Enterprise Only | Change active JWT key on the cluster | | JWTStatusUpdate | no | 10m0s | no | Enterprise Only | Update status of JWT propagation | | KillMemberPod | no | 10m0s | no | Community & Enterprise | Execute Delete on Pod (put pod in Terminating state) | +| LicenseGenerate | no | 10m0s | no | Community & Enterprise | Generates License using ArangoDB LicenseManager Endpoint | | LicenseSet | no | 10m0s | no | Community & Enterprise | Update Cluster license (3.9+) | | MarkToRemoveMember | no | 10m0s | no | Community & Enterprise | Marks member to be removed. Used when member Pod is annotated with replace annotation | | MemberPhaseUpdate | no | 10m0s | no | Community & Enterprise | Change member phase | @@ -146,6 +147,7 @@ spec: JWTSetActive: 10m0s JWTStatusUpdate: 10m0s KillMemberPod: 10m0s + LicenseGenerate: 10m0s LicenseSet: 10m0s MarkToRemoveMember: 10m0s MemberPhaseUpdate: 10m0s diff --git a/internal/actions.yaml b/internal/actions.yaml index 24960c560..160d104f1 100644 --- a/internal/actions.yaml +++ b/internal/actions.yaml @@ -247,6 +247,8 @@ actions: - High LicenseSet: description: Update Cluster license (3.9+) + LicenseGenerate: + description: Generates License using ArangoDB LicenseManager Endpoint RuntimeContainerImageUpdate: description: Update Container Image in runtime RuntimeContainerSyncTolerations: diff --git a/pkg/apis/deployment/v1/actions.generated.go b/pkg/apis/deployment/v1/actions.generated.go index b37feb102..8508dd375 100644 --- a/pkg/apis/deployment/v1/actions.generated.go +++ b/pkg/apis/deployment/v1/actions.generated.go @@ -131,6 +131,9 @@ const ( // ActionKillMemberPodDefaultTimeout define default timeout for action ActionKillMemberPod ActionKillMemberPodDefaultTimeout time.Duration = ActionsDefaultTimeout + // ActionLicenseGenerateDefaultTimeout define default timeout for action ActionLicenseGenerate + ActionLicenseGenerateDefaultTimeout time.Duration = ActionsDefaultTimeout + // ActionLicenseSetDefaultTimeout define default timeout for action ActionLicenseSet ActionLicenseSetDefaultTimeout time.Duration = ActionsDefaultTimeout @@ -401,6 +404,9 @@ const ( // ActionTypeKillMemberPod in scopes High and Normal. Execute Delete on Pod (put pod in Terminating state) ActionTypeKillMemberPod ActionType = "KillMemberPod" + // ActionTypeLicenseGenerate in scopes Normal. Generates License using ArangoDB LicenseManager Endpoint + ActionTypeLicenseGenerate ActionType = "LicenseGenerate" + // ActionTypeLicenseSet in scopes Normal. Update Cluster license (3.9+) ActionTypeLicenseSet ActionType = "LicenseSet" @@ -639,6 +645,8 @@ func (a ActionType) DefaultTimeout() time.Duration { return ActionJWTStatusUpdateDefaultTimeout case ActionTypeKillMemberPod: return ActionKillMemberPodDefaultTimeout + case ActionTypeLicenseGenerate: + return ActionLicenseGenerateDefaultTimeout case ActionTypeLicenseSet: return ActionLicenseSetDefaultTimeout case ActionTypeMarkToRemoveMember: @@ -823,6 +831,8 @@ func (a ActionType) Priority() ActionPriority { return ActionPriorityNormal case ActionTypeKillMemberPod: return ActionPriorityHigh + case ActionTypeLicenseGenerate: + return ActionPriorityNormal case ActionTypeLicenseSet: return ActionPriorityNormal case ActionTypeMarkToRemoveMember: @@ -1033,6 +1043,8 @@ func (a ActionType) Optional() bool { return false case ActionTypeKillMemberPod: return false + case ActionTypeLicenseGenerate: + return false case ActionTypeLicenseSet: return false case ActionTypeMarkToRemoveMember: diff --git a/pkg/apis/deployment/v1/deployment_status.go b/pkg/apis/deployment/v1/deployment_status.go index 6f422ff8c..4068610be 100644 --- a/pkg/apis/deployment/v1/deployment_status.go +++ b/pkg/apis/deployment/v1/deployment_status.go @@ -1,7 +1,7 @@ // // DISCLAIMER // -// Copyright 2016-2024 ArangoDB GmbH, Cologne, Germany +// Copyright 2016-2025 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. @@ -53,6 +53,7 @@ type DeploymentStatus struct { // Images holds a list of ArangoDB images with their ID and ArangoDB version. Images ImageInfoList `json:"arangodb-images,omitempty"` + // Image that is currently being used when new pods are created CurrentImage *ImageInfo `json:"current-image,omitempty"` @@ -147,6 +148,14 @@ func (ds *DeploymentStatus) IsPlanEmpty() bool { return ds.Plan.IsEmpty() && ds.HighPriorityPlan.IsEmpty() } +func (ds *DeploymentStatus) IsEnterprise() bool { + if ds == nil { + return false + } + + return ds.CurrentImage.IsEnterprise() +} + func (ds *DeploymentStatus) NonInternalActions() int { return ds.Plan.NonInternalActions() + ds.HighPriorityPlan.NonInternalActions() } diff --git a/pkg/apis/deployment/v1/image_info.go b/pkg/apis/deployment/v1/image_info.go index ed5f0265d..17e965dab 100644 --- a/pkg/apis/deployment/v1/image_info.go +++ b/pkg/apis/deployment/v1/image_info.go @@ -1,7 +1,7 @@ // // DISCLAIMER // -// Copyright 2016-2022 ArangoDB GmbH, Cologne, Germany +// Copyright 2016-2025 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. @@ -48,6 +48,15 @@ func (i *ImageInfo) String() string { return fmt.Sprintf("ArangoDB %s %s (%s)", e, string(i.ArangoDBVersion), i.Image) } +// IsEnterprise returns true if image is enterprise +func (i *ImageInfo) IsEnterprise() bool { + if i == nil { + return false + } + + return i.Enterprise +} + // ImageInfoList is a list of image infos type ImageInfoList []ImageInfo diff --git a/pkg/apis/deployment/v1/license_spec.go b/pkg/apis/deployment/v1/license_spec.go index af8fbb37f..e7bb689ab 100644 --- a/pkg/apis/deployment/v1/license_spec.go +++ b/pkg/apis/deployment/v1/license_spec.go @@ -1,7 +1,7 @@ // // DISCLAIMER // -// Copyright 2016-2023 ArangoDB GmbH, Cologne, Germany +// Copyright 2016-2025 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. @@ -21,16 +21,52 @@ package v1 import ( + "time" + + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + shared "github.com/arangodb/kube-arangodb/pkg/apis/shared" "github.com/arangodb/kube-arangodb/pkg/util" + "github.com/arangodb/kube-arangodb/pkg/util/errors" +) + +const ( + DefaultLicenseExpirationGracePeriod = 3 * 24 * time.Hour + DefaultLicenseTTL = 14 * 24 * time.Hour ) +type LicenseMode string + +const ( + LicenseModeDefault = LicenseModeKey + LicenseModeKey LicenseMode = "key" + LicenseModeMaster LicenseMode = "master" +) + +func (l *LicenseMode) Get() LicenseMode { + return util.OptionalType(l, LicenseModeDefault) +} + // LicenseSpec holds the license related information type LicenseSpec struct { // SecretName setting specifies the name of a kubernetes `Secret` that contains - // the license key token used for enterprise images. This value is not used for + // the license key token or master key used for enterprise images. This value is not used for // the Community Edition. SecretName *string `json:"secretName,omitempty"` + + // Mode Defines the mode of license + // +doc/default: key + // +doc/enum: key|Use License Key mechanism + // +doc/enum: master|Use License Master Key mechanism + Mode *LicenseMode `json:"mode,omitempty"` + + // TTL Sets the requested License TTL + // +doc/default: 336h + TTL *meta.Duration `json:"ttl,omitempty"` + + // ExpirationGracePeriod defines the expiration grace period for the license + // +doc/default: 72h + ExpirationGracePeriod *meta.Duration `json:"expirationGracePeriod,omitempty"` } // HasSecretName returns true if a license key secret name was set @@ -43,20 +79,60 @@ func (s LicenseSpec) GetSecretName() string { return util.TypeOrDefault[string](s.SecretName) } +// GetTTL returns the license TTL +func (s LicenseSpec) GetTTL() time.Duration { + if s.TTL == nil { + return DefaultLicenseTTL + } + return s.TTL.Duration +} + +// GetExpirationGracePeriod returns the expiration period +func (s LicenseSpec) GetExpirationGracePeriod() time.Duration { + if s.ExpirationGracePeriod == nil { + return DefaultLicenseExpirationGracePeriod + } + return s.ExpirationGracePeriod.Duration +} + // Validate validates the LicenseSpec func (s LicenseSpec) Validate() error { - if s.HasSecretName() { - if err := shared.ValidateResourceName(s.GetSecretName()); err != nil { - return err - } + if !s.HasSecretName() { + return nil } + return shared.WithErrors( + // Secret + shared.PrefixResourceErrorFunc("secretName", func() error { + return shared.ValidateResourceName(s.GetSecretName()) + }), + // Expiration + shared.PrefixResourceErrorFunc("expirationGracePeriod", func() error { + if s.GetExpirationGracePeriod() <= 0 { + return errors.Errorf("Expiration grace period must be greater than zero") + } + + if s.GetExpirationGracePeriod() >= s.GetTTL() { + return errors.Errorf("Expiration grace period must be less than TTL") + } + + return nil + }), + // TTL + shared.PrefixResourceErrorFunc("ttl", func() error { + if s.GetTTL() <= 0 { + return errors.Errorf("TTL must be greater than zero") + } - return nil + return nil + }), + ) } // SetDefaultsFrom fills all values not set in s with values from other func (s *LicenseSpec) SetDefaultsFrom(other LicenseSpec) { if !s.HasSecretName() { - s.SecretName = util.NewTypeOrNil[string](other.SecretName) + s.SecretName = util.NewTypeOrNil(other.SecretName) } + s.TTL = util.NewTypeOrNil(other.TTL) + s.ExpirationGracePeriod = util.NewTypeOrNil(other.ExpirationGracePeriod) } diff --git a/pkg/apis/deployment/v1/zz_generated.deepcopy.go b/pkg/apis/deployment/v1/zz_generated.deepcopy.go index 8c3d5b677..117e7ef21 100644 --- a/pkg/apis/deployment/v1/zz_generated.deepcopy.go +++ b/pkg/apis/deployment/v1/zz_generated.deepcopy.go @@ -1869,6 +1869,21 @@ func (in *LicenseSpec) DeepCopyInto(out *LicenseSpec) { *out = new(string) **out = **in } + if in.Mode != nil { + in, out := &in.Mode, &out.Mode + *out = new(LicenseMode) + **out = **in + } + if in.TTL != nil { + in, out := &in.TTL, &out.TTL + *out = new(metav1.Duration) + **out = **in + } + if in.ExpirationGracePeriod != nil { + in, out := &in.ExpirationGracePeriod, &out.ExpirationGracePeriod + *out = new(metav1.Duration) + **out = **in + } return } diff --git a/pkg/apis/deployment/v2alpha1/actions.generated.go b/pkg/apis/deployment/v2alpha1/actions.generated.go index 854a3f8f3..6c56dd633 100644 --- a/pkg/apis/deployment/v2alpha1/actions.generated.go +++ b/pkg/apis/deployment/v2alpha1/actions.generated.go @@ -131,6 +131,9 @@ const ( // ActionKillMemberPodDefaultTimeout define default timeout for action ActionKillMemberPod ActionKillMemberPodDefaultTimeout time.Duration = ActionsDefaultTimeout + // ActionLicenseGenerateDefaultTimeout define default timeout for action ActionLicenseGenerate + ActionLicenseGenerateDefaultTimeout time.Duration = ActionsDefaultTimeout + // ActionLicenseSetDefaultTimeout define default timeout for action ActionLicenseSet ActionLicenseSetDefaultTimeout time.Duration = ActionsDefaultTimeout @@ -401,6 +404,9 @@ const ( // ActionTypeKillMemberPod in scopes High and Normal. Execute Delete on Pod (put pod in Terminating state) ActionTypeKillMemberPod ActionType = "KillMemberPod" + // ActionTypeLicenseGenerate in scopes Normal. Generates License using ArangoDB LicenseManager Endpoint + ActionTypeLicenseGenerate ActionType = "LicenseGenerate" + // ActionTypeLicenseSet in scopes Normal. Update Cluster license (3.9+) ActionTypeLicenseSet ActionType = "LicenseSet" @@ -639,6 +645,8 @@ func (a ActionType) DefaultTimeout() time.Duration { return ActionJWTStatusUpdateDefaultTimeout case ActionTypeKillMemberPod: return ActionKillMemberPodDefaultTimeout + case ActionTypeLicenseGenerate: + return ActionLicenseGenerateDefaultTimeout case ActionTypeLicenseSet: return ActionLicenseSetDefaultTimeout case ActionTypeMarkToRemoveMember: @@ -823,6 +831,8 @@ func (a ActionType) Priority() ActionPriority { return ActionPriorityNormal case ActionTypeKillMemberPod: return ActionPriorityHigh + case ActionTypeLicenseGenerate: + return ActionPriorityNormal case ActionTypeLicenseSet: return ActionPriorityNormal case ActionTypeMarkToRemoveMember: @@ -1033,6 +1043,8 @@ func (a ActionType) Optional() bool { return false case ActionTypeKillMemberPod: return false + case ActionTypeLicenseGenerate: + return false case ActionTypeLicenseSet: return false case ActionTypeMarkToRemoveMember: diff --git a/pkg/apis/deployment/v2alpha1/deployment_status.go b/pkg/apis/deployment/v2alpha1/deployment_status.go index b348225f5..d983c2489 100644 --- a/pkg/apis/deployment/v2alpha1/deployment_status.go +++ b/pkg/apis/deployment/v2alpha1/deployment_status.go @@ -1,7 +1,7 @@ // // DISCLAIMER // -// Copyright 2016-2024 ArangoDB GmbH, Cologne, Germany +// Copyright 2016-2025 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. @@ -53,6 +53,7 @@ type DeploymentStatus struct { // Images holds a list of ArangoDB images with their ID and ArangoDB version. Images ImageInfoList `json:"arangodb-images,omitempty"` + // Image that is currently being used when new pods are created CurrentImage *ImageInfo `json:"current-image,omitempty"` @@ -147,6 +148,14 @@ func (ds *DeploymentStatus) IsPlanEmpty() bool { return ds.Plan.IsEmpty() && ds.HighPriorityPlan.IsEmpty() } +func (ds *DeploymentStatus) IsEnterprise() bool { + if ds == nil { + return false + } + + return ds.CurrentImage.IsEnterprise() +} + func (ds *DeploymentStatus) NonInternalActions() int { return ds.Plan.NonInternalActions() + ds.HighPriorityPlan.NonInternalActions() } diff --git a/pkg/apis/deployment/v2alpha1/image_info.go b/pkg/apis/deployment/v2alpha1/image_info.go index 761e9df70..be60cafdf 100644 --- a/pkg/apis/deployment/v2alpha1/image_info.go +++ b/pkg/apis/deployment/v2alpha1/image_info.go @@ -1,7 +1,7 @@ // // DISCLAIMER // -// Copyright 2016-2022 ArangoDB GmbH, Cologne, Germany +// Copyright 2016-2025 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. @@ -48,6 +48,15 @@ func (i *ImageInfo) String() string { return fmt.Sprintf("ArangoDB %s %s (%s)", e, string(i.ArangoDBVersion), i.Image) } +// IsEnterprise returns true if image is enterprise +func (i *ImageInfo) IsEnterprise() bool { + if i == nil { + return false + } + + return i.Enterprise +} + // ImageInfoList is a list of image infos type ImageInfoList []ImageInfo diff --git a/pkg/apis/deployment/v2alpha1/license_spec.go b/pkg/apis/deployment/v2alpha1/license_spec.go index bc3618f4c..30e3e1b7f 100644 --- a/pkg/apis/deployment/v2alpha1/license_spec.go +++ b/pkg/apis/deployment/v2alpha1/license_spec.go @@ -1,7 +1,7 @@ // // DISCLAIMER // -// Copyright 2016-2023 ArangoDB GmbH, Cologne, Germany +// Copyright 2016-2025 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. @@ -21,16 +21,52 @@ package v2alpha1 import ( + "time" + + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + shared "github.com/arangodb/kube-arangodb/pkg/apis/shared" "github.com/arangodb/kube-arangodb/pkg/util" + "github.com/arangodb/kube-arangodb/pkg/util/errors" +) + +const ( + DefaultLicenseExpirationGracePeriod = 3 * 24 * time.Hour + DefaultLicenseTTL = 14 * 24 * time.Hour ) +type LicenseMode string + +const ( + LicenseModeDefault = LicenseModeKey + LicenseModeKey LicenseMode = "key" + LicenseModeMaster LicenseMode = "master" +) + +func (l *LicenseMode) Get() LicenseMode { + return util.OptionalType(l, LicenseModeDefault) +} + // LicenseSpec holds the license related information type LicenseSpec struct { // SecretName setting specifies the name of a kubernetes `Secret` that contains - // the license key token used for enterprise images. This value is not used for + // the license key token or master key used for enterprise images. This value is not used for // the Community Edition. SecretName *string `json:"secretName,omitempty"` + + // Mode Defines the mode of license + // +doc/default: key + // +doc/enum: key|Use License Key mechanism + // +doc/enum: master|Use License Master Key mechanism + Mode *LicenseMode `json:"mode,omitempty"` + + // TTL Sets the requested License TTL + // +doc/default: 336h + TTL *meta.Duration `json:"ttl,omitempty"` + + // ExpirationGracePeriod defines the expiration grace period for the license + // +doc/default: 72h + ExpirationGracePeriod *meta.Duration `json:"expirationGracePeriod,omitempty"` } // HasSecretName returns true if a license key secret name was set @@ -43,20 +79,60 @@ func (s LicenseSpec) GetSecretName() string { return util.TypeOrDefault[string](s.SecretName) } +// GetTTL returns the license TTL +func (s LicenseSpec) GetTTL() time.Duration { + if s.TTL == nil { + return DefaultLicenseTTL + } + return s.TTL.Duration +} + +// GetExpirationGracePeriod returns the expiration period +func (s LicenseSpec) GetExpirationGracePeriod() time.Duration { + if s.ExpirationGracePeriod == nil { + return DefaultLicenseExpirationGracePeriod + } + return s.ExpirationGracePeriod.Duration +} + // Validate validates the LicenseSpec func (s LicenseSpec) Validate() error { - if s.HasSecretName() { - if err := shared.ValidateResourceName(s.GetSecretName()); err != nil { - return err - } + if !s.HasSecretName() { + return nil } + return shared.WithErrors( + // Secret + shared.PrefixResourceErrorFunc("secretName", func() error { + return shared.ValidateResourceName(s.GetSecretName()) + }), + // Expiration + shared.PrefixResourceErrorFunc("expirationGracePeriod", func() error { + if s.GetExpirationGracePeriod() <= 0 { + return errors.Errorf("Expiration grace period must be greater than zero") + } + + if s.GetExpirationGracePeriod() >= s.GetTTL() { + return errors.Errorf("Expiration grace period must be less than TTL") + } + + return nil + }), + // TTL + shared.PrefixResourceErrorFunc("ttl", func() error { + if s.GetTTL() <= 0 { + return errors.Errorf("TTL must be greater than zero") + } - return nil + return nil + }), + ) } // SetDefaultsFrom fills all values not set in s with values from other func (s *LicenseSpec) SetDefaultsFrom(other LicenseSpec) { if !s.HasSecretName() { - s.SecretName = util.NewTypeOrNil[string](other.SecretName) + s.SecretName = util.NewTypeOrNil(other.SecretName) } + s.TTL = util.NewTypeOrNil(other.TTL) + s.ExpirationGracePeriod = util.NewTypeOrNil(other.ExpirationGracePeriod) } diff --git a/pkg/apis/deployment/v2alpha1/zz_generated.deepcopy.go b/pkg/apis/deployment/v2alpha1/zz_generated.deepcopy.go index b53da60f5..9b8f2cec4 100644 --- a/pkg/apis/deployment/v2alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/deployment/v2alpha1/zz_generated.deepcopy.go @@ -1869,6 +1869,21 @@ func (in *LicenseSpec) DeepCopyInto(out *LicenseSpec) { *out = new(string) **out = **in } + if in.Mode != nil { + in, out := &in.Mode, &out.Mode + *out = new(LicenseMode) + **out = **in + } + if in.TTL != nil { + in, out := &in.TTL, &out.TTL + *out = new(metav1.Duration) + **out = **in + } + if in.ExpirationGracePeriod != nil { + in, out := &in.ExpirationGracePeriod, &out.ExpirationGracePeriod + *out = new(metav1.Duration) + **out = **in + } return } diff --git a/pkg/crd/crds/database-deployment.schema.generated.yaml b/pkg/crd/crds/database-deployment.schema.generated.yaml index f68adf776..b2f84124e 100644 --- a/pkg/crd/crds/database-deployment.schema.generated.yaml +++ b/pkg/crd/crds/database-deployment.schema.generated.yaml @@ -10178,12 +10178,24 @@ v1: license: description: License holds license settings properties: + expirationGracePeriod: + description: ExpirationGracePeriod defines the expiration grace period for the license + type: string + mode: + description: Mode Defines the mode of license + enum: + - key + - master + type: string secretName: description: |- SecretName setting specifies the name of a kubernetes `Secret` that contains - the license key token used for enterprise images. This value is not used for + the license key token or master key used for enterprise images. This value is not used for the Community Edition. type: string + ttl: + description: TTL Sets the requested License TTL + type: string type: object lifecycle: description: Lifecycle holds lifecycle configuration settings @@ -27492,12 +27504,24 @@ v2alpha1: license: description: License holds license settings properties: + expirationGracePeriod: + description: ExpirationGracePeriod defines the expiration grace period for the license + type: string + mode: + description: Mode Defines the mode of license + enum: + - key + - master + type: string secretName: description: |- SecretName setting specifies the name of a kubernetes `Secret` that contains - the license key token used for enterprise images. This value is not used for + the license key token or master key used for enterprise images. This value is not used for the Community Edition. type: string + ttl: + description: TTL Sets the requested License TTL + type: string type: object lifecycle: description: Lifecycle holds lifecycle configuration settings diff --git a/pkg/deployment/client/license.go b/pkg/deployment/client/license.go index 873880697..916f36824 100644 --- a/pkg/deployment/client/license.go +++ b/pkg/deployment/client/license.go @@ -23,6 +23,7 @@ package client import ( "context" goHttp "net/http" + "time" ) const AdminLicenseUrl = "/_admin/license" @@ -34,6 +35,19 @@ type LicenseClient interface { type License struct { Hash string `json:"hash,omitempty"` + + Features *LicenseFeatures `json:"features,omitempty"` +} + +type LicenseFeatures struct { + Expires *int64 `json:"expires,omitempty"` +} + +func (l *License) Expires() time.Time { + if l == nil || l.Features == nil || l.Features.Expires == nil { + return time.Time{} + } + return time.Unix(*l.Features.Expires, 0).UTC() } func (c *client) GetLicense(ctx context.Context) (License, error) { diff --git a/pkg/deployment/reconcile/action.register.generated.go b/pkg/deployment/reconcile/action.register.generated.go index 6e7d99d6e..0a797ce81 100644 --- a/pkg/deployment/reconcile/action.register.generated.go +++ b/pkg/deployment/reconcile/action.register.generated.go @@ -126,6 +126,9 @@ var ( _ Action = &actionKillMemberPod{} _ actionFactory = newKillMemberPodAction + _ Action = &actionLicenseGenerate{} + _ actionFactory = newLicenseGenerateAction + _ Action = &actionLicenseSet{} _ actionFactory = newLicenseSetAction @@ -768,6 +771,20 @@ func init() { registerAction(action, function) } + // LicenseGenerate + { + // Get Action type + action := api.ActionTypeLicenseGenerate + + // Get Action defition + function := newLicenseGenerateAction + + // Wrap action main function + + // Register action + registerAction(action, function) + } + // LicenseSet { // Get Action type diff --git a/pkg/deployment/reconcile/action.register.generated_test.go b/pkg/deployment/reconcile/action.register.generated_test.go index c607aa564..cc6c58de2 100644 --- a/pkg/deployment/reconcile/action.register.generated_test.go +++ b/pkg/deployment/reconcile/action.register.generated_test.go @@ -386,6 +386,16 @@ func Test_Actions(t *testing.T) { }) }) + t.Run("LicenseGenerate", func(t *testing.T) { + ActionsExistence(t, api.ActionTypeLicenseGenerate) + t.Run("Internal", func(t *testing.T) { + require.False(t, api.ActionTypeLicenseGenerate.Internal()) + }) + t.Run("Optional", func(t *testing.T) { + require.False(t, api.ActionTypeLicenseGenerate.Optional()) + }) + }) + t.Run("LicenseSet", func(t *testing.T) { ActionsExistence(t, api.ActionTypeLicenseSet) t.Run("Internal", func(t *testing.T) { diff --git a/pkg/deployment/reconcile/action_license_generate.go b/pkg/deployment/reconcile/action_license_generate.go new file mode 100644 index 000000000..28fde10ca --- /dev/null +++ b/pkg/deployment/reconcile/action_license_generate.go @@ -0,0 +1,119 @@ +// +// DISCLAIMER +// +// Copyright 2025 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 reconcile + +import ( + "context" + "fmt" + + "google.golang.org/protobuf/types/known/durationpb" + core "k8s.io/api/core/v1" + + api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" + "github.com/arangodb/kube-arangodb/pkg/deployment/client" + "github.com/arangodb/kube-arangodb/pkg/license_manager" + "github.com/arangodb/kube-arangodb/pkg/platform/inventory" + "github.com/arangodb/kube-arangodb/pkg/util" + "github.com/arangodb/kube-arangodb/pkg/util/globals" + ugrpc "github.com/arangodb/kube-arangodb/pkg/util/grpc" + "github.com/arangodb/kube-arangodb/pkg/util/k8sutil" +) + +func newLicenseGenerateAction(action api.Action, actionCtx ActionContext) Action { + a := &actionLicenseGenerate{} + + a.actionImpl = newActionImplDefRef(action, actionCtx) + + return a +} + +type actionLicenseGenerate struct { + actionImpl + + actionEmptyCheckProgress +} + +func (a *actionLicenseGenerate) Start(ctx context.Context) (bool, error) { + ctxChild, cancel := globals.GetGlobals().Timeouts().ArangoD().WithTimeout(ctx) + defer cancel() + spec := a.actionCtx.GetSpec() + if !spec.License.HasSecretName() { + a.log.Error("License is not set") + return true, nil + } + + l, err := k8sutil.GetLicenseFromSecret(a.actionCtx.ACS().CurrentClusterCache(), spec.License.GetSecretName()) + if err != nil { + return true, err + } + + if l.Master == nil { + return true, nil + } + + m, ok := a.actionCtx.GetMemberStatusByID(a.action.MemberID) + if !ok { + a.log.Error("No such member") + return true, nil + } + + c, err := a.actionCtx.GetMembersState().GetMemberClient(m.ID) + if err != nil { + a.log.Err(err).Error("Unable to get client") + return true, nil + } + + inv, err := inventory.FetchInventorySpec(ctx, a.log, 4, c.Connection(), &inventory.Configuration{Telemetry: util.NewType(true)}) + if err != nil { + a.log.Err(err).Error("Unable to generate inventory") + return true, nil + } + + lm, err := license_manager.NewClient(license_manager.ArangoLicenseManagerEndpoint, l.Master.ClientID, l.Master.ClientSecret) + if err != nil { + a.log.Err(err).Error("Unable to create inventory client") + return true, nil + } + + license, err := lm.License(ctx, license_manager.LicenseRequest{ + DeploymentID: util.NewType(inv.DeploymentId), + TTL: util.NewType(ugrpc.NewObject(durationpb.New(spec.License.GetTTL()))), + Inventory: util.NewType(ugrpc.NewObject(inv)), + }) + if err != nil { + a.log.Err(err).Error("Unable to create license") + a.actionCtx.CreateEvent(&k8sutil.Event{ + InvolvedObject: a.actionCtx.GetAPIObject(), + Type: core.EventTypeWarning, + Reason: "License Generation Failed", + Message: fmt.Sprintf("License Generation Failed with: %s", err), + }) + return true, nil + } + a.log.Str("id", license.ID).Info("License Generated") + + if err := client.NewClient(c.Connection(), a.log).SetLicense(ctxChild, license.License, true); err != nil { + a.log.Err(err).Error("Unable to set license") + return true, nil + } + + return true, nil +} diff --git a/pkg/deployment/reconcile/action_set_license.go b/pkg/deployment/reconcile/action_license_set.go similarity index 97% rename from pkg/deployment/reconcile/action_set_license.go rename to pkg/deployment/reconcile/action_license_set.go index 1e2f8eeba..22cf3c42f 100644 --- a/pkg/deployment/reconcile/action_set_license.go +++ b/pkg/deployment/reconcile/action_license_set.go @@ -1,7 +1,7 @@ // // DISCLAIMER // -// Copyright 2016-2023 ArangoDB GmbH, Cologne, Germany +// Copyright 2016-2025 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. diff --git a/pkg/deployment/reconcile/plan_builder_license.go b/pkg/deployment/reconcile/plan_builder_license.go index 8f083c789..3c4c011e6 100644 --- a/pkg/deployment/reconcile/plan_builder_license.go +++ b/pkg/deployment/reconcile/plan_builder_license.go @@ -1,7 +1,7 @@ // // DISCLAIMER // -// Copyright 2016-2023 ArangoDB GmbH, Cologne, Germany +// Copyright 2016-2025 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. @@ -22,6 +22,7 @@ package reconcile import ( "context" + "time" api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" "github.com/arangodb/kube-arangodb/pkg/deployment/actions" @@ -39,6 +40,17 @@ func (r *Reconciler) updateClusterLicense(ctx context.Context, apiObject k8sutil return nil } + switch spec.License.Mode.Get() { + case api.LicenseModeKey: + return r.updateClusterLicenseKey(ctx, spec, status, context) + case api.LicenseModeMaster: + return r.updateClusterLicenseMaster(ctx, spec, status, context) + } + + return nil +} + +func (r *Reconciler) updateClusterLicenseKey(ctx context.Context, spec api.DeploymentSpec, status api.DeploymentStatus, context PlanBuilderContext) api.Plan { l, err := k8sutil.GetLicenseFromSecret(context.ACS().CurrentClusterCache(), spec.License.GetSecretName()) if err != nil { r.log.Err(err).Error("License secret error") @@ -90,3 +102,59 @@ func (r *Reconciler) updateClusterLicense(ctx context.Context, apiObject k8sutil return api.Plan{sharedReconcile.RemoveConditionActionV2("License is not set", api.ConditionTypeLicenseSet), actions.NewAction(api.ActionTypeLicenseSet, member.Group, member.Member, "Setting license")} } + +func (r *Reconciler) updateClusterLicenseMaster(ctx context.Context, spec api.DeploymentSpec, status api.DeploymentStatus, context PlanBuilderContext) api.Plan { + l, err := k8sutil.GetLicenseFromSecret(context.ACS().CurrentClusterCache(), spec.License.GetSecretName()) + if err != nil { + r.log.Err(err).Error("License secret error") + return nil + } + + if l.Master == nil { + r.log.Str("secret", spec.License.GetSecretName()).Error("V2 License key is not set") + return nil + } + + members := status.Members.AsListInGroups(api.ServerGroupCoordinators, api.ServerGroupSingle).Filter(func(a api.DeploymentStatusMemberElement) bool { + i := a.Member.Image + if i == nil { + return false + } + + return i.ArangoDBVersion.CompareTo("3.9.0") >= 0 && i.Enterprise + }) + + if len(members) == 0 { + // No member found to take this action + r.log.Trace("No enterprise member in version 3.9.0 or above") + return nil + } + + member := members[0] + + ctxChild, cancel := globals.GetGlobals().Timeouts().ArangoD().WithTimeout(ctx) + defer cancel() + + c, err := context.GetMembersState().GetMemberClient(member.Member.ID) + if err != nil { + r.log.Err(err).Error("Unable to get client") + return nil + } + + internalClient := client.NewClient(c.Connection(), r.log) + + currentLicense, err := internalClient.GetLicense(ctxChild) + if err != nil { + r.log.Err(err).Error("Unable to get current license") + return nil + } + + if time.Until(currentLicense.Expires()) > spec.License.GetExpirationGracePeriod() { + if c, _ := status.Conditions.Get(api.ConditionTypeLicenseSet); !c.IsTrue() { + return api.Plan{sharedReconcile.UpdateConditionActionV2("License is set", api.ConditionTypeLicenseSet, true, "License UpToDate", "", currentLicense.Hash)} + } + return nil + } + + return api.Plan{sharedReconcile.RemoveConditionActionV2("License is not set", api.ConditionTypeLicenseSet), actions.NewAction(api.ActionTypeLicenseGenerate, member.Group, member.Member, "Generating license")} +} diff --git a/pkg/license/license.community.go b/pkg/license/license.community.go deleted file mode 100644 index 4bb77f6c8..000000000 --- a/pkg/license/license.community.go +++ /dev/null @@ -1,45 +0,0 @@ -// -// DISCLAIMER -// -// Copyright 2023 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 - -//go:build !enterprise - -package license - -import ( - "context" - - "github.com/arangodb/kube-arangodb/pkg/util/assertion" -) - -func NewLicense(loader Loader) License { - return emptyLicense{} -} - -type emptyLicense struct { -} - -func (e emptyLicense) Refresh(ctx context.Context) error { - return nil -} - -// Validate for the community returns that license is always missing, as it should be not used -func (e emptyLicense) Validate(feature Feature, subFeatures ...Feature) Status { - assertion.Assert(true, assertion.CommunityLicenseCheckKey, "Feature %s has been validated in the community version", feature) - return StatusMissing -} diff --git a/pkg/license/license.go b/pkg/license/license.go deleted file mode 100644 index bf6f3c0cd..000000000 --- a/pkg/license/license.go +++ /dev/null @@ -1,141 +0,0 @@ -// -// DISCLAIMER -// -// Copyright 2023 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 license - -import ( - "context" - "strconv" -) - -type Status int - -const ( - // StatusMissing define state when the license could not be loaded from the start or was not provided - // NotLicensed - StatusMissing Status = iota - - // StatusInvalid define state when the license and any of the fields are not valid - // NotLicensed - StatusInvalid - - // StatusInvalidSignature define state when license signature could not be validated - // NotLicensed - StatusInvalidSignature - - // StatusNotYetValid define state when the license contains nbf and current time (UTC) is before a specified time - // NotLicensed - StatusNotYetValid - - // StatusNotAnymoreValid define state when the license contains exp and current time (UTC) is after a specified time - // NotLicensed - StatusNotAnymoreValid - - // StatusFeatureNotEnabled define state when features requirements does not match one requested by the feature in Operator - // NotLicensed - StatusFeatureNotEnabled - - // StatusFeatureExpired define state when token is valid, but feature itself is expired - // NotLicensed - StatusFeatureExpired - - // StatusValid define state when Operator should continue execution - // Licensed - StatusValid -) - -func (s Status) Valid() bool { - return s == StatusValid -} - -func (s Status) Validate(feature Feature, subFeatures ...Feature) Status { - return s -} -func (s Status) String() string { - switch s { - case StatusMissing: - return "Missing" - case StatusInvalid: - return "Invalid" - case StatusInvalidSignature: - return "InvalidSignature" - case StatusNotYetValid: - return "NotYetValid" - case StatusNotAnymoreValid: - return "NotAnymoreValid" - case StatusFeatureNotEnabled: - return "FeatureNotEnabled" - case StatusFeatureExpired: - return "FeatureExpired" - case StatusValid: - return "Valid" - default: - return strconv.Itoa(int(s)) - } -} - -type Feature string - -const ( - // FeatureAll define feature name for all features - FeatureAll Feature = "*" - - // FeatureArangoDB define feature name for ArangoDB - FeatureArangoDB Feature = "ArangoDB" - - // FeatureArangoSearch define feature name for ArangoSearch - FeatureArangoSearch Feature = "ArangoSearch" - - // FeatureDataSciencePackage define feature name for DataSciencePackage - FeatureDataSciencePackage Feature = "DataSciencePackage" - - // SubFeatureGraphML define feature name for GraphML - SubFeature of DataSciencePackage - SubFeatureGraphML Feature = "GraphML" - - // SubFeatureAnalytics define feature name for Analytics - SubFeature of DataSciencePackage - SubFeatureAnalytics Feature = "Analytics" -) - -func (f Feature) In(features []Feature) bool { - for _, v := range features { - if v == f { - return true - } - } - return false -} - -type License interface { - // Validate validates the license scope. In case of: - // - if feature is '*' - checks if: - // -- license is valid and not expired - // - if feature is not '*' and subFeatures list is empty - checks if: - // -- license is valid and not expired - // -- feature is enabled and not expired - // - if feature is not '*' and subFeatures list is not empty - checks if: - // -- license is valid and not expired - // -- feature is enabled and not expired - // -- for each subFeature defined in subFeatures: - // --- checks if subFeature or '*' is in the list of License Feature enabled SubFeatures - Validate(feature Feature, subFeatures ...Feature) Status - - // Refresh refreshes the license from the source (Secret) and verifies the signature - Refresh(ctx context.Context) error -} diff --git a/pkg/license/loader.go b/pkg/license/loader.go deleted file mode 100644 index 5b5373a18..000000000 --- a/pkg/license/loader.go +++ /dev/null @@ -1,29 +0,0 @@ -// -// DISCLAIMER -// -// Copyright 2023 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 license - -import "context" - -type Loader interface { - // Refresh reloads license in a specified manner. - // It returns license (base64 encoded), found, error - Refresh(ctx context.Context) (string, bool, error) -} diff --git a/pkg/license/loader_arangodeployment.go b/pkg/license/loader_arangodeployment.go deleted file mode 100644 index 6cd58a1ec..000000000 --- a/pkg/license/loader_arangodeployment.go +++ /dev/null @@ -1,87 +0,0 @@ -// -// DISCLAIMER -// -// Copyright 2023-2025 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 license - -import ( - "context" - "encoding/base64" - - "k8s.io/apimachinery/pkg/api/errors" - meta "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes" - - api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" - utilConstants "github.com/arangodb/kube-arangodb/pkg/util/constants" - "github.com/arangodb/kube-arangodb/pkg/util/k8sutil" -) - -func NewArangoDeploymentLicenseLoader(client kubernetes.Interface, deployment *api.ArangoDeployment) Loader { - return arangoDeploymentLicenseLoader{ - client: client, - deployment: deployment, - } -} - -type arangoDeploymentLicenseLoader struct { - client kubernetes.Interface - - deployment *api.ArangoDeployment -} - -func (a arangoDeploymentLicenseLoader) Refresh(ctx context.Context) (string, bool, error) { - spec := a.deployment.GetAcceptedSpec() - - if !spec.License.HasSecretName() { - return "", false, nil - } - - secret, err := a.client.CoreV1().Secrets(a.deployment.GetNamespace()).Get(ctx, spec.License.GetSecretName(), meta.GetOptions{}) - if err != nil { - if errors.IsNotFound(err) { - return "", false, nil - } - - return "", false, err - } - - var licenseData []byte - - if lic, ok := secret.Data[utilConstants.SecretKeyV2License]; ok { - licenseData = lic - } else if lic2, ok := secret.Data[utilConstants.SecretKeyV2Token]; ok { - licenseData = lic2 - } - - if len(licenseData) == 0 { - return "", false, nil - } - - if !k8sutil.IsJSON(licenseData) { - d, err := base64.StdEncoding.DecodeString(string(licenseData)) - if err != nil { - return "", false, err - } - - licenseData = d - } - - return string(licenseData), true, nil -} diff --git a/pkg/license/loader_test.go b/pkg/license/loader_test.go deleted file mode 100644 index 88d6c9472..000000000 --- a/pkg/license/loader_test.go +++ /dev/null @@ -1,35 +0,0 @@ -// -// DISCLAIMER -// -// Copyright 2023 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 license - -import ( - "context" - - "github.com/stretchr/testify/mock" -) - -type MockLoader struct { - mock.Mock -} - -func (m *MockLoader) Refresh(ctx context.Context) (string, bool, error) { - args := m.Called(ctx) - return args.String(0), args.Bool(1), args.Error(2) -} diff --git a/pkg/license/manager/client.go b/pkg/license_manager/client.go similarity index 87% rename from pkg/license/manager/client.go rename to pkg/license_manager/client.go index 9f3dbafe7..a643e5a7e 100644 --- a/pkg/license/manager/client.go +++ b/pkg/license_manager/client.go @@ -18,13 +18,14 @@ // Copyright holder is ArangoDB GmbH, Cologne, Germany // -package manager +package license_manager import ( "context" "fmt" goHttp "net/http" - "time" + + "google.golang.org/protobuf/types/known/durationpb" "github.com/arangodb/go-driver" "github.com/arangodb/go-driver/http" @@ -36,6 +37,10 @@ import ( operatorHTTP "github.com/arangodb/kube-arangodb/pkg/util/http" ) +const ( + ArangoLicenseManagerEndpoint = "license.arango.ai" +) + func NewClient(endpoint, id, key string, mods ...util.Mod[goHttp.Transport]) (Client, error) { transport := operatorHTTP.Transport(mods...) @@ -73,9 +78,9 @@ type Client interface { } type LicenseRequest struct { - DeploymentID *string `json:"deployment_id,omitempty"` - TTL *time.Duration `json:"ttl,omitempty"` - Inventory *ugrpc.Object[*inventory.Spec] `json:"inventory,omitempty"` + DeploymentID *string `json:"deployment_id,omitempty"` + TTL *ugrpc.Object[*durationpb.Duration] `json:"ttl,omitempty"` + Inventory *ugrpc.Object[*inventory.Spec] `json:"inventory,omitempty"` } type LicenseResponse struct { diff --git a/pkg/license/manager/registry.go b/pkg/license_manager/registry.go similarity index 98% rename from pkg/license/manager/registry.go rename to pkg/license_manager/registry.go index 0c66317c2..64b51b8de 100644 --- a/pkg/license/manager/registry.go +++ b/pkg/license_manager/registry.go @@ -18,7 +18,7 @@ // Copyright holder is ArangoDB GmbH, Cologne, Germany // -package manager +package license_manager import ( "encoding/base64" diff --git a/pkg/license/manager/stage.go b/pkg/license_manager/stage.go similarity index 98% rename from pkg/license/manager/stage.go rename to pkg/license_manager/stage.go index 8774a73fd..b55ebb712 100644 --- a/pkg/license/manager/stage.go +++ b/pkg/license_manager/stage.go @@ -18,7 +18,7 @@ // Copyright holder is ArangoDB GmbH, Cologne, Germany // -package manager +package license_manager import ( "fmt" diff --git a/pkg/platform/inventory/fetcher.deployment.id.go b/pkg/platform/inventory/fetcher.deployment.id.go index 0ac7a2370..fef2c77f8 100644 --- a/pkg/platform/inventory/fetcher.deployment.id.go +++ b/pkg/platform/inventory/fetcher.deployment.id.go @@ -58,7 +58,9 @@ func init() { } return errors.Errors( - Produce(out, "ARANGO_DEPLOYMENT_ID", nil, health.ID), + Produce(out, "ARANGO_DEPLOYMENT", map[string]string{ + "detail": "id", + }, health.ID), ) } }) diff --git a/pkg/platform/inventory/inventory.go b/pkg/platform/inventory/inventory.go index 4840d7e27..d258fa02b 100644 --- a/pkg/platform/inventory/inventory.go +++ b/pkg/platform/inventory/inventory.go @@ -22,17 +22,59 @@ package inventory import ( "context" + "reflect" "github.com/arangodb/go-driver" shared "github.com/arangodb/kube-arangodb/pkg/apis/shared" "github.com/arangodb/kube-arangodb/pkg/logging" + "github.com/arangodb/kube-arangodb/pkg/util" "github.com/arangodb/kube-arangodb/pkg/util/errors" "github.com/arangodb/kube-arangodb/pkg/util/executor" ) type Items []*Item +func FetchInventorySpec(ctx context.Context, logger logging.Logger, threads int, conn driver.Connection, cfg *Configuration) (*Spec, error) { + obj, err := FetchInventory(ctx, logger, threads, conn, cfg) + + if err != nil { + return nil, err + } + + obj = util.FilterList(obj, func(item *Item) bool { + return item != nil + }) + + did := util.FilterList(obj, util.MultiFilterList( + func(item *Item) bool { + return item.Type == "ARANGO_DEPLOYMENT" + }, + func(item *Item) bool { + v, ok := item.Dimensions["detail"] + return ok && v == "id" + }, + )) + + if len(did) != 1 { + return nil, errors.Errorf("Expected to find a single ARANGO_DEPLOYMENT ID") + } + + tz, err := did[0].GetValue().Type() + if err != nil { + return nil, err + } + + if tz != reflect.TypeFor[string]() { + return nil, errors.Errorf("Expected to find type for ARANGO_DEPLOYMENT ID") + } + + return &Spec{ + DeploymentId: did[0].GetValue().GetStr(), + Items: obj, + }, nil +} + func FetchInventory(ctx context.Context, logger logging.Logger, threads int, conn driver.Connection, cfg *Configuration) (Items, error) { var out []*Item done := make(chan struct{}) diff --git a/pkg/platform/license.go b/pkg/platform/license.go index 8f768d01a..5ed4287eb 100644 --- a/pkg/platform/license.go +++ b/pkg/platform/license.go @@ -22,9 +22,7 @@ package platform import ( goHttp "net/http" - "reflect" - "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/arangodb/go-driver" @@ -81,41 +79,5 @@ func buildInventory(cmd *cobra.Command) (*inventory.Spec, error) { logger.Info("Discovered Arango %s (%s)", resp.Version, resp.License) - obj, err := inventory.FetchInventory(cmd.Context(), logger, 8, conn, &cfg) - - if err != nil { - return nil, err - } - - obj = util.FilterList(obj, func(item *inventory.Item) bool { - return item != nil - }) - - did := util.FilterList(obj, util.MultiFilterList( - func(item *inventory.Item) bool { - return item.Type == "ARANGO_DEPLOYMENT" - }, - func(item *inventory.Item) bool { - v, ok := item.Dimensions["detail"] - return ok && v == "id" - }, - )) - - if len(did) != 1 { - return nil, errors.Errorf("Expected to find a single ARANGO_DEPLOYMENT ID") - } - - tz, err := did[0].GetValue().Type() - if err != nil { - return nil, err - } - - if tz != reflect.TypeFor[string]() { - return nil, errors.Errorf("Expected to find type for ARANGO_DEPLOYMENT ID") - } - - return &inventory.Spec{ - DeploymentId: did[0].GetValue().GetStr(), - Items: obj, - }, nil + return inventory.FetchInventorySpec(cmd.Context(), logger, 8, conn, &cfg) } diff --git a/pkg/platform/license_activate.go b/pkg/platform/license_activate.go index c56a30a2f..06c7d37c4 100644 --- a/pkg/platform/license_activate.go +++ b/pkg/platform/license_activate.go @@ -26,7 +26,7 @@ import ( "github.com/spf13/cobra" "github.com/arangodb/kube-arangodb/pkg/deployment/client" - "github.com/arangodb/kube-arangodb/pkg/license/manager" + "github.com/arangodb/kube-arangodb/pkg/license_manager" "github.com/arangodb/kube-arangodb/pkg/logging" "github.com/arangodb/kube-arangodb/pkg/util" "github.com/arangodb/kube-arangodb/pkg/util/cli" @@ -84,7 +84,7 @@ func licenseActivateRun(cmd *cobra.Command, args []string) error { } } -func licenseActivateExecute(cmd *cobra.Command, logger logging.Logger, mc manager.Client) error { +func licenseActivateExecute(cmd *cobra.Command, logger logging.Logger, mc license_manager.Client) error { conn, err := flagDeployment.Connection(cmd) if err != nil { return err @@ -103,7 +103,7 @@ func licenseActivateExecute(cmd *cobra.Command, logger logging.Logger, mc manage l.Info("Generating License") - lic, err := mc.License(cmd.Context(), manager.LicenseRequest{ + lic, err := mc.License(cmd.Context(), license_manager.LicenseRequest{ DeploymentID: util.NewType(inv.DeploymentId), Inventory: util.NewType(ugrpc.NewObject(inv)), }) diff --git a/pkg/platform/license_generate.go b/pkg/platform/license_generate.go index 660e20943..98cef0185 100644 --- a/pkg/platform/license_generate.go +++ b/pkg/platform/license_generate.go @@ -26,7 +26,7 @@ import ( "github.com/spf13/cobra" - "github.com/arangodb/kube-arangodb/pkg/license/manager" + "github.com/arangodb/kube-arangodb/pkg/license_manager" "github.com/arangodb/kube-arangodb/pkg/platform/inventory" "github.com/arangodb/kube-arangodb/pkg/util" "github.com/arangodb/kube-arangodb/pkg/util/cli" @@ -74,7 +74,7 @@ func licenseGenerateRun(cmd *cobra.Command, args []string) error { l.Info("Generating License") - lic, err := mc.License(cmd.Context(), manager.LicenseRequest{ + lic, err := mc.License(cmd.Context(), license_manager.LicenseRequest{ DeploymentID: util.NewType(did), Inventory: util.NewType(ugrpc.NewObject(inv)), }) diff --git a/pkg/platform/license_secret.go b/pkg/platform/license_secret.go index 75e8080f0..b51650085 100644 --- a/pkg/platform/license_secret.go +++ b/pkg/platform/license_secret.go @@ -30,7 +30,7 @@ import ( meta "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/yaml" - "github.com/arangodb/kube-arangodb/pkg/license/manager" + manager2 "github.com/arangodb/kube-arangodb/pkg/license_manager" "github.com/arangodb/kube-arangodb/pkg/util/cli" "github.com/arangodb/kube-arangodb/pkg/util/k8sutil/kerrors" ) @@ -93,7 +93,7 @@ func licenseSecretRun(cmd *cobra.Command, args []string) error { logger.Info("Creating new Registry Token") - r, err := manager.NewRegistryAuth(endpoint, id, secret.Token, manager.ParseStages(stages...)...) + r, err := manager2.NewRegistryAuth(endpoint, id, secret.Token, manager2.ParseStages(stages...)...) if err != nil { return err } diff --git a/pkg/util/cli/lm.go b/pkg/util/cli/lm.go index 614bdc0b9..ce44bb56a 100644 --- a/pkg/util/cli/lm.go +++ b/pkg/util/cli/lm.go @@ -26,7 +26,7 @@ import ( "github.com/google/uuid" "github.com/spf13/cobra" - "github.com/arangodb/kube-arangodb/pkg/license/manager" + "github.com/arangodb/kube-arangodb/pkg/license_manager" "github.com/arangodb/kube-arangodb/pkg/util/errors" ) @@ -34,7 +34,7 @@ func NewLicenseManager(prefix string) LicenseManager { return licenseManager{ endpoint: Flag[string]{ Name: fmt.Sprintf("%s.endpoint", prefix), - Default: "license.arango.ai", + Default: license_manager.ArangoLicenseManagerEndpoint, Description: "LicenseManager Endpoint", Check: func(in string) error { if len(in) == 0 { @@ -100,7 +100,7 @@ type LicenseManager interface { ClientID(cmd *cobra.Command) (string, error) ClientSecret(cmd *cobra.Command) (string, error) - Client(cmd *cobra.Command) (manager.Client, error) + Client(cmd *cobra.Command) (license_manager.Client, error) } type licenseManager struct { @@ -129,7 +129,7 @@ func (l licenseManager) GetName() string { return "lm" } -func (l licenseManager) Client(cmd *cobra.Command) (manager.Client, error) { +func (l licenseManager) Client(cmd *cobra.Command) (license_manager.Client, error) { endpoint, err := l.endpoint.Get(cmd) if err != nil { return nil, err @@ -145,7 +145,7 @@ func (l licenseManager) Client(cmd *cobra.Command) (manager.Client, error) { return nil, err } - return manager.NewClient(endpoint, cid, cs) + return license_manager.NewClient(endpoint, cid, cs) } func (l licenseManager) Register(cmd *cobra.Command) error { diff --git a/pkg/util/constants/constants.go b/pkg/util/constants/constants.go index e5685fe5c..9e48908d2 100644 --- a/pkg/util/constants/constants.go +++ b/pkg/util/constants/constants.go @@ -45,10 +45,12 @@ const ( EnvArangoLicenseKey = "ARANGO_LICENSE_KEY" // Contains the License Key for the Docker Image EnvArangoSyncMonitoringToken = "ARANGOSYNC_MONITORING_TOKEN" // Constains monitoring token for ArangoSync servers - SecretEncryptionKey = "key" // Key in a Secret.Data used to store an 32-byte encryption key - SecretKeyToken = "token" // Key inside a Secret used to hold a JWT or monitoring token - SecretKeyV2Token = "token-v2" // Key inside a Secret used to hold a License in V2 Format - SecretKeyV2License = "license-v2" // Key inside a Secret used to hold a License in V2 Format + SecretEncryptionKey = "key" // Key in a Secret.Data used to store an 32-byte encryption key + SecretKeyToken = "token" // Key inside a Secret used to hold a JWT or monitoring token + SecretKeyV2Token = "token-v2" // Key inside a Secret used to hold a License in V2 Format + SecretKeyV2License = "license-v2" // Key inside a Secret used to hold a License in V2 Format + SecretKeyMasterClientID = "master-client-id" // Key inside a Secret used to hold a JWT or monitoring token + SecretKeyMasterClientSecret = "master-client-secret" // Key inside a Secret used to hold a JWT or monitoring token SecretCACertificate = "ca.crt" // Key in Secret.data used to store a PEM encoded CA certificate (public key) SecretCAKey = "ca.key" // Key in Secret.data used to store a PEM encoded CA private key diff --git a/pkg/util/k8sutil/license.go b/pkg/util/k8sutil/license.go index 4bb6aa11d..9a28a687d 100644 --- a/pkg/util/k8sutil/license.go +++ b/pkg/util/k8sutil/license.go @@ -41,8 +41,14 @@ func (l License) V2Hash() string { } type LicenseSecret struct { - V1 string - V2 License + V1 string + V2 License + Master *LicenseSecretMaster +} + +type LicenseSecretMaster struct { + ClientID string + ClientSecret string } func GetLicenseFromSecret(secret secret.Inspector, name string) (LicenseSecret, error) { @@ -57,6 +63,15 @@ func GetLicenseFromSecret(secret secret.Inspector, name string) (LicenseSecret, l.V1 = string(v) } + if cid, ok := s.Data[utilConstants.SecretKeyMasterClientID]; ok { + if cs, ok := s.Data[utilConstants.SecretKeyMasterClientSecret]; ok { + l.Master = &LicenseSecretMaster{ + ClientID: string(cid), + ClientSecret: string(cs), + } + } + } + if v1, ok1 := s.Data[utilConstants.SecretKeyV2License]; ok1 { // some customers put the raw JSON-encoded value, but operator and DB servers expect the base64-encoded value if IsJSON(v1) { @@ -71,9 +86,9 @@ func GetLicenseFromSecret(secret secret.Inspector, name string) (LicenseSecret, } else { l.V2 = License(v2) } - } else { - return LicenseSecret{}, errors.Errorf("Key (%s, %s or %s) is missing in the license secret (%s)", - utilConstants.SecretKeyToken, utilConstants.SecretKeyV2License, utilConstants.SecretKeyV2Token, name) + } else if l.Master == nil { + return LicenseSecret{}, errors.Errorf("Key (%s, %s, %s, or %s+%s) is missing in the license secret (%s)", + utilConstants.SecretKeyToken, utilConstants.SecretKeyV2License, utilConstants.SecretKeyV2Token, utilConstants.SecretKeyMasterClientID, utilConstants.SecretKeyMasterClientSecret, name) } return l, nil From de69cd005a221515f236bc3b3002bb05ee499a14 Mon Sep 17 00:00:00 2001 From: ajanikow <12255597+ajanikow@users.noreply.github.com> Date: Mon, 3 Nov 2025 08:56:15 +0000 Subject: [PATCH 2/9] Iter --- docs/api/ArangoDeployment.V1.md | 21 +++- docs/generated/actions.md | 4 + internal/actions.yaml | 7 ++ pkg/apis/deployment/v1/actions.generated.go | 26 ++++ pkg/apis/deployment/v1/deployment_status.go | 5 +- .../v1/deployment_status_license.go | 54 ++++++++ pkg/apis/deployment/v1/license_spec.go | 20 ++- .../deployment/v1/zz_generated.deepcopy.go | 28 +++++ .../deployment/v2alpha1/actions.generated.go | 26 ++++ .../deployment/v2alpha1/deployment_status.go | 5 +- .../v2alpha1/deployment_status_license.go | 54 ++++++++ pkg/apis/deployment/v2alpha1/license_spec.go | 20 ++- .../v2alpha1/zz_generated.deepcopy.go | 28 +++++ .../database-deployment.schema.generated.yaml | 8 ++ .../reconcile/action.register.generated.go | 34 +++++ .../action.register.generated_test.go | 20 +++ pkg/deployment/reconcile/action_context.go | 12 +- .../reconcile/action_license_clean.go | 58 +++++++++ .../reconcile/action_license_generate.go | 43 ++++++- .../reconcile/action_license_set.go | 32 +++-- .../reconcile/action_set_annotation.go | 98 +++++++++++++++ .../reconcile/plan_builder_license.go | 116 +++++++++++++++--- pkg/license_manager/client.go | 6 +- pkg/platform/inventory/consts.go | 23 ++++ .../inventory/fetcher.deployment.id.go | 2 +- pkg/util/constants/constants.go | 12 +- pkg/util/k8sutil/license.go | 16 +-- 27 files changed, 702 insertions(+), 76 deletions(-) create mode 100644 pkg/apis/deployment/v1/deployment_status_license.go create mode 100644 pkg/apis/deployment/v2alpha1/deployment_status_license.go create mode 100644 pkg/deployment/reconcile/action_license_clean.go create mode 100644 pkg/deployment/reconcile/action_set_annotation.go create mode 100644 pkg/platform/inventory/consts.go diff --git a/docs/api/ArangoDeployment.V1.md b/docs/api/ArangoDeployment.V1.md index 9d1a25f92..fa54967a7 100644 --- a/docs/api/ArangoDeployment.V1.md +++ b/docs/api/ArangoDeployment.V1.md @@ -4793,7 +4793,7 @@ Possible Values: ### .spec.license.expirationGracePeriod -Type: `integer` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.3.1/pkg/apis/deployment/v1/license_spec.go#L69) +Type: `integer` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.3.1/pkg/apis/deployment/v1/license_spec.go#L72) ExpirationGracePeriod defines the expiration grace period for the license @@ -4803,19 +4803,20 @@ Default Value: `72h` ### .spec.license.mode -Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.3.1/pkg/apis/deployment/v1/license_spec.go#L61) +Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.3.1/pkg/apis/deployment/v1/license_spec.go#L64) Mode Defines the mode of license Possible Values: -* `"key"` (default) - Use License Key mechanism +* `"discover"` (default) - Discovers the LicenseMode based on the keys +* `"key"` - Use License Key mechanism * `"master"` - Use License Master Key mechanism *** ### .spec.license.secretName -Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.3.1/pkg/apis/deployment/v1/license_spec.go#L55) +Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.3.1/pkg/apis/deployment/v1/license_spec.go#L57) SecretName setting specifies the name of a kubernetes `Secret` that contains the license key token or master key used for enterprise images. This value is not used for @@ -4823,9 +4824,19 @@ the Community Edition. *** +### .spec.license.telemetry + +Type: `boolean` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.3.1/pkg/apis/deployment/v1/license_spec.go#L76) + +Telemetry defines if telemetry is collected + +Default Value: `true` + +*** + ### .spec.license.ttl -Type: `integer` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.3.1/pkg/apis/deployment/v1/license_spec.go#L65) +Type: `integer` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.3.1/pkg/apis/deployment/v1/license_spec.go#L68) TTL Sets the requested License TTL diff --git a/docs/generated/actions.md b/docs/generated/actions.md index d860e7d13..a044c79a8 100644 --- a/docs/generated/actions.md +++ b/docs/generated/actions.md @@ -47,6 +47,7 @@ nav_order: 11 | JWTSetActive | no | 10m0s | no | Enterprise Only | Change active JWT key on the cluster | | JWTStatusUpdate | no | 10m0s | no | Enterprise Only | Update status of JWT propagation | | KillMemberPod | no | 10m0s | no | Community & Enterprise | Execute Delete on Pod (put pod in Terminating state) | +| LicenseClean | no | 10m0s | no | Community & Enterprise | Removes the License reference from the status | | LicenseGenerate | no | 10m0s | no | Community & Enterprise | Generates License using ArangoDB LicenseManager Endpoint | | LicenseSet | no | 10m0s | no | Community & Enterprise | Update Cluster license (3.9+) | | MarkToRemoveMember | no | 10m0s | no | Community & Enterprise | Marks member to be removed. Used when member Pod is annotated with replace annotation | @@ -79,6 +80,7 @@ nav_order: 11 | RuntimeContainerArgsLogLevelUpdate | no | 10m0s | no | Community & Enterprise | Change ArangoDB Member log levels in runtime | | RuntimeContainerImageUpdate | no | 10m0s | no | Community & Enterprise | Update Container Image in runtime | | RuntimeContainerSyncTolerations | no | 10m0s | no | Community & Enterprise | Update Pod Tolerations in runtime | +| SetAnnotation | yes | 10m0s | no | Community & Enterprise | Set ArangoDeployment annotation | | ~~SetCondition~~ | no | 10m0s | no | Community & Enterprise | Set deployment condition | | SetConditionV2 | yes | 10m0s | no | Community & Enterprise | Set deployment condition | | SetCurrentImage | no | 6h0m0s | no | Community & Enterprise | Update deployment current image after image discovery | @@ -147,6 +149,7 @@ spec: JWTSetActive: 10m0s JWTStatusUpdate: 10m0s KillMemberPod: 10m0s + LicenseClean: 10m0s LicenseGenerate: 10m0s LicenseSet: 10m0s MarkToRemoveMember: 10m0s @@ -179,6 +182,7 @@ spec: RuntimeContainerArgsLogLevelUpdate: 10m0s RuntimeContainerImageUpdate: 10m0s RuntimeContainerSyncTolerations: 10m0s + SetAnnotation: 10m0s SetCondition: 10m0s SetConditionV2: 10m0s SetCurrentImage: 6h0m0s diff --git a/internal/actions.yaml b/internal/actions.yaml index 160d104f1..6c9408c9c 100644 --- a/internal/actions.yaml +++ b/internal/actions.yaml @@ -232,6 +232,11 @@ actions: scopes: - High isInternal: true + SetAnnotation: + description: Set ArangoDeployment annotation + scopes: + - High + isInternal: true MemberRIDUpdate: description: Update Run ID of member scopes: @@ -249,6 +254,8 @@ actions: description: Update Cluster license (3.9+) LicenseGenerate: description: Generates License using ArangoDB LicenseManager Endpoint + LicenseClean: + description: Removes the License reference from the status RuntimeContainerImageUpdate: description: Update Container Image in runtime RuntimeContainerSyncTolerations: diff --git a/pkg/apis/deployment/v1/actions.generated.go b/pkg/apis/deployment/v1/actions.generated.go index 8508dd375..1c0bd5d9e 100644 --- a/pkg/apis/deployment/v1/actions.generated.go +++ b/pkg/apis/deployment/v1/actions.generated.go @@ -131,6 +131,9 @@ const ( // ActionKillMemberPodDefaultTimeout define default timeout for action ActionKillMemberPod ActionKillMemberPodDefaultTimeout time.Duration = ActionsDefaultTimeout + // ActionLicenseCleanDefaultTimeout define default timeout for action ActionLicenseClean + ActionLicenseCleanDefaultTimeout time.Duration = ActionsDefaultTimeout + // ActionLicenseGenerateDefaultTimeout define default timeout for action ActionLicenseGenerate ActionLicenseGenerateDefaultTimeout time.Duration = ActionsDefaultTimeout @@ -227,6 +230,9 @@ const ( // ActionRuntimeContainerSyncTolerationsDefaultTimeout define default timeout for action ActionRuntimeContainerSyncTolerations ActionRuntimeContainerSyncTolerationsDefaultTimeout time.Duration = ActionsDefaultTimeout + // ActionSetAnnotationDefaultTimeout define default timeout for action ActionSetAnnotation + ActionSetAnnotationDefaultTimeout time.Duration = ActionsDefaultTimeout + // ActionSetConditionDefaultTimeout define default timeout for action ActionSetCondition ActionSetConditionDefaultTimeout time.Duration = ActionsDefaultTimeout @@ -404,6 +410,9 @@ const ( // ActionTypeKillMemberPod in scopes High and Normal. Execute Delete on Pod (put pod in Terminating state) ActionTypeKillMemberPod ActionType = "KillMemberPod" + // ActionTypeLicenseClean in scopes Normal. Removes the License reference from the status + ActionTypeLicenseClean ActionType = "LicenseClean" + // ActionTypeLicenseGenerate in scopes Normal. Generates License using ArangoDB LicenseManager Endpoint ActionTypeLicenseGenerate ActionType = "LicenseGenerate" @@ -502,6 +511,9 @@ const ( // ActionTypeRuntimeContainerSyncTolerations in scopes Normal. Update Pod Tolerations in runtime ActionTypeRuntimeContainerSyncTolerations ActionType = "RuntimeContainerSyncTolerations" + // ActionTypeSetAnnotation in scopes High. Set ArangoDeployment annotation + ActionTypeSetAnnotation ActionType = "SetAnnotation" + // ActionTypeSetCondition in scopes High. Set deployment condition // // Deprecated: action is not used anymore @@ -645,6 +657,8 @@ func (a ActionType) DefaultTimeout() time.Duration { return ActionJWTStatusUpdateDefaultTimeout case ActionTypeKillMemberPod: return ActionKillMemberPodDefaultTimeout + case ActionTypeLicenseClean: + return ActionLicenseCleanDefaultTimeout case ActionTypeLicenseGenerate: return ActionLicenseGenerateDefaultTimeout case ActionTypeLicenseSet: @@ -709,6 +723,8 @@ func (a ActionType) DefaultTimeout() time.Duration { return ActionRuntimeContainerImageUpdateDefaultTimeout case ActionTypeRuntimeContainerSyncTolerations: return ActionRuntimeContainerSyncTolerationsDefaultTimeout + case ActionTypeSetAnnotation: + return ActionSetAnnotationDefaultTimeout case ActionTypeSetCondition: return ActionSetConditionDefaultTimeout case ActionTypeSetConditionV2: @@ -831,6 +847,8 @@ func (a ActionType) Priority() ActionPriority { return ActionPriorityNormal case ActionTypeKillMemberPod: return ActionPriorityHigh + case ActionTypeLicenseClean: + return ActionPriorityNormal case ActionTypeLicenseGenerate: return ActionPriorityNormal case ActionTypeLicenseSet: @@ -895,6 +913,8 @@ func (a ActionType) Priority() ActionPriority { return ActionPriorityNormal case ActionTypeRuntimeContainerSyncTolerations: return ActionPriorityNormal + case ActionTypeSetAnnotation: + return ActionPriorityHigh case ActionTypeSetCondition: return ActionPriorityHigh case ActionTypeSetConditionV2: @@ -955,6 +975,8 @@ func (a ActionType) Internal() bool { return true case ActionTypeRebalancerGenerateV2: return true + case ActionTypeSetAnnotation: + return true case ActionTypeSetConditionV2: return true case ActionTypeSetMaintenanceCondition: @@ -1043,6 +1065,8 @@ func (a ActionType) Optional() bool { return false case ActionTypeKillMemberPod: return false + case ActionTypeLicenseClean: + return false case ActionTypeLicenseGenerate: return false case ActionTypeLicenseSet: @@ -1107,6 +1131,8 @@ func (a ActionType) Optional() bool { return false case ActionTypeRuntimeContainerSyncTolerations: return false + case ActionTypeSetAnnotation: + return false case ActionTypeSetCondition: return false case ActionTypeSetConditionV2: diff --git a/pkg/apis/deployment/v1/deployment_status.go b/pkg/apis/deployment/v1/deployment_status.go index 4068610be..595e9b2f2 100644 --- a/pkg/apis/deployment/v1/deployment_status.go +++ b/pkg/apis/deployment/v1/deployment_status.go @@ -98,6 +98,8 @@ type DeploymentStatus struct { Timezone *string `json:"timezone,omitempty"` + License *DeploymentStatusLicense `json:"license,omitempty"` + Single *ServerGroupStatus `json:"single,omitempty"` Agents *ServerGroupStatus `json:"agents,omitempty"` DBServers *ServerGroupStatus `json:"dbservers,omitempty"` @@ -136,7 +138,8 @@ func (ds *DeploymentStatus) Equal(other DeploymentStatus) bool { ds.Coordinators.Equal(other.Coordinators) && ds.SyncMasters.Equal(other.SyncMasters) && ds.SyncWorkers.Equal(other.SyncWorkers) && - strings.CompareStringPointers(ds.Timezone, other.Timezone) + strings.CompareStringPointers(ds.Timezone, other.Timezone) && + ds.License.Equal(other.License) } // IsForceReload returns true if ForceStatusReload is set to true diff --git a/pkg/apis/deployment/v1/deployment_status_license.go b/pkg/apis/deployment/v1/deployment_status_license.go new file mode 100644 index 000000000..4e0678e62 --- /dev/null +++ b/pkg/apis/deployment/v1/deployment_status_license.go @@ -0,0 +1,54 @@ +// +// DISCLAIMER +// +// Copyright 2025 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 v1 + +import meta "k8s.io/apimachinery/pkg/apis/meta/v1" + +// DeploymentStatusLicense contains the status part of a Cluster resource. +type DeploymentStatusLicense struct { + // ID Defines the License ID + ID string `json:"id,omitempty"` + + // Hash Defines the License Hash + Hash string `json:"hash,omitempty"` + + // Expires Defines the expiration time of the License + Expires meta.Time `json:"expires,omitempty"` + + // Regenerate Defines the time when license will be regenerated + Regenerate meta.Time `json:"regenerate,omitempty"` + + // Mode defines the license mode + Mode LicenseMode `json:"mode,omitempty"` +} + +// Equal checks for equality +func (ds *DeploymentStatusLicense) Equal(other *DeploymentStatusLicense) bool { + if ds == nil && other == nil { + return true + } + + if ds == nil || other == nil { + return false + } + + return ds.ID == other.ID && ds.Hash == other.Hash +} diff --git a/pkg/apis/deployment/v1/license_spec.go b/pkg/apis/deployment/v1/license_spec.go index e7bb689ab..d6e25ec40 100644 --- a/pkg/apis/deployment/v1/license_spec.go +++ b/pkg/apis/deployment/v1/license_spec.go @@ -31,6 +31,7 @@ import ( ) const ( + LicenseExpirationGraceRatio = 0.9 DefaultLicenseExpirationGracePeriod = 3 * 24 * time.Hour DefaultLicenseTTL = 14 * 24 * time.Hour ) @@ -38,9 +39,10 @@ const ( type LicenseMode string const ( - LicenseModeDefault = LicenseModeKey - LicenseModeKey LicenseMode = "key" - LicenseModeMaster LicenseMode = "master" + LicenseModeDefault = LicenseModeDiscover + LicenseModeDiscover LicenseMode = "discover" + LicenseModeKey LicenseMode = "key" + LicenseModeAPI LicenseMode = "api" ) func (l *LicenseMode) Get() LicenseMode { @@ -55,7 +57,8 @@ type LicenseSpec struct { SecretName *string `json:"secretName,omitempty"` // Mode Defines the mode of license - // +doc/default: key + // +doc/default: discover + // +doc/enum: discover|Discovers the LicenseMode based on the keys // +doc/enum: key|Use License Key mechanism // +doc/enum: master|Use License Master Key mechanism Mode *LicenseMode `json:"mode,omitempty"` @@ -67,6 +70,10 @@ type LicenseSpec struct { // ExpirationGracePeriod defines the expiration grace period for the license // +doc/default: 72h ExpirationGracePeriod *meta.Duration `json:"expirationGracePeriod,omitempty"` + + // Telemetry defines if telemetry is collected + // +doc/default: true + Telemetry *bool `json:"telemetry,omitempty"` } // HasSecretName returns true if a license key secret name was set @@ -87,6 +94,11 @@ func (s LicenseSpec) GetTTL() time.Duration { return s.TTL.Duration } +// GetTelemetry returns the license Telemetry +func (s LicenseSpec) GetTelemetry() bool { + return util.OptionalType(s.Telemetry, true) +} + // GetExpirationGracePeriod returns the expiration period func (s LicenseSpec) GetExpirationGracePeriod() time.Duration { if s.ExpirationGracePeriod == nil { diff --git a/pkg/apis/deployment/v1/zz_generated.deepcopy.go b/pkg/apis/deployment/v1/zz_generated.deepcopy.go index 117e7ef21..bb607ab9e 100644 --- a/pkg/apis/deployment/v1/zz_generated.deepcopy.go +++ b/pkg/apis/deployment/v1/zz_generated.deepcopy.go @@ -1426,6 +1426,11 @@ func (in *DeploymentStatus) DeepCopyInto(out *DeploymentStatus) { *out = new(string) **out = **in } + if in.License != nil { + in, out := &in.License, &out.License + *out = new(DeploymentStatusLicense) + (*in).DeepCopyInto(*out) + } if in.Single != nil { in, out := &in.Single, &out.Single *out = new(ServerGroupStatus) @@ -1607,6 +1612,24 @@ func (in *DeploymentStatusHashesTLS) DeepCopy() *DeploymentStatusHashesTLS { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeploymentStatusLicense) DeepCopyInto(out *DeploymentStatusLicense) { + *out = *in + in.Expires.DeepCopyInto(&out.Expires) + in.Regenerate.DeepCopyInto(&out.Regenerate) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeploymentStatusLicense. +func (in *DeploymentStatusLicense) DeepCopy() *DeploymentStatusLicense { + if in == nil { + return nil + } + out := new(DeploymentStatusLicense) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DeploymentStatusMemberElement) DeepCopyInto(out *DeploymentStatusMemberElement) { *out = *in @@ -1884,6 +1907,11 @@ func (in *LicenseSpec) DeepCopyInto(out *LicenseSpec) { *out = new(metav1.Duration) **out = **in } + if in.Telemetry != nil { + in, out := &in.Telemetry, &out.Telemetry + *out = new(bool) + **out = **in + } return } diff --git a/pkg/apis/deployment/v2alpha1/actions.generated.go b/pkg/apis/deployment/v2alpha1/actions.generated.go index 6c56dd633..9da9964bb 100644 --- a/pkg/apis/deployment/v2alpha1/actions.generated.go +++ b/pkg/apis/deployment/v2alpha1/actions.generated.go @@ -131,6 +131,9 @@ const ( // ActionKillMemberPodDefaultTimeout define default timeout for action ActionKillMemberPod ActionKillMemberPodDefaultTimeout time.Duration = ActionsDefaultTimeout + // ActionLicenseCleanDefaultTimeout define default timeout for action ActionLicenseClean + ActionLicenseCleanDefaultTimeout time.Duration = ActionsDefaultTimeout + // ActionLicenseGenerateDefaultTimeout define default timeout for action ActionLicenseGenerate ActionLicenseGenerateDefaultTimeout time.Duration = ActionsDefaultTimeout @@ -227,6 +230,9 @@ const ( // ActionRuntimeContainerSyncTolerationsDefaultTimeout define default timeout for action ActionRuntimeContainerSyncTolerations ActionRuntimeContainerSyncTolerationsDefaultTimeout time.Duration = ActionsDefaultTimeout + // ActionSetAnnotationDefaultTimeout define default timeout for action ActionSetAnnotation + ActionSetAnnotationDefaultTimeout time.Duration = ActionsDefaultTimeout + // ActionSetConditionDefaultTimeout define default timeout for action ActionSetCondition ActionSetConditionDefaultTimeout time.Duration = ActionsDefaultTimeout @@ -404,6 +410,9 @@ const ( // ActionTypeKillMemberPod in scopes High and Normal. Execute Delete on Pod (put pod in Terminating state) ActionTypeKillMemberPod ActionType = "KillMemberPod" + // ActionTypeLicenseClean in scopes Normal. Removes the License reference from the status + ActionTypeLicenseClean ActionType = "LicenseClean" + // ActionTypeLicenseGenerate in scopes Normal. Generates License using ArangoDB LicenseManager Endpoint ActionTypeLicenseGenerate ActionType = "LicenseGenerate" @@ -502,6 +511,9 @@ const ( // ActionTypeRuntimeContainerSyncTolerations in scopes Normal. Update Pod Tolerations in runtime ActionTypeRuntimeContainerSyncTolerations ActionType = "RuntimeContainerSyncTolerations" + // ActionTypeSetAnnotation in scopes High. Set ArangoDeployment annotation + ActionTypeSetAnnotation ActionType = "SetAnnotation" + // ActionTypeSetCondition in scopes High. Set deployment condition // // Deprecated: action is not used anymore @@ -645,6 +657,8 @@ func (a ActionType) DefaultTimeout() time.Duration { return ActionJWTStatusUpdateDefaultTimeout case ActionTypeKillMemberPod: return ActionKillMemberPodDefaultTimeout + case ActionTypeLicenseClean: + return ActionLicenseCleanDefaultTimeout case ActionTypeLicenseGenerate: return ActionLicenseGenerateDefaultTimeout case ActionTypeLicenseSet: @@ -709,6 +723,8 @@ func (a ActionType) DefaultTimeout() time.Duration { return ActionRuntimeContainerImageUpdateDefaultTimeout case ActionTypeRuntimeContainerSyncTolerations: return ActionRuntimeContainerSyncTolerationsDefaultTimeout + case ActionTypeSetAnnotation: + return ActionSetAnnotationDefaultTimeout case ActionTypeSetCondition: return ActionSetConditionDefaultTimeout case ActionTypeSetConditionV2: @@ -831,6 +847,8 @@ func (a ActionType) Priority() ActionPriority { return ActionPriorityNormal case ActionTypeKillMemberPod: return ActionPriorityHigh + case ActionTypeLicenseClean: + return ActionPriorityNormal case ActionTypeLicenseGenerate: return ActionPriorityNormal case ActionTypeLicenseSet: @@ -895,6 +913,8 @@ func (a ActionType) Priority() ActionPriority { return ActionPriorityNormal case ActionTypeRuntimeContainerSyncTolerations: return ActionPriorityNormal + case ActionTypeSetAnnotation: + return ActionPriorityHigh case ActionTypeSetCondition: return ActionPriorityHigh case ActionTypeSetConditionV2: @@ -955,6 +975,8 @@ func (a ActionType) Internal() bool { return true case ActionTypeRebalancerGenerateV2: return true + case ActionTypeSetAnnotation: + return true case ActionTypeSetConditionV2: return true case ActionTypeSetMaintenanceCondition: @@ -1043,6 +1065,8 @@ func (a ActionType) Optional() bool { return false case ActionTypeKillMemberPod: return false + case ActionTypeLicenseClean: + return false case ActionTypeLicenseGenerate: return false case ActionTypeLicenseSet: @@ -1107,6 +1131,8 @@ func (a ActionType) Optional() bool { return false case ActionTypeRuntimeContainerSyncTolerations: return false + case ActionTypeSetAnnotation: + return false case ActionTypeSetCondition: return false case ActionTypeSetConditionV2: diff --git a/pkg/apis/deployment/v2alpha1/deployment_status.go b/pkg/apis/deployment/v2alpha1/deployment_status.go index d983c2489..b94dbf07a 100644 --- a/pkg/apis/deployment/v2alpha1/deployment_status.go +++ b/pkg/apis/deployment/v2alpha1/deployment_status.go @@ -98,6 +98,8 @@ type DeploymentStatus struct { Timezone *string `json:"timezone,omitempty"` + License *DeploymentStatusLicense `json:"license,omitempty"` + Single *ServerGroupStatus `json:"single,omitempty"` Agents *ServerGroupStatus `json:"agents,omitempty"` DBServers *ServerGroupStatus `json:"dbservers,omitempty"` @@ -136,7 +138,8 @@ func (ds *DeploymentStatus) Equal(other DeploymentStatus) bool { ds.Coordinators.Equal(other.Coordinators) && ds.SyncMasters.Equal(other.SyncMasters) && ds.SyncWorkers.Equal(other.SyncWorkers) && - strings.CompareStringPointers(ds.Timezone, other.Timezone) + strings.CompareStringPointers(ds.Timezone, other.Timezone) && + ds.License.Equal(other.License) } // IsForceReload returns true if ForceStatusReload is set to true diff --git a/pkg/apis/deployment/v2alpha1/deployment_status_license.go b/pkg/apis/deployment/v2alpha1/deployment_status_license.go new file mode 100644 index 000000000..cc3fe88ac --- /dev/null +++ b/pkg/apis/deployment/v2alpha1/deployment_status_license.go @@ -0,0 +1,54 @@ +// +// DISCLAIMER +// +// Copyright 2025 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 v2alpha1 + +import meta "k8s.io/apimachinery/pkg/apis/meta/v1" + +// DeploymentStatusLicense contains the status part of a Cluster resource. +type DeploymentStatusLicense struct { + // ID Defines the License ID + ID string `json:"id,omitempty"` + + // Hash Defines the License Hash + Hash string `json:"hash,omitempty"` + + // Expires Defines the expiration time of the License + Expires meta.Time `json:"expires,omitempty"` + + // Regenerate Defines the time when license will be regenerated + Regenerate meta.Time `json:"regenerate,omitempty"` + + // Mode defines the license mode + Mode LicenseMode `json:"mode,omitempty"` +} + +// Equal checks for equality +func (ds *DeploymentStatusLicense) Equal(other *DeploymentStatusLicense) bool { + if ds == nil && other == nil { + return true + } + + if ds == nil || other == nil { + return false + } + + return ds.ID == other.ID && ds.Hash == other.Hash +} diff --git a/pkg/apis/deployment/v2alpha1/license_spec.go b/pkg/apis/deployment/v2alpha1/license_spec.go index 30e3e1b7f..263dbbf73 100644 --- a/pkg/apis/deployment/v2alpha1/license_spec.go +++ b/pkg/apis/deployment/v2alpha1/license_spec.go @@ -31,6 +31,7 @@ import ( ) const ( + LicenseExpirationGraceRatio = 0.9 DefaultLicenseExpirationGracePeriod = 3 * 24 * time.Hour DefaultLicenseTTL = 14 * 24 * time.Hour ) @@ -38,9 +39,10 @@ const ( type LicenseMode string const ( - LicenseModeDefault = LicenseModeKey - LicenseModeKey LicenseMode = "key" - LicenseModeMaster LicenseMode = "master" + LicenseModeDefault = LicenseModeDiscover + LicenseModeDiscover LicenseMode = "discover" + LicenseModeKey LicenseMode = "key" + LicenseModeAPI LicenseMode = "api" ) func (l *LicenseMode) Get() LicenseMode { @@ -55,7 +57,8 @@ type LicenseSpec struct { SecretName *string `json:"secretName,omitempty"` // Mode Defines the mode of license - // +doc/default: key + // +doc/default: discover + // +doc/enum: discover|Discovers the LicenseMode based on the keys // +doc/enum: key|Use License Key mechanism // +doc/enum: master|Use License Master Key mechanism Mode *LicenseMode `json:"mode,omitempty"` @@ -67,6 +70,10 @@ type LicenseSpec struct { // ExpirationGracePeriod defines the expiration grace period for the license // +doc/default: 72h ExpirationGracePeriod *meta.Duration `json:"expirationGracePeriod,omitempty"` + + // Telemetry defines if telemetry is collected + // +doc/default: true + Telemetry *bool `json:"telemetry,omitempty"` } // HasSecretName returns true if a license key secret name was set @@ -87,6 +94,11 @@ func (s LicenseSpec) GetTTL() time.Duration { return s.TTL.Duration } +// GetTelemetry returns the license Telemetry +func (s LicenseSpec) GetTelemetry() bool { + return util.OptionalType(s.Telemetry, true) +} + // GetExpirationGracePeriod returns the expiration period func (s LicenseSpec) GetExpirationGracePeriod() time.Duration { if s.ExpirationGracePeriod == nil { diff --git a/pkg/apis/deployment/v2alpha1/zz_generated.deepcopy.go b/pkg/apis/deployment/v2alpha1/zz_generated.deepcopy.go index 9b8f2cec4..448e4e627 100644 --- a/pkg/apis/deployment/v2alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/deployment/v2alpha1/zz_generated.deepcopy.go @@ -1426,6 +1426,11 @@ func (in *DeploymentStatus) DeepCopyInto(out *DeploymentStatus) { *out = new(string) **out = **in } + if in.License != nil { + in, out := &in.License, &out.License + *out = new(DeploymentStatusLicense) + (*in).DeepCopyInto(*out) + } if in.Single != nil { in, out := &in.Single, &out.Single *out = new(ServerGroupStatus) @@ -1607,6 +1612,24 @@ func (in *DeploymentStatusHashesTLS) DeepCopy() *DeploymentStatusHashesTLS { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeploymentStatusLicense) DeepCopyInto(out *DeploymentStatusLicense) { + *out = *in + in.Expires.DeepCopyInto(&out.Expires) + in.Regenerate.DeepCopyInto(&out.Regenerate) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeploymentStatusLicense. +func (in *DeploymentStatusLicense) DeepCopy() *DeploymentStatusLicense { + if in == nil { + return nil + } + out := new(DeploymentStatusLicense) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DeploymentStatusMemberElement) DeepCopyInto(out *DeploymentStatusMemberElement) { *out = *in @@ -1884,6 +1907,11 @@ func (in *LicenseSpec) DeepCopyInto(out *LicenseSpec) { *out = new(metav1.Duration) **out = **in } + if in.Telemetry != nil { + in, out := &in.Telemetry, &out.Telemetry + *out = new(bool) + **out = **in + } return } diff --git a/pkg/crd/crds/database-deployment.schema.generated.yaml b/pkg/crd/crds/database-deployment.schema.generated.yaml index b2f84124e..f7cdb0393 100644 --- a/pkg/crd/crds/database-deployment.schema.generated.yaml +++ b/pkg/crd/crds/database-deployment.schema.generated.yaml @@ -10184,6 +10184,7 @@ v1: mode: description: Mode Defines the mode of license enum: + - discover - key - master type: string @@ -10193,6 +10194,9 @@ v1: the license key token or master key used for enterprise images. This value is not used for the Community Edition. type: string + telemetry: + description: Telemetry defines if telemetry is collected + type: boolean ttl: description: TTL Sets the requested License TTL type: string @@ -27510,6 +27514,7 @@ v2alpha1: mode: description: Mode Defines the mode of license enum: + - discover - key - master type: string @@ -27519,6 +27524,9 @@ v2alpha1: the license key token or master key used for enterprise images. This value is not used for the Community Edition. type: string + telemetry: + description: Telemetry defines if telemetry is collected + type: boolean ttl: description: TTL Sets the requested License TTL type: string diff --git a/pkg/deployment/reconcile/action.register.generated.go b/pkg/deployment/reconcile/action.register.generated.go index 0a797ce81..f4a735aaf 100644 --- a/pkg/deployment/reconcile/action.register.generated.go +++ b/pkg/deployment/reconcile/action.register.generated.go @@ -126,6 +126,9 @@ var ( _ Action = &actionKillMemberPod{} _ actionFactory = newKillMemberPodAction + _ Action = &actionLicenseClean{} + _ actionFactory = newLicenseCleanAction + _ Action = &actionLicenseGenerate{} _ actionFactory = newLicenseGenerateAction @@ -219,6 +222,9 @@ var ( _ Action = &actionRuntimeContainerSyncTolerations{} _ actionFactory = newRuntimeContainerSyncTolerationsAction + _ Action = &actionSetAnnotation{} + _ actionFactory = newSetAnnotationAction + _ Action = &actionSetConditionV2{} _ actionFactory = newSetConditionV2Action @@ -771,6 +777,20 @@ func init() { registerAction(action, function) } + // LicenseClean + { + // Get Action type + action := api.ActionTypeLicenseClean + + // Get Action defition + function := newLicenseCleanAction + + // Wrap action main function + + // Register action + registerAction(action, function) + } + // LicenseGenerate { // Get Action type @@ -1224,6 +1244,20 @@ func init() { registerAction(action, function) } + // SetAnnotation + { + // Get Action type + action := api.ActionTypeSetAnnotation + + // Get Action defition + function := newSetAnnotationAction + + // Wrap action main function + + // Register action + registerAction(action, function) + } + // SetCondition { // Get Action type diff --git a/pkg/deployment/reconcile/action.register.generated_test.go b/pkg/deployment/reconcile/action.register.generated_test.go index cc6c58de2..6088d95f3 100644 --- a/pkg/deployment/reconcile/action.register.generated_test.go +++ b/pkg/deployment/reconcile/action.register.generated_test.go @@ -386,6 +386,16 @@ func Test_Actions(t *testing.T) { }) }) + t.Run("LicenseClean", func(t *testing.T) { + ActionsExistence(t, api.ActionTypeLicenseClean) + t.Run("Internal", func(t *testing.T) { + require.False(t, api.ActionTypeLicenseClean.Internal()) + }) + t.Run("Optional", func(t *testing.T) { + require.False(t, api.ActionTypeLicenseClean.Optional()) + }) + }) + t.Run("LicenseGenerate", func(t *testing.T) { ActionsExistence(t, api.ActionTypeLicenseGenerate) t.Run("Internal", func(t *testing.T) { @@ -711,6 +721,16 @@ func Test_Actions(t *testing.T) { }) }) + t.Run("SetAnnotation", func(t *testing.T) { + ActionsExistence(t, api.ActionTypeSetAnnotation) + t.Run("Internal", func(t *testing.T) { + require.True(t, api.ActionTypeSetAnnotation.Internal()) + }) + t.Run("Optional", func(t *testing.T) { + require.False(t, api.ActionTypeSetAnnotation.Optional()) + }) + }) + t.Run("SetCondition", func(t *testing.T) { // nolint:staticcheck ActionsExistence(t, api.ActionTypeSetCondition) diff --git a/pkg/deployment/reconcile/action_context.go b/pkg/deployment/reconcile/action_context.go index cfb1223a6..e5a1edecf 100644 --- a/pkg/deployment/reconcile/action_context.go +++ b/pkg/deployment/reconcile/action_context.go @@ -1,7 +1,7 @@ // // DISCLAIMER // -// Copyright 2016-2024 ArangoDB GmbH, Cologne, Germany +// Copyright 2016-2025 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. @@ -34,6 +34,7 @@ import ( agencyCache "github.com/arangodb/kube-arangodb/pkg/deployment/agency" "github.com/arangodb/kube-arangodb/pkg/deployment/agency/state" "github.com/arangodb/kube-arangodb/pkg/deployment/member" + "github.com/arangodb/kube-arangodb/pkg/deployment/patch" "github.com/arangodb/kube-arangodb/pkg/deployment/reconciler" "github.com/arangodb/kube-arangodb/pkg/logging" "github.com/arangodb/kube-arangodb/pkg/util" @@ -49,6 +50,7 @@ type ActionContext interface { reconciler.DeploymentAgencyMaintenance reconciler.DeploymentPodRenderer reconciler.ArangoAgencyGet + reconciler.ArangoApplier reconciler.DeploymentInfoGetter reconciler.DeploymentDatabaseClient reconciler.KubernetesEventGenerator @@ -141,6 +143,14 @@ type actionContext struct { metrics *Metrics } +func (ac *actionContext) ApplyPatchOnPod(ctx context.Context, pod *core.Pod, p ...patch.Item) error { + return ac.context.ApplyPatchOnPod(ctx, pod, p...) +} + +func (ac *actionContext) ApplyPatch(ctx context.Context, p ...patch.Item) error { + return ac.context.ApplyPatch(ctx, p...) +} + func (ac *actionContext) IsSyncEnabled() bool { return ac.context.IsSyncEnabled() } diff --git a/pkg/deployment/reconcile/action_license_clean.go b/pkg/deployment/reconcile/action_license_clean.go new file mode 100644 index 000000000..c8cc4cd0c --- /dev/null +++ b/pkg/deployment/reconcile/action_license_clean.go @@ -0,0 +1,58 @@ +// +// DISCLAIMER +// +// Copyright 2016-2025 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 reconcile + +import ( + "context" + + api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" +) + +func newLicenseCleanAction(action api.Action, actionCtx ActionContext) Action { + a := &actionLicenseClean{} + + a.actionImpl = newActionImplDefRef(action, actionCtx) + + return a +} + +type actionLicenseClean struct { + actionImpl + + actionEmptyCheckProgress +} + +func (a *actionLicenseClean) Start(ctx context.Context) (bool, error) { + if err := a.actionCtx.WithStatusUpdate(ctx, func(s *api.DeploymentStatus) bool { + if s.License == nil { + return false + } + + s.License = nil + + return true + }); err != nil { + a.log.Err(err).Error("Unable to clean license") + return true, nil + } + + return true, nil +} diff --git a/pkg/deployment/reconcile/action_license_generate.go b/pkg/deployment/reconcile/action_license_generate.go index 28fde10ca..78d4432ff 100644 --- a/pkg/deployment/reconcile/action_license_generate.go +++ b/pkg/deployment/reconcile/action_license_generate.go @@ -23,9 +23,12 @@ package reconcile import ( "context" "fmt" + "math" + "time" "google.golang.org/protobuf/types/known/durationpb" core "k8s.io/api/core/v1" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" "github.com/arangodb/kube-arangodb/pkg/deployment/client" @@ -65,7 +68,7 @@ func (a *actionLicenseGenerate) Start(ctx context.Context) (bool, error) { return true, err } - if l.Master == nil { + if l.API == nil { return true, nil } @@ -81,19 +84,19 @@ func (a *actionLicenseGenerate) Start(ctx context.Context) (bool, error) { return true, nil } - inv, err := inventory.FetchInventorySpec(ctx, a.log, 4, c.Connection(), &inventory.Configuration{Telemetry: util.NewType(true)}) + inv, err := inventory.FetchInventorySpec(ctx, a.log, 4, c.Connection(), &inventory.Configuration{Telemetry: util.NewType(spec.License.GetTelemetry())}) if err != nil { a.log.Err(err).Error("Unable to generate inventory") return true, nil } - lm, err := license_manager.NewClient(license_manager.ArangoLicenseManagerEndpoint, l.Master.ClientID, l.Master.ClientSecret) + lm, err := license_manager.NewClient(license_manager.ArangoLicenseManagerEndpoint, l.API.ClientID, l.API.ClientSecret) if err != nil { a.log.Err(err).Error("Unable to create inventory client") return true, nil } - license, err := lm.License(ctx, license_manager.LicenseRequest{ + generatedLicense, err := lm.License(ctx, license_manager.LicenseRequest{ DeploymentID: util.NewType(inv.DeploymentId), TTL: util.NewType(ugrpc.NewObject(durationpb.New(spec.License.GetTTL()))), Inventory: util.NewType(ugrpc.NewObject(inv)), @@ -108,12 +111,40 @@ func (a *actionLicenseGenerate) Start(ctx context.Context) (bool, error) { }) return true, nil } - a.log.Str("id", license.ID).Info("License Generated") + a.log.Str("id", generatedLicense.ID).Info("License Generated") - if err := client.NewClient(c.Connection(), a.log).SetLicense(ctxChild, license.License, true); err != nil { + client := client.NewClient(c.Connection(), a.log) + + if err := client.SetLicense(ctxChild, generatedLicense.License, true); err != nil { a.log.Err(err).Error("Unable to set license") return true, nil } + license, err := client.GetLicense(ctxChild) + if err != nil { + a.log.Err(err).Error("Unable to get license") + return true, nil + } + + expires := time.Now().Add(spec.License.GetExpirationGracePeriod()) + + if expires.After(license.Expires()) { + // License will expire before grace period, reduce to 90% + expires = time.Now().Add(time.Duration(math.Round(float64(time.Since(license.Expires())) * api.LicenseExpirationGraceRatio))) + } + + if err := a.actionCtx.WithStatusUpdate(ctx, func(s *api.DeploymentStatus) bool { + s.License = &api.DeploymentStatusLicense{ + ID: generatedLicense.ID, + Hash: license.Hash, + Expires: meta.Time{Time: license.Expires()}, + Mode: api.LicenseModeAPI, + Regenerate: meta.Time{Time: expires}, + } + return true + }); err != nil { + a.log.Err(err).Error("Unable to register license") + return true, nil + } return true, nil } diff --git a/pkg/deployment/reconcile/action_license_set.go b/pkg/deployment/reconcile/action_license_set.go index 22cf3c42f..19d1fdd6a 100644 --- a/pkg/deployment/reconcile/action_license_set.go +++ b/pkg/deployment/reconcile/action_license_set.go @@ -23,6 +23,8 @@ package reconcile import ( "context" + meta "k8s.io/apimachinery/pkg/apis/meta/v1" + api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" "github.com/arangodb/kube-arangodb/pkg/deployment/client" "github.com/arangodb/kube-arangodb/pkg/util/globals" @@ -75,32 +77,28 @@ func (a *actionLicenseSet) Start(ctx context.Context) (bool, error) { client := client.NewClient(c.Connection(), a.log) - if ok, err := licenseV2Compare(ctxChild, client, l.V2); err != nil { - a.log.Err(err).Error("Unable to verify license") - return true, nil - } else if ok { - // Already latest license - return true, nil - } - if err := client.SetLicense(ctxChild, string(l.V2), true); err != nil { a.log.Err(err).Error("Unable to set license") return true, nil } - return true, nil -} - -func licenseV2Compare(ctx context.Context, client client.Client, license k8sutil.License) (bool, error) { - currentLicense, err := client.GetLicense(ctx) + license, err := client.GetLicense(ctxChild) if err != nil { - return false, err + a.log.Err(err).Error("Unable to get license") + return true, nil } - if currentLicense.Hash == license.V2Hash() { - // Already latest license + if err := a.actionCtx.WithStatusUpdate(ctx, func(s *api.DeploymentStatus) bool { + s.License = &api.DeploymentStatusLicense{ + Hash: license.Hash, + Expires: meta.Time{Time: license.Expires()}, + Mode: api.LicenseModeKey, + } + return true + }); err != nil { + a.log.Err(err).Error("Unable to register license") return true, nil } - return false, nil + return true, nil } diff --git a/pkg/deployment/reconcile/action_set_annotation.go b/pkg/deployment/reconcile/action_set_annotation.go new file mode 100644 index 000000000..3aab70e43 --- /dev/null +++ b/pkg/deployment/reconcile/action_set_annotation.go @@ -0,0 +1,98 @@ +// +// DISCLAIMER +// +// Copyright 2025 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 reconcile + +import ( + "context" + + api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1" + "github.com/arangodb/kube-arangodb/pkg/deployment/patch" +) + +const ( + SetAnnotationActionKey string = "key" + SetAnnotationActionValue string = "value" + SetAnnotationActionValueRemove string = "-" +) + +func newSetAnnotationAction(action api.Action, actionCtx ActionContext) Action { + a := &actionSetAnnotation{} + + a.actionImpl = newActionImplDefRef(action, actionCtx) + + return a +} + +type actionSetAnnotation struct { + // actionImpl implement timeout and member id functions + actionImpl + + actionEmptyCheckProgress +} + +// Start starts the action for changing conditions on the provided member. +func (a actionSetAnnotation) Start(ctx context.Context) (bool, error) { + key, ok := a.action.Params[SetAnnotationActionKey] + if !ok { + a.log.Info("key %s is missing in action definition", SetAnnotationActionKey) + return true, nil + } + + value, ok := a.action.Params[SetAnnotationActionValue] + if !ok { + a.log.Info("key %s is missing in action definition", SetAnnotationActionValue) + return true, nil + } + + if value == SetAnnotationActionValueRemove { + if _, ok := a.actionCtx.GetAPIObject().GetAnnotations()[key]; ok { + if err := a.actionCtx.ApplyPatch(ctx, patch.ItemRemove(patch.NewPath("metadata", "annotations", key))); err != nil { + a.log.Str("key", key).Err(err).Warn("Unable to remove annotation") + return true, nil + } + + a.log.Str("key", key).Info("Removed annotation") + return true, nil + } + a.log.Str("key", key).Info("Annotation already gone") + return true, nil + } else { + if z, ok := a.actionCtx.GetAPIObject().GetAnnotations()[key]; ok { + if value != z { + if err := a.actionCtx.ApplyPatch(ctx, patch.ItemReplace(patch.NewPath("metadata", "annotations", key), value)); err != nil { + a.log.Str("key", key).Str("value", value).Err(err).Warn("Unable to update annotation") + return true, nil + } + + a.log.Str("key", key).Str("value", value).Info("Updated annotation") + return true, nil + } + a.log.Str("key", key).Str("value", value).Info("Annotation update not required") + return true, nil + } + if err := a.actionCtx.ApplyPatch(ctx, patch.ItemAdd(patch.NewPath("metadata", "annotations", key), value)); err != nil { + a.log.Str("key", key).Str("value", value).Err(err).Warn("Unable to add annotation") + return true, nil + } + a.log.Str("key", key).Str("value", value).Info("Added annotation") + return true, nil + } +} diff --git a/pkg/deployment/reconcile/plan_builder_license.go b/pkg/deployment/reconcile/plan_builder_license.go index 3c4c011e6..f971f6947 100644 --- a/pkg/deployment/reconcile/plan_builder_license.go +++ b/pkg/deployment/reconcile/plan_builder_license.go @@ -29,6 +29,7 @@ import ( "github.com/arangodb/kube-arangodb/pkg/deployment/client" sharedReconcile "github.com/arangodb/kube-arangodb/pkg/deployment/reconcile/shared" "github.com/arangodb/kube-arangodb/pkg/util/arangod" + "github.com/arangodb/kube-arangodb/pkg/util/errors" "github.com/arangodb/kube-arangodb/pkg/util/globals" "github.com/arangodb/kube-arangodb/pkg/util/k8sutil" ) @@ -36,20 +37,83 @@ import ( func (r *Reconciler) updateClusterLicense(ctx context.Context, apiObject k8sutil.APIObject, spec api.DeploymentSpec, status api.DeploymentStatus, context PlanBuilderContext) api.Plan { + if l := status.License; l == nil { + // Cleanup the Condition + if status.Conditions.IsTrue(api.ConditionTypeLicenseSet) { + // Cleanup the old condition if we do not expect license + if !spec.License.HasSecretName() { + return api.Plan{sharedReconcile.RemoveConditionActionV2("License is not set", api.ConditionTypeLicenseSet)} + } else { + // Cleanup the old condition if we do not expect license + return api.Plan{sharedReconcile.UpdateConditionActionV2("License is not set", api.ConditionTypeLicenseSet, false, "License Pending", "", "")} + + } + } + } else { + // Set the Condition + if !status.Conditions.IsTrue(api.ConditionTypeLicenseSet) { + // Cleanup the old condition + return api.Plan{sharedReconcile.UpdateConditionActionV2("License is set", api.ConditionTypeLicenseSet, true, "License UpToDate", "", l.Hash)} + } + } + if !spec.License.HasSecretName() { + if status.License != nil { + return api.Plan{actions.NewClusterAction(api.ActionTypeLicenseClean, "Removing license reference")} + } return nil } - switch spec.License.Mode.Get() { + mode, err := r.updateClusterLicenseDiscover(spec, context) + if err != nil { + r.log.Err(err).Warn("Unable to discover license mode") + } + + if l := status.License; l != nil { + if mode != l.Mode { + return api.Plan{actions.NewClusterAction(api.ActionTypeLicenseClean, "Removing license reference - invalid mode")} + } + } + + switch mode { case api.LicenseModeKey: - return r.updateClusterLicenseKey(ctx, spec, status, context) - case api.LicenseModeMaster: - return r.updateClusterLicenseMaster(ctx, spec, status, context) + if p := r.updateClusterLicenseKey(ctx, spec, status, context); len(p) > 0 { + return p + } + case api.LicenseModeAPI: + if p := r.updateClusterLicenseAPI(ctx, spec, status, context); len(p) > 0 { + return p + } } return nil } +func (r *Reconciler) updateClusterLicenseDiscover(spec api.DeploymentSpec, context PlanBuilderContext) (api.LicenseMode, error) { + switch spec.License.Mode.Get() { + case api.LicenseModeKey: + return api.LicenseModeKey, nil + case api.LicenseModeAPI: + return api.LicenseModeAPI, nil + } + + // Run the discovery + l, err := k8sutil.GetLicenseFromSecret(context.ACS().CurrentClusterCache(), spec.License.GetSecretName()) + if err != nil { + return "", err + } + + if l.V2.IsV2Set() { + return api.LicenseModeKey, nil + } + + if l.API != nil { + return api.LicenseModeAPI, nil + } + + return "", errors.Errorf("Unable to discover LicenseAPIKey mode") +} + func (r *Reconciler) updateClusterLicenseKey(ctx context.Context, spec api.DeploymentSpec, status api.DeploymentStatus, context PlanBuilderContext) api.Plan { l, err := k8sutil.GetLicenseFromSecret(context.ACS().CurrentClusterCache(), spec.License.GetSecretName()) if err != nil { @@ -88,29 +152,34 @@ func (r *Reconciler) updateClusterLicenseKey(ctx context.Context, spec api.Deplo return nil } + if status.License == nil { + // Run the set + return api.Plan{actions.NewAction(api.ActionTypeLicenseSet, member.Group, member.Member, "Generating license")} + } + internalClient := client.NewClient(c.Connection(), r.log) - if ok, err := licenseV2Compare(ctxChild, internalClient, l.V2); err != nil { - r.log.Err(err).Error("Unable to verify license") - return nil - } else if ok { - if c, _ := status.Conditions.Get(api.ConditionTypeLicenseSet); !c.IsTrue() || c.Hash != l.V2.V2Hash() { - return api.Plan{sharedReconcile.UpdateConditionActionV2("License is set", api.ConditionTypeLicenseSet, true, "License UpToDate", "", l.V2.V2Hash())} - } + license, err := internalClient.GetLicense(ctxChild) + if err != nil { + r.log.Err(err).Error("Unable to get client") return nil } - return api.Plan{sharedReconcile.RemoveConditionActionV2("License is not set", api.ConditionTypeLicenseSet), actions.NewAction(api.ActionTypeLicenseSet, member.Group, member.Member, "Setting license")} + if status.License.Hash != license.Hash { + return api.Plan{actions.NewClusterAction(api.ActionTypeLicenseClean, "Removing license reference - Invalid Hash")} + } + + return nil } -func (r *Reconciler) updateClusterLicenseMaster(ctx context.Context, spec api.DeploymentSpec, status api.DeploymentStatus, context PlanBuilderContext) api.Plan { +func (r *Reconciler) updateClusterLicenseAPI(ctx context.Context, spec api.DeploymentSpec, status api.DeploymentStatus, context PlanBuilderContext) api.Plan { l, err := k8sutil.GetLicenseFromSecret(context.ACS().CurrentClusterCache(), spec.License.GetSecretName()) if err != nil { r.log.Err(err).Error("License secret error") return nil } - if l.Master == nil { + if l.API == nil { r.log.Str("secret", spec.License.GetSecretName()).Error("V2 License key is not set") return nil } @@ -141,6 +210,11 @@ func (r *Reconciler) updateClusterLicenseMaster(ctx context.Context, spec api.De return nil } + if status.License == nil { + // Run the generation + return api.Plan{actions.NewAction(api.ActionTypeLicenseGenerate, member.Group, member.Member, "Generating license")} + } + internalClient := client.NewClient(c.Connection(), r.log) currentLicense, err := internalClient.GetLicense(ctxChild) @@ -149,12 +223,14 @@ func (r *Reconciler) updateClusterLicenseMaster(ctx context.Context, spec api.De return nil } - if time.Until(currentLicense.Expires()) > spec.License.GetExpirationGracePeriod() { - if c, _ := status.Conditions.Get(api.ConditionTypeLicenseSet); !c.IsTrue() { - return api.Plan{sharedReconcile.UpdateConditionActionV2("License is set", api.ConditionTypeLicenseSet, true, "License UpToDate", "", currentLicense.Hash)} - } - return nil + if currentLicense.Hash != status.License.Hash { + // Invalid hash, cleanup + return api.Plan{actions.NewClusterAction(api.ActionTypeLicenseClean, "Removing license reference - Invalid Hash")} } - return api.Plan{sharedReconcile.RemoveConditionActionV2("License is not set", api.ConditionTypeLicenseSet), actions.NewAction(api.ActionTypeLicenseGenerate, member.Group, member.Member, "Generating license")} + if status.License.Regenerate.After(time.Now()) { + return api.Plan{actions.NewClusterAction(api.ActionTypeLicenseClean, "Removing license reference - Regeneration Required")} + } + + return nil } diff --git a/pkg/license_manager/client.go b/pkg/license_manager/client.go index a643e5a7e..73c137e2a 100644 --- a/pkg/license_manager/client.go +++ b/pkg/license_manager/client.go @@ -26,6 +26,7 @@ import ( goHttp "net/http" "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/timestamppb" "github.com/arangodb/go-driver" "github.com/arangodb/go-driver/http" @@ -84,8 +85,9 @@ type LicenseRequest struct { } type LicenseResponse struct { - ID string `json:"id"` - License string `json:"license"` + ID string `json:"id"` + License string `json:"license"` + Expires *ugrpc.Object[*timestamppb.Timestamp] `json:"expires,omitempty"` } type RegistryResponse struct { diff --git a/pkg/platform/inventory/consts.go b/pkg/platform/inventory/consts.go new file mode 100644 index 000000000..ff3e1725c --- /dev/null +++ b/pkg/platform/inventory/consts.go @@ -0,0 +1,23 @@ +// +// DISCLAIMER +// +// Copyright 2025 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 inventory + +const FixedSingleDeploymentID = "" diff --git a/pkg/platform/inventory/fetcher.deployment.id.go b/pkg/platform/inventory/fetcher.deployment.id.go index fef2c77f8..2f39b1916 100644 --- a/pkg/platform/inventory/fetcher.deployment.id.go +++ b/pkg/platform/inventory/fetcher.deployment.id.go @@ -54,7 +54,7 @@ func init() { health, err := arangod.GetRequestWithTimeout[driver.ClusterHealth](ctx, globals.GetGlobals().Timeouts().ArangoD().Get(), conn, "_admin", "cluster", "health").AcceptCode(goHttp.StatusOK).Response() if err != nil { - return err + log.Warn("ClusterHealth Endpoint does not work, fallback to the Fixed Single Deployment ID") } return errors.Errors( diff --git a/pkg/util/constants/constants.go b/pkg/util/constants/constants.go index 9e48908d2..ae30f7295 100644 --- a/pkg/util/constants/constants.go +++ b/pkg/util/constants/constants.go @@ -45,12 +45,12 @@ const ( EnvArangoLicenseKey = "ARANGO_LICENSE_KEY" // Contains the License Key for the Docker Image EnvArangoSyncMonitoringToken = "ARANGOSYNC_MONITORING_TOKEN" // Constains monitoring token for ArangoSync servers - SecretEncryptionKey = "key" // Key in a Secret.Data used to store an 32-byte encryption key - SecretKeyToken = "token" // Key inside a Secret used to hold a JWT or monitoring token - SecretKeyV2Token = "token-v2" // Key inside a Secret used to hold a License in V2 Format - SecretKeyV2License = "license-v2" // Key inside a Secret used to hold a License in V2 Format - SecretKeyMasterClientID = "master-client-id" // Key inside a Secret used to hold a JWT or monitoring token - SecretKeyMasterClientSecret = "master-client-secret" // Key inside a Secret used to hold a JWT or monitoring token + SecretEncryptionKey = "key" // Key in a Secret.Data used to store an 32-byte encryption key + SecretKeyToken = "token" // Key inside a Secret used to hold a JWT or monitoring token + SecretKeyV2Token = "token-v2" // Key inside a Secret used to hold a License in V2 Format + SecretKeyV2License = "license-v2" // Key inside a Secret used to hold a License in V2 Format + SecretKeyLicenseClientID = "license-client-id" // Key inside a Secret used to hold a License Credentials in API Format + SecretKeyLicenseClientSecret = "license-client-secret" // Key inside a Secret used to hold a License Credentials in API Format SecretCACertificate = "ca.crt" // Key in Secret.data used to store a PEM encoded CA certificate (public key) SecretCAKey = "ca.key" // Key in Secret.data used to store a PEM encoded CA private key diff --git a/pkg/util/k8sutil/license.go b/pkg/util/k8sutil/license.go index 9a28a687d..71d833697 100644 --- a/pkg/util/k8sutil/license.go +++ b/pkg/util/k8sutil/license.go @@ -41,9 +41,9 @@ func (l License) V2Hash() string { } type LicenseSecret struct { - V1 string - V2 License - Master *LicenseSecretMaster + V1 string + V2 License + API *LicenseSecretMaster } type LicenseSecretMaster struct { @@ -63,9 +63,9 @@ func GetLicenseFromSecret(secret secret.Inspector, name string) (LicenseSecret, l.V1 = string(v) } - if cid, ok := s.Data[utilConstants.SecretKeyMasterClientID]; ok { - if cs, ok := s.Data[utilConstants.SecretKeyMasterClientSecret]; ok { - l.Master = &LicenseSecretMaster{ + if cid, ok := s.Data[utilConstants.SecretKeyLicenseClientID]; ok { + if cs, ok := s.Data[utilConstants.SecretKeyLicenseClientSecret]; ok { + l.API = &LicenseSecretMaster{ ClientID: string(cid), ClientSecret: string(cs), } @@ -86,9 +86,9 @@ func GetLicenseFromSecret(secret secret.Inspector, name string) (LicenseSecret, } else { l.V2 = License(v2) } - } else if l.Master == nil { + } else if l.API == nil { return LicenseSecret{}, errors.Errorf("Key (%s, %s, %s, or %s+%s) is missing in the license secret (%s)", - utilConstants.SecretKeyToken, utilConstants.SecretKeyV2License, utilConstants.SecretKeyV2Token, utilConstants.SecretKeyMasterClientID, utilConstants.SecretKeyMasterClientSecret, name) + utilConstants.SecretKeyToken, utilConstants.SecretKeyV2License, utilConstants.SecretKeyV2Token, utilConstants.SecretKeyLicenseClientID, utilConstants.SecretKeyLicenseClientSecret, name) } return l, nil From f53d33a64c52cb5e7c9762fed97ad4c719b4811e Mon Sep 17 00:00:00 2001 From: ajanikow <12255597+ajanikow@users.noreply.github.com> Date: Mon, 3 Nov 2025 10:01:46 +0000 Subject: [PATCH 3/9] Iter --- docs/api/ArangoDeployment.V1.md | 20 ++++++-- pkg/apis/deployment/v1/license_spec.go | 49 ++++++++---------- .../deployment/v1/zz_generated.deepcopy.go | 5 ++ pkg/apis/deployment/v2alpha1/license_spec.go | 49 ++++++++---------- .../v2alpha1/zz_generated.deepcopy.go | 5 ++ .../database-deployment.schema.generated.yaml | 3 ++ .../reconcile/action_license_generate.go | 50 ++++++++++++++++--- .../inventory/fetcher.deployment.id.go | 44 +++++++++------- pkg/util/arangod/error.go | 41 ++++++++++++++- pkg/util/arangod/request.go | 24 ++------- 10 files changed, 183 insertions(+), 107 deletions(-) diff --git a/docs/api/ArangoDeployment.V1.md b/docs/api/ArangoDeployment.V1.md index fa54967a7..0310be671 100644 --- a/docs/api/ArangoDeployment.V1.md +++ b/docs/api/ArangoDeployment.V1.md @@ -4793,7 +4793,7 @@ Possible Values: ### .spec.license.expirationGracePeriod -Type: `integer` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.3.1/pkg/apis/deployment/v1/license_spec.go#L72) +Type: `integer` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.3.1/pkg/apis/deployment/v1/license_spec.go#L68) ExpirationGracePeriod defines the expiration grace period for the license @@ -4801,9 +4801,19 @@ Default Value: `72h` *** +### .spec.license.inventory + +Type: `boolean` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.3.1/pkg/apis/deployment/v1/license_spec.go#L76) + +Inventory defines if inventory is collected + +Default Value: `true` + +*** + ### .spec.license.mode -Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.3.1/pkg/apis/deployment/v1/license_spec.go#L64) +Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.3.1/pkg/apis/deployment/v1/license_spec.go#L60) Mode Defines the mode of license @@ -4816,7 +4826,7 @@ Possible Values: ### .spec.license.secretName -Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.3.1/pkg/apis/deployment/v1/license_spec.go#L57) +Type: `string` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.3.1/pkg/apis/deployment/v1/license_spec.go#L53) SecretName setting specifies the name of a kubernetes `Secret` that contains the license key token or master key used for enterprise images. This value is not used for @@ -4826,7 +4836,7 @@ the Community Edition. ### .spec.license.telemetry -Type: `boolean` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.3.1/pkg/apis/deployment/v1/license_spec.go#L76) +Type: `boolean` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.3.1/pkg/apis/deployment/v1/license_spec.go#L72) Telemetry defines if telemetry is collected @@ -4836,7 +4846,7 @@ Default Value: `true` ### .spec.license.ttl -Type: `integer` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.3.1/pkg/apis/deployment/v1/license_spec.go#L68) +Type: `integer` [\[ref\]](https://github.com/arangodb/kube-arangodb/blob/1.3.1/pkg/apis/deployment/v1/license_spec.go#L64) TTL Sets the requested License TTL diff --git a/pkg/apis/deployment/v1/license_spec.go b/pkg/apis/deployment/v1/license_spec.go index d6e25ec40..3857cd477 100644 --- a/pkg/apis/deployment/v1/license_spec.go +++ b/pkg/apis/deployment/v1/license_spec.go @@ -21,8 +21,6 @@ package v1 import ( - "time" - meta "k8s.io/apimachinery/pkg/apis/meta/v1" shared "github.com/arangodb/kube-arangodb/pkg/apis/shared" @@ -31,9 +29,7 @@ import ( ) const ( - LicenseExpirationGraceRatio = 0.9 - DefaultLicenseExpirationGracePeriod = 3 * 24 * time.Hour - DefaultLicenseTTL = 14 * 24 * time.Hour + LicenseExpirationGraceRatio = 0.75 ) type LicenseMode string @@ -74,6 +70,10 @@ type LicenseSpec struct { // Telemetry defines if telemetry is collected // +doc/default: true Telemetry *bool `json:"telemetry,omitempty"` + + // Inventory defines if inventory is collected + // +doc/default: true + Inventory *bool `json:"inventory,omitempty"` } // HasSecretName returns true if a license key secret name was set @@ -86,25 +86,14 @@ func (s LicenseSpec) GetSecretName() string { return util.TypeOrDefault[string](s.SecretName) } -// GetTTL returns the license TTL -func (s LicenseSpec) GetTTL() time.Duration { - if s.TTL == nil { - return DefaultLicenseTTL - } - return s.TTL.Duration -} - // GetTelemetry returns the license Telemetry func (s LicenseSpec) GetTelemetry() bool { return util.OptionalType(s.Telemetry, true) } -// GetExpirationGracePeriod returns the expiration period -func (s LicenseSpec) GetExpirationGracePeriod() time.Duration { - if s.ExpirationGracePeriod == nil { - return DefaultLicenseExpirationGracePeriod - } - return s.ExpirationGracePeriod.Duration +// GetInventory returns the license Inventory +func (s LicenseSpec) GetInventory() bool { + return util.OptionalType(s.Telemetry, true) } // Validate validates the LicenseSpec @@ -119,20 +108,26 @@ func (s LicenseSpec) Validate() error { }), // Expiration shared.PrefixResourceErrorFunc("expirationGracePeriod", func() error { - if s.GetExpirationGracePeriod() <= 0 { - return errors.Errorf("Expiration grace period must be greater than zero") - } - - if s.GetExpirationGracePeriod() >= s.GetTTL() { - return errors.Errorf("Expiration grace period must be less than TTL") + if v := s.ExpirationGracePeriod; v != nil { + if v.Duration <= 0 { + return errors.Errorf("Expiration grace period must be greater than zero") + } + + if t := s.TTL; t != nil { + if v.Duration >= t.Duration { + return errors.Errorf("Expiration grace period must be less than TTL") + } + } } return nil }), // TTL shared.PrefixResourceErrorFunc("ttl", func() error { - if s.GetTTL() <= 0 { - return errors.Errorf("TTL must be greater than zero") + if t := s.TTL; t != nil { + if t.Duration <= 0 { + return errors.Errorf("TTL must be greater than zero") + } } return nil diff --git a/pkg/apis/deployment/v1/zz_generated.deepcopy.go b/pkg/apis/deployment/v1/zz_generated.deepcopy.go index bb607ab9e..33fe9768d 100644 --- a/pkg/apis/deployment/v1/zz_generated.deepcopy.go +++ b/pkg/apis/deployment/v1/zz_generated.deepcopy.go @@ -1912,6 +1912,11 @@ func (in *LicenseSpec) DeepCopyInto(out *LicenseSpec) { *out = new(bool) **out = **in } + if in.Inventory != nil { + in, out := &in.Inventory, &out.Inventory + *out = new(bool) + **out = **in + } return } diff --git a/pkg/apis/deployment/v2alpha1/license_spec.go b/pkg/apis/deployment/v2alpha1/license_spec.go index 263dbbf73..8af5bb6cd 100644 --- a/pkg/apis/deployment/v2alpha1/license_spec.go +++ b/pkg/apis/deployment/v2alpha1/license_spec.go @@ -21,8 +21,6 @@ package v2alpha1 import ( - "time" - meta "k8s.io/apimachinery/pkg/apis/meta/v1" shared "github.com/arangodb/kube-arangodb/pkg/apis/shared" @@ -31,9 +29,7 @@ import ( ) const ( - LicenseExpirationGraceRatio = 0.9 - DefaultLicenseExpirationGracePeriod = 3 * 24 * time.Hour - DefaultLicenseTTL = 14 * 24 * time.Hour + LicenseExpirationGraceRatio = 0.75 ) type LicenseMode string @@ -74,6 +70,10 @@ type LicenseSpec struct { // Telemetry defines if telemetry is collected // +doc/default: true Telemetry *bool `json:"telemetry,omitempty"` + + // Inventory defines if inventory is collected + // +doc/default: true + Inventory *bool `json:"inventory,omitempty"` } // HasSecretName returns true if a license key secret name was set @@ -86,25 +86,14 @@ func (s LicenseSpec) GetSecretName() string { return util.TypeOrDefault[string](s.SecretName) } -// GetTTL returns the license TTL -func (s LicenseSpec) GetTTL() time.Duration { - if s.TTL == nil { - return DefaultLicenseTTL - } - return s.TTL.Duration -} - // GetTelemetry returns the license Telemetry func (s LicenseSpec) GetTelemetry() bool { return util.OptionalType(s.Telemetry, true) } -// GetExpirationGracePeriod returns the expiration period -func (s LicenseSpec) GetExpirationGracePeriod() time.Duration { - if s.ExpirationGracePeriod == nil { - return DefaultLicenseExpirationGracePeriod - } - return s.ExpirationGracePeriod.Duration +// GetInventory returns the license Inventory +func (s LicenseSpec) GetInventory() bool { + return util.OptionalType(s.Telemetry, true) } // Validate validates the LicenseSpec @@ -119,20 +108,26 @@ func (s LicenseSpec) Validate() error { }), // Expiration shared.PrefixResourceErrorFunc("expirationGracePeriod", func() error { - if s.GetExpirationGracePeriod() <= 0 { - return errors.Errorf("Expiration grace period must be greater than zero") - } - - if s.GetExpirationGracePeriod() >= s.GetTTL() { - return errors.Errorf("Expiration grace period must be less than TTL") + if v := s.ExpirationGracePeriod; v != nil { + if v.Duration <= 0 { + return errors.Errorf("Expiration grace period must be greater than zero") + } + + if t := s.TTL; t != nil { + if v.Duration >= t.Duration { + return errors.Errorf("Expiration grace period must be less than TTL") + } + } } return nil }), // TTL shared.PrefixResourceErrorFunc("ttl", func() error { - if s.GetTTL() <= 0 { - return errors.Errorf("TTL must be greater than zero") + if t := s.TTL; t != nil { + if t.Duration <= 0 { + return errors.Errorf("TTL must be greater than zero") + } } return nil diff --git a/pkg/apis/deployment/v2alpha1/zz_generated.deepcopy.go b/pkg/apis/deployment/v2alpha1/zz_generated.deepcopy.go index 448e4e627..8c6697560 100644 --- a/pkg/apis/deployment/v2alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/deployment/v2alpha1/zz_generated.deepcopy.go @@ -1912,6 +1912,11 @@ func (in *LicenseSpec) DeepCopyInto(out *LicenseSpec) { *out = new(bool) **out = **in } + if in.Inventory != nil { + in, out := &in.Inventory, &out.Inventory + *out = new(bool) + **out = **in + } return } diff --git a/pkg/crd/crds/database-deployment.schema.generated.yaml b/pkg/crd/crds/database-deployment.schema.generated.yaml index f7cdb0393..dd0291f3a 100644 --- a/pkg/crd/crds/database-deployment.schema.generated.yaml +++ b/pkg/crd/crds/database-deployment.schema.generated.yaml @@ -10181,6 +10181,9 @@ v1: expirationGracePeriod: description: ExpirationGracePeriod defines the expiration grace period for the license type: string + inventory: + description: Inventory defines if inventory is collected + type: boolean mode: description: Mode Defines the mode of license enum: diff --git a/pkg/deployment/reconcile/action_license_generate.go b/pkg/deployment/reconcile/action_license_generate.go index 78d4432ff..07462bfb8 100644 --- a/pkg/deployment/reconcile/action_license_generate.go +++ b/pkg/deployment/reconcile/action_license_generate.go @@ -84,23 +84,41 @@ func (a *actionLicenseGenerate) Start(ctx context.Context) (bool, error) { return true, nil } - inv, err := inventory.FetchInventorySpec(ctx, a.log, 4, c.Connection(), &inventory.Configuration{Telemetry: util.NewType(spec.License.GetTelemetry())}) + var req license_manager.LicenseRequest + did, err := inventory.ExtractDeploymentID(ctx, c.Connection()) if err != nil { - a.log.Err(err).Error("Unable to generate inventory") + a.log.Err(err).Error("Unable to get deployment id") return true, nil } + req.DeploymentID = util.NewType(did) + + if spec.License.GetInventory() { + inv, err := inventory.FetchInventorySpec(ctx, a.log, 4, c.Connection(), &inventory.Configuration{Telemetry: util.NewType(spec.License.GetTelemetry())}) + if err != nil { + a.log.Err(err).Error("Unable to generate inventory") + return true, nil + } + + if inv.DeploymentId != did { + a.log.Err(err).Error("Invalid deployment ID in inventory") + return true, nil + } + + req.Inventory = util.NewType(ugrpc.NewObject(inv)) + } + + if q := spec.License.TTL; q != nil { + req.TTL = util.NewType(ugrpc.NewObject(durationpb.New(q.Duration))) + } + lm, err := license_manager.NewClient(license_manager.ArangoLicenseManagerEndpoint, l.API.ClientID, l.API.ClientSecret) if err != nil { a.log.Err(err).Error("Unable to create inventory client") return true, nil } - generatedLicense, err := lm.License(ctx, license_manager.LicenseRequest{ - DeploymentID: util.NewType(inv.DeploymentId), - TTL: util.NewType(ugrpc.NewObject(durationpb.New(spec.License.GetTTL()))), - Inventory: util.NewType(ugrpc.NewObject(inv)), - }) + generatedLicense, err := lm.License(ctx, req) if err != nil { a.log.Err(err).Error("Unable to create license") a.actionCtx.CreateEvent(&k8sutil.Event{ @@ -126,7 +144,23 @@ func (a *actionLicenseGenerate) Start(ctx context.Context) (bool, error) { return true, nil } - expires := time.Now().Add(spec.License.GetExpirationGracePeriod()) + expiration := time.Until(license.Expires()) + if expiration <= 0 { + a.log.Error("Unable to get license - invalid timestamp") + return true, nil + } + + if q := spec.License.ExpirationGracePeriod; q != nil { + expiration = expiration - q.Duration + } else { + expiration = time.Duration(math.Round(api.LicenseExpirationGraceRatio * float64(expiration))) + } + if expiration <= 0 { + a.log.Error("Unable to get license - invalid after evaluation") + return true, nil + } + + expires := time.Now().Add(expiration) if expires.After(license.Expires()) { // License will expire before grace period, reduce to 90% diff --git a/pkg/platform/inventory/fetcher.deployment.id.go b/pkg/platform/inventory/fetcher.deployment.id.go index 2f39b1916..1dd789168 100644 --- a/pkg/platform/inventory/fetcher.deployment.id.go +++ b/pkg/platform/inventory/fetcher.deployment.id.go @@ -37,31 +37,39 @@ import ( func init() { global.MustRegister("deployment.id", func(conn driver.Connection, cfg *Configuration, out chan<- *Item) executor.RunFunc { return func(ctx context.Context, log logging.Logger, t executor.Thread, h executor.Handler) error { - if handler := arangod.GetRequestWithTimeout[client.DeploymentID](ctx, globals.GetGlobals().Timeouts().ArangoD().Get(), conn, "_admin", "deployment", "id"); handler.Code() == goHttp.StatusOK { - resp, err := handler.Response() - if err != nil { - return err - } - - return errors.Errors( - Produce(out, "ARANGO_DEPLOYMENT", map[string]string{ - "detail": "id", - }, resp.Id), - ) - } - - log.Warn("Fallback to the ClusterHealth Endpoint") - - health, err := arangod.GetRequestWithTimeout[driver.ClusterHealth](ctx, globals.GetGlobals().Timeouts().ArangoD().Get(), conn, "_admin", "cluster", "health").AcceptCode(goHttp.StatusOK).Response() + did, err := ExtractDeploymentID(ctx, conn) if err != nil { - log.Warn("ClusterHealth Endpoint does not work, fallback to the Fixed Single Deployment ID") + return err } return errors.Errors( Produce(out, "ARANGO_DEPLOYMENT", map[string]string{ "detail": "id", - }, health.ID), + }, did), ) } }) } + +func ExtractDeploymentID(ctx context.Context, conn driver.Connection) (string, error) { + if handler := arangod.GetRequestWithTimeout[client.DeploymentID](ctx, globals.GetGlobals().Timeouts().ArangoD().Get(), conn, "_admin", "deployment", "id"); handler.Code() == goHttp.StatusOK { + resp, err := handler.Response() + if err != nil { + return "", err + } + + return resp.Id, nil + } + + health, err := arangod.GetRequestWithTimeout[driver.ClusterHealth](ctx, globals.GetGlobals().Timeouts().ArangoD().Get(), conn, "_admin", "cluster", "health").AcceptCode(goHttp.StatusOK).Response() + if err == nil { + return health.ID, nil + } else { + if _, ok := arangod.IsInvalidCode(err); ok { + // Fallback to the Deployment ID Single + return FixedSingleDeploymentID, nil + } + + return "", err + } +} diff --git a/pkg/util/arangod/error.go b/pkg/util/arangod/error.go index 4624ae86a..384c05b71 100644 --- a/pkg/util/arangod/error.go +++ b/pkg/util/arangod/error.go @@ -20,7 +20,13 @@ package arangod -import "github.com/arangodb/kube-arangodb/pkg/util/errors" +import ( + "fmt" + goStrings "strings" + + "github.com/arangodb/kube-arangodb/pkg/util" + "github.com/arangodb/kube-arangodb/pkg/util/errors" +) var ( KeyNotFoundError = errors.New("Key not found") @@ -50,3 +56,36 @@ func IsNotLeader(err error) (string, bool) { } return "", false } + +func IsInvalidCode(err error) (InvalidCode, bool) { + var v InvalidCode + if errors.As(err, &v) { + return v, true + } + + return InvalidCode{}, false +} + +func EvaluateCode(code int, accepted ...int) error { + for _, c := range accepted { + if c == code { + return nil + } + } + + return InvalidCode{ + Expected: accepted, + Got: code, + } +} + +type InvalidCode struct { + Expected []int + Got int +} + +func (i InvalidCode) Error() string { + return fmt.Sprintf("Code %d not allowed in expected status codes: %s", i.Got, goStrings.Join(util.FormatList(i.Expected, func(a int) string { + return fmt.Sprintf("%d", a) + }), ", ")) +} diff --git a/pkg/util/arangod/request.go b/pkg/util/arangod/request.go index 450a57405..ce6d46914 100644 --- a/pkg/util/arangod/request.go +++ b/pkg/util/arangod/request.go @@ -22,7 +22,6 @@ package arangod import ( "context" - "encoding/json" "fmt" goHttp "net/http" goStrings "strings" @@ -31,7 +30,6 @@ import ( "github.com/arangodb/go-driver" "github.com/arangodb/kube-arangodb/pkg/util" - "github.com/arangodb/kube-arangodb/pkg/util/errors" ) type Response[OUT any] interface { @@ -77,27 +75,11 @@ func (r response[OUT]) Code() int { } func (r response[OUT]) AcceptCode(codes ...int) Response[OUT] { - for _, code := range codes { - if r.resp.StatusCode() == code { - return r - } - } - - var data string - var obj = map[string]interface{}{} - if err := r.resp.ParseBody("", &obj); err != nil { - data = fmt.Sprintf("Error: %s", err.Error()) - } else { - if dz, err := json.Marshal(obj); err != nil { - data = fmt.Sprintf("Error: %s", err.Error()) - } else { - data = fmt.Sprintf("Data: %s", string(dz)) - } + if err := EvaluateCode(r.resp.StatusCode(), codes...); err != nil { + return NewResponseError[OUT](err) } - return NewResponseError[OUT](errors.Errorf("Code %d not allowed in expected status codes: %s. Body: %s", r.resp.StatusCode(), goStrings.Join(util.FormatList(codes, func(a int) string { - return fmt.Sprintf("%d", a) - }), ", "), data)) + return r } func (r response[OUT]) Response() (OUT, error) { From ec10c0e125229c09af280e0ce273e1506781b6b6 Mon Sep 17 00:00:00 2001 From: ajanikow <12255597+ajanikow@users.noreply.github.com> Date: Mon, 3 Nov 2025 10:38:48 +0000 Subject: [PATCH 4/9] Iter --- pkg/platform/inventory/consts.go | 2 +- pkg/platform/inventory/consts_test.go | 32 +++++++++++++++++++ .../inventory/fetcher.deployment.id.go | 2 +- 3 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 pkg/platform/inventory/consts_test.go diff --git a/pkg/platform/inventory/consts.go b/pkg/platform/inventory/consts.go index ff3e1725c..c44749f58 100644 --- a/pkg/platform/inventory/consts.go +++ b/pkg/platform/inventory/consts.go @@ -20,4 +20,4 @@ package inventory -const FixedSingleDeploymentID = "" +const FixedSingleDeploymentID = "00000000-0000-0000-0000-000000000000" diff --git a/pkg/platform/inventory/consts_test.go b/pkg/platform/inventory/consts_test.go new file mode 100644 index 000000000..0a3b87b27 --- /dev/null +++ b/pkg/platform/inventory/consts_test.go @@ -0,0 +1,32 @@ +// +// DISCLAIMER +// +// Copyright 2025 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 inventory + +import ( + "github.com/google/uuid" + "github.com/stretchr/testify/require" + "testing" +) + +func Test_UUID(t *testing.T) { + _, err := uuid.Parse(FixedSingleDeploymentID) + require.NoError(t, err) +} diff --git a/pkg/platform/inventory/fetcher.deployment.id.go b/pkg/platform/inventory/fetcher.deployment.id.go index 1dd789168..2b8144d34 100644 --- a/pkg/platform/inventory/fetcher.deployment.id.go +++ b/pkg/platform/inventory/fetcher.deployment.id.go @@ -65,7 +65,7 @@ func ExtractDeploymentID(ctx context.Context, conn driver.Connection) (string, e if err == nil { return health.ID, nil } else { - if _, ok := arangod.IsInvalidCode(err); ok { + if c, ok := arangod.IsInvalidCode(err); ok && c.Got == goHttp.StatusNotFound { // Fallback to the Deployment ID Single return FixedSingleDeploymentID, nil } From 2f90bae45871e1dbc767e60809ec510212294724 Mon Sep 17 00:00:00 2001 From: ajanikow <12255597+ajanikow@users.noreply.github.com> Date: Mon, 3 Nov 2025 14:13:45 +0000 Subject: [PATCH 5/9] Iter --- pkg/platform/inventory/fetcher.deployment.id.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/platform/inventory/fetcher.deployment.id.go b/pkg/platform/inventory/fetcher.deployment.id.go index 2b8144d34..87680585d 100644 --- a/pkg/platform/inventory/fetcher.deployment.id.go +++ b/pkg/platform/inventory/fetcher.deployment.id.go @@ -65,7 +65,7 @@ func ExtractDeploymentID(ctx context.Context, conn driver.Connection) (string, e if err == nil { return health.ID, nil } else { - if c, ok := arangod.IsInvalidCode(err); ok && c.Got == goHttp.StatusNotFound { + if c, ok := arangod.IsInvalidCode(err); ok && c.Got == goHttp.StatusForbidden { // Fallback to the Deployment ID Single return FixedSingleDeploymentID, nil } From 8ab8ae12db19d3f6498674f67f3e3ec29980a2db Mon Sep 17 00:00:00 2001 From: ajanikow <12255597+ajanikow@users.noreply.github.com> Date: Tue, 4 Nov 2025 08:22:55 +0000 Subject: [PATCH 6/9] Iter --- pkg/platform/inventory/consts_test.go | 3 ++- pkg/util/arangod/error.go | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/platform/inventory/consts_test.go b/pkg/platform/inventory/consts_test.go index 0a3b87b27..6258ad40a 100644 --- a/pkg/platform/inventory/consts_test.go +++ b/pkg/platform/inventory/consts_test.go @@ -21,9 +21,10 @@ package inventory import ( + "testing" + "github.com/google/uuid" "github.com/stretchr/testify/require" - "testing" ) func Test_UUID(t *testing.T) { diff --git a/pkg/util/arangod/error.go b/pkg/util/arangod/error.go index 384c05b71..10aec9c87 100644 --- a/pkg/util/arangod/error.go +++ b/pkg/util/arangod/error.go @@ -1,7 +1,7 @@ // // DISCLAIMER // -// Copyright 2016-2022 ArangoDB GmbH, Cologne, Germany +// Copyright 2016-2025 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. From 94df73824c5823aeaae0acedc05d885db82553e8 Mon Sep 17 00:00:00 2001 From: ajanikow <12255597+ajanikow@users.noreply.github.com> Date: Tue, 4 Nov 2025 09:39:31 +0000 Subject: [PATCH 7/9] Iter --- pkg/apis/deployment/v1/license_spec.go | 2 +- pkg/apis/deployment/v2alpha1/license_spec.go | 2 +- pkg/crd/crds/database-deployment.schema.generated.yaml | 3 +++ pkg/deployment/reconcile/action_license_generate.go | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pkg/apis/deployment/v1/license_spec.go b/pkg/apis/deployment/v1/license_spec.go index 3857cd477..16d72f039 100644 --- a/pkg/apis/deployment/v1/license_spec.go +++ b/pkg/apis/deployment/v1/license_spec.go @@ -93,7 +93,7 @@ func (s LicenseSpec) GetTelemetry() bool { // GetInventory returns the license Inventory func (s LicenseSpec) GetInventory() bool { - return util.OptionalType(s.Telemetry, true) + return util.OptionalType(s.Inventory, true) } // Validate validates the LicenseSpec diff --git a/pkg/apis/deployment/v2alpha1/license_spec.go b/pkg/apis/deployment/v2alpha1/license_spec.go index 8af5bb6cd..c7cf229b1 100644 --- a/pkg/apis/deployment/v2alpha1/license_spec.go +++ b/pkg/apis/deployment/v2alpha1/license_spec.go @@ -93,7 +93,7 @@ func (s LicenseSpec) GetTelemetry() bool { // GetInventory returns the license Inventory func (s LicenseSpec) GetInventory() bool { - return util.OptionalType(s.Telemetry, true) + return util.OptionalType(s.Inventory, true) } // Validate validates the LicenseSpec diff --git a/pkg/crd/crds/database-deployment.schema.generated.yaml b/pkg/crd/crds/database-deployment.schema.generated.yaml index dd0291f3a..b295922e2 100644 --- a/pkg/crd/crds/database-deployment.schema.generated.yaml +++ b/pkg/crd/crds/database-deployment.schema.generated.yaml @@ -27514,6 +27514,9 @@ v2alpha1: expirationGracePeriod: description: ExpirationGracePeriod defines the expiration grace period for the license type: string + inventory: + description: Inventory defines if inventory is collected + type: boolean mode: description: Mode Defines the mode of license enum: diff --git a/pkg/deployment/reconcile/action_license_generate.go b/pkg/deployment/reconcile/action_license_generate.go index 07462bfb8..4476519b8 100644 --- a/pkg/deployment/reconcile/action_license_generate.go +++ b/pkg/deployment/reconcile/action_license_generate.go @@ -164,7 +164,7 @@ func (a *actionLicenseGenerate) Start(ctx context.Context) (bool, error) { if expires.After(license.Expires()) { // License will expire before grace period, reduce to 90% - expires = time.Now().Add(time.Duration(math.Round(float64(time.Since(license.Expires())) * api.LicenseExpirationGraceRatio))) + expires = time.Now().Add(time.Duration(math.Round(float64(time.Until(license.Expires())) * api.LicenseExpirationGraceRatio))) } if err := a.actionCtx.WithStatusUpdate(ctx, func(s *api.DeploymentStatus) bool { From 70846693b1cce99dd5514d76fd77815bcfebb6c5 Mon Sep 17 00:00:00 2001 From: ajanikow <12255597+ajanikow@users.noreply.github.com> Date: Tue, 4 Nov 2025 11:57:45 +0000 Subject: [PATCH 8/9] Iter --- pkg/deployment/reconcile/action_license_generate.go | 2 +- pkg/deployment/reconcile/plan_builder_license.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/deployment/reconcile/action_license_generate.go b/pkg/deployment/reconcile/action_license_generate.go index 4476519b8..6e07e7250 100644 --- a/pkg/deployment/reconcile/action_license_generate.go +++ b/pkg/deployment/reconcile/action_license_generate.go @@ -163,7 +163,7 @@ func (a *actionLicenseGenerate) Start(ctx context.Context) (bool, error) { expires := time.Now().Add(expiration) if expires.After(license.Expires()) { - // License will expire before grace period, reduce to 90% + // License will expire before grace period, reduce to 75% expires = time.Now().Add(time.Duration(math.Round(float64(time.Until(license.Expires())) * api.LicenseExpirationGraceRatio))) } diff --git a/pkg/deployment/reconcile/plan_builder_license.go b/pkg/deployment/reconcile/plan_builder_license.go index f971f6947..25f0e8a6f 100644 --- a/pkg/deployment/reconcile/plan_builder_license.go +++ b/pkg/deployment/reconcile/plan_builder_license.go @@ -228,7 +228,7 @@ func (r *Reconciler) updateClusterLicenseAPI(ctx context.Context, spec api.Deplo return api.Plan{actions.NewClusterAction(api.ActionTypeLicenseClean, "Removing license reference - Invalid Hash")} } - if status.License.Regenerate.After(time.Now()) { + if status.License.Regenerate.Time.Before(time.Now()) { return api.Plan{actions.NewClusterAction(api.ActionTypeLicenseClean, "Removing license reference - Regeneration Required")} } From 3e7dc91fa4b2d8959e03665f3379744b3a81a55f Mon Sep 17 00:00:00 2001 From: ajanikow <12255597+ajanikow@users.noreply.github.com> Date: Tue, 4 Nov 2025 12:23:18 +0000 Subject: [PATCH 9/9] Iter --- pkg/deployment/reconcile/action_license_generate.go | 2 +- pkg/deployment/reconcile/plan_builder_license.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/deployment/reconcile/action_license_generate.go b/pkg/deployment/reconcile/action_license_generate.go index 6e07e7250..faaabdccc 100644 --- a/pkg/deployment/reconcile/action_license_generate.go +++ b/pkg/deployment/reconcile/action_license_generate.go @@ -101,7 +101,7 @@ func (a *actionLicenseGenerate) Start(ctx context.Context) (bool, error) { } if inv.DeploymentId != did { - a.log.Err(err).Error("Invalid deployment ID in inventory") + a.log.Error("Invalid deployment ID in inventory") return true, nil } diff --git a/pkg/deployment/reconcile/plan_builder_license.go b/pkg/deployment/reconcile/plan_builder_license.go index 25f0e8a6f..d2cc0d426 100644 --- a/pkg/deployment/reconcile/plan_builder_license.go +++ b/pkg/deployment/reconcile/plan_builder_license.go @@ -111,7 +111,7 @@ func (r *Reconciler) updateClusterLicenseDiscover(spec api.DeploymentSpec, conte return api.LicenseModeAPI, nil } - return "", errors.Errorf("Unable to discover LicenseAPIKey mode") + return "", errors.Errorf("Unable to discover License mode") } func (r *Reconciler) updateClusterLicenseKey(ctx context.Context, spec api.DeploymentSpec, status api.DeploymentStatus, context PlanBuilderContext) api.Plan {