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