diff --git a/.golangci.yaml b/.golangci.yaml index 6591b88d8..59d103bdf 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -33,6 +33,8 @@ linters-settings: importas: no-unaliased: true alias: + - alias: lmanager + pkg: github.com/arangodb/kube-arangodb/pkg/license_manager - alias: pbImplMetaV1 pkg: github.com/arangodb/kube-arangodb/integrations/meta/v1 - alias: pbMetaV1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 1309a6fa2..c63544737 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - (Feature) Simplify Operator ID Process - (Feature) (License) Activation API Integration - (Feature) (Platform) Chart & Service Kubernetes Events +- (Feature) (Platform) Registry Secret ## [1.3.1](https://github.com/arangodb/kube-arangodb/tree/1.3.1) (2025-10-07) - (Documentation) Add ArangoPlatformStorage Docs & Examples diff --git a/pkg/apis/deployment/v1/deployment_status_license.go b/pkg/apis/deployment/v1/deployment_status_license.go index 4e0678e62..682fc9521 100644 --- a/pkg/apis/deployment/v1/deployment_status_license.go +++ b/pkg/apis/deployment/v1/deployment_status_license.go @@ -30,6 +30,9 @@ type DeploymentStatusLicense struct { // Hash Defines the License Hash Hash string `json:"hash,omitempty"` + // InputHash Defines the Input License Hash + InputHash string `json:"inputHash,omitempty"` + // Expires Defines the expiration time of the License Expires meta.Time `json:"expires,omitempty"` diff --git a/pkg/apis/deployment/v2alpha1/deployment_status_license.go b/pkg/apis/deployment/v2alpha1/deployment_status_license.go index cc3fe88ac..4e9618667 100644 --- a/pkg/apis/deployment/v2alpha1/deployment_status_license.go +++ b/pkg/apis/deployment/v2alpha1/deployment_status_license.go @@ -30,6 +30,9 @@ type DeploymentStatusLicense struct { // Hash Defines the License Hash Hash string `json:"hash,omitempty"` + // InputHash Defines the Input License Hash + InputHash string `json:"inputHash,omitempty"` + // Expires Defines the expiration time of the License Expires meta.Time `json:"expires,omitempty"` diff --git a/pkg/deployment/pod/encryption.go b/pkg/deployment/pod/encryption.go index ebf41a712..8c458b309 100644 --- a/pkg/deployment/pod/encryption.go +++ b/pkg/deployment/pod/encryption.go @@ -108,6 +108,13 @@ func GetEncryptionFolderSecretName(name string) string { return n } +// GetLicenseRegistryCredentialsSecretName returns the secret name for storing registry credentials used to pull licensed images +func GetLicenseRegistryCredentialsSecretName(name string) string { + n := fmt.Sprintf("%s-rlm", name) + + return n +} + func IsEncryptionEnabled(i Input) bool { return i.Deployment.RocksDB.IsEncrypted() } diff --git a/pkg/deployment/reconcile/action_license_generate.go b/pkg/deployment/reconcile/action_license_generate.go index faaabdccc..b81b5e19e 100644 --- a/pkg/deployment/reconcile/action_license_generate.go +++ b/pkg/deployment/reconcile/action_license_generate.go @@ -32,12 +32,15 @@ import ( 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/deployment/pod" + lmanager "github.com/arangodb/kube-arangodb/pkg/license_manager" "github.com/arangodb/kube-arangodb/pkg/platform/inventory" "github.com/arangodb/kube-arangodb/pkg/util" + utilConstants "github.com/arangodb/kube-arangodb/pkg/util/constants" "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" + "github.com/arangodb/kube-arangodb/pkg/util/k8sutil/patcher" ) func newLicenseGenerateAction(action api.Action, actionCtx ActionContext) Action { @@ -84,7 +87,7 @@ func (a *actionLicenseGenerate) Start(ctx context.Context) (bool, error) { return true, nil } - var req license_manager.LicenseRequest + var req lmanager.LicenseRequest did, err := inventory.ExtractDeploymentID(ctx, c.Connection()) if err != nil { a.log.Err(err).Error("Unable to get deployment id") @@ -112,7 +115,7 @@ func (a *actionLicenseGenerate) Start(ctx context.Context) (bool, error) { req.TTL = util.NewType(ugrpc.NewObject(durationpb.New(q.Duration))) } - lm, err := license_manager.NewClient(license_manager.ArangoLicenseManagerEndpoint, l.API.ClientID, l.API.ClientSecret) + lm, err := lmanager.NewClient(lmanager.ArangoLicenseManagerEndpoint, l.API.ClientID, l.API.ClientSecret) if err != nil { a.log.Err(err).Error("Unable to create inventory client") return true, nil @@ -167,6 +170,52 @@ func (a *actionLicenseGenerate) Start(ctx context.Context) (bool, error) { expires = time.Now().Add(time.Duration(math.Round(float64(time.Until(license.Expires())) * api.LicenseExpirationGraceRatio))) } + cache := a.actionCtx.ACS().CurrentClusterCache() + + if s, ok := cache.Secret().V1().GetSimple(pod.GetLicenseRegistryCredentialsSecretName(a.actionCtx.GetName())); ok { + if string(util.Optional(s.Data, utilConstants.ChecksumKey, []byte{})) != l.API.Hash() { + // Update + + token, err := lm.RegistryConfig(ctx, lmanager.ArangoLicenseManagerEndpoint, l.API.ClientID, &l.API.ClientSecret, lmanager.StageDev, lmanager.StageQA, lmanager.StagePrd) + if err != nil { + a.log.Err(err).Debug("Failed to generate License Registry") + return true, nil + } + + if _, _, err := patcher.Patcher[*core.Secret](ctx, cache.Client().Kubernetes().CoreV1().Secrets(a.actionCtx.GetNamespace()), s, meta.PatchOptions{}, + patcher.PatchSecretData(map[string][]byte{ + core.DockerConfigJsonKey: token, + utilConstants.ChecksumKey: []byte(l.API.Hash()), + })); err != nil { + a.log.Err(err).Debug("Failed to patch License Secret") + return true, nil + } + } + } else { + token, err := lm.RegistryConfig(ctx, lmanager.ArangoLicenseManagerEndpoint, l.API.ClientID, &l.API.ClientSecret, lmanager.StageDev, lmanager.StageQA, lmanager.StagePrd) + if err != nil { + a.log.Err(err).Debug("Failed to generate License Registry") + return true, nil + } + + if _, err := cache.Client().Kubernetes().CoreV1().Secrets(a.actionCtx.GetNamespace()).Create(ctx, &core.Secret{ + ObjectMeta: meta.ObjectMeta{ + Name: pod.GetLicenseRegistryCredentialsSecretName(a.actionCtx.GetName()), + OwnerReferences: []meta.OwnerReference{ + a.actionCtx.GetAPIObject().AsOwner(), + }, + }, + Data: map[string][]byte{ + core.DockerConfigJsonKey: token, + utilConstants.ChecksumKey: []byte(l.API.Hash()), + }, + Type: core.SecretTypeDockerConfigJson, + }, meta.CreateOptions{}); err != nil { + a.log.Err(err).Debug("Failed to create License Secret") + return true, nil + } + } + if err := a.actionCtx.WithStatusUpdate(ctx, func(s *api.DeploymentStatus) bool { s.License = &api.DeploymentStatusLicense{ ID: generatedLicense.ID, @@ -174,6 +223,7 @@ func (a *actionLicenseGenerate) Start(ctx context.Context) (bool, error) { Expires: meta.Time{Time: license.Expires()}, Mode: api.LicenseModeAPI, Regenerate: meta.Time{Time: expires}, + InputHash: l.API.Hash(), } return true }); err != nil { diff --git a/pkg/deployment/reconcile/action_license_set.go b/pkg/deployment/reconcile/action_license_set.go index 19d1fdd6a..0c2dbdb18 100644 --- a/pkg/deployment/reconcile/action_license_set.go +++ b/pkg/deployment/reconcile/action_license_set.go @@ -90,9 +90,10 @@ func (a *actionLicenseSet) Start(ctx context.Context) (bool, error) { 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, + Hash: license.Hash, + Expires: meta.Time{Time: license.Expires()}, + Mode: api.LicenseModeKey, + InputHash: l.V2.V2Hash(), } return true }); err != nil { diff --git a/pkg/deployment/reconcile/plan_builder_license.go b/pkg/deployment/reconcile/plan_builder_license.go index d2cc0d426..5ac97b75b 100644 --- a/pkg/deployment/reconcile/plan_builder_license.go +++ b/pkg/deployment/reconcile/plan_builder_license.go @@ -27,8 +27,11 @@ import ( 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" + "github.com/arangodb/kube-arangodb/pkg/deployment/pod" sharedReconcile "github.com/arangodb/kube-arangodb/pkg/deployment/reconcile/shared" + "github.com/arangodb/kube-arangodb/pkg/util" "github.com/arangodb/kube-arangodb/pkg/util/arangod" + utilConstants "github.com/arangodb/kube-arangodb/pkg/util/constants" "github.com/arangodb/kube-arangodb/pkg/util/errors" "github.com/arangodb/kube-arangodb/pkg/util/globals" "github.com/arangodb/kube-arangodb/pkg/util/k8sutil" @@ -165,7 +168,7 @@ func (r *Reconciler) updateClusterLicenseKey(ctx context.Context, spec api.Deplo return nil } - if status.License.Hash != license.Hash { + if status.License.Hash != license.Hash || status.License.InputHash != l.V2.V2Hash() { return api.Plan{actions.NewClusterAction(api.ActionTypeLicenseClean, "Removing license reference - Invalid Hash")} } @@ -223,6 +226,11 @@ func (r *Reconciler) updateClusterLicenseAPI(ctx context.Context, spec api.Deplo return nil } + if status.License.InputHash != l.API.Hash() { + // Invalid hash, cleanup + return api.Plan{actions.NewClusterAction(api.ActionTypeLicenseClean, "Removing license reference - Invalid Input")} + } + if currentLicense.Hash != status.License.Hash { // Invalid hash, cleanup return api.Plan{actions.NewClusterAction(api.ActionTypeLicenseClean, "Removing license reference - Invalid Hash")} @@ -232,5 +240,15 @@ func (r *Reconciler) updateClusterLicenseAPI(ctx context.Context, spec api.Deplo return api.Plan{actions.NewClusterAction(api.ActionTypeLicenseClean, "Removing license reference - Regeneration Required")} } + cache := r.context.ACS().CurrentClusterCache() + + if s, ok := cache.Secret().V1().GetSimple(pod.GetLicenseRegistryCredentialsSecretName(r.context.GetName())); ok { + if string(util.Optional(s.Data, utilConstants.ChecksumKey, []byte{})) != l.API.Hash() { + return api.Plan{actions.NewClusterAction(api.ActionTypeLicenseClean, "Removing license reference - Registry Change Required")} + } + } else { + return api.Plan{actions.NewClusterAction(api.ActionTypeLicenseClean, "Removing license reference - Registry Change Required")} + } + return nil } diff --git a/pkg/deployment/resources/arango_profiles.go b/pkg/deployment/resources/arango_profiles.go index ff09a210b..d10f14786 100644 --- a/pkg/deployment/resources/arango_profiles.go +++ b/pkg/deployment/resources/arango_profiles.go @@ -39,6 +39,7 @@ import ( schedulerPodResourcesApi "github.com/arangodb/kube-arangodb/pkg/apis/scheduler/v1beta1/pod/resources" shared "github.com/arangodb/kube-arangodb/pkg/apis/shared" "github.com/arangodb/kube-arangodb/pkg/deployment/patch" + "github.com/arangodb/kube-arangodb/pkg/deployment/pod" integrationsSidecar "github.com/arangodb/kube-arangodb/pkg/integrations/sidecar" "github.com/arangodb/kube-arangodb/pkg/metrics" "github.com/arangodb/kube-arangodb/pkg/util" @@ -190,6 +191,7 @@ func (r *Resources) EnsureArangoProfiles(ctx context.Context, cachedStatus inspe r.arangoDeploymentCATemplate(), r.templateKubernetesEnvs(), r.templateResourceEnvs(), + r.templateImagePullSecrets(), ) if err != nil { return "", nil, err @@ -361,6 +363,22 @@ func (r *Resources) templateKubernetesEnvs() *schedulerApi.ProfileTemplate { } } +func (r *Resources) templateImagePullSecrets() *schedulerApi.ProfileTemplate { + if _, ok := r.context.ACS().CurrentClusterCache().Secret().V1().GetSimple(pod.GetLicenseRegistryCredentialsSecretName(r.name)); ok { + return &schedulerApi.ProfileTemplate{ + Pod: &schedulerPodApi.Pod{ + Image: &schedulerPodResourcesApi.Image{ + ImagePullSecrets: []string{ + pod.GetLicenseRegistryCredentialsSecretName(r.name), + }, + }, + }, + } + } + + return &schedulerApi.ProfileTemplate{} +} + func (r *Resources) templateResourceEnvs() *schedulerApi.ProfileTemplate { return &schedulerApi.ProfileTemplate{ Container: &schedulerApi.ProfileContainerTemplate{ diff --git a/pkg/license_manager/client.go b/pkg/license_manager/client.go index 73c137e2a..8ac8ee777 100644 --- a/pkg/license_manager/client.go +++ b/pkg/license_manager/client.go @@ -22,6 +22,7 @@ package license_manager import ( "context" + "encoding/json" "fmt" goHttp "net/http" @@ -76,6 +77,7 @@ type Client interface { License(ctx context.Context, req LicenseRequest) (LicenseResponse, error) Registry(ctx context.Context) (RegistryResponse, error) + RegistryConfig(ctx context.Context, endpoint, id string, token *string, stages ...Stage) ([]byte, error) } type LicenseRequest struct { @@ -98,6 +100,32 @@ type client struct { conn driver.Connection } +func (c client) RegistryConfig(ctx context.Context, endpoint, id string, token *string, stages ...Stage) ([]byte, error) { + var t string + + if token != nil { + t = *token + } else { + tk, err := c.Registry(ctx) + if err != nil { + return nil, err + } + t = tk.Token + } + + r, err := NewRegistryAuth(endpoint, id, t, stages...) + if err != nil { + return nil, err + } + + data, err := json.Marshal(r) + if err != nil { + return nil, err + } + + return data, nil +} + func (c client) License(ctx context.Context, req LicenseRequest) (LicenseResponse, error) { return arangod.PostRequest[LicenseRequest, LicenseResponse](ctx, c.conn, req, "_api", "v1", "license").AcceptCode(200).Response() } diff --git a/pkg/platform/license_activate.go b/pkg/platform/license_activate.go index 06c7d37c4..ec16b4861 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" + lmanager "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 license_manager.Client) error { +func licenseActivateExecute(cmd *cobra.Command, logger logging.Logger, mc lmanager.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 licens l.Info("Generating License") - lic, err := mc.License(cmd.Context(), license_manager.LicenseRequest{ + lic, err := mc.License(cmd.Context(), lmanager.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 98cef0185..b1454e7d4 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" + lmanager "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(), license_manager.LicenseRequest{ + lic, err := mc.License(cmd.Context(), lmanager.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 b51650085..9ed77498b 100644 --- a/pkg/platform/license_secret.go +++ b/pkg/platform/license_secret.go @@ -21,7 +21,6 @@ package platform import ( - "encoding/json" "fmt" "os" @@ -30,7 +29,7 @@ import ( meta "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/yaml" - manager2 "github.com/arangodb/kube-arangodb/pkg/license_manager" + lmanager "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" ) @@ -86,21 +85,9 @@ func licenseSecretRun(cmd *cobra.Command, args []string) error { return err } - secret, err := mc.Registry(cmd.Context()) - if err != nil { - return err - } - logger.Info("Creating new Registry Token") - r, err := manager2.NewRegistryAuth(endpoint, id, secret.Token, manager2.ParseStages(stages...)...) - if err != nil { - return err - } - - logger.Info("New Registry Token Created") - - data, err := json.Marshal(r) + data, err := mc.RegistryConfig(cmd.Context(), endpoint, id, nil, lmanager.ParseStages(stages...)...) if err != nil { return err } diff --git a/pkg/util/cli/lm.go b/pkg/util/cli/lm.go index ce44bb56a..d32fb45e8 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" + lmanager "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_manager.ArangoLicenseManagerEndpoint, + Default: lmanager.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) (license_manager.Client, error) + Client(cmd *cobra.Command) (lmanager.Client, error) } type licenseManager struct { @@ -129,7 +129,7 @@ func (l licenseManager) GetName() string { return "lm" } -func (l licenseManager) Client(cmd *cobra.Command) (license_manager.Client, error) { +func (l licenseManager) Client(cmd *cobra.Command) (lmanager.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) (license_manager.Client, erro return nil, err } - return license_manager.NewClient(endpoint, cid, cs) + return lmanager.NewClient(endpoint, cid, cs) } func (l licenseManager) Register(cmd *cobra.Command) error { diff --git a/pkg/util/constants/gateway.go b/pkg/util/constants/gateway.go index 05f1a26e1..4e2feda69 100644 --- a/pkg/util/constants/gateway.go +++ b/pkg/util/constants/gateway.go @@ -29,7 +29,8 @@ const ( MaxEnvoyUpstreamTimeout = time.Hour MinEnvoyUpstreamTimeout = time.Duration(0) - ConfigMapChecksumKey = "CHECKSUM" + ConfigMapChecksumKey = ChecksumKey + ChecksumKey = "CHECKSUM" ArangoGatewayExecutor = "/usr/local/bin/envoy" diff --git a/pkg/util/k8sutil/license.go b/pkg/util/k8sutil/license.go index 71d833697..efe6f6413 100644 --- a/pkg/util/k8sutil/license.go +++ b/pkg/util/k8sutil/license.go @@ -51,6 +51,14 @@ type LicenseSecretMaster struct { ClientSecret string } +func (l *LicenseSecretMaster) Hash() string { + if l == nil { + return "" + } + + return util.SHA256FromStringArray(l.ClientID, l.ClientSecret) +} + func GetLicenseFromSecret(secret secret.Inspector, name string) (LicenseSecret, error) { s, ok := secret.Secret().V1().GetSimple(name) if !ok { diff --git a/pkg/util/k8sutil/patcher/secret.go b/pkg/util/k8sutil/patcher/secret.go new file mode 100644 index 000000000..5668e29bc --- /dev/null +++ b/pkg/util/k8sutil/patcher/secret.go @@ -0,0 +1,40 @@ +// +// DISCLAIMER +// +// Copyright 2024-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 patcher + +import ( + core "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/equality" + + "github.com/arangodb/kube-arangodb/pkg/deployment/patch" +) + +func PatchSecretData(data map[string][]byte) Patch[*core.Secret] { + return func(in *core.Secret) []patch.Item { + if len(data) == len(in.Data) && equality.Semantic.DeepDerivative(data, in.Data) { + return nil + } + + return []patch.Item{ + patch.ItemReplace(patch.NewPath("data"), data), + } + } +}