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..0310be671 100644
--- a/docs/api/ArangoDeployment.V1.md
+++ b/docs/api/ArangoDeployment.V1.md
@@ -4791,16 +4791,69 @@ 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#L68)
+
+ExpirationGracePeriod defines the expiration grace period for the license
+
+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#L60)
+
+Mode Defines the mode of license
+
+Possible Values:
+* `"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#L33)
+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 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.telemetry
+
+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
+
+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#L64)
+
+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..a044c79a8 100644
--- a/docs/generated/actions.md
+++ b/docs/generated/actions.md
@@ -47,6 +47,8 @@ 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 |
| MemberPhaseUpdate | no | 10m0s | no | Community & Enterprise | Change member phase |
@@ -78,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 |
@@ -146,6 +149,8 @@ spec:
JWTSetActive: 10m0s
JWTStatusUpdate: 10m0s
KillMemberPod: 10m0s
+ LicenseClean: 10m0s
+ LicenseGenerate: 10m0s
LicenseSet: 10m0s
MarkToRemoveMember: 10m0s
MemberPhaseUpdate: 10m0s
@@ -177,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 24960c560..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:
@@ -247,6 +252,10 @@ actions:
- High
LicenseSet:
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 b37feb102..1c0bd5d9e 100644
--- a/pkg/apis/deployment/v1/actions.generated.go
+++ b/pkg/apis/deployment/v1/actions.generated.go
@@ -131,6 +131,12 @@ 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
+
// ActionLicenseSetDefaultTimeout define default timeout for action ActionLicenseSet
ActionLicenseSetDefaultTimeout time.Duration = ActionsDefaultTimeout
@@ -224,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
@@ -401,6 +410,12 @@ 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"
+
// ActionTypeLicenseSet in scopes Normal. Update Cluster license (3.9+)
ActionTypeLicenseSet ActionType = "LicenseSet"
@@ -496,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
@@ -639,6 +657,10 @@ func (a ActionType) DefaultTimeout() time.Duration {
return ActionJWTStatusUpdateDefaultTimeout
case ActionTypeKillMemberPod:
return ActionKillMemberPodDefaultTimeout
+ case ActionTypeLicenseClean:
+ return ActionLicenseCleanDefaultTimeout
+ case ActionTypeLicenseGenerate:
+ return ActionLicenseGenerateDefaultTimeout
case ActionTypeLicenseSet:
return ActionLicenseSetDefaultTimeout
case ActionTypeMarkToRemoveMember:
@@ -701,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:
@@ -823,6 +847,10 @@ func (a ActionType) Priority() ActionPriority {
return ActionPriorityNormal
case ActionTypeKillMemberPod:
return ActionPriorityHigh
+ case ActionTypeLicenseClean:
+ return ActionPriorityNormal
+ case ActionTypeLicenseGenerate:
+ return ActionPriorityNormal
case ActionTypeLicenseSet:
return ActionPriorityNormal
case ActionTypeMarkToRemoveMember:
@@ -885,6 +913,8 @@ func (a ActionType) Priority() ActionPriority {
return ActionPriorityNormal
case ActionTypeRuntimeContainerSyncTolerations:
return ActionPriorityNormal
+ case ActionTypeSetAnnotation:
+ return ActionPriorityHigh
case ActionTypeSetCondition:
return ActionPriorityHigh
case ActionTypeSetConditionV2:
@@ -945,6 +975,8 @@ func (a ActionType) Internal() bool {
return true
case ActionTypeRebalancerGenerateV2:
return true
+ case ActionTypeSetAnnotation:
+ return true
case ActionTypeSetConditionV2:
return true
case ActionTypeSetMaintenanceCondition:
@@ -1033,6 +1065,10 @@ func (a ActionType) Optional() bool {
return false
case ActionTypeKillMemberPod:
return false
+ case ActionTypeLicenseClean:
+ return false
+ case ActionTypeLicenseGenerate:
+ return false
case ActionTypeLicenseSet:
return false
case ActionTypeMarkToRemoveMember:
@@ -1095,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 6f422ff8c..595e9b2f2 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"`
@@ -97,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"`
@@ -135,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
@@ -147,6 +151,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/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/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..16d72f039 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,59 @@
package v1
import (
+ 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 (
+ LicenseExpirationGraceRatio = 0.75
+)
+
+type LicenseMode string
+
+const (
+ LicenseModeDefault = LicenseModeDiscover
+ LicenseModeDiscover LicenseMode = "discover"
+ LicenseModeKey LicenseMode = "key"
+ LicenseModeAPI LicenseMode = "api"
)
+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: 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"`
+
+ // 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"`
+
+ // 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
@@ -43,20 +86,60 @@ func (s LicenseSpec) GetSecretName() string {
return util.TypeOrDefault[string](s.SecretName)
}
+// GetTelemetry returns the license Telemetry
+func (s LicenseSpec) GetTelemetry() bool {
+ return util.OptionalType(s.Telemetry, true)
+}
+
+// GetInventory returns the license Inventory
+func (s LicenseSpec) GetInventory() bool {
+ return util.OptionalType(s.Inventory, true)
+}
+
// 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 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 t := s.TTL; t != nil {
+ if t.Duration <= 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..33fe9768d 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
@@ -1869,6 +1892,31 @@ 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
+ }
+ if in.Telemetry != nil {
+ in, out := &in.Telemetry, &out.Telemetry
+ *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/actions.generated.go b/pkg/apis/deployment/v2alpha1/actions.generated.go
index 854a3f8f3..9da9964bb 100644
--- a/pkg/apis/deployment/v2alpha1/actions.generated.go
+++ b/pkg/apis/deployment/v2alpha1/actions.generated.go
@@ -131,6 +131,12 @@ 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
+
// ActionLicenseSetDefaultTimeout define default timeout for action ActionLicenseSet
ActionLicenseSetDefaultTimeout time.Duration = ActionsDefaultTimeout
@@ -224,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
@@ -401,6 +410,12 @@ 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"
+
// ActionTypeLicenseSet in scopes Normal. Update Cluster license (3.9+)
ActionTypeLicenseSet ActionType = "LicenseSet"
@@ -496,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
@@ -639,6 +657,10 @@ func (a ActionType) DefaultTimeout() time.Duration {
return ActionJWTStatusUpdateDefaultTimeout
case ActionTypeKillMemberPod:
return ActionKillMemberPodDefaultTimeout
+ case ActionTypeLicenseClean:
+ return ActionLicenseCleanDefaultTimeout
+ case ActionTypeLicenseGenerate:
+ return ActionLicenseGenerateDefaultTimeout
case ActionTypeLicenseSet:
return ActionLicenseSetDefaultTimeout
case ActionTypeMarkToRemoveMember:
@@ -701,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:
@@ -823,6 +847,10 @@ func (a ActionType) Priority() ActionPriority {
return ActionPriorityNormal
case ActionTypeKillMemberPod:
return ActionPriorityHigh
+ case ActionTypeLicenseClean:
+ return ActionPriorityNormal
+ case ActionTypeLicenseGenerate:
+ return ActionPriorityNormal
case ActionTypeLicenseSet:
return ActionPriorityNormal
case ActionTypeMarkToRemoveMember:
@@ -885,6 +913,8 @@ func (a ActionType) Priority() ActionPriority {
return ActionPriorityNormal
case ActionTypeRuntimeContainerSyncTolerations:
return ActionPriorityNormal
+ case ActionTypeSetAnnotation:
+ return ActionPriorityHigh
case ActionTypeSetCondition:
return ActionPriorityHigh
case ActionTypeSetConditionV2:
@@ -945,6 +975,8 @@ func (a ActionType) Internal() bool {
return true
case ActionTypeRebalancerGenerateV2:
return true
+ case ActionTypeSetAnnotation:
+ return true
case ActionTypeSetConditionV2:
return true
case ActionTypeSetMaintenanceCondition:
@@ -1033,6 +1065,10 @@ func (a ActionType) Optional() bool {
return false
case ActionTypeKillMemberPod:
return false
+ case ActionTypeLicenseClean:
+ return false
+ case ActionTypeLicenseGenerate:
+ return false
case ActionTypeLicenseSet:
return false
case ActionTypeMarkToRemoveMember:
@@ -1095,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 b348225f5..b94dbf07a 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"`
@@ -97,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"`
@@ -135,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
@@ -147,6 +151,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/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/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..c7cf229b1 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,59 @@
package v2alpha1
import (
+ 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 (
+ LicenseExpirationGraceRatio = 0.75
+)
+
+type LicenseMode string
+
+const (
+ LicenseModeDefault = LicenseModeDiscover
+ LicenseModeDiscover LicenseMode = "discover"
+ LicenseModeKey LicenseMode = "key"
+ LicenseModeAPI LicenseMode = "api"
)
+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: 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"`
+
+ // 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"`
+
+ // 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
@@ -43,20 +86,60 @@ func (s LicenseSpec) GetSecretName() string {
return util.TypeOrDefault[string](s.SecretName)
}
+// GetTelemetry returns the license Telemetry
+func (s LicenseSpec) GetTelemetry() bool {
+ return util.OptionalType(s.Telemetry, true)
+}
+
+// GetInventory returns the license Inventory
+func (s LicenseSpec) GetInventory() bool {
+ return util.OptionalType(s.Inventory, true)
+}
+
// 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 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 t := s.TTL; t != nil {
+ if t.Duration <= 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..8c6697560 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
@@ -1869,6 +1892,31 @@ 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
+ }
+ if in.Telemetry != nil {
+ in, out := &in.Telemetry, &out.Telemetry
+ *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 f68adf776..b295922e2 100644
--- a/pkg/crd/crds/database-deployment.schema.generated.yaml
+++ b/pkg/crd/crds/database-deployment.schema.generated.yaml
@@ -10178,12 +10178,31 @@ v1:
license:
description: License holds license settings
properties:
+ 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:
+ - discover
+ - 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
+ telemetry:
+ description: Telemetry defines if telemetry is collected
+ type: boolean
+ ttl:
+ description: TTL Sets the requested License TTL
+ type: string
type: object
lifecycle:
description: Lifecycle holds lifecycle configuration settings
@@ -27492,12 +27511,31 @@ v2alpha1:
license:
description: License holds license settings
properties:
+ 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:
+ - discover
+ - 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
+ telemetry:
+ description: Telemetry defines if telemetry is collected
+ type: boolean
+ 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..f4a735aaf 100644
--- a/pkg/deployment/reconcile/action.register.generated.go
+++ b/pkg/deployment/reconcile/action.register.generated.go
@@ -126,6 +126,12 @@ var (
_ Action = &actionKillMemberPod{}
_ actionFactory = newKillMemberPodAction
+ _ Action = &actionLicenseClean{}
+ _ actionFactory = newLicenseCleanAction
+
+ _ Action = &actionLicenseGenerate{}
+ _ actionFactory = newLicenseGenerateAction
+
_ Action = &actionLicenseSet{}
_ actionFactory = newLicenseSetAction
@@ -216,6 +222,9 @@ var (
_ Action = &actionRuntimeContainerSyncTolerations{}
_ actionFactory = newRuntimeContainerSyncTolerationsAction
+ _ Action = &actionSetAnnotation{}
+ _ actionFactory = newSetAnnotationAction
+
_ Action = &actionSetConditionV2{}
_ actionFactory = newSetConditionV2Action
@@ -768,6 +777,34 @@ 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
+ action := api.ActionTypeLicenseGenerate
+
+ // Get Action defition
+ function := newLicenseGenerateAction
+
+ // Wrap action main function
+
+ // Register action
+ registerAction(action, function)
+ }
+
// LicenseSet
{
// Get Action type
@@ -1207,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 c607aa564..6088d95f3 100644
--- a/pkg/deployment/reconcile/action.register.generated_test.go
+++ b/pkg/deployment/reconcile/action.register.generated_test.go
@@ -386,6 +386,26 @@ 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) {
+ 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) {
@@ -701,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
new file mode 100644
index 000000000..faaabdccc
--- /dev/null
+++ b/pkg/deployment/reconcile/action_license_generate.go
@@ -0,0 +1,184 @@
+//
+// 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"
+ "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"
+ "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.API == 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
+ }
+
+ var req license_manager.LicenseRequest
+ did, err := inventory.ExtractDeploymentID(ctx, c.Connection())
+ if err != nil {
+ 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.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, req)
+ 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", generatedLicense.ID).Info("License Generated")
+
+ 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
+ }
+
+ 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 75%
+ 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 {
+ 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_set_license.go b/pkg/deployment/reconcile/action_license_set.go
similarity index 81%
rename from pkg/deployment/reconcile/action_set_license.go
rename to pkg/deployment/reconcile/action_license_set.go
index 1e2f8eeba..19d1fdd6a 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.
@@ -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 8f083c789..d2cc0d426 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,12 +22,14 @@ package reconcile
import (
"context"
+ "time"
api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1"
"github.com/arangodb/kube-arangodb/pkg/deployment/actions"
"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"
)
@@ -35,10 +37,84 @@ 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
}
+ 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:
+ 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 License 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 {
r.log.Err(err).Error("License secret error")
@@ -76,17 +152,85 @@ func (r *Reconciler) updateClusterLicense(ctx context.Context, apiObject k8sutil
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")
+ license, err := internalClient.GetLicense(ctxChild)
+ if err != nil {
+ r.log.Err(err).Error("Unable to get client")
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())}
+ }
+
+ if status.License.Hash != license.Hash {
+ return api.Plan{actions.NewClusterAction(api.ActionTypeLicenseClean, "Removing license reference - Invalid Hash")}
+ }
+
+ return nil
+}
+
+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.API == 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
}
- return api.Plan{sharedReconcile.RemoveConditionActionV2("License is not set", api.ConditionTypeLicenseSet), actions.NewAction(api.ActionTypeLicenseSet, member.Group, member.Member, "Setting license")}
+ 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
+ }
+
+ 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)
+ if err != nil {
+ r.log.Err(err).Error("Unable to get current license")
+ return nil
+ }
+
+ if currentLicense.Hash != status.License.Hash {
+ // Invalid hash, cleanup
+ return api.Plan{actions.NewClusterAction(api.ActionTypeLicenseClean, "Removing license reference - Invalid Hash")}
+ }
+
+ if status.License.Regenerate.Time.Before(time.Now()) {
+ return api.Plan{actions.NewClusterAction(api.ActionTypeLicenseClean, "Removing license reference - Regeneration Required")}
+ }
+
+ return nil
}
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_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/manager/client.go b/pkg/license_manager/client.go
similarity index 80%
rename from pkg/license/manager/client.go
rename to pkg/license_manager/client.go
index 9f3dbafe7..73c137e2a 100644
--- a/pkg/license/manager/client.go
+++ b/pkg/license_manager/client.go
@@ -18,13 +18,15 @@
// 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"
+ "google.golang.org/protobuf/types/known/timestamppb"
"github.com/arangodb/go-driver"
"github.com/arangodb/go-driver/http"
@@ -36,6 +38,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,14 +79,15 @@ 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 {
- 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/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/license/loader_test.go b/pkg/platform/inventory/consts.go
similarity index 66%
rename from pkg/license/loader_test.go
rename to pkg/platform/inventory/consts.go
index 88d6c9472..c44749f58 100644
--- a/pkg/license/loader_test.go
+++ b/pkg/platform/inventory/consts.go
@@ -1,7 +1,7 @@
//
// DISCLAIMER
//
-// Copyright 2023 ArangoDB GmbH, Cologne, Germany
+// 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.
@@ -16,20 +16,8 @@
// 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
-}
+package inventory
-func (m *MockLoader) Refresh(ctx context.Context) (string, bool, error) {
- args := m.Called(ctx)
- return args.String(0), args.Bool(1), args.Error(2)
-}
+const FixedSingleDeploymentID = "00000000-0000-0000-0000-000000000000"
diff --git a/pkg/license/loader.go b/pkg/platform/inventory/consts_test.go
similarity index 70%
rename from pkg/license/loader.go
rename to pkg/platform/inventory/consts_test.go
index 5b5373a18..6258ad40a 100644
--- a/pkg/license/loader.go
+++ b/pkg/platform/inventory/consts_test.go
@@ -1,7 +1,7 @@
//
// DISCLAIMER
//
-// Copyright 2023 ArangoDB GmbH, Cologne, Germany
+// 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.
@@ -18,12 +18,16 @@
// Copyright holder is ArangoDB GmbH, Cologne, Germany
//
-package license
+package inventory
-import "context"
+import (
+ "testing"
-type Loader interface {
- // Refresh reloads license in a specified manner.
- // It returns license (base64 encoded), found, error
- Refresh(ctx context.Context) (string, bool, error)
+ "github.com/google/uuid"
+ "github.com/stretchr/testify/require"
+)
+
+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 0ac7a2370..87680585d 100644
--- a/pkg/platform/inventory/fetcher.deployment.id.go
+++ b/pkg/platform/inventory/fetcher.deployment.id.go
@@ -37,29 +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 {
return err
}
return errors.Errors(
- Produce(out, "ARANGO_DEPLOYMENT_ID", nil, health.ID),
+ Produce(out, "ARANGO_DEPLOYMENT", map[string]string{
+ "detail": "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 c, ok := arangod.IsInvalidCode(err); ok && c.Got == goHttp.StatusForbidden {
+ // Fallback to the Deployment ID Single
+ return FixedSingleDeploymentID, nil
+ }
+
+ return "", err
+ }
+}
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/arangod/error.go b/pkg/util/arangod/error.go
index 4624ae86a..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.
@@ -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) {
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..ae30f7295 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
+ 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 4bb6aa11d..71d833697 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
+ API *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.SecretKeyLicenseClientID]; ok {
+ if cs, ok := s.Data[utilConstants.SecretKeyLicenseClientSecret]; ok {
+ l.API = &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.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.SecretKeyLicenseClientID, utilConstants.SecretKeyLicenseClientSecret, name)
}
return l, nil