diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b7f6e330..5ecd5a8da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - (Bugfix) (Platform) Ensure Inventory picks active leader - (Bugfix) (Platform) Reload Config on Inventory Change - (Bugfix) (Platform) Ensure Inventory uses the serving group for license generation +- (Bugfix) (Platform) Installer move to OCI ## [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/cli/arangodb_operator_platform.md b/docs/cli/arangodb_operator_platform.md index 4f8e5edfb..b190f61a5 100644 --- a/docs/cli/arangodb_operator_platform.md +++ b/docs/cli/arangodb_operator_platform.md @@ -41,7 +41,6 @@ Available Commands: import Imports the package from the ZIP format install Installs the specified setup of the platform merge Merges definitions into single file - registry Points all images to the new registry Flags: -h, --help help for package @@ -83,9 +82,13 @@ Usage: arangodb_operator_platform package install [flags] ... packages Flags: - -h, --help help for install - --platform.endpoint string Platform Repository URL (default "https://arangodb-platform-prd-chart-registry.s3.amazonaws.com") - --platform.name string Kubernetes Platform Name (name of the ArangoDeployment) + -h, --help help for install + --license.client.id string LicenseManager Client ID (ENV: LICENSE_CLIENT_ID) + --license.client.secret string LicenseManager Client Secret (ENV: LICENSE_CLIENT_SECRET) + --license.endpoint string LicenseManager Endpoint (default "license.arango.ai") + --platform.name string Kubernetes Platform Name (name of the ArangoDeployment) + --registry.docker.credentials Use Docker Credentials + --registry.docker.insecure strings List of insecure registries Global Flags: --kubeconfig string Kubernetes Config File @@ -161,9 +164,8 @@ Flags: --arango.insecure Arango Endpoint Insecure --arango.token string Arango JWT Token for Authentication -h, --help help for activate - --license.client.id string LicenseManager Client ID - --license.client.secret string LicenseManager Client Secret - --license.client.stage strings LicenseManager Stages (default [prd]) + --license.client.id string LicenseManager Client ID (ENV: LICENSE_CLIENT_ID) + --license.client.secret string LicenseManager Client Secret (ENV: LICENSE_CLIENT_SECRET) --license.endpoint string LicenseManager Endpoint (default "license.arango.ai") --license.interval duration Interval of the license synchronization --telemetry Enables Telemetry (default true) @@ -187,9 +189,8 @@ Flags: --deployment.id string Deployment ID -h, --help help for generate --inventory string Path to the Inventory File - --license.client.id string LicenseManager Client ID - --license.client.secret string LicenseManager Client Secret - --license.client.stage strings LicenseManager Stages (default [prd]) + --license.client.id string LicenseManager Client ID (ENV: LICENSE_CLIENT_ID) + --license.client.secret string LicenseManager Client Secret (ENV: LICENSE_CLIENT_SECRET) --license.endpoint string LicenseManager Endpoint (default "license.arango.ai") Global Flags: @@ -209,9 +210,8 @@ Usage: Flags: -h, --help help for secret - --license.client.id string LicenseManager Client ID - --license.client.secret string LicenseManager Client Secret - --license.client.stage strings LicenseManager Stages (default [prd]) + --license.client.id string LicenseManager Client ID (ENV: LICENSE_CLIENT_ID) + --license.client.secret string LicenseManager Client Secret (ENV: LICENSE_CLIENT_SECRET) --license.endpoint string LicenseManager Endpoint (default "license.arango.ai") --secret string Kubernetes Secret Name diff --git a/pkg/deployment/reconcile/action_license_generate.go b/pkg/deployment/reconcile/action_license_generate.go index b81b5e19e..a1be806f0 100644 --- a/pkg/deployment/reconcile/action_license_generate.go +++ b/pkg/deployment/reconcile/action_license_generate.go @@ -22,6 +22,7 @@ package reconcile import ( "context" + "encoding/base64" "fmt" "math" "time" @@ -32,6 +33,7 @@ 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/deployment/patch" "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" @@ -216,6 +218,25 @@ func (a *actionLicenseGenerate) Start(ctx context.Context) (bool, error) { } } + if spec.Sync.IsEnabled() { + if !spec.License.HasSecretName() { + a.log.Debug("License Secret Gone") + return true, nil + } + s, ok := cache.Secret().V1().GetSimple(spec.License.GetSecretName()) + if !ok { + a.log.Debug("License Secret Gone") + return true, nil + } + + if _, _, err := patcher.Patcher[*core.Secret](ctx, cache.Client().Kubernetes().CoreV1().Secrets(a.actionCtx.GetNamespace()), s, meta.PatchOptions{}, func(in *core.Secret) []patch.Item { + return []patch.Item{patch.ItemReplace(patch.NewPath("data", utilConstants.SecretKeyToken), base64.StdEncoding.EncodeToString([]byte(generatedLicense.License)))} + }); err != nil { + a.log.Err(err).Debug("Failed to patch License Secret") + return true, nil + } + } + if err := a.actionCtx.WithStatusUpdate(ctx, func(s *api.DeploymentStatus) bool { s.License = &api.DeploymentStatusLicense{ ID: generatedLicense.ID, diff --git a/pkg/deployment/reconcile/plan_builder_license.go b/pkg/deployment/reconcile/plan_builder_license.go index 68159c5f4..7c643db78 100644 --- a/pkg/deployment/reconcile/plan_builder_license.go +++ b/pkg/deployment/reconcile/plan_builder_license.go @@ -107,14 +107,14 @@ func (r *Reconciler) updateClusterLicenseDiscover(spec api.DeploymentSpec, conte return "", err } - if l.V2.IsV2Set() { - return api.LicenseModeKey, nil - } - if l.API != nil { return api.LicenseModeAPI, nil } + if l.V2.IsV2Set() { + return api.LicenseModeKey, nil + } + return "", errors.Errorf("Unable to discover License mode") } @@ -256,5 +256,12 @@ func (r *Reconciler) updateClusterLicenseAPI(ctx context.Context, spec api.Deplo return api.Plan{actions.NewClusterAction(api.ActionTypeLicenseClean, "Removing license reference - Registry Change Required")} } + if spec.Sync.IsEnabled() { + // Feedback of the token is required + if l.V2.V2Hash() != status.License.Hash { + return api.Plan{actions.NewClusterAction(api.ActionTypeLicenseClean, "Removing license reference - Sync Token Required")} + } + } + return nil } diff --git a/pkg/platform/chart_manager.go b/pkg/platform/chart_manager.go deleted file mode 100644 index d9c00d744..000000000 --- a/pkg/platform/chart_manager.go +++ /dev/null @@ -1,38 +0,0 @@ -// -// 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 platform - -import ( - goHttp "net/http" - - "github.com/spf13/cobra" - - "github.com/arangodb/kube-arangodb/pkg/util/k8sutil/helm" -) - -func getChartManager(cmd *cobra.Command) (helm.ChartManager, error) { - endpoint, err := flagPlatformEndpoint.Get(cmd) - if err != nil { - return nil, err - } - - return helm.NewChartManager(cmd.Context(), goHttp.DefaultClient, "%s/index.yaml", endpoint) -} diff --git a/pkg/platform/flags.go b/pkg/platform/flags.go index 283d2fc76..dca30a437 100644 --- a/pkg/platform/flags.go +++ b/pkg/platform/flags.go @@ -121,13 +121,6 @@ var ( }, } - flagPlatformEndpoint = cli.Flag[string]{ - Name: "platform.endpoint", - Description: "Platform Repository URL", - Default: "https://arangodb-platform-prd-chart-registry.s3.amazonaws.com", - Persistent: true, - } - flagUpgradeVersions = cli.Flag[bool]{ Name: "upgrade", Short: "u", @@ -176,26 +169,7 @@ var ( Default: true, } - flagRegistryUseCredentials = cli.Flag[bool]{ - Name: "registry.docker.credentials", - Description: "Use Docker Credentials", - Default: false, - Check: func(in bool) error { - return nil - }, - } - - flagRegistryInsecure = cli.Flag[[]string]{ - Name: "registry.docker.insecure", - Description: "List of insecure registries", - Default: nil, - } - - flagRegistryList = cli.Flag[[]string]{ - Name: "registry.docker.endpoint", - Description: "List of boosted registries", - Default: nil, - } + flagRegistry = cli.NewRegistry() flagActivateInterval = cli.Flag[time.Duration]{ Name: "license.interval", diff --git a/pkg/platform/pack/cache.go b/pkg/platform/pack/cache.go new file mode 100644 index 000000000..012b7f940 --- /dev/null +++ b/pkg/platform/pack/cache.go @@ -0,0 +1,221 @@ +// +// 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 pack + +import ( + "crypto/sha256" + "fmt" + "hash" + "io" + "os" + "path" + "sync" + + "github.com/pkg/errors" +) + +func NewCache(path string) Cache { + return &cache{ + lock: sync.Mutex{}, + path: path, + files: map[string]string{}, + writers: map[string]*cacheWriter{}, + } +} + +type Cache interface { + CacheObject(checksum string, path string, args ...any) (io.WriteCloser, error) + + Get(checksum string, path string, args ...any) (io.ReadCloser, error) + + Saved() int +} + +type cache struct { + lock sync.Mutex + + path string + + saved int + + files map[string]string + + writers map[string]*cacheWriter +} + +func (c *cache) Saved() int { + return c.saved +} + +func (c *cache) Get(checksum string, p string, args ...any) (io.ReadCloser, error) { + c.lock.Lock() + defer c.lock.Unlock() + + z := fmt.Sprintf(p, args...) + + if v, ok := c.files[z]; !ok || v != checksum { + return nil, os.ErrNotExist + } + + return os.Open(path.Join(c.path, z)) +} + +func (c *cache) CacheObject(checksum string, p string, args ...any) (io.WriteCloser, error) { + c.lock.Lock() + defer c.lock.Unlock() + + z := fmt.Sprintf(p, args...) + + if v, ok := c.files[z]; ok { + if v == checksum { + return nil, os.ErrExist + } else { + return nil, errors.Errorf("cache object %s already exists with checksum %s, expected %s", z, v, checksum) + } + } + + if _, ok := c.writers[z]; ok { + return nil, nil + } + + if current, err := c.readChecksum(p, args...); err != nil { + if !os.IsNotExist(err) { + return nil, err + } + } else if current == checksum { + c.files[z] = checksum + return nil, os.ErrExist + } + + fd, err := os.CreateTemp("", "tmp-") + if err != nil { + return nil, err + } + + q := &cacheWriter{ + cache: c, + remote: fd, + dest: z, + hash: checksum, + currentHash: sha256.New(), + } + + c.writers[z] = q + + return q, nil +} + +func (c *cache) readChecksum(p string, args ...any) (string, error) { + f, err := os.Open(path.Join(c.path, fmt.Sprintf(p, args...))) + if err != nil { + return "", err + } + + defer f.Close() + + h := sha256.New() + if _, err := io.Copy(h, f); err != nil { + return "", err + } + + return fmt.Sprintf("%x", h.Sum(nil)), nil +} + +func (c *cache) complete(z *cacheWriter) error { + if err := os.MkdirAll(path.Dir(path.Join(c.path, z.dest)), 0755); err != nil { + if !os.IsExist(err) { + return err + } + } + + if _, err := z.remote.Seek(0, 0); err != nil { + return err + } + + hash := fmt.Sprintf("%x", z.currentHash.Sum(nil)) + + if hash != z.hash { + return errors.Errorf("checksum mismatch for %s != %s", hash, z.hash) + } + + out, err := os.OpenFile(path.Join(c.path, z.dest), os.O_CREATE|os.O_RDWR, 0644) + if err != nil { + return err + } + + defer out.Close() + + if _, err := io.Copy(out, z.remote); err != nil { + return err + } + + if err := z.remote.Close(); err != nil { + return err + } + + if err := os.Remove(z.remote.Name()); err != nil { + return err + } + + c.lock.Lock() + defer c.lock.Unlock() + + c.saved += 1 + + c.files[z.dest] = hash + + delete(c.writers, z.dest) + + return nil +} + +type cacheWriter struct { + lock sync.Mutex + + cache *cache + + remote *os.File + currentHash hash.Hash + + dest string + hash string +} + +func (c *cacheWriter) Write(p []byte) (n int, err error) { + c.lock.Lock() + defer c.lock.Unlock() + + n, err = c.remote.Write(p) + if err != nil { + return n, err + } + + c.currentHash.Write(p[:n]) + + return n, nil +} + +func (c *cacheWriter) Close() error { + c.lock.Lock() + defer c.lock.Unlock() + + return c.cache.complete(c) +} diff --git a/pkg/platform/pack/cache_test.go b/pkg/platform/pack/cache_test.go new file mode 100644 index 000000000..5c9c6cbfc --- /dev/null +++ b/pkg/platform/pack/cache_test.go @@ -0,0 +1,84 @@ +// +// 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 pack + +import ( + "os" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/arangodb/kube-arangodb/pkg/util" +) + +func TestCache(t *testing.T) { + c := NewCache(t.TempDir()) + + var d = make([]byte, 1024) + + checksum := util.SHA256(d) + + _, err := c.Get(checksum, "some/file") + require.ErrorAs(t, err, &os.ErrNotExist) + + t.Run("Invalid Sha", func(t *testing.T) { + out, err := c.CacheObject("ABCD", "some/file") + require.NoError(t, err) + + z, err := out.Write(d) + require.NoError(t, err) + require.Len(t, d, z) + + require.Error(t, out.Close()) + + require.EqualValues(t, 0, c.Saved()) + }) + + t.Run("Valid Sha", func(t *testing.T) { + out, err := c.CacheObject(util.SHA256(d), "some/file") + require.NoError(t, err) + + z, err := out.Write(d) + require.NoError(t, err) + require.Len(t, d, z) + + require.NoError(t, out.Close()) + + require.EqualValues(t, 1, c.Saved()) + }) + + t.Run("Multi Upload Sha", func(t *testing.T) { + out, err := c.CacheObject(util.SHA256(d), "some/file2") + require.NoError(t, err) + + nout, err := c.CacheObject(util.SHA256(d), "some/file2") + require.NoError(t, err) + require.Nil(t, nout) + + z, err := out.Write(d) + require.NoError(t, err) + require.Len(t, d, z) + + require.NoError(t, out.Close()) + + require.EqualValues(t, 1, c.Saved()) + }) +} diff --git a/pkg/platform/pack/export.go b/pkg/platform/pack/export.go index 131483f5d..77a4ea201 100644 --- a/pkg/platform/pack/export.go +++ b/pkg/platform/pack/export.go @@ -26,8 +26,10 @@ import ( "context" "fmt" "io" + "io/ioutil" "os" "sync" + "time" "github.com/pkg/errors" "github.com/regclient/regclient" @@ -44,7 +46,7 @@ import ( "github.com/arangodb/kube-arangodb/pkg/util/k8sutil/helm" ) -func Export(ctx context.Context, path string, m helm.ChartManager, client *regclient.RegClient, p helm.Package, images ...ProtoImage) error { +func Export(ctx context.Context, cache Cache, endpoint, path string, client *regclient.RegClient, p helm.Package, images ...ProtoImage) error { out, err := os.Create(path) if err != nil { return err @@ -53,10 +55,11 @@ func Export(ctx context.Context, path string, m helm.ChartManager, client *regcl tw := zip.NewWriter(out) var r = exportPackageSet{ - m: m, images: images, client: client, + endpoint: endpoint, wr: tw, + cache: cache, existence: map[string]bool{}, } @@ -82,13 +85,16 @@ func Export(ctx context.Context, path string, m helm.ChartManager, client *regcl type exportPackageSet struct { lock sync.Mutex + endpoint string + proto Proto - m helm.ChartManager client *regclient.RegClient images []ProtoImage + cache Cache + existence map[string]bool wr *zip.Writer @@ -120,22 +126,17 @@ func (r *exportPackageSet) exportPackage(name string, spec helm.PackageSpec) exe var chart helm.Chart if spec.Chart.IsZero() { - repo, ok := r.m.Get(name) - if !ok { - return errors.Errorf("Chart `%s` not found", name) - } - - ver, ok := repo.Get(spec.Version) - if !ok { - return errors.Errorf("Chart `%s=%s` not found", name, spec.Version) + ref, err := ChartReference(r.endpoint, spec.GetStage(), name, spec.Version) + if err != nil { + return err } - c, err := ver.Get(ctx) + loadedChart, err := ExportChart(ctx, r.client, ref) if err != nil { return err } - chart = c + chart = loadedChart } else { chart = helm.Chart(spec.Chart) } @@ -184,7 +185,9 @@ func (r *exportPackageSet) exportPackage(name string, spec helm.PackageSpec) exe return in }) - return r.writeOutFile(bytes.NewReader(chart.Raw()), "chart/%s-%s.tgz", name, spec.Version) + return r.save(t, log, util.SHA256(chart.Raw()), wrapFromStatic(func() ([]byte, error) { + return chart.Raw(), nil + }), "chart/%s-%s.tgz", name, spec.Version) } } @@ -239,10 +242,6 @@ func (r *exportPackageSet) exportManifest(src ref.Ref) executor.RunFunc { return err } - if !r.once("manifests/%s", m.GetDescriptor().Digest.Hex()) { - return nil - } - if manifestIndex, ok := m.(manifest.Indexer); ok && m.IsSet() { manifests, err := manifestIndex.GetManifestList() if err != nil { @@ -287,7 +286,7 @@ func (r *exportPackageSet) exportManifest(src ref.Ref) executor.RunFunc { h.WaitForSubThreads(t) - return r.writeOutData(m.MarshalJSON, "manifests/%s", m.GetDescriptor().Digest.Hex()) + return r.saveData(t, log, m.GetDescriptor().Digest.Hex(), m.MarshalJSON, "manifests/%s", m.GetDescriptor().Digest.Hex()) } } @@ -300,54 +299,102 @@ func (r *exportPackageSet) exportBlob(src ref.Ref, desc descriptor.Descriptor) e log.Info("Extracted blob") }() - if o, err := r.client.BlobGet(ctx, src, desc); err != nil { - return err - } else { - return r.exportBlobData(desc, o) - } + return r.exportBlobData(t, log, desc, func() (io.ReadCloser, error) { + return r.client.BlobGet(ctx, src, desc) + }) } } -func (r *exportPackageSet) exportBlobData(desc descriptor.Descriptor, blob io.ReadCloser) error { - if !r.once("blobs/%s", desc.Digest.Hex()) { - return nil +func (r *exportPackageSet) save(t executor.Thread, log logging.Logger, checksum string, factory func() (io.ReadCloser, error), p string, args ...any) error { + log.Info("Started Extraction") + + for { + obj, err := r.cache.CacheObject(checksum, p, args...) + if err != nil { + if os.IsExist(err) { + log.Info("Exists") + break + } + return err + } + + if obj == nil { + t.Wait(time.Second) + continue + } + + log.Info("Downloading") + + blob, err := factory() + if err != nil { + return err + } + + defer blob.Close() + + if _, err := io.Copy(obj, blob); err != nil { + return err + } + + log.Info("Finalizing") + + if err := obj.Close(); err != nil { + return err + } + + if err := blob.Close(); err != nil { + return err + } + + break } - f, err := os.CreateTemp("", "tmp-") - if err != nil { - return err + for { + if r.lock.TryLock() { + break + } + + t.Wait(time.Second) } - if _, err := io.Copy(f, blob); err != nil { + defer r.lock.Unlock() + + pt := fmt.Sprintf(p, args...) + + if _, ok := r.existence[pt]; ok { return nil } - if err := blob.Close(); err != nil { + f, err := r.cache.Get(checksum, p, args...) + if err != nil { return err } - if _, err := f.Seek(0, 0); err != nil { - return err - } + defer f.Close() - if err := r.writeOutFile(f, "blobs/%s", desc.Digest.Hex()); err != nil { + q, err := r.wr.Create(fmt.Sprintf(p, args...)) + if err != nil { return err } - if err := f.Close(); err != nil { + if _, err := io.Copy(q, f); err != nil { return err } - return os.Remove(f.Name()) + return nil } -func (r *exportPackageSet) writeOutData(in func() ([]byte, error), f string, args ...any) error { - data, err := in() - if err != nil { - return err +func (r *exportPackageSet) exportBlobData(t executor.Thread, log logging.Logger, desc descriptor.Descriptor, reader reader) error { + if err := r.save(t, log, desc.Digest.Hex(), reader, "blobs/%s", desc.Digest.Hex()); err != nil { + if !os.IsExist(err) { + return err + } } + return nil +} - return r.writeOutFile(bytes.NewReader(data), f, args...) +func (r *exportPackageSet) saveData(t executor.Thread, log logging.Logger, checksum string, in func() ([]byte, error), f string, args ...any) error { + return r.save(t, log, checksum, wrapFromStatic(in), f, args...) } func (r *exportPackageSet) withProto(mod util.ModR[Proto]) { @@ -357,22 +404,6 @@ func (r *exportPackageSet) withProto(mod util.ModR[Proto]) { r.proto = mod(r.proto) } -func (r *exportPackageSet) writeOutFile(in io.Reader, f string, args ...any) error { - r.lock.Lock() - defer r.lock.Unlock() - - q, err := r.wr.Create(fmt.Sprintf(f, args...)) - if err != nil { - return err - } - - if _, err := io.Copy(q, in); err != nil { - return err - } - - return nil -} - func (r *exportPackageSet) saveProto() error { out, err := r.wr.Create("proto.yaml") if err != nil { @@ -391,18 +422,15 @@ func (r *exportPackageSet) saveProto() error { return nil } -func (r *exportPackageSet) once(f string, args ...any) bool { - r.lock.Lock() - defer r.lock.Unlock() - - k := fmt.Sprintf(f, args...) +func wrapFromStatic(in func() ([]byte, error)) reader { + return func() (io.ReadCloser, error) { + d, err := in() + if err != nil { + return nil, err + } - _, ok := r.existence[k] - if !ok { - return true + return ioutil.NopCloser(bytes.NewReader(d)), nil } - - r.existence[k] = true - - return false } + +type reader func() (io.ReadCloser, error) diff --git a/pkg/platform/pack/utils.go b/pkg/platform/pack/utils.go new file mode 100644 index 000000000..f98355a80 --- /dev/null +++ b/pkg/platform/pack/utils.go @@ -0,0 +1,86 @@ +// +// 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 pack + +import ( + "context" + "fmt" + "io" + + "github.com/regclient/regclient" + "github.com/regclient/regclient/types/mediatype" + "github.com/regclient/regclient/types/ref" + + "github.com/arangodb/kube-arangodb/pkg/util/errors" + "github.com/arangodb/kube-arangodb/pkg/util/k8sutil/helm" +) + +func ChartReference(endpoint, stage, name, version string) (ref.Ref, error) { + if stage == "prd" { + endpoint = fmt.Sprintf("helm.%s", endpoint) + } else { + endpoint = fmt.Sprintf("%s.helm.%s", stage, endpoint) + } + + return ref.New(fmt.Sprintf("%s/%s:%s", endpoint, name, version)) +} + +func ExportChart(ctx context.Context, client *regclient.RegClient, src ref.Ref) (helm.Chart, error) { + m, err := client.ManifestGet(ctx, src) + if err != nil { + return nil, err + } + + if m.GetMediaType() != mediatype.OCI1Manifest { + return nil, errors.Errorf("Manifest is not %s, got %s", mediatype.OCI1Manifest, m.GetMediaType()) + } + + layers, err := m.GetLayers() + if err != nil { + return nil, err + } + + if len(layers) != 1 { + return nil, errors.Errorf("Expected one layer in the OCI") + } + + layer := layers[0] + + if layer.MediaType != "application/vnd.cncf.helm.chart.content.v1.tar+gzip" { + return nil, errors.Errorf("Manifest is not %s, got %s", "application/vnd.cncf.helm.chart.content.v1.tar+gzip", layer.MediaType) + } + + o, err := client.BlobGet(ctx, src, layer) + if err != nil { + return nil, err + } + + data, err := io.ReadAll(o) + if err != nil { + return nil, err + } + + if err := o.Close(); err != nil { + return nil, err + } + + return data, nil +} diff --git a/pkg/platform/package.go b/pkg/platform/package.go index 228b8cced..38f85aec7 100644 --- a/pkg/platform/package.go +++ b/pkg/platform/package.go @@ -42,7 +42,6 @@ func pkg() (*cobra.Command, error) { packageExport, packageImport, packageMerge, - packageRegistry, ); err != nil { return nil, err } diff --git a/pkg/platform/package_export.go b/pkg/platform/package_export.go index fa4bffe97..1fc471777 100644 --- a/pkg/platform/package_export.go +++ b/pkg/platform/package_export.go @@ -34,7 +34,7 @@ func packageExport() (*cobra.Command, error) { cmd.Use = "export [flags] package output" cmd.Short = "Export the package in the ZIP Format" - if err := cli.RegisterFlags(&cmd, flagPlatformEndpoint, flagRegistryUseCredentials, flagRegistryInsecure, flagRegistryList); err != nil { + if err := cli.RegisterFlags(&cmd, flagLicenseManager, flagRegistry); err != nil { return nil, err } @@ -56,15 +56,15 @@ func packageExportRun(cmd *cobra.Command, args []string) error { out := args[1] - cm, err := getChartManager(cmd) + rc, err := flagRegistry.Client(cmd, flagLicenseManager) if err != nil { return err } - rc, err := getRegClient(cmd) + endpoint, err := flagLicenseManager.Endpoint(cmd) if err != nil { return err } - return pack.Export(cmd.Context(), out, cm, rc, pkg) + return pack.Export(cmd.Context(), pack.NewCache("cache"), endpoint, out, rc, pkg) } diff --git a/pkg/platform/package_import.go b/pkg/platform/package_import.go index 982bc94f3..a8c0aeb8b 100644 --- a/pkg/platform/package_import.go +++ b/pkg/platform/package_import.go @@ -37,7 +37,7 @@ func packageImport() (*cobra.Command, error) { cmd.Use = "import [flags] registry package output" cmd.Short = "Imports the package from the ZIP format" - if err := cli.RegisterFlags(&cmd, flagRegistryUseCredentials, flagRegistryInsecure, flagRegistryList); err != nil { + if err := cli.RegisterFlags(&cmd, flagRegistry); err != nil { return nil, err } @@ -55,7 +55,7 @@ func packageImportRun(cmd *cobra.Command, args []string) error { dest := args[1] out := args[2] - rc, err := getRegClient(cmd) + rc, err := flagRegistry.Client(cmd, nil) if err != nil { return err } diff --git a/pkg/platform/package_install.go b/pkg/platform/package_install.go index e41813fb3..f8306f947 100644 --- a/pkg/platform/package_install.go +++ b/pkg/platform/package_install.go @@ -25,6 +25,7 @@ import ( "io" "time" + "github.com/regclient/regclient" "github.com/spf13/cobra" meta "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -32,6 +33,7 @@ import ( platformApi "github.com/arangodb/kube-arangodb/pkg/apis/platform/v1beta1" sharedApi "github.com/arangodb/kube-arangodb/pkg/apis/shared/v1" "github.com/arangodb/kube-arangodb/pkg/logging" + "github.com/arangodb/kube-arangodb/pkg/platform/pack" "github.com/arangodb/kube-arangodb/pkg/util" "github.com/arangodb/kube-arangodb/pkg/util/cli" "github.com/arangodb/kube-arangodb/pkg/util/errors" @@ -47,7 +49,7 @@ func packageInstall() (*cobra.Command, error) { cmd.Use = "install [flags] ... packages" cmd.Short = "Installs the specified setup of the platform" - if err := cli.RegisterFlags(&cmd, flagPlatformEndpoint, flagPlatformName); err != nil { + if err := cli.RegisterFlags(&cmd, flagPlatformName, flagLicenseManager, flagRegistry); err != nil { return nil, err } @@ -62,17 +64,22 @@ func packageInstallRun(cmd *cobra.Command, args []string) error { return err } - hm, err := getChartManager(cmd) + ns, err := flagNamespace.Get(cmd) if err != nil { return err } - ns, err := flagNamespace.Get(cmd) + deployment, err := flagPlatformName.Get(cmd) if err != nil { return err } - deployment, err := flagPlatformName.Get(cmd) + reg, err := flagRegistry.Client(cmd, flagLicenseManager) + if err != nil { + return err + } + + endpoint, err := flagLicenseManager.Endpoint(cmd) if err != nil { return err } @@ -91,10 +98,14 @@ func packageInstallRun(cmd *cobra.Command, args []string) error { return err } - if err := packageInstallRunInstallCharts(cmd, client, hm, ns, r); err != nil { + logger.Info("Chart Update") + + if err := packageInstallRunInstallCharts(cmd, client, reg, ns, endpoint, r); err != nil { return err } + logger.Info("Service Update") + if err := packageInstallRunInstallServices(cmd, client, dApi, r); err != nil { return err } @@ -114,6 +125,10 @@ func packageInstallRunInstallServices(cmd *cobra.Command, client kclient.Client, func packageInstallRunInstallRelease(cmd *cobra.Command, h executor.Handler, client kclient.Client, deployment *api.ArangoDeployment, name string, packageSpec helm.PackageRelease) { h.RunAsync(cmd.Context(), func(ctx context.Context, log logging.Logger, t executor.Thread, h executor.Handler) error { + log = log.Str("type", "release").Str("name", name) + + log.Info("Calculating installation") + chart, err := client.Arango().PlatformV1beta1().ArangoPlatformCharts(deployment.GetNamespace()).Get(ctx, packageSpec.Package, meta.GetOptions{}) if err != nil { return err @@ -128,7 +143,7 @@ func packageInstallRunInstallRelease(cmd *cobra.Command, h executor.Handler, cli return err } - logger.Debug("Installing Service: %s", name) + log.Debug("Installing Service") // Prepare Object if _, err := client.Arango().PlatformV1beta1().ArangoPlatformServices(deployment.GetNamespace()).Create(ctx, &platformApi.ArangoPlatformService{ @@ -150,7 +165,7 @@ func packageInstallRunInstallRelease(cmd *cobra.Command, h executor.Handler, cli return err } - logger.Info("Installed Service: %s", name) + log.Info("Installed Service") } else { if svc.Spec.Deployment.GetName() != deployment.GetName() { return errors.Errorf("Unable to change Deployment name for %s", name) @@ -166,13 +181,15 @@ func packageInstallRunInstallRelease(cmd *cobra.Command, h executor.Handler, cli if err != nil { return err } - logger.Info("Updated Service: %s", name) + log.Info("Updated Service: %s", name) } } // Ensure we wait for reconcile time.Sleep(time.Second) + log.Info("Waiting...") + if err := h.Timeout(ctx, t, func(ctx context.Context, log logging.Logger, t executor.Thread, h executor.Handler) error { svc, err := client.Arango().PlatformV1beta1().ArangoPlatformServices(deployment.GetNamespace()).Get(ctx, name, meta.GetOptions{}) if err != nil { @@ -180,31 +197,35 @@ func packageInstallRunInstallRelease(cmd *cobra.Command, h executor.Handler, cli } if svc.Status.ChartInfo == nil { + log.Warn("No chart info") return nil } if svc.Status.ChartInfo.Checksum != chart.Status.Info.GetChecksum() { + log.Warn("Chart not yet updated") return nil } if !svc.Status.Conditions.IsTrue(platformApi.ReleaseReadyCondition) { + log.Warn("Service not yet ready") return nil } return io.EOF - }, 5*time.Minute, time.Second); err != nil { + }, 5*time.Minute, 15*time.Second); err != nil { if errors.Is(err, io.EOF) { return errors.Errorf("Service %s is not ready", name) } return err } + log.Info("Ready Release") return nil }) } -func packageInstallRunInstallCharts(cmd *cobra.Command, client kclient.Client, hm helm.ChartManager, ns string, r helm.Package) error { +func packageInstallRunInstallCharts(cmd *cobra.Command, client kclient.Client, reg *regclient.RegClient, ns, endpoint string, r helm.Package) error { return executor.Run(cmd.Context(), logger, 8, func(ctx context.Context, log logging.Logger, t executor.Thread, h executor.Handler) error { charts, err := fetchLocallyInstalledCharts(cmd) if err != nil { @@ -212,16 +233,25 @@ func packageInstallRunInstallCharts(cmd *cobra.Command, client kclient.Client, h } for name, packageSpec := range r.Packages { - packageInstallRunInstallChart(cmd, h, client, hm, ns, charts, name, packageSpec) + packageInstallRunInstallChart(cmd, h, client, reg, ns, endpoint, charts, name, packageSpec) } return nil }) } -func packageInstallRunInstallChart(cmd *cobra.Command, h executor.Handler, client kclient.Client, hm helm.ChartManager, ns string, charts map[string]*platformApi.ArangoPlatformChart, name string, packageSpec helm.PackageSpec) { +func packageInstallRunInstallChart(cmd *cobra.Command, h executor.Handler, client kclient.Client, reg *regclient.RegClient, ns, endpoint string, charts map[string]*platformApi.ArangoPlatformChart, name string, packageSpec helm.PackageSpec) { h.RunAsync(cmd.Context(), func(ctx context.Context, log logging.Logger, t executor.Thread, h executor.Handler) error { - chart, err := packageInstallRunChartExtract(cmd, hm, name, packageSpec) + log = log.Str("type", "chart").Str("name", name) + + log.Info("Calculating installation") + + ref, err := pack.ChartReference(endpoint, packageSpec.GetStage(), name, packageSpec.Version) + if err != nil { + return err + } + + chart, err := pack.ExportChart(cmd.Context(), reg, ref) if err != nil { return err } @@ -229,7 +259,7 @@ func packageInstallRunInstallChart(cmd *cobra.Command, h executor.Handler, clien logger := logger.Str("chart", name).Str("version", packageSpec.Version) if c, ok := charts[name]; !ok { - logger.Debug("Installing Chart: %s", name) + log.Debug("Installing Chart") _, err := client.Arango().PlatformV1beta1().ArangoPlatformCharts(ns).Create(cmd.Context(), &platformApi.ArangoPlatformChart{ ObjectMeta: meta.ObjectMeta{ @@ -245,7 +275,7 @@ func packageInstallRunInstallChart(cmd *cobra.Command, h executor.Handler, clien return err } - logger.Info("Installed Chart: %s", name) + log.Info("Installed Chart: %s") } else { if c.Spec.Definition.SHA256() != chart.SHA256SUM() || !packageSpec.Overrides.Equals(helm.Values(c.Spec.Overrides)) { c.Spec.Definition = sharedApi.Data(chart) @@ -254,10 +284,12 @@ func packageInstallRunInstallChart(cmd *cobra.Command, h executor.Handler, clien if err != nil { return err } - logger.Info("Updated Chart: %s", name) + log.Info("Updated Chart") } } + log.Info("Waiting...") + if err := h.Timeout(ctx, t, func(ctx context.Context, log logging.Logger, t executor.Thread, h executor.Handler) error { c, err := client.Arango().PlatformV1beta1().ArangoPlatformCharts(ns).Get(ctx, name, meta.GetOptions{}) if err != nil { @@ -265,44 +297,25 @@ func packageInstallRunInstallChart(cmd *cobra.Command, h executor.Handler, clien } if !c.Ready() { + logger.Warn("Chart not yet ready") return nil } if c.Status.Info == nil { + logger.Warn("Chart not yet accepted") return nil } return io.EOF - }, 5*time.Minute, time.Second); err != nil { + }, 5*time.Minute, 5*time.Second); err != nil { if errors.Is(err, io.EOF) { return errors.Errorf("Chart %s is not ready", name) } return err } + log.Info("Ready Chart") return nil }) } - -func packageInstallRunChartExtract(cmd *cobra.Command, hm helm.ChartManager, name string, spec helm.PackageSpec) (helm.Chart, error) { - if !spec.Chart.IsZero() { - return helm.Chart(spec.Chart), nil - } - def, ok := hm.Get(name) - if !ok { - return helm.Chart{}, errors.Errorf("Unable to get '%s' chart", name) - } - - ver, ok := def.Get(spec.Version) - if !ok { - return helm.Chart{}, errors.Errorf("Unable to get '%s' chart in version `%s`", name, spec.Version) - } - - c, err := ver.Get(cmd.Context()) - if err != nil { - return helm.Chart{}, errors.Wrapf(err, "Unable to download chart %s-%s", name, ver.Version()) - } - - return c, nil -} diff --git a/pkg/platform/package_registry.go b/pkg/platform/package_registry.go deleted file mode 100644 index 1bb098779..000000000 --- a/pkg/platform/package_registry.go +++ /dev/null @@ -1,83 +0,0 @@ -// -// 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 platform - -import ( - "bytes" - "os" - - "github.com/spf13/cobra" - "sigs.k8s.io/yaml" - - "github.com/arangodb/kube-arangodb/pkg/platform/pack" - "github.com/arangodb/kube-arangodb/pkg/util/cli" - "github.com/arangodb/kube-arangodb/pkg/util/errors" -) - -func packageRegistry() (*cobra.Command, error) { - var cmd cobra.Command - - cmd.Use = "registry [flags] registry package output" - cmd.Short = "Points all images to the new registry" - - if err := cli.RegisterFlags(&cmd, flagPlatformEndpoint); err != nil { - return nil, err - } - - cmd.RunE = getRunner().With(packageRegistryRun).Run - - return &cmd, nil -} - -func packageRegistryRun(cmd *cobra.Command, args []string) error { - if len(args) != 3 { - return errors.Errorf("Invalid arguments") - } - - registry := args[0] - - pkg, err := getHelmPackages(args[1]) - if err != nil { - logger.Err(err).Error("Unable to read the file") - return err - } - - out := args[2] - - cm, err := getChartManager(cmd) - if err != nil { - return err - } - - p, err := pack.Registry(cmd.Context(), registry, cm, pkg) - if err != nil { - return err - } - - data, err := yaml.Marshal(p) - if err != nil { - return err - } - - data = bytes.Join([][]byte{[]byte("---\n\n"), data}, nil) - - return os.WriteFile(out, data, 0644) -} diff --git a/pkg/platform/regclient.go b/pkg/platform/regclient.go deleted file mode 100644 index dc5372090..000000000 --- a/pkg/platform/regclient.go +++ /dev/null @@ -1,95 +0,0 @@ -// -// 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 platform - -import ( - "log/slog" - goHttp "net/http" - - "github.com/regclient/regclient" - "github.com/regclient/regclient/config" - "github.com/regclient/regclient/scheme/reg" - "github.com/spf13/cobra" -) - -func getRegClient(cmd *cobra.Command) (*regclient.RegClient, error) { - var flags = make([]regclient.Opt, 0, 3) - - slog.SetLogLoggerLevel(slog.LevelDebug) - - flags = append(flags, regclient.WithConfigHostDefault(config.Host{ - ReqConcurrent: 8, - })) - - flags = append(flags, regclient.WithRegOpts(reg.WithTransport(&goHttp.Transport{ - MaxConnsPerHost: 64, - MaxIdleConns: 100, - })), regclient.WithSlog(slog.Default())) - - configs := map[string]config.Host{} - - ins, err := flagRegistryInsecure.Get(cmd) - if err != nil { - return nil, err - } - - for _, el := range ins { - v, ok := configs[el] - if !ok { - v.Name = el - v.Hostname = el - } - - v.TLS = config.TLSDisabled - v.ReqConcurrent = 8 - - configs[el] = v - } - - regs, err := flagRegistryList.Get(cmd) - if err != nil { - return nil, err - } - - for _, el := range regs { - v, ok := configs[el] - if !ok { - v.Name = el - v.Hostname = el - } - - v.ReqConcurrent = 8 - - configs[el] = v - } - - if creds, err := flagRegistryUseCredentials.Get(cmd); err != nil { - return nil, err - } else if creds { - flags = append(flags, regclient.WithDockerCreds()) - } - - for _, v := range configs { - flags = append(flags, regclient.WithConfigHost(v)) - } - - return regclient.New(flags...), nil -} diff --git a/pkg/platform/runner.go b/pkg/platform/runner.go index ea38761ae..34f20f807 100644 --- a/pkg/platform/runner.go +++ b/pkg/platform/runner.go @@ -32,13 +32,13 @@ func getRunner() cli.Runner { flagNamespace, flagSecret, flagPlatformName, - flagPlatformEndpoint, flagOutput, flagUpgradeVersions, flagAll, flagValues, flagDeployment, flagLicenseManager, + flagRegistry, ), } } diff --git a/pkg/util/cli/flag.go b/pkg/util/cli/flag.go index 0b82561e3..9671b5220 100644 --- a/pkg/util/cli/flag.go +++ b/pkg/util/cli/flag.go @@ -21,7 +21,11 @@ package cli import ( + "fmt" + "os" "reflect" + "strconv" + goStrings "strings" "time" "github.com/spf13/cobra" @@ -66,6 +70,8 @@ type Flag[T any] struct { Description string Default T + EnvEnabled bool + Persistent bool Check func(in T) error @@ -104,35 +110,46 @@ func (f Flag[T]) Register(cmd *cobra.Command) error { v := reflect.TypeOf(f.Default) - z := reflect.ValueOf(f.Default).Interface() + desc := f.Description + + if f.EnvEnabled { + desc = fmt.Sprintf("%s (ENV: %s)", f.Description, f.Env()) + } + + p, err := f.GetFromEnv() + if err != nil { + return err + } + + z := any(p) if v == util.TypeOf[string]() { v := z.(string) if short := f.Short; short == "" { - flags.String(f.Name, v, f.Description) + flags.String(f.Name, v, desc) } else { - flags.StringP(f.Name, short, v, f.Description) + flags.StringP(f.Name, short, v, desc) } } else if v == util.TypeOf[bool]() { v := z.(bool) if short := f.Short; short == "" { - flags.Bool(f.Name, v, f.Description) + flags.Bool(f.Name, v, desc) } else { - flags.BoolP(f.Name, short, v, f.Description) + flags.BoolP(f.Name, short, v, desc) } } else if v == util.TypeOf[[]string]() { v := z.([]string) if short := f.Short; short == "" { - flags.StringSlice(f.Name, v, f.Description) + flags.StringSlice(f.Name, v, desc) } else { - flags.StringSliceP(f.Name, short, v, f.Description) + flags.StringSliceP(f.Name, short, v, desc) } } else if v == util.TypeOf[time.Duration]() { v := z.(time.Duration) if short := f.Short; short == "" { - flags.Duration(f.Name, v, f.Description) + flags.Duration(f.Name, v, desc) } else { - flags.DurationP(f.Name, short, v, f.Description) + flags.DurationP(f.Name, short, v, desc) } } else { return errors.Errorf("Unsupported type for kind: %s", reflect.ValueOf(f.Default).Type().String()) @@ -154,10 +171,8 @@ func (f Flag[T]) Register(cmd *cobra.Command) error { } func (f Flag[T]) Get(cmd *cobra.Command) (T, error) { - - v := reflect.TypeOf(f.Default) - - if v == util.TypeOf[string]() { + switch util.TypeOf[T]() { + case util.TypeOf[string](): v, err := cmd.Flags().GetString(f.Name) if err != nil { return util.Default[T](), err @@ -169,7 +184,7 @@ func (f Flag[T]) Get(cmd *cobra.Command) (T, error) { } return q, nil - } else if v == util.TypeOf[[]string]() { + case util.TypeOf[[]string](): v, err := cmd.Flags().GetStringSlice(f.Name) if err != nil { return util.Default[T](), err @@ -181,7 +196,7 @@ func (f Flag[T]) Get(cmd *cobra.Command) (T, error) { } return q, nil - } else if v == util.TypeOf[bool]() { + case util.TypeOf[bool](): v, err := cmd.Flags().GetBool(f.Name) if err != nil { return util.Default[T](), err @@ -193,7 +208,7 @@ func (f Flag[T]) Get(cmd *cobra.Command) (T, error) { } return q, nil - } else if v == util.TypeOf[time.Duration]() { + case util.TypeOf[time.Duration](): v, err := cmd.Flags().GetDuration(f.Name) if err != nil { return util.Default[T](), err @@ -205,7 +220,7 @@ func (f Flag[T]) Get(cmd *cobra.Command) (T, error) { } return q, nil - } else { + default: return util.Default[T](), errors.Errorf("Unsupported type for kind: %s", reflect.ValueOf(f.Default).Type().String()) } } @@ -213,3 +228,59 @@ func (f Flag[T]) Get(cmd *cobra.Command) (T, error) { func (f Flag[T]) AsInterface() FlagInterface[T] { return f } + +func (f Flag[T]) Env() string { + return goStrings.Join(goStrings.Split(goStrings.ToUpper(f.Name), "."), "_") +} + +func (f Flag[T]) GetFromEnv() (T, error) { + v, ok := os.LookupEnv(f.Env()) + if !ok { + return f.Default, nil + } + + switch util.TypeOf[T]() { + case util.TypeOf[string](): + q, ok := reflect.ValueOf(v).Interface().(T) + if !ok { + return util.Default[T](), errors.Errorf("Unable to parse type for kind: %s", reflect.ValueOf(f.Default).Type().String()) + } + + return q, nil + case util.TypeOf[[]string](): + v := goStrings.Split(v, ",") + + q, ok := reflect.ValueOf(v).Interface().(T) + if !ok { + return util.Default[T](), errors.Errorf("Unable to parse type for kind: %s", reflect.ValueOf(f.Default).Type().String()) + } + + return q, nil + case util.TypeOf[bool](): + v, err := strconv.ParseBool(v) + if err != nil { + return util.Default[T](), err + } + + q, ok := reflect.ValueOf(v).Interface().(T) + if !ok { + return util.Default[T](), errors.Errorf("Unable to parse type for kind: %s", reflect.ValueOf(f.Default).Type().String()) + } + + return q, nil + case util.TypeOf[time.Duration](): + v, err := time.ParseDuration(v) + if err != nil { + return util.Default[T](), err + } + + q, ok := reflect.ValueOf(v).Interface().(T) + if !ok { + return util.Default[T](), errors.Errorf("Unable to parse type for kind: %s", reflect.ValueOf(f.Default).Type().String()) + } + + return q, nil + default: + return util.Default[T](), errors.Errorf("Unsupported type for kind: %s", reflect.ValueOf(f.Default).Type().String()) + } +} diff --git a/pkg/util/cli/lm.go b/pkg/util/cli/lm.go index d32fb45e8..995ce379e 100644 --- a/pkg/util/cli/lm.go +++ b/pkg/util/cli/lm.go @@ -24,9 +24,11 @@ import ( "fmt" "github.com/google/uuid" + "github.com/regclient/regclient/config" "github.com/spf13/cobra" lmanager "github.com/arangodb/kube-arangodb/pkg/license_manager" + "github.com/arangodb/kube-arangodb/pkg/util" "github.com/arangodb/kube-arangodb/pkg/util/errors" ) @@ -50,6 +52,7 @@ func NewLicenseManager(prefix string) LicenseManager { Name: fmt.Sprintf("%s.client.id", prefix), Description: "LicenseManager Client ID", Default: "", + EnvEnabled: true, Persistent: false, Check: func(in string) error { if in == "" { @@ -72,12 +75,14 @@ func NewLicenseManager(prefix string) LicenseManager { return nil }, + Hidden: true, }, clientSecret: Flag[string]{ Name: "license.client.secret", Description: "LicenseManager Client Secret", Default: "", + EnvEnabled: true, Persistent: false, Check: func(in string) error { if _, err := uuid.Parse(in); err != nil { @@ -101,6 +106,8 @@ type LicenseManager interface { ClientSecret(cmd *cobra.Command) (string, error) Client(cmd *cobra.Command) (lmanager.Client, error) + + RegistryHosts(cmd *cobra.Command) (map[string]util.ModR[config.Host], error) } type licenseManager struct { @@ -109,6 +116,50 @@ type licenseManager struct { client licenseManagerClient } +func (l licenseManager) RegistryHosts(cmd *cobra.Command) (map[string]util.ModR[config.Host], error) { + clientID, err := l.client.clientID.Get(cmd) + if err != nil { + return nil, err + } + + clientSecret, err := l.client.clientSecret.Get(cmd) + if err != nil { + return nil, err + } + + stages, err := l.client.stages.Get(cmd) + if err != nil { + return nil, err + } + + endpoint, err := l.endpoint.Get(cmd) + if err != nil { + return nil, err + } + + var apply util.ModR[config.Host] = func(in config.Host) config.Host { + in.User = clientID + in.Pass = clientSecret + in.ReqConcurrent = 8 + in.ReqPerSec = 128 + return in + } + + ret := map[string]util.ModR[config.Host]{} + + for _, stage := range stages { + ret[fmt.Sprintf("%s.registry.%s", stage, endpoint)] = apply + ret[fmt.Sprintf("%s.helm.%s", stage, endpoint)] = apply + + if stage == "prd" { + ret[fmt.Sprintf("registry.%s", endpoint)] = apply + ret[fmt.Sprintf("helm.%s", endpoint)] = apply + } + } + + return ret, nil +} + func (l licenseManager) Endpoint(cmd *cobra.Command) (string, error) { return l.endpoint.Get(cmd) } diff --git a/pkg/util/cli/registry.go b/pkg/util/cli/registry.go new file mode 100644 index 000000000..c27006fbf --- /dev/null +++ b/pkg/util/cli/registry.go @@ -0,0 +1,174 @@ +// +// 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 cli + +import ( + "log/slog" + goHttp "net/http" + + "github.com/regclient/regclient" + "github.com/regclient/regclient/config" + "github.com/regclient/regclient/scheme/reg" + "github.com/spf13/cobra" +) + +func NewRegistry() Registry { + return registry{ + flagRegistryUseCredentials: Flag[bool]{ + Name: "registry.docker.credentials", + Description: "Use Docker Credentials", + Default: false, + Check: func(in bool) error { + return nil + }, + }, + + flagRegistryInsecure: Flag[[]string]{ + Name: "registry.docker.insecure", + Description: "List of insecure registries", + Default: nil, + }, + + flagRegistryList: Flag[[]string]{ + Name: "registry.docker.endpoint", + Description: "List of boosted registries", + Default: nil, + Hidden: true, + }, + } +} + +type Registry interface { + FlagRegisterer + + Client(cmd *cobra.Command, lm LicenseManager) (*regclient.RegClient, error) +} + +type registry struct { + flagRegistryUseCredentials Flag[bool] + flagRegistryInsecure Flag[[]string] + flagRegistryList Flag[[]string] +} + +func (r registry) GetName() string { + return "registry" +} + +func (r registry) Register(cmd *cobra.Command) error { + return RegisterFlags( + cmd, + r.flagRegistryList, + r.flagRegistryInsecure, + r.flagRegistryUseCredentials, + ) +} + +func (r registry) Validate(cmd *cobra.Command) error { + return ValidateFlags( + r.flagRegistryList, + r.flagRegistryInsecure, + r.flagRegistryUseCredentials, + )(cmd, nil) +} + +func (r registry) Client(cmd *cobra.Command, lm LicenseManager) (*regclient.RegClient, error) { + var flags = make([]regclient.Opt, 0, 3) + + slog.SetLogLoggerLevel(slog.LevelDebug) + + flags = append(flags, regclient.WithConfigHostDefault(config.Host{ + ReqConcurrent: 8, + }), regclient.WithSlog(slog.Default())) + + flags = append(flags, regclient.WithRegOpts(reg.WithTransport(&goHttp.Transport{ + MaxConnsPerHost: 64, + MaxIdleConns: 100, + }))) + + configs := map[string]config.Host{} + + ins, err := r.flagRegistryInsecure.Get(cmd) + if err != nil { + return nil, err + } + + for _, el := range ins { + v, ok := configs[el] + if !ok { + v.Name = el + v.Hostname = el + } + + v.TLS = config.TLSDisabled + v.ReqConcurrent = 8 + + configs[el] = v + } + + regs, err := r.flagRegistryList.Get(cmd) + if err != nil { + return nil, err + } + + for _, el := range regs { + v, ok := configs[el] + if !ok { + v.Name = el + v.Hostname = el + } + + v.ReqConcurrent = 8 + + configs[el] = v + } + + // Hosts + if lm != nil { + registryConfigs, err := lm.RegistryHosts(cmd) + if err != nil { + return nil, err + } + + for n, m := range registryConfigs { + v, ok := configs[n] + if !ok { + v.Name = n + v.Hostname = n + } + + v = m(v) + + configs[n] = v + } + } + + if creds, err := r.flagRegistryUseCredentials.Get(cmd); err != nil { + return nil, err + } else if creds { + flags = append(flags, regclient.WithDockerCreds()) + } + + for _, v := range configs { + flags = append(flags, regclient.WithConfigHost(v)) + } + + return regclient.New(flags...), nil +} diff --git a/pkg/util/executor/executor.go b/pkg/util/executor/executor.go index c5670756c..6b52e7831 100644 --- a/pkg/util/executor/executor.go +++ b/pkg/util/executor/executor.go @@ -96,6 +96,8 @@ func (h *handler) Timeout(ctx context.Context, t Thread, f RunFunc, timeout, int return os.ErrDeadlineExceeded case <-ctx.Done(): return os.ErrDeadlineExceeded + default: + continue } } } diff --git a/pkg/util/k8sutil/helm/package.go b/pkg/util/k8sutil/helm/package.go index 7806a22ab..06904046d 100644 --- a/pkg/util/k8sutil/helm/package.go +++ b/pkg/util/k8sutil/helm/package.go @@ -44,6 +44,8 @@ func (pkg *Package) Validate() error { } type PackageSpec struct { + Stage *string `json:"stage,omitempty"` + Version string `json:"version"` Chart sharedApi.Data `json:"chart,omitempty"` @@ -51,6 +53,14 @@ type PackageSpec struct { Overrides Values `json:"overrides,omitempty"` } +func (p PackageSpec) GetStage() string { + if p.Stage == nil { + return "prd" + } + + return *p.Stage +} + type PackageRelease struct { Package string `json:"package"`