From 865e50cee0821c916a82321e480e0b9ad2694077 Mon Sep 17 00:00:00 2001 From: Eric Stroczynski Date: Fri, 18 Sep 2026 14:30:27 -0700 Subject: [PATCH] fix(nvca): persist rendered MiniService charts in a Secret instead of a local cache After a Helm MiniService install completed, the ReVal render output lived only in an emptyDir-backed file cache on the agent pod. When the cache was lost (agent restart or LRU eviction) status checks and values updates called ReVal again, and a valid=false answer was treated as a terminal error that moved a Running instance to Failed and cleaned it up. Store the render in a per-instance Secret (nvcf-miniservice-rendered) in the instance namespace, owned by the MiniService, the way Helm stores release records. The Secret is written before workload objects are applied, verified on read by a render-input hash and a render-output hash, overwritten on values updates, and retried when a write fails. A small in-memory copy bridges informer lag. Status checks only re-render as a last resort and never fail a running instance on a render error. Remove the chartcache package, the agent's reval-rendered-helmcharts emptyDir in the operator, and the now-unused github.com/hashicorp/golang-lru/v2 dependency from the nvca module and NOTICE. The NVCFBackend cacheDirSize field is kept as a deprecated no-op to avoid a breaking CRD change. Fixes #1956 Co-Authored-By: Claude Fable 5.1 Signed-off-by: Eric Stroczynski --- NOTICE | 1 - src/compute-plane-services/nvca/AGENTS.md | 2 +- src/compute-plane-services/nvca/go.mod | 1 - src/compute-plane-services/nvca/go.sum | 2 - .../nvca/internal/miniservice/BUILD.bazel | 5 +- .../miniservice/chartcache/BUILD.bazel | 32 -- .../miniservice/chartcache/chartcache.go | 306 ------------ .../miniservice/chartcache/chartcache_test.go | 317 ------------- .../nvca/internal/miniservice/controller.go | 15 - .../internal/miniservice/controller_test.go | 1 - .../nvca/internal/miniservice/reconcile.go | 89 +--- .../internal/miniservice/reconcile_test.go | 116 +---- .../miniservice/reconcile_update_test.go | 19 +- .../internal/miniservice/rendered_secret.go | 387 +++++++++++++++ .../miniservice/rendered_secret_test.go | 440 ++++++++++++++++++ .../rendered_secret_update_test.go | 249 ++++++++++ .../internal/miniservice/revision_test.go | 16 +- .../nvca/internal/miniservice/status.go | 19 +- .../internal/miniservice/status_byoo_test.go | 25 +- .../pkg/apis/nvcf/v1/nvcfbackend_types.go | 9 +- .../operator/reconcile/nvcaagent_reconcile.go | 18 - .../reconcile/nvcaagent_reconcile_test.go | 36 -- .../hashicorp/golang-lru/v2/.gitignore | 23 - .../hashicorp/golang-lru/v2/.golangci.yml | 46 -- .../github.com/hashicorp/golang-lru/v2/2q.go | 267 ----------- .../hashicorp/golang-lru/v2/BUILD.bazel | 20 - .../hashicorp/golang-lru/v2/LICENSE | 364 --------------- .../hashicorp/golang-lru/v2/README.md | 79 ---- .../github.com/hashicorp/golang-lru/v2/doc.go | 24 - .../golang-lru/v2/internal/BUILD.bazel | 15 - .../hashicorp/golang-lru/v2/internal/list.go | 145 ------ .../github.com/hashicorp/golang-lru/v2/lru.go | 250 ---------- .../golang-lru/v2/simplelru/BUILD.bazel | 19 - .../golang-lru/v2/simplelru/LICENSE_list | 29 -- .../hashicorp/golang-lru/v2/simplelru/lru.go | 177 ------- .../golang-lru/v2/simplelru/lru_interface.go | 46 -- .../nvca/vendor/modules.txt | 5 - 37 files changed, 1154 insertions(+), 2460 deletions(-) delete mode 100644 src/compute-plane-services/nvca/internal/miniservice/chartcache/BUILD.bazel delete mode 100644 src/compute-plane-services/nvca/internal/miniservice/chartcache/chartcache.go delete mode 100644 src/compute-plane-services/nvca/internal/miniservice/chartcache/chartcache_test.go create mode 100644 src/compute-plane-services/nvca/internal/miniservice/rendered_secret.go create mode 100644 src/compute-plane-services/nvca/internal/miniservice/rendered_secret_test.go create mode 100644 src/compute-plane-services/nvca/internal/miniservice/rendered_secret_update_test.go delete mode 100644 src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/.gitignore delete mode 100644 src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/.golangci.yml delete mode 100644 src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/2q.go delete mode 100644 src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/BUILD.bazel delete mode 100644 src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/LICENSE delete mode 100644 src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/README.md delete mode 100644 src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/doc.go delete mode 100644 src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/internal/BUILD.bazel delete mode 100644 src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/internal/list.go delete mode 100644 src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/lru.go delete mode 100644 src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/simplelru/BUILD.bazel delete mode 100644 src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/simplelru/LICENSE_list delete mode 100644 src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/simplelru/lru.go delete mode 100644 src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/simplelru/lru_interface.go diff --git a/NOTICE b/NOTICE index 47c025fcc6..b2acc8494c 100644 --- a/NOTICE +++ b/NOTICE @@ -181,7 +181,6 @@ The following third-party licenses are included in this repository: src/compute-plane-services/nvca/vendor/github.com/grpc-ecosystem/grpc-gateway/v2/LICENSE src/compute-plane-services/nvca/vendor/github.com/hashicorp/go-cleanhttp/LICENSE src/compute-plane-services/nvca/vendor/github.com/hashicorp/go-retryablehttp/LICENSE - src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/LICENSE src/compute-plane-services/nvca/vendor/github.com/imdario/mergo/LICENSE src/compute-plane-services/nvca/vendor/github.com/inconshreveable/mousetrap/LICENSE src/compute-plane-services/nvca/vendor/github.com/itchyny/gojq/LICENSE diff --git a/src/compute-plane-services/nvca/AGENTS.md b/src/compute-plane-services/nvca/AGENTS.md index f5083788ca..64be0d6cd2 100644 --- a/src/compute-plane-services/nvca/AGENTS.md +++ b/src/compute-plane-services/nvca/AGENTS.md @@ -330,7 +330,7 @@ gofmt -w $(find . -name '*.go' -not -path './vendor/*') && make test && make lin 5. **Local vs CI** - local tests may pass but CI may have additional checks 6. **Queue message idempotency** - handlers may receive duplicate messages 7. **Storage controller timing** - PVC operations are async, handle races carefully -8. **MiniService chartcache key must include namespace** - The `chartcache.ChartCacheInput` struct (in `internal/miniservice/chartcache/`) is used to generate cache keys for rendered Helm charts. Any field that affects the Helm template output (e.g., `.Release.Namespace`) MUST be included in this struct. If namespace is missing from the cache key, cached output from namespace A can be incorrectly returned for namespace B. When adding new fields to `HelmReValRenderInput` that affect rendering, also add them to `ChartCacheInput` and update `getCacheKey()` in `reconcile.go`. +8. **MiniService rendered charts are persisted in a Secret** - After a successful ReVal render, the controller stores the output in the `nvcf-miniservice-rendered` Secret in the instance namespace (`internal/miniservice/rendered_secret.go`), like a Helm release record. Status checks, updates, and cleanup read from that Secret (and an in-memory copy) and never re-render while the inputs are unchanged. The Secret is validated by a render-input hash and a render-output hash (`status.renderedDetails.hash`). Any new `HelmReValRenderInput` field that affects template output MUST be added to `renderInput` so a stored render is not reused for different inputs. ## Code Generation Triggers diff --git a/src/compute-plane-services/nvca/go.mod b/src/compute-plane-services/nvca/go.mod index d7c64156e6..f46b7dbf1c 100644 --- a/src/compute-plane-services/nvca/go.mod +++ b/src/compute-plane-services/nvca/go.mod @@ -18,7 +18,6 @@ require ( github.com/gorilla/handlers v1.5.2 github.com/gorilla/mux v1.8.1 github.com/hashicorp/go-retryablehttp v0.7.8 - github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/imdario/mergo v0.3.16 github.com/nats-io/nats-server/v2 v2.12.12 github.com/nats-io/nats.go v1.51.0 diff --git a/src/compute-plane-services/nvca/go.sum b/src/compute-plane-services/nvca/go.sum index c7dcf59075..d0c74530d5 100644 --- a/src/compute-plane-services/nvca/go.sum +++ b/src/compute-plane-services/nvca/go.sum @@ -192,8 +192,6 @@ github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB1 github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= -github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= -github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= diff --git a/src/compute-plane-services/nvca/internal/miniservice/BUILD.bazel b/src/compute-plane-services/nvca/internal/miniservice/BUILD.bazel index c307de6f78..f6e28c4f65 100644 --- a/src/compute-plane-services/nvca/internal/miniservice/BUILD.bazel +++ b/src/compute-plane-services/nvca/internal/miniservice/BUILD.bazel @@ -16,6 +16,7 @@ go_library( "prereqs.go", "reconcile.go", "reconcile_storagerequests.go", + "rendered_secret.go", "reval_client.go", "reval_types.go", "revision.go", @@ -33,7 +34,6 @@ go_library( "//src/compute-plane-services/nvca/internal/kubeclients", "//src/compute-plane-services/nvca/internal/logging", "//src/compute-plane-services/nvca/internal/metrics", - "//src/compute-plane-services/nvca/internal/miniservice/chartcache", "//src/compute-plane-services/nvca/internal/otel", "//src/compute-plane-services/nvca/internal/transporttls", "//src/compute-plane-services/nvca/internal/util/k8sutil", @@ -135,6 +135,8 @@ go_test( "reconcile_storagerequests_test.go", "reconcile_test.go", "reconcile_update_test.go", + "rendered_secret_test.go", + "rendered_secret_update_test.go", "reval_client_test.go", "revision_test.go", "status_karta_test.go", @@ -163,7 +165,6 @@ go_test( "//src/compute-plane-services/nvca/internal/envtest", "//src/compute-plane-services/nvca/internal/icms", "//src/compute-plane-services/nvca/internal/metrics", - "//src/compute-plane-services/nvca/internal/miniservice/chartcache", "//src/compute-plane-services/nvca/internal/otel", "//src/compute-plane-services/nvca/internal/transporttls", "//src/compute-plane-services/nvca/internal/util/k8sutil", diff --git a/src/compute-plane-services/nvca/internal/miniservice/chartcache/BUILD.bazel b/src/compute-plane-services/nvca/internal/miniservice/chartcache/BUILD.bazel deleted file mode 100644 index 8a8fe036f1..0000000000 --- a/src/compute-plane-services/nvca/internal/miniservice/chartcache/BUILD.bazel +++ /dev/null @@ -1,32 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -load("@rules_go//go:def.bzl", "go_library", "go_test") - -go_library( - name = "chartcache", - srcs = ["chartcache.go"], - importpath = "github.com/NVIDIA/nvcf/src/compute-plane-services/nvca/internal/miniservice/chartcache", - visibility = ["//src/compute-plane-services/nvca:__subpackages__"], - deps = [ - "//src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2:golang-lru", - "//src/compute-plane-services/nvca/vendor/sigs.k8s.io/controller-runtime/pkg/log", - "//src/compute-plane-services/nvca/vendor/sigs.k8s.io/controller-runtime/pkg/manager", - ], -) - -alias( - name = "go_default_library", - actual = ":chartcache", - visibility = ["//src/compute-plane-services/nvca:__subpackages__"], -) - -go_test( - name = "chartcache_test", - srcs = ["chartcache_test.go"], - embed = [":chartcache"], - deps = [ - "//src/compute-plane-services/nvca/vendor/github.com/stretchr/testify/assert", - "//src/compute-plane-services/nvca/vendor/github.com/stretchr/testify/require", - ], -) diff --git a/src/compute-plane-services/nvca/internal/miniservice/chartcache/chartcache.go b/src/compute-plane-services/nvca/internal/miniservice/chartcache/chartcache.go deleted file mode 100644 index bfd2751b6d..0000000000 --- a/src/compute-plane-services/nvca/internal/miniservice/chartcache/chartcache.go +++ /dev/null @@ -1,306 +0,0 @@ -/* -SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -SPDX-License-Identifier: Apache-2.0 - -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. -*/ - -package chartcache - -import ( - "compress/gzip" - "context" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "io" - "io/fs" - "os" - "path/filepath" - "sort" - "strings" - "sync" - - lru "github.com/hashicorp/golang-lru/v2" - logf "sigs.k8s.io/controller-runtime/pkg/log" - "sigs.k8s.io/controller-runtime/pkg/manager" -) - -type Cache interface { - manager.Runnable - Get(in ChartCacheInput, w io.Writer) (bool, error) - Put(in ChartCacheInput, r io.ReadSeeker, size int64) (string, error) - Delete(in ChartCacheInput) error -} - -func New(dir string) Cache { - return &localChartCache{ - dir: dir, - putFile: func(fp string) (writerFile, error) { - return os.OpenFile(fp, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0600) - }, - } -} - -type ChartCacheInput struct { - HelmChartURL string - HelmChartServicePort *int32 - HelmChartServiceName string - Values json.RawMessage - K8sVersion string - APIVersions []string - // Namespace is the target namespace for the Helm release. - // This MUST be included in the cache key because Helm templates using - // .Release.Namespace will render namespace-specific values that cannot - // be shared across different namespaces. - Namespace string -} - -const renderedFileName = "rendered.json.gz" - -func (in ChartCacheInput) makeItemFilePath(baseDir, cacheKey string) string { - return filepath.Join(baseDir, cacheKey, renderedFileName) -} - -func (in ChartCacheInput) makeCacheKey() (string, error) { - sort.Strings(in.APIVersions) - inBytes, err := json.Marshal(in) - if err != nil { - return "", err - } - keyBytes := sha256.Sum256(inBytes) - // The first 20 characters are sufficient randomness for uniqueness. - // Don't want to make file names too long, which can cause issues on some systems. - return hex.EncodeToString(keyBytes[:])[:20], nil -} - -// Make LRU cache size arbitrarily large since the only constraint is disk space. -const cacheSize = 100_000 - -type localChartCache struct { - dir string - // Since the local cache is not heavily concurrent, - // a simple mutex will prevent data races. - mu sync.RWMutex - cache *lru.Cache[string, int64] - - // Mocked in test. - putFile func(fp string) (writerFile, error) -} - -type writerFile interface { - fs.File - io.Writer -} - -func (c *localChartCache) Start(ctx context.Context) error { - c.mu.Lock() - defer c.mu.Unlock() - - log := logf.FromContext(ctx) - - if err := os.MkdirAll(c.dir, 0700); err != nil { - // If dir's mount is not configured correctly, try using a tempdir. - tmpDir, terr := os.MkdirTemp("", "nvca-chartcache.*") - if terr != nil { - err = fmt.Errorf("%w (unable to create temp chartcache: %s)", err, terr) - return err - } - log.Info("Using tempdir for chartcache on mkdir error", "error", err, "dir", tmpDir) - c.dir = tmpDir - // Clean up dir in between restarts. - go func() { - <-ctx.Done() - if err := os.RemoveAll(tmpDir); err != nil { - log.Error(err, "Failed to remove tempdir on cleanup", "dir", tmpDir) - } - }() - } - - var err error - if c.cache, err = lru.New[string, int64](cacheSize); err != nil { - return err - } - - // Build cache by looking for rendered files and using their parent dirs as cache keys. - if err := filepath.WalkDir(c.dir, func(path string, d fs.DirEntry, err error) error { - if err != nil || filepath.Base(path) != renderedFileName { - return err - } - di, err := d.Info() - if err != nil { - return err - } - cacheKey := strings.TrimPrefix(filepath.Dir(path), c.dir+string(filepath.Separator)) - c.cache.Add(cacheKey, di.Size()) - return nil - }); err != nil { - return err - } - - return nil -} - -func (c *localChartCache) Get(in ChartCacheInput, w io.Writer) (bool, error) { - c.mu.RLock() - defer c.mu.RUnlock() - - cacheKey, err := in.makeCacheKey() - if err != nil { - return false, err - } - itemFilePath := in.makeItemFilePath(c.dir, cacheKey) - - if _, ok := c.cache.Get(cacheKey); !ok { - return false, nil - } - - switch _, err := os.Stat(itemFilePath); { - case err == nil: - f, err := os.Open(itemFilePath) - if err != nil { - return false, fmt.Errorf("open cache item file: %v", err) - } - defer f.Close() - - gzr, err := gzip.NewReader(f) - if err != nil { - return false, fmt.Errorf("create cache item file gzip reader: %v", err) - } - defer gzr.Close() - - n, err := io.Copy(w, gzr) - if err != nil { - return false, fmt.Errorf("copy cache item file: %v", err) - } - - c.cache.Add(cacheKey, n) - - return true, nil - case errors.Is(err, fs.ErrNotExist): - c.cache.Remove(cacheKey) - return false, nil - } - return false, err -} - -func (c *localChartCache) Put(in ChartCacheInput, r io.ReadSeeker, size int64) (string, error) { - c.mu.Lock() - defer c.mu.Unlock() - - cacheKey, err := in.makeCacheKey() - if err != nil { - return "", err - } - itemFilePath := in.makeItemFilePath(c.dir, cacheKey) - - n, h, err := c.writeFile(itemFilePath, r, size) - if err != nil { - return "", fmt.Errorf("write cache item file: %v", err) - } - - c.cache.Add(cacheKey, n) - return h, nil -} - -func (c *localChartCache) Delete(in ChartCacheInput) error { - c.mu.Lock() - defer c.mu.Unlock() - - cacheKey, err := in.makeCacheKey() - if err != nil { - return err - } - itemFilePath := in.makeItemFilePath(c.dir, cacheKey) - - if err := os.RemoveAll(filepath.Dir(itemFilePath)); err != nil { - return err - } - c.cache.Remove(cacheKey) - return nil -} - -func (c *localChartCache) writeFile(itemFilePath string, r io.ReadSeeker, size int64) (int64, string, error) { - n, h, err := c.tryWriteFile(itemFilePath, r) - if err == nil { - return n, h, nil - } - // syscall.ENOSPC means there is no space left on the device. - // When returned, eviction should occur then the file write reattempted. - // - // This error is not represented in a case in syscall.Errno.Is(), - // use string comparison. - // https://github.com/golang/go/issues/37627 - if !strings.HasSuffix(err.Error(), "no space left on device") { - return n, h, err - } - if _, err := r.Seek(0, 0); err != nil { - return n, h, fmt.Errorf("seek while writing cache file: %v", err) - } - // TODO: may want to re-attempt eviction if syscall.ENOSPC occurs again. - if err := c.evictLRUs(size); err != nil { - return n, h, err - } - return c.tryWriteFile(itemFilePath, r) -} - -func (c *localChartCache) tryWriteFile(itemFilePath string, r io.Reader) (int64, string, error) { - if err := os.MkdirAll(filepath.Dir(itemFilePath), 0700); err != nil { - return 0, "", err - } - - f, err := c.putFile(itemFilePath) - if err != nil { - return 0, "", err - } - defer f.Close() - - gzw := gzip.NewWriter(f) - h := sha256.New() - mw := io.MultiWriter(gzw, h) - - n, err := io.Copy(mw, r) - if err != nil { - gzw.Close() - return n, "", err - } - gzw.Close() - - // Stat after close to get total compressed file size. - fi, err := f.Stat() - if err != nil { - return n, "", err - } - - sum := h.Sum(nil) - return fi.Size(), "sha256:" + hex.EncodeToString(sum[:]), nil -} - -func (c *localChartCache) evictLRUs(size int64) error { - for { - cacheKey, itemSize, ok := c.cache.GetOldest() - if !ok { - return fmt.Errorf("cannot evict enough items for size %d", size) - } - if err := os.RemoveAll(filepath.Join(c.dir, cacheKey)); err != nil { - return err - } - c.cache.Remove(cacheKey) - if itemSize >= size { - return nil - } - size -= itemSize - } -} diff --git a/src/compute-plane-services/nvca/internal/miniservice/chartcache/chartcache_test.go b/src/compute-plane-services/nvca/internal/miniservice/chartcache/chartcache_test.go deleted file mode 100644 index 4f998868df..0000000000 --- a/src/compute-plane-services/nvca/internal/miniservice/chartcache/chartcache_test.go +++ /dev/null @@ -1,317 +0,0 @@ -/* -SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -SPDX-License-Identifier: Apache-2.0 - -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. -*/ - -package chartcache - -import ( - "bytes" - "compress/gzip" - "context" - "io" - "os" - "path/filepath" - "syscall" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -var data = []byte(`{"hello":"world"}`) - -func TestChartCache(t *testing.T) { - var err error - var servicePort int32 = 8000 - baseInput := ChartCacheInput{ - HelmChartURL: "https://foo.bar.com/mychart-1.0.1.tgz", - HelmChartServicePort: &servicePort, - HelmChartServiceName: "entrypoint", - Values: []byte(`{"foo": "bar"}`), - K8sVersion: "1.29.2", - APIVersions: []string{"foo", "bar"}, - } - t.Run("Start", func(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - t.Cleanup(cancel) - // Start with no dir to test temp handler. - chartCache := New("").(*localChartCache) - err = chartCache.Start(ctx) - require.NoError(t, err) - assert.NotEmpty(t, chartCache.dir) - - tmpDir := t.TempDir() - chartCache = New(tmpDir).(*localChartCache) - - cacheKey, err := baseInput.makeCacheKey() - require.NoError(t, err) - assert.Equal(t, "70dc68f23e1da069ae90", cacheKey) - expFilePathFunction := baseInput.makeItemFilePath(tmpDir, cacheKey) - writeGZipFile(t, chartCache, expFilePathFunction, data) - - // Test case: Start should have added all cache items to cache. - err = chartCache.Start(ctx) - require.NoError(t, err) - gotFound, err := chartCache.Get(baseInput, io.Discard) - require.NoError(t, err) - assert.True(t, gotFound) - }) - t.Run("Get", func(t *testing.T) { - ctx, cancel := context.WithCancel(t.Context()) - t.Cleanup(cancel) - - tmpDir := t.TempDir() - chartCache := New(tmpDir).(*localChartCache) - err := chartCache.Start(ctx) - require.NoError(t, err) - - cacheKey, err := baseInput.makeCacheKey() - require.NoError(t, err) - - input1 := baseInput - w := &bytes.Buffer{} - expFilePath := input1.makeItemFilePath(tmpDir, cacheKey) - - // Test case: the LRU cache has not been written to yet so found should be false. - gotFound, err := chartCache.Get(input1, w) - require.NoError(t, err) - assert.False(t, gotFound) - - _, err = chartCache.Put(input1, bytes.NewReader(data), int64(len(data))) - require.NoError(t, err) - assert.FileExists(t, expFilePath) - - gotFound, err = chartCache.Get(input1, w) - require.NoError(t, err) - assert.True(t, gotFound) - assert.Equal(t, string(data), w.String()) - - // Test case: different item does not exist in cache - input2 := baseInput - input2.APIVersions = nil - w.Reset() - gotFound, err = chartCache.Get(input2, w) - require.NoError(t, err) - assert.False(t, gotFound) - }) - - t.Run("Put", func(t *testing.T) { - ctx, cancel := context.WithCancel(t.Context()) - t.Cleanup(cancel) - - tmpDir := t.TempDir() - chartCache := New(tmpDir).(*localChartCache) - err := chartCache.Start(ctx) - require.NoError(t, err) - - input1 := baseInput - - cacheKey1, err := baseInput.makeCacheKey() - require.NoError(t, err) - - expFilePath1 := input1.makeItemFilePath(tmpDir, cacheKey1) - - // Test case: write item to cache - r := bytes.NewReader(data) - gotHash, err := chartCache.Put(input1, r, int64(r.Len())) - require.NoError(t, err) - assert.Equal(t, "sha256:93a23971a914e5eacbf0a8d25154cda309c3c1c72fbb9914d47c60f3cb681588", gotHash) - _, gotFound := chartCache.cache.Peek(cacheKey1) - assert.True(t, gotFound) - assert.FileExists(t, expFilePath1) - - // Test case: write item to cache, but ENOSPC error leads to eviction - i := 0 - chartCache.putFile = func(fp string) (writerFile, error) { - i++ - f, err := os.OpenFile(fp, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0600) - // Return no error the second time this function is called. - if i > 1 { - return f, err - } - return &mockWriterFile{ - File: f, - err: syscall.ENOSPC, - }, err - } - input2 := baseInput - input2.Values = []byte(`{"bar":"foo"}`) - r = bytes.NewReader(data) - _, err = chartCache.Put(input2, r, int64(r.Len())) - require.NoError(t, err) - _, gotFound = chartCache.cache.Peek(cacheKey1) - assert.False(t, gotFound) - cacheKey2, err := input2.makeCacheKey() - require.NoError(t, err) - _, gotFound = chartCache.cache.Peek(cacheKey2) - assert.True(t, gotFound) - }) - - t.Run("Delete", func(t *testing.T) { - ctx, cancel := context.WithCancel(t.Context()) - t.Cleanup(cancel) - - tmpDir := t.TempDir() - chartCache := New(tmpDir).(*localChartCache) - err := chartCache.Start(ctx) - require.NoError(t, err) - // Test case: item exists in cache - input1 := baseInput - input2 := baseInput - input2.HelmChartServiceName = "entrypoint2" - input3 := baseInput - input3.HelmChartServiceName = "entrypoint3" - input4 := baseInput - input4.HelmChartServiceName = "entrypoint4" - cacheKey1, err := input1.makeCacheKey() - require.NoError(t, err) - expFilePath1 := input1.makeItemFilePath(tmpDir, cacheKey1) - cacheKey2, err := input2.makeCacheKey() - require.NoError(t, err) - expFilePath2 := input2.makeItemFilePath(tmpDir, cacheKey2) - cacheKey3, err := input3.makeCacheKey() - require.NoError(t, err) - expFilePath3 := input3.makeItemFilePath(tmpDir, cacheKey3) - cacheKey4, err := input4.makeCacheKey() - require.NoError(t, err) - expFilePath4 := input4.makeItemFilePath(tmpDir, cacheKey4) - - r := bytes.NewReader(data) - _, err = chartCache.Put(input1, r, int64(r.Len())) - require.NoError(t, err) - assert.FileExists(t, expFilePath1) - r = bytes.NewReader(data) - _, err = chartCache.Put(input2, r, int64(r.Len())) - require.NoError(t, err) - assert.FileExists(t, expFilePath2) - r = bytes.NewReader(data) - _, err = chartCache.Put(input3, r, int64(r.Len())) - require.NoError(t, err) - assert.FileExists(t, expFilePath3) - r = bytes.NewReader(data) - _, err = chartCache.Put(input4, r, int64(r.Len())) - require.NoError(t, err) - assert.FileExists(t, expFilePath4) - _, gotFound := chartCache.cache.Peek(cacheKey1) - assert.True(t, gotFound) - _, gotFound = chartCache.cache.Peek(cacheKey2) - assert.True(t, gotFound) - _, gotFound = chartCache.cache.Peek(cacheKey3) - assert.True(t, gotFound) - _, gotFound = chartCache.cache.Peek(cacheKey4) - assert.True(t, gotFound) - - gotFuncEntries, err := os.ReadDir(tmpDir) - require.NoError(t, err) - assert.Len(t, gotFuncEntries, 4) - - // Deleting input1 should remove the directory but not its siblings. - err = chartCache.Delete(input1) - require.NoError(t, err) - gotFuncEntries, err = os.ReadDir(tmpDir) - require.NoError(t, err) - assert.Len(t, gotFuncEntries, 3) - assert.NoFileExists(t, expFilePath1) - assert.FileExists(t, expFilePath2) - assert.FileExists(t, expFilePath3) - assert.FileExists(t, expFilePath4) - _, gotFound = chartCache.cache.Peek(cacheKey1) - assert.False(t, gotFound) - _, gotFound = chartCache.cache.Peek(cacheKey2) - assert.True(t, gotFound) - _, gotFound = chartCache.cache.Peek(cacheKey3) - assert.True(t, gotFound) - _, gotFound = chartCache.cache.Peek(cacheKey4) - assert.True(t, gotFound) - - // Delete the second one, same outcome. - err = chartCache.Delete(input2) - require.NoError(t, err) - gotFuncEntries, err = os.ReadDir(tmpDir) - require.NoError(t, err) - assert.Len(t, gotFuncEntries, 2) - assert.NoFileExists(t, expFilePath1) - assert.NoFileExists(t, expFilePath2) - assert.FileExists(t, expFilePath3) - assert.FileExists(t, expFilePath4) - _, gotFound = chartCache.cache.Peek(cacheKey1) - assert.False(t, gotFound) - _, gotFound = chartCache.cache.Peek(cacheKey2) - assert.False(t, gotFound) - _, gotFound = chartCache.cache.Peek(cacheKey3) - assert.True(t, gotFound) - _, gotFound = chartCache.cache.Peek(cacheKey4) - assert.True(t, gotFound) - - // Delete them all but ensure parent dir still exists - err = chartCache.Delete(input3) - require.NoError(t, err) - err = chartCache.Delete(input4) - require.NoError(t, err) - gotFuncEntries, err = os.ReadDir(tmpDir) - require.NoError(t, err) - assert.Len(t, gotFuncEntries, 0) - assert.NoFileExists(t, expFilePath1) - assert.NoFileExists(t, expFilePath2) - assert.NoFileExists(t, expFilePath3) - assert.NoFileExists(t, expFilePath4) - - // Put then delete. - r = bytes.NewReader(data) - gotHash, err := chartCache.Put(input1, r, int64(r.Len())) - require.NoError(t, err) - assert.Equal(t, "sha256:93a23971a914e5eacbf0a8d25154cda309c3c1c72fbb9914d47c60f3cb681588", gotHash) - exists, err := chartCache.Get(input1, io.Discard) - require.NoError(t, err) - assert.True(t, exists) - gotFuncEntries, err = os.ReadDir(tmpDir) - require.NoError(t, err) - assert.Len(t, gotFuncEntries, 1) - assert.FileExists(t, expFilePath1) - err = chartCache.Delete(input1) - require.NoError(t, err) - gotFuncEntries, err = os.ReadDir(tmpDir) - require.NoError(t, err) - assert.Len(t, gotFuncEntries, 0) - assert.NoFileExists(t, expFilePath1) - - }) -} - -type mockWriterFile struct { - *os.File - err error -} - -func (w *mockWriterFile) Write(p []byte) (int, error) { - if w.err != nil { - return 0, w.err - } - return w.File.Write(p) -} - -func writeGZipFile(t *testing.T, chartCache *localChartCache, filePath string, data []byte) { - t.Helper() - err := os.MkdirAll(filepath.Dir(filePath), 0700) - require.NoError(t, err) - tf, err := chartCache.putFile(filePath) - require.NoError(t, err) - gzwt := gzip.NewWriter(tf) - _, err = gzwt.Write(data) - require.NoError(t, err) - err = gzwt.Close() - require.NoError(t, err) -} diff --git a/src/compute-plane-services/nvca/internal/miniservice/controller.go b/src/compute-plane-services/nvca/internal/miniservice/controller.go index 1e744691b4..2da8a64110 100644 --- a/src/compute-plane-services/nvca/internal/miniservice/controller.go +++ b/src/compute-plane-services/nvca/internal/miniservice/controller.go @@ -54,7 +54,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/NVIDIA/nvcf/src/compute-plane-services/nvca/internal/metrics" - "github.com/NVIDIA/nvcf/src/compute-plane-services/nvca/internal/miniservice/chartcache" "github.com/NVIDIA/nvcf/src/compute-plane-services/nvca/internal/otel" "github.com/NVIDIA/nvcf/src/compute-plane-services/nvca/internal/util/k8sutil" nvcav1 "github.com/NVIDIA/nvcf/src/compute-plane-services/nvca/pkg/apis/nvca/v1" @@ -125,9 +124,6 @@ type ControllerOptions struct { // NsightProfilingAllowlist tracks which functions should have NVIDIA Nsight GPU // profiling enabled. Shared with BackendK8sCache. NsightProfilingAllowlist *profiling.Allowlist - - // Internal use. - cacheDir string } // Only Get is needed here. @@ -137,8 +133,6 @@ type registrationInstanceTypeCache interface { const ( controllerName = "miniservice-controller" - - defaultCacheDir = "/var/run/nvca/reval-rendered-helmcharts" ) func BuildController(ctx context.Context, @@ -150,10 +144,6 @@ func BuildController(ctx context.Context, enabledAttrs featureflag.Attributes, opts ControllerOptions, ) error { - if opts.cacheDir == "" { - opts.cacheDir = defaultCacheDir - } - gvkc := newGVKCache(mgr.GetScheme()) gvkc.PrePopulate(&corev1.Pod{}, podGVK) gvkc.PrePopulate(&appsv1.Deployment{}, deploymentGVK) @@ -188,7 +178,6 @@ func BuildController(ctx context.Context, Decoder: newFlexibleDecoder(mgr.GetScheme(), extraGVKs...), NFClient: nflient, eventRecorder: mgr.GetEventRecorderFor(controllerName), - chartCache: chartcache.New(opts.cacheDir), regITCache: regITCache, enabledAttrs: enabledAttrs, gvkCache: gvkc, @@ -245,10 +234,6 @@ func BuildController(ctx context.Context, r.statusCheckers[kartaGVKToSchemaGVK(*karta.Spec.StructureDefinition.RootComponent.Kind)] = checker } - if err := mgr.Add(r.chartCache); err != nil { - return fmt.Errorf("add local chart cache: %v", err) - } - if r.FeatureFlagFetcher.IsAttributeEnabled(featureflag.AttrNVLinkOptimized) { if err := mgr.Add(r.newNVLinkOptMetricsRunnable(mgr)); err != nil { return fmt.Errorf("add NVLink optimized metrics runnable: %v", err) diff --git a/src/compute-plane-services/nvca/internal/miniservice/controller_test.go b/src/compute-plane-services/nvca/internal/miniservice/controller_test.go index e8220ef0ad..e70d6c1051 100644 --- a/src/compute-plane-services/nvca/internal/miniservice/controller_test.go +++ b/src/compute-plane-services/nvca/internal/miniservice/controller_test.go @@ -235,7 +235,6 @@ func testController(t *testing.T, tc controllerTestCase) { "1.2.3", metrics.WithRegisterer(prometheus.NewRegistry()), ), - cacheDir: t.TempDir(), } cfg := nvcaconfig.Config{ diff --git a/src/compute-plane-services/nvca/internal/miniservice/reconcile.go b/src/compute-plane-services/nvca/internal/miniservice/reconcile.go index 5792d1a5a5..2567971df8 100644 --- a/src/compute-plane-services/nvca/internal/miniservice/reconcile.go +++ b/src/compute-plane-services/nvca/internal/miniservice/reconcile.go @@ -18,7 +18,6 @@ limitations under the License. package mscontroller import ( - "bytes" "context" "crypto/sha256" "encoding/base64" @@ -66,7 +65,6 @@ import ( nvcalogging "github.com/NVIDIA/nvcf/src/compute-plane-services/nvca/internal/logging" "github.com/NVIDIA/nvcf/src/compute-plane-services/nvca/internal/metrics" - "github.com/NVIDIA/nvcf/src/compute-plane-services/nvca/internal/miniservice/chartcache" nvcaotel "github.com/NVIDIA/nvcf/src/compute-plane-services/nvca/internal/otel" "github.com/NVIDIA/nvcf/src/compute-plane-services/nvca/internal/util/k8sutil" "github.com/NVIDIA/nvcf/src/compute-plane-services/nvca/pkg/apis/nvca/v1alpha1" @@ -93,8 +91,9 @@ type Reconciler struct { eventRecorder record.EventRecorder - // Chart cache. - chartCache chartcache.Cache + // In-memory rendered Helm Charts by MiniService name, backed by the rendered Secret + // in each instance namespace (see rendered_secret.go). + renderedCache sync.Map // Instance type cache regITCache registrationInstanceTypeCache // Attributes for enforcement @@ -531,10 +530,7 @@ func (r *Reconciler) prepareUpdateIfNeeded(ctx context.Context, ms *v1alpha1.Min return nil } - oldCacheKey := getCacheKey(ms) - if err := r.chartCache.Delete(oldCacheKey); err != nil { - log.V(1).Info("Failed to delete old chart cache entry (may not exist)", "error", err) - } + r.forgetRenderedData(ms) ms.Status.RenderDetails = nil ms.Status.Revision++ @@ -617,9 +613,7 @@ func (r *Reconciler) doInstall(ctx context.Context, return reconcile.Result{}, err } - if err := r.saveRenderedData(ctx, ms, objsData); err != nil { - return reconcile.Result{}, err - } + r.saveRenderedData(ctx, ms, objsData) } workloadObjs, resources, workloadConfig, err := decodeObjects(ctx, r.Decoder, objsData) @@ -665,6 +659,12 @@ func (r *Reconciler) doInstall(ctx context.Context, return reconcile.Result{}, err } + // Persist the rendered chart before applying any objects, as Helm stores the release record + // before installing, so later reconciles never depend on ReVal for this render. + if err := r.persistRenderedData(ctx, ms, objsData); err != nil { + return reconcile.Result{}, err + } + transportTLSObjs := append([]client.Object{utilsPod}, workloadObjs...) if err := r.prepareTransportTLSForWorkloads(ctx, ms, transportTLSObjs); err != nil { return reconcile.Result{}, err @@ -972,9 +972,13 @@ func (r *Reconciler) prepareUpdateWorkload(ctx context.Context, }) r.failedWorkloadUpdateRevisionCacheLock.Unlock() - if err := r.saveRenderedData(ctx, ms, objsData); err != nil { - return nil, nil, nil, "", "", err - } + r.saveRenderedData(ctx, ms, objsData) + } + + // Persist before applying, like Helm records the release before install. This is a no-op once the + // rendered Secret is in sync, and retries a previously failed Secret write on later reconciles. + if err := r.persistRenderedData(ctx, ms, objsData); err != nil { + return nil, nil, nil, "", "", err } workloadObjs, resources, workloadConfig, err := decodeObjects(ctx, r.Decoder, objsData) @@ -1455,10 +1459,8 @@ func (r *Reconciler) doCleanup(ctx context.Context, //nolint:gocyclo log.Info("Miniservice namespace terminated, waiting for deletion", "namespace", ms.Spec.Namespace) } - // Clean up function from cache after namespace termination to prevent resource leakage. - if err := r.chartCache.Delete(getCacheKey(ms)); err != nil { - return reconcile.Result{}, err - } + // The rendered Secret is deleted with the namespace; drop the in-memory copy. + r.forgetRenderedData(ms) stList := &nvcav2beta1.StorageRequestList{} if err := r.Client.List(ctx, stList, client.InNamespace(ms.Spec.Namespace)); err != nil { @@ -1539,20 +1541,6 @@ func getTaskPodsToDelete(ms *v1alpha1.MiniService, podList *corev1.PodList) (tas return taskPodsToDelete, false } -func getCacheKey(ms *v1alpha1.MiniService) chartcache.ChartCacheInput { - return chartcache.ChartCacheInput{ - HelmChartURL: ms.Spec.HelmChartConfig.URL, - HelmChartServicePort: ms.Spec.HelmChartConfig.ServicePort, - HelmChartServiceName: ms.Spec.HelmChartConfig.ServiceName, - Values: ms.Spec.HelmChartConfig.Values, - APIVersions: nil, // Not used right now. - // Namespace must be included in cache key because Helm templates using - // .Release.Namespace render namespace-specific values (e.g., service URLs). - // Without this, cached output from namespace A is incorrectly returned for B. - Namespace: ms.Spec.Namespace, - } -} - func (r *Reconciler) applyInfra(ctx context.Context, ms *v1alpha1.MiniService, objectMutators objectMutatorSet, @@ -1952,43 +1940,6 @@ func (r *Reconciler) getClusterAPIVersions(_ context.Context) ([]string, error) return nil, nil } -func (r *Reconciler) saveRenderedData(ctx context.Context, - ms *v1alpha1.MiniService, - data []byte, -) error { - logf.FromContext(ctx).Info("Saving rendered Helm Chart data") - - h, err := r.chartCache.Put(getCacheKey(ms), bytes.NewReader(data), int64(len(data))) - if err != nil { - return fmt.Errorf("put cache item: %v", err) - } - - ms.Status.RenderDetails = &v1alpha1.RenderDetailsStatus{ - Hash: h, - } - return nil -} - -func (r *Reconciler) getRenderedData(ctx context.Context, - ms *v1alpha1.MiniService, -) ([]byte, bool, error) { - log := logf.FromContext(ctx) - - rd := ms.Status.RenderDetails - if rd == nil { - log.V(1).Info("Rendered data not found") - return nil, false, nil - } - - // TODO: reuse buffer for efficiency. - buf := &bytes.Buffer{} - found, err := r.chartCache.Get(getCacheKey(ms), buf) - if err == nil && found { - return buf.Bytes(), true, nil - } - return nil, found, err -} - func getFunctionNameAndTaskName( fnLaunchSpec *function.LaunchSpecification, taskLaunchSpec *task.LaunchSpecification, diff --git a/src/compute-plane-services/nvca/internal/miniservice/reconcile_test.go b/src/compute-plane-services/nvca/internal/miniservice/reconcile_test.go index a58274f0b4..3a6edb10b1 100644 --- a/src/compute-plane-services/nvca/internal/miniservice/reconcile_test.go +++ b/src/compute-plane-services/nvca/internal/miniservice/reconcile_test.go @@ -27,6 +27,7 @@ import ( "maps" "net/http" "net/http/httptest" + "slices" "sort" "strconv" "strings" @@ -71,7 +72,6 @@ import ( "github.com/NVIDIA/nvcf/src/compute-plane-services/nvca/internal/icms" "github.com/NVIDIA/nvcf/src/compute-plane-services/nvca/internal/metrics" - "github.com/NVIDIA/nvcf/src/compute-plane-services/nvca/internal/miniservice/chartcache" "github.com/NVIDIA/nvcf/src/compute-plane-services/nvca/internal/otel" "github.com/NVIDIA/nvcf/src/compute-plane-services/nvca/internal/util/k8sutil" k8smock "github.com/NVIDIA/nvcf/src/compute-plane-services/nvca/internal/util/k8sutil/mock" @@ -279,7 +279,6 @@ func TestReconcile_Function(t *testing.T) { NFClient: nfClient, tracer: otel.NewTracer(), eventRecorder: record.NewFakeRecorder(256), - chartCache: chartcache.New(t.TempDir()), regITCache: regITCache, now: time.Now, newPermissionsChecker: newFakePermissionsChecker, @@ -337,9 +336,6 @@ func TestReconcile_Function(t *testing.T) { require.NoError(t, err) r.cfg.Workload.Tolerations = []corev1.Toleration{configuredToleration} - err = r.chartCache.Start(ctx) - require.NoError(t, err) - helmObjs := []client.Object{ &appsv1.Deployment{ TypeMeta: metav1.TypeMeta{ @@ -897,6 +893,10 @@ rules: gotSecrets := &corev1.SecretList{} err = r.Client.List(ctx, gotSecrets, client.InNamespace(ms.Spec.Namespace)) require.NoError(t, err) + // The rendered chart Secret is asserted separately. + gotSecrets.Items = slices.DeleteFunc(gotSecrets.Items, func(s corev1.Secret) bool { + return s.Name == RenderedSecretName + }) require.Len(t, gotSecrets.Items, 4) sort.Slice(gotSecrets.Items, func(i, j int) bool { return gotSecrets.Items[i].Name < gotSecrets.Items[j].Name @@ -1337,7 +1337,6 @@ func TestReconcile_Function_SaveRevisionHistoryFails(t *testing.T) { NFClient: nfClient, tracer: otel.NewTracer(), eventRecorder: record.NewFakeRecorder(256), - chartCache: chartcache.New(t.TempDir()), regITCache: regITCache, now: time.Now, newPermissionsChecker: newFakePermissionsChecker, @@ -1350,8 +1349,6 @@ func TestReconcile_Function_SaveRevisionHistoryFails(t *testing.T) { r.cfg.Agent.SharedStorage.Server.Image = "smb:latest" require.NoError(t, k8sutil.SetConfigDefaultResources(&r.cfg)) - require.NoError(t, r.chartCache.Start(ctx)) - helmObjs := []client.Object{ &appsv1.Deployment{ TypeMeta: metav1.TypeMeta{APIVersion: "apps/v1", Kind: "Deployment"}, @@ -1848,7 +1845,6 @@ func testReconcileNVLinkOptimizedHelper(t *testing.T, helmObjs []client.Object, NFClient: nfClient, tracer: otel.NewTracer(), eventRecorder: record.NewFakeRecorder(256), - chartCache: chartcache.New(t.TempDir()), regITCache: regITCache, now: time.Now, newPermissionsChecker: newFakePermissionsChecker, @@ -1864,9 +1860,6 @@ func testReconcileNVLinkOptimizedHelper(t *testing.T, helmObjs []client.Object, err := k8sutil.SetConfigDefaultResources(&r.cfg) require.NoError(t, err) - err = r.chartCache.Start(ctx) - require.NoError(t, err) - objBytes, err := json.MarshalIndent(helmObjs, "", " ") require.NoError(t, err) wantRevalErr := new(error) @@ -2223,7 +2216,6 @@ func TestReconcile_Task(t *testing.T) { NFClient: nfClient, tracer: otel.NewTracer(), eventRecorder: record.NewFakeRecorder(256), - chartCache: chartcache.New(t.TempDir()), regITCache: regITCache, now: time.Now, newPermissionsChecker: newFakePermissionsChecker, @@ -2275,9 +2267,6 @@ func TestReconcile_Task(t *testing.T) { err := k8sutil.SetConfigDefaultResources(&r.cfg) require.NoError(t, err) - err = r.chartCache.Start(ctx) - require.NoError(t, err) - helmObjs := []client.Object{ &batchv1.Job{ TypeMeta: metav1.TypeMeta{ @@ -2749,6 +2738,10 @@ rules: gotSecrets := &corev1.SecretList{} err = r.Client.List(ctx, gotSecrets, client.InNamespace(ms.Spec.Namespace)) require.NoError(t, err) + // The rendered chart Secret is asserted separately. + gotSecrets.Items = slices.DeleteFunc(gotSecrets.Items, func(s corev1.Secret) bool { + return s.Name == RenderedSecretName + }) require.Len(t, gotSecrets.Items, 4) sort.Slice(gotSecrets.Items, func(i, j int) bool { return gotSecrets.Items[i].Name < gotSecrets.Items[j].Name @@ -3021,7 +3014,6 @@ func TestReconcile_TaskStatus(t *testing.T) { NFClient: nfClient, tracer: otel.NewTracer(), eventRecorder: fakeRecorder, - chartCache: chartcache.New(t.TempDir()), regITCache: regITCache, now: time.Now, newPermissionsChecker: newFakePermissionsChecker, @@ -3037,9 +3029,6 @@ func TestReconcile_TaskStatus(t *testing.T) { err := k8sutil.SetConfigDefaultResources(&r.cfg) require.NoError(t, err) - err = r.chartCache.Start(ctx) - require.NoError(t, err) - tgps := int64((2 * time.Hour).Seconds()) helmObjs := []client.Object{ &batchv1.Job{ @@ -4064,11 +4053,9 @@ func TestDoUpdateWorkload(t *testing.T) { }, Client: c, Decoder: serializer.NewCodecFactory(testScheme).UniversalDeserializer(), - chartCache: chartcache.New(t.TempDir()), newPermissionsChecker: newFakePermissionsChecker, now: time.Now, } - require.NoError(t, r.chartCache.Start(ctx)) return r } @@ -4078,7 +4065,7 @@ func TestDoUpdateWorkload(t *testing.T) { r := newReconciler(t, c) ms := newMiniService() icmsReq := newICMSRequest() - require.NoError(t, r.saveRenderedData(ctx, ms, newRenderedObjectsData(t))) + r.saveRenderedData(ctx, ms, newRenderedObjectsData(t)) return r, ms, icmsReq } @@ -4938,89 +4925,6 @@ func TestEnsureInstanceNamespace_TerminatingRequeues(t *testing.T) { // namespace, so two MiniServices with different namespaces produce different // cache keys. This prevents rendered output from namespace A being incorrectly // returned for namespace B when deployments happen close together. -func TestGetCacheKey_NamespaceIncluded(t *testing.T) { - var servicePort int32 = 8080 - - // Create two MiniServices with DIFFERENT namespaces but SAME chart configuration - msNamespaceA := &v1alpha1.MiniService{ - ObjectMeta: metav1.ObjectMeta{ - Name: "miniservice-a", - }, - Spec: v1alpha1.MiniServiceSpec{ - Namespace: "sr-9dee7e5a-843d-44ae-bcec-1b4b23dea297", // Namespace A - ICMSRequestName: "request-a", - HelmChartConfig: common.HelmConfig{ - URL: "oci://helm.ngc.nvidia.com/org/team/srtx-benchmarks:0.0.8", - ServicePort: &servicePort, - ServiceName: "nvcf-service", - Values: []byte(`{"global":{"replicaCount":3}}`), - }, - }, - } - - msNamespaceB := &v1alpha1.MiniService{ - ObjectMeta: metav1.ObjectMeta{ - Name: "miniservice-b", - }, - Spec: v1alpha1.MiniServiceSpec{ - Namespace: "sr-bb09f1cb-3277-4ce1-b9a1-a904a88bc900", // Namespace B (DIFFERENT!) - ICMSRequestName: "request-b", - HelmChartConfig: common.HelmConfig{ - URL: "oci://helm.ngc.nvidia.com/org/team/srtx-benchmarks:0.0.8", // Same chart - ServicePort: &servicePort, // Same port - ServiceName: "nvcf-service", // Same service - Values: []byte(`{"global":{"replicaCount":3}}`), // Same values - }, - }, - } - - // Get cache keys for both MiniServices - cacheKeyA := getCacheKey(msNamespaceA) - cacheKeyB := getCacheKey(msNamespaceB) - - t.Run("different namespaces produce different cache keys", func(t *testing.T) { - // Verify the namespaces are actually different (test setup validation) - require.NotEqual(t, msNamespaceA.Spec.Namespace, msNamespaceB.Spec.Namespace, - "Test setup error: namespaces should be different") - - // FIXED: Different namespaces now produce different cache keys - assert.NotEqual(t, cacheKeyA, cacheKeyB, - "MiniServices with different namespaces (%s vs %s) should produce different cache keys. "+ - "This ensures rendered output with .Release.Namespace from namespace A is not "+ - "incorrectly returned for namespace B.", - msNamespaceA.Spec.Namespace, msNamespaceB.Spec.Namespace) - - // Verify namespace is included in the cache input - assert.Equal(t, msNamespaceA.Spec.Namespace, cacheKeyA.Namespace, - "Cache key should include the namespace") - assert.Equal(t, msNamespaceB.Spec.Namespace, cacheKeyB.Namespace, - "Cache key should include the namespace") - }) - - t.Run("same namespace and config produces same cache key", func(t *testing.T) { - // Two MiniServices with the same namespace and config should share cache - msSameNamespace := &v1alpha1.MiniService{ - ObjectMeta: metav1.ObjectMeta{ - Name: "miniservice-c", - }, - Spec: v1alpha1.MiniServiceSpec{ - Namespace: msNamespaceA.Spec.Namespace, // Same namespace as A - ICMSRequestName: "request-c", - HelmChartConfig: common.HelmConfig{ - URL: "oci://helm.ngc.nvidia.com/org/team/srtx-benchmarks:0.0.8", - ServicePort: &servicePort, - ServiceName: "nvcf-service", - Values: []byte(`{"global":{"replicaCount":3}}`), - }, - }, - } - - cacheKeySameNS := getCacheKey(msSameNamespace) - assert.Equal(t, cacheKeyA, cacheKeySameNS, - "MiniServices with the same namespace and config should produce the same cache key") - }) -} - func TestUtilsPodGXCacheSkipAnnotation(t *testing.T) { tests := []struct { name string diff --git a/src/compute-plane-services/nvca/internal/miniservice/reconcile_update_test.go b/src/compute-plane-services/nvca/internal/miniservice/reconcile_update_test.go index 20d0440f03..a13d3b6920 100644 --- a/src/compute-plane-services/nvca/internal/miniservice/reconcile_update_test.go +++ b/src/compute-plane-services/nvca/internal/miniservice/reconcile_update_test.go @@ -42,7 +42,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" "github.com/NVIDIA/nvcf/src/compute-plane-services/nvca/internal/metrics" - "github.com/NVIDIA/nvcf/src/compute-plane-services/nvca/internal/miniservice/chartcache" "github.com/NVIDIA/nvcf/src/compute-plane-services/nvca/internal/otel" "github.com/NVIDIA/nvcf/src/compute-plane-services/nvca/internal/util/k8sutil" "github.com/NVIDIA/nvcf/src/compute-plane-services/nvca/pkg/apis/nvca/v1alpha1" @@ -208,11 +207,9 @@ func newUpdateTestReconciler(t *testing.T, c client.Client, scheme *runtime.Sche Decoder: serializer.NewCodecFactory(scheme).UniversalDeserializer(), eventRecorder: record.NewFakeRecorder(256), tracer: otel.NewTracer(), - chartCache: chartcache.New(t.TempDir()), newPermissionsChecker: newFakePermissionsChecker, now: time.Now, } - require.NoError(t, r.chartCache.Start(context.Background())) return r } @@ -259,7 +256,7 @@ func TestDoUpdateWorkload_UnwrapsTerminalApplyError(t *testing.T) { r := newUpdateTestReconciler(t, c, testScheme) ms := newUpdateMiniService(`{"key":"value-v2"}`) icmsReq := newUpdateICMSRequest(true) - require.NoError(t, r.saveRenderedData(ctx, ms, newUpdateRenderedData(t, workloadObjectName, "v2"))) + r.saveRenderedData(ctx, ms, newUpdateRenderedData(t, workloadObjectName, "v2")) gotRes, err := r.doUpdateWorkload(ctx, ms, icmsReq) require.Error(t, err) @@ -295,7 +292,7 @@ func TestDoUpdateWorkload_ApplyFailureRetainsRenderedCache(t *testing.T) { ms := newUpdateMiniService(`{"key":"value-v2"}`) icmsReq := newUpdateICMSRequest(true) renderedData := newUpdateRenderedData(t, workloadObjectName, "v2") - require.NoError(t, r.saveRenderedData(ctx, ms, renderedData)) + r.saveRenderedData(ctx, ms, renderedData) require.NotNil(t, ms.Status.RenderDetails) oldHash := ms.Status.RenderDetails.Hash @@ -345,7 +342,7 @@ func TestReconcile_UpdateFailureStaysInstalling(t *testing.T) { ms := &v1alpha1.MiniService{} require.NoError(t, r.Client.Get(ctx, client.ObjectKey{Name: updateMSName}, ms)) - require.NoError(t, r.saveRenderedData(ctx, ms, newUpdateRenderedData(t, workloadObjectName, "v2"))) + r.saveRenderedData(ctx, ms, newUpdateRenderedData(t, workloadObjectName, "v2")) require.NoError(t, r.Client.Status().Update(ctx, ms)) req := reconcile.Request{NamespacedName: client.ObjectKey{Name: updateMSName}} @@ -396,7 +393,7 @@ func TestReconcile_UpdateFailureThenNewUpdateSucceeds(t *testing.T) { ms := &v1alpha1.MiniService{} require.NoError(t, r.Client.Get(ctx, client.ObjectKey{Name: updateMSName}, ms)) oldRenderedData := newUpdateRenderedData(t, workloadObjectName, "v2") - require.NoError(t, r.saveRenderedData(ctx, ms, oldRenderedData)) + r.saveRenderedData(ctx, ms, oldRenderedData) require.NoError(t, r.Client.Status().Update(ctx, ms)) // First reconcile: update fails on object apply, but should remain in update state. @@ -480,7 +477,7 @@ func TestReconcile_FailedUpdateRetryReusesCache(t *testing.T) { ms := &v1alpha1.MiniService{} require.NoError(t, r.Client.Get(ctx, client.ObjectKey{Name: updateMSName}, ms)) - require.NoError(t, r.saveRenderedData(ctx, ms, newUpdateRenderedData(t, workloadObjectName, "v2"))) + r.saveRenderedData(ctx, ms, newUpdateRenderedData(t, workloadObjectName, "v2")) require.NoError(t, r.Client.Status().Update(ctx, ms)) req := reconcile.Request{NamespacedName: client.ObjectKey{Name: updateMSName}} @@ -567,7 +564,7 @@ func setupUpdateReconcileForWorkloadConfig( existing := &v1alpha1.MiniService{} require.NoError(t, r.Client.Get(ctx, client.ObjectKey{Name: updateMSName}, existing)) - require.NoError(t, r.saveRenderedData(ctx, existing, renderedData)) + r.saveRenderedData(ctx, existing, renderedData) require.NoError(t, r.Client.Status().Update(ctx, existing)) return r, reconcile.Request{NamespacedName: client.ObjectKey{Name: updateMSName}} @@ -605,7 +602,7 @@ func setupDoUpdateWorkloadForWorkloadConfig( // render cache and persist the updated status so getRenderedData can find the hash. stored := &v1alpha1.MiniService{} require.NoError(t, r.Client.Get(ctx, client.ObjectKey{Name: updateMSName}, stored)) - require.NoError(t, r.saveRenderedData(ctx, stored, renderedData)) + r.saveRenderedData(ctx, stored, renderedData) require.NoError(t, r.Client.Status().Update(ctx, stored)) // Return stored so the caller has an in-sync MS to pass to doUpdateWorkload. @@ -725,7 +722,7 @@ func TestDoUpdateWorkload_FailedApply_RetainsPriorWorkloadConfig(t *testing.T) { ms := newUpdateMiniService(`{"key":"value-v2"}`) ms.Spec.WorkloadConfig = prior icmsReq := newUpdateICMSRequest(true) - require.NoError(t, r.saveRenderedData(ctx, ms, renderedData)) + r.saveRenderedData(ctx, ms, renderedData) _, err := r.doUpdateWorkload(ctx, ms, icmsReq) require.Error(t, err) diff --git a/src/compute-plane-services/nvca/internal/miniservice/rendered_secret.go b/src/compute-plane-services/nvca/internal/miniservice/rendered_secret.go new file mode 100644 index 0000000000..f9bcbde3fa --- /dev/null +++ b/src/compute-plane-services/nvca/internal/miniservice/rendered_secret.go @@ -0,0 +1,387 @@ +/* +SPDX-FileCopyrightText: Copyright (c) NVIDIA CORPORATION & AFFILIATES. All rights reserved. +SPDX-License-Identifier: Apache-2.0 + +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. +*/ + +package mscontroller + +import ( + "bytes" + "compress/gzip" + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "strconv" + "time" + + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + logf "sigs.k8s.io/controller-runtime/pkg/log" + + "github.com/NVIDIA/nvcf/src/compute-plane-services/nvca/pkg/apis/nvca/v1alpha1" +) + +// The rendered Helm Chart of a MiniService is persisted in a Secret in the instance namespace, +// similar to how Helm stores a release record (sh.helm.release.v1..v) in the release +// namespace. The Secret is the durable copy of the ReVal render output for the lifetime of the +// instance, so status checks and cleanup never need to call ReVal again after a successful render. +// +// The Secret always holds the latest successful render and is overwritten on Helm values updates. +// It is written before workload objects are applied, so while an update is failing to apply, its +// revision label can be ahead of the latest revision ConfigMap (see revision.go), which remains the +// history of applied values, chart URL, and render hash per revision. +// +//nolint:gosec // These are Secret object names, keys, and annotation keys, not credentials (G101). +const ( + // RenderedSecretName is the name of the Secret holding the rendered Helm Chart in the instance namespace. + RenderedSecretName = "nvcf-miniservice-rendered" + // renderedSecretType versions the Secret format. Bump the suffix on incompatible changes, as Helm does. + renderedSecretType = corev1.SecretType("nvca.nvcf.nvidia.io/rendered-chart.v1") + // renderedSecretDataKey holds the gzipped ReVal render output. + renderedSecretDataKey = "rendered.json.gz" + + // renderedSecretOutputHashAnnotation is the sha256 of the uncompressed render output, + // matching MiniService status.renderedDetails.hash. + renderedSecretOutputHashAnnotation = "nvca.nvcf.nvidia.io/render-hash" + // renderedSecretInputHashAnnotation is the sha256 of the render inputs that affect template output. + // A stored render is only reused when the inputs of the current spec hash to the same value. + renderedSecretInputHashAnnotation = "nvca.nvcf.nvidia.io/render-input-hash" + renderedSecretChartURLAnnotation = "nvca.nvcf.nvidia.io/chart-url" + renderedSecretTimestampAnnotation = "nvca.nvcf.nvidia.io/rendered-at" + + // renderedSecretMaxCompressedBytes leaves headroom under the 1 MiB etcd object size limit. + // Larger renders are not persisted and fall back to re-rendering on demand. + renderedSecretMaxCompressedBytes = 900 << 10 +) + +// renderedEntry is the in-memory copy of a MiniService's rendered chart. Entries are immutable +// and replaced wholesale, so they can be shared without locking. +type renderedEntry struct { + inputHash string + outputHash string + data []byte + // synced is true once the entry has been reconciled with the rendered Secret + // (stored, found already stored, or skipped because it is too large). + synced bool +} + +// renderInput mirrors the fields of HelmReValRenderInput that affect Helm template output. +// Namespace is included because templates using .Release.Namespace render namespace-specific values. +type renderInput struct { + HelmChartURL string `json:"helmChartURL"` + HelmChartServicePort *int32 `json:"helmChartServicePort,omitempty"` + HelmChartServiceName string `json:"helmChartServiceName,omitempty"` + Values json.RawMessage `json:"values,omitempty"` + Namespace string `json:"namespace"` +} + +// renderInputHash returns a hash identifying the render inputs of ms. +func renderInputHash(ms *v1alpha1.MiniService) string { + in := renderInput{ + HelmChartURL: ms.Spec.HelmChartConfig.URL, + HelmChartServicePort: ms.Spec.HelmChartConfig.ServicePort, + HelmChartServiceName: ms.Spec.HelmChartConfig.ServiceName, + Values: ms.Spec.HelmChartConfig.Values, + Namespace: ms.Spec.Namespace, + } + if len(in.Values) == 0 { + in.Values = nil + } + // Marshal cannot fail for this struct unless Values is invalid JSON, in which case + // ReVal rejects the render anyway; fall back to hashing the raw fields. + b, err := json.Marshal(in) + if err != nil { + b = []byte(in.HelmChartURL + "|" + in.HelmChartServiceName + "|" + in.Namespace + "|" + string(in.Values)) + } + sum := sha256.Sum256(b) + return "sha256:" + hex.EncodeToString(sum[:]) +} + +// renderOutputHash returns the hash of rendered data stored in MiniService status.renderedDetails.hash. +func renderOutputHash(data []byte) string { + sum := sha256.Sum256(data) + return "sha256:" + hex.EncodeToString(sum[:]) +} + +// saveRenderedData records freshly rendered data in the MiniService status and in memory. +// It does not persist the Secret because the instance namespace may not exist yet during install; +// callers must call persistRenderedData once the namespace exists. +func (r *Reconciler) saveRenderedData(ctx context.Context, ms *v1alpha1.MiniService, data []byte) { + logf.FromContext(ctx).Info("Saving rendered Helm Chart data") + + outputHash := renderOutputHash(data) + ms.Status.RenderDetails = &v1alpha1.RenderDetailsStatus{ + Hash: outputHash, + } + r.renderedCache.Store(ms.Name, &renderedEntry{ + inputHash: renderInputHash(ms), + outputHash: outputHash, + data: data, + }) +} + +// getRenderedData returns the rendered chart for ms from memory or from the rendered Secret. +// It returns false when no render matching the current spec is available, in which case +// callers render via ReVal. +// +// When status has no render details (first install, or a values update whose status patch was +// lost before it landed), the Secret is still consulted: a render stored for identical inputs is +// reused and its hash restored to status instead of calling ReVal again. +func (r *Reconciler) getRenderedData(ctx context.Context, ms *v1alpha1.MiniService) ([]byte, bool, error) { + log := logf.FromContext(ctx) + + var expectedOutputHash string + if rd := ms.Status.RenderDetails; rd != nil { + expectedOutputHash = rd.Hash + } else { + log.V(1).Info("Rendered data not found in status") + } + + inputHash := renderInputHash(ms) + if entry, ok := r.loadRenderedEntry(ms); ok { + if entry.inputHash == inputHash && (expectedOutputHash == "" || entry.outputHash == expectedOutputHash) { + return entry.data, true, nil + } + log.V(1).Info("Discarding in-memory rendered data for different inputs or hash") + r.renderedCache.Delete(ms.Name) + } + + data, found, err := r.loadRenderedSecret(ctx, ms, inputHash, expectedOutputHash) + if err != nil || !found { + return nil, false, err + } + outputHash := renderOutputHash(data) + log.V(1).Info("Loaded rendered Helm Chart data from Secret") + if ms.Status.RenderDetails == nil { + ms.Status.RenderDetails = &v1alpha1.RenderDetailsStatus{Hash: outputHash} + } + r.renderedCache.Store(ms.Name, &renderedEntry{ + inputHash: inputHash, + outputHash: outputHash, + data: data, + synced: true, + }) + return data, true, nil +} + +func (r *Reconciler) loadRenderedEntry(ms *v1alpha1.MiniService) (*renderedEntry, bool) { + v, ok := r.renderedCache.Load(ms.Name) + if !ok { + return nil, false + } + entry, ok := v.(*renderedEntry) + return entry, ok +} + +// forgetRenderedData drops the in-memory rendered data for ms. The rendered Secret is owned by +// the MiniService and lives in the instance namespace, so it is garbage collected with either. +func (r *Reconciler) forgetRenderedData(ms *v1alpha1.MiniService) { + r.renderedCache.Delete(ms.Name) +} + +// persistRenderedData ensures the rendered Secret in the instance namespace holds data. +// It is idempotent and cheap once the in-memory entry is marked synced. The instance namespace +// must exist. Like Helm, callers persist the record before applying workload objects. +func (r *Reconciler) persistRenderedData(ctx context.Context, ms *v1alpha1.MiniService, data []byte) error { + inputHash := renderInputHash(ms) + outputHash := renderOutputHash(data) + + if entry, ok := r.loadRenderedEntry(ms); ok && + entry.synced && entry.inputHash == inputHash && entry.outputHash == outputHash { + return nil + } + + if err := r.saveRenderedSecret(ctx, ms, data, inputHash, outputHash); err != nil { + return err + } + + r.renderedCache.Store(ms.Name, &renderedEntry{ + inputHash: inputHash, + outputHash: outputHash, + data: data, + synced: true, + }) + return nil +} + +func renderedSecretKey(ms *v1alpha1.MiniService) client.ObjectKey { + return client.ObjectKey{Namespace: ms.Spec.Namespace, Name: RenderedSecretName} +} + +// saveRenderedSecret creates or updates the rendered Secret for ms. +func (r *Reconciler) saveRenderedSecret(ctx context.Context, + ms *v1alpha1.MiniService, + data []byte, + inputHash, outputHash string, +) error { + log := logf.FromContext(ctx).WithValues("secret", RenderedSecretName, "namespace", ms.Spec.Namespace) + + existing := &corev1.Secret{} + err := r.Client.Get(ctx, renderedSecretKey(ms), existing) + switch { + case err == nil: + if existing.Type == renderedSecretType && + existing.Annotations[renderedSecretInputHashAnnotation] == inputHash && + existing.Annotations[renderedSecretOutputHashAnnotation] == outputHash { + log.V(1).Info("Rendered Secret is up to date") + return nil + } + if existing.Type != renderedSecretType { + // Secret types are immutable, so an older format can only be replaced. + log.Info("Replacing rendered Secret with a different type", "type", existing.Type) + if err := r.Client.Delete(ctx, existing); err != nil && !apierrors.IsNotFound(err) { + return fmt.Errorf("delete rendered secret of type %q: %w", existing.Type, err) + } + existing = nil + } + case apierrors.IsNotFound(err): + existing = nil + default: + return fmt.Errorf("get rendered secret: %w", err) + } + + compressed, err := gzipBytes(data) + if err != nil { + return fmt.Errorf("compress rendered data: %w", err) + } + if len(compressed) > renderedSecretMaxCompressedBytes { + // Nothing more can be done within the etcd object size limit; status checks will + // fall back to re-rendering on demand for this instance. + log.Info("Rendered Helm Chart is too large to persist in a Secret, skipping", + "compressedBytes", len(compressed), "maxBytes", renderedSecretMaxCompressedBytes) + return nil + } + + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: RenderedSecretName, + Namespace: ms.Spec.Namespace, + Labels: map[string]string{ + managedByLabel: managedByValue, + miniserviceNameLabel: ms.Name, + revisionLabel: strconv.FormatInt(ms.Status.Revision, 10), + }, + Annotations: map[string]string{ + renderedSecretInputHashAnnotation: inputHash, + renderedSecretOutputHashAnnotation: outputHash, + renderedSecretChartURLAnnotation: ms.Spec.HelmChartConfig.URL, + renderedSecretTimestampAnnotation: r.now().UTC().Format(time.RFC3339Nano), + }, + OwnerReferences: []metav1.OwnerReference{{ + APIVersion: v1alpha1.SchemeGroupVersion.String(), + Kind: miniServiceKind, + Name: ms.Name, + UID: ms.UID, + }}, + }, + Type: renderedSecretType, + Data: map[string][]byte{renderedSecretDataKey: compressed}, + } + + if existing == nil { + log.Info("Creating rendered Secret", "revision", ms.Status.Revision) + err = r.Client.Create(ctx, secret) + if apierrors.IsAlreadyExists(err) { + // Created concurrently or not yet visible in the informer cache; fall through to update. + if err = r.Client.Get(ctx, renderedSecretKey(ms), existing); err != nil { + return fmt.Errorf("get rendered secret after create conflict: %w", err) + } + } else if err != nil { + return fmt.Errorf("create rendered secret: %w", err) + } else { + return nil + } + } + + log.Info("Updating rendered Secret", "revision", ms.Status.Revision) + secret.ResourceVersion = existing.ResourceVersion + if err := r.Client.Update(ctx, secret); err != nil { + return fmt.Errorf("update rendered secret: %w", err) + } + return nil +} + +// loadRenderedSecret returns the rendered data stored for ms if it matches the given hashes. +// Like Flux's artifact verification, the content digest is verified before it is trusted. +func (r *Reconciler) loadRenderedSecret(ctx context.Context, + ms *v1alpha1.MiniService, + inputHash, outputHash string, +) ([]byte, bool, error) { + log := logf.FromContext(ctx).WithValues("secret", RenderedSecretName, "namespace", ms.Spec.Namespace) + + secret := &corev1.Secret{} + if err := r.Client.Get(ctx, renderedSecretKey(ms), secret); err != nil { + if apierrors.IsNotFound(err) { + log.V(1).Info("Rendered Secret not found") + return nil, false, nil + } + return nil, false, fmt.Errorf("get rendered secret: %w", err) + } + + if got := secret.Annotations[renderedSecretInputHashAnnotation]; got != inputHash { + log.V(1).Info("Rendered Secret was rendered from different inputs, ignoring", "storedInputHash", got) + return nil, false, nil + } + storedOutputHash := secret.Annotations[renderedSecretOutputHashAnnotation] + if outputHash != "" && storedOutputHash != outputHash { + log.V(1).Info("Rendered Secret hash does not match MiniService status, ignoring", + "storedHash", storedOutputHash, "statusHash", outputHash) + return nil, false, nil + } + + data, err := gunzipBytes(secret.Data[renderedSecretDataKey]) + if err != nil { + log.Error(err, "Failed to decompress rendered Secret, ignoring") + return nil, false, nil + } + if renderOutputHash(data) != storedOutputHash { + log.Error(nil, "Rendered Secret content does not match its hash, ignoring") + return nil, false, nil + } + return data, true, nil +} + +func gzipBytes(data []byte) ([]byte, error) { + buf := &bytes.Buffer{} + // Best compression, as Helm uses for release records, to stay within the object size limit. + w, err := gzip.NewWriterLevel(buf, gzip.BestCompression) + if err != nil { + return nil, err + } + if _, err := w.Write(data); err != nil { + return nil, err + } + if err := w.Close(); err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +func gunzipBytes(data []byte) ([]byte, error) { + if len(data) == 0 { + return nil, fmt.Errorf("no data") + } + gzr, err := gzip.NewReader(bytes.NewReader(data)) + if err != nil { + return nil, err + } + defer gzr.Close() + return io.ReadAll(gzr) +} diff --git a/src/compute-plane-services/nvca/internal/miniservice/rendered_secret_test.go b/src/compute-plane-services/nvca/internal/miniservice/rendered_secret_test.go new file mode 100644 index 0000000000..302b6a24d8 --- /dev/null +++ b/src/compute-plane-services/nvca/internal/miniservice/rendered_secret_test.go @@ -0,0 +1,440 @@ +/* +SPDX-FileCopyrightText: Copyright (c) NVIDIA CORPORATION & AFFILIATES. All rights reserved. +SPDX-License-Identifier: Apache-2.0 + +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. +*/ + +package mscontroller + +import ( + "context" + "crypto/rand" + "errors" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/interceptor" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/NVIDIA/nvcf/src/compute-plane-services/nvca/pkg/apis/nvca/v1alpha1" + "github.com/NVIDIA/nvcf/src/compute-plane-services/nvca/pkg/featureflag" +) + +// outputReValClient returns a fixed ReVal output and counts calls. +type outputReValClient struct { + calls int + output HelmReValRenderOutput + err error +} + +func (c *outputReValClient) Render(_ context.Context, _ HelmReValRenderInput) (HelmReValRenderOutput, error) { + c.calls++ + return c.output, c.err +} + +func getRenderedSecret(t *testing.T, c client.Client, ns string) *corev1.Secret { + t.Helper() + secret := &corev1.Secret{} + err := c.Get(context.Background(), client.ObjectKey{Namespace: ns, Name: RenderedSecretName}, secret) + require.NoError(t, err) + return secret +} + +func TestRenderedSecret_PersistAndLoadAcrossReconcilers(t *testing.T) { + ctx := newTestContext() + c, _ := newFakeClient(mgrScheme, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: updateTestNamespace}}) + + r := newUpdateTestReconciler(t, c, mgrScheme) + ms := newUpdateMiniService(`{"key":"value"}`) + ms.Status.Revision = 0 + rendered := newUpdateRenderedData(t, "workload-cm", "v1") + + r.saveRenderedData(ctx, ms, rendered) + require.NotNil(t, ms.Status.RenderDetails) + require.NoError(t, r.persistRenderedData(ctx, ms, rendered)) + + secret := getRenderedSecret(t, c, updateTestNamespace) + assert.Equal(t, renderedSecretType, secret.Type) + assert.Equal(t, managedByValue, secret.Labels[managedByLabel]) + assert.Equal(t, ms.Name, secret.Labels[miniserviceNameLabel]) + assert.Equal(t, "0", secret.Labels[revisionLabel]) + assert.Equal(t, ms.Status.RenderDetails.Hash, secret.Annotations[renderedSecretOutputHashAnnotation]) + assert.Equal(t, renderInputHash(ms), secret.Annotations[renderedSecretInputHashAnnotation]) + assert.Equal(t, ms.Spec.HelmChartConfig.URL, secret.Annotations[renderedSecretChartURLAnnotation]) + require.Len(t, secret.OwnerReferences, 1) + assert.Equal(t, miniServiceKind, secret.OwnerReferences[0].Kind) + assert.Equal(t, ms.UID, secret.OwnerReferences[0].UID) + data, err := gunzipBytes(secret.Data[renderedSecretDataKey]) + require.NoError(t, err) + assert.JSONEq(t, string(rendered), string(data)) + + // Persisting again is a no-op that does not error. + rv := secret.ResourceVersion + require.NoError(t, r.persistRenderedData(ctx, ms, rendered)) + assert.Equal(t, rv, getRenderedSecret(t, c, updateTestNamespace).ResourceVersion) + + // A fresh reconciler (simulating an agent restart) loads the render from the Secret. + r2 := newUpdateTestReconciler(t, c, mgrScheme) + got, found, err := r2.getRenderedData(ctx, ms) + require.NoError(t, err) + require.True(t, found) + assert.Equal(t, rendered, got) + // And the second read is served from memory. + entry, ok := r2.loadRenderedEntry(ms) + require.True(t, ok) + assert.True(t, entry.synced) +} + +func TestRenderedSecret_IgnoredWhenInputsOrHashDiffer(t *testing.T) { + ctx := newTestContext() + rendered := newUpdateRenderedData(t, "workload-cm", "v1") + + persist := func(t *testing.T) (client.Client, *v1alpha1.MiniService) { + t.Helper() + c, _ := newFakeClient(mgrScheme, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: updateTestNamespace}}) + r := newUpdateTestReconciler(t, c, mgrScheme) + ms := newUpdateMiniService(`{"key":"value"}`) + r.saveRenderedData(ctx, ms, rendered) + require.NoError(t, r.persistRenderedData(ctx, ms, rendered)) + return c, ms + } + + t.Run("helm values changed", func(t *testing.T) { + c, ms := persist(t) + ms.Spec.HelmChartConfig.Values = []byte(`{"key":"changed"}`) + r := newUpdateTestReconciler(t, c, mgrScheme) + _, found, err := r.getRenderedData(ctx, ms) + require.NoError(t, err) + assert.False(t, found) + }) + + t.Run("namespace changed", func(t *testing.T) { + c, ms := persist(t) + require.NoError(t, c.Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "other-ns"}})) + secret := getRenderedSecret(t, c, updateTestNamespace) + secret.ResourceVersion = "" + secret.Namespace = "other-ns" + require.NoError(t, c.Create(ctx, secret)) + ms.Spec.Namespace = "other-ns" + r := newUpdateTestReconciler(t, c, mgrScheme) + _, found, err := r.getRenderedData(ctx, ms) + require.NoError(t, err) + assert.False(t, found, "a render for another namespace must not be reused") + }) + + t.Run("status hash differs", func(t *testing.T) { + c, ms := persist(t) + ms.Status.RenderDetails.Hash = "sha256:0000" + r := newUpdateTestReconciler(t, c, mgrScheme) + _, found, err := r.getRenderedData(ctx, ms) + require.NoError(t, err) + assert.False(t, found) + }) + + t.Run("content corrupted", func(t *testing.T) { + c, ms := persist(t) + secret := getRenderedSecret(t, c, updateTestNamespace) + corrupted, err := gzipBytes([]byte(`[{"kind":"ConfigMap"}]`)) + require.NoError(t, err) + secret.Data[renderedSecretDataKey] = corrupted + require.NoError(t, c.Update(ctx, secret)) + r := newUpdateTestReconciler(t, c, mgrScheme) + _, found, err := r.getRenderedData(ctx, ms) + require.NoError(t, err) + assert.False(t, found, "content not matching its hash must not be trusted") + }) + + t.Run("in-memory entry for stale inputs is discarded", func(t *testing.T) { + c, _ := newFakeClient(mgrScheme, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: updateTestNamespace}}) + r := newUpdateTestReconciler(t, c, mgrScheme) + ms := newUpdateMiniService(`{"key":"value"}`) + r.saveRenderedData(ctx, ms, rendered) + ms.Spec.HelmChartConfig.Values = []byte(`{"key":"changed"}`) + _, found, err := r.getRenderedData(ctx, ms) + require.NoError(t, err) + assert.False(t, found) + _, ok := r.loadRenderedEntry(ms) + assert.False(t, ok) + }) + + t.Run("no render details reuses matching secret and restores hash", func(t *testing.T) { + c, ms := persist(t) + storedHash := ms.Status.RenderDetails.Hash + ms.Status.RenderDetails = nil + r := newUpdateTestReconciler(t, c, mgrScheme) + got, found, err := r.getRenderedData(ctx, ms) + require.NoError(t, err) + require.True(t, found) + assert.Equal(t, rendered, got) + require.NotNil(t, ms.Status.RenderDetails) + assert.Equal(t, storedHash, ms.Status.RenderDetails.Hash) + }) + + t.Run("no render details and no secret", func(t *testing.T) { + c, _ := newFakeClient(mgrScheme, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: updateTestNamespace}}) + ms := newUpdateMiniService(`{"key":"value"}`) + r := newUpdateTestReconciler(t, c, mgrScheme) + _, found, err := r.getRenderedData(ctx, ms) + require.NoError(t, err) + assert.False(t, found) + assert.Nil(t, ms.Status.RenderDetails) + }) +} + +func TestRenderedSecret_UpdateOverwritesPreviousRevision(t *testing.T) { + ctx := newTestContext() + c, _ := newFakeClient(mgrScheme, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: updateTestNamespace}}) + r := newUpdateTestReconciler(t, c, mgrScheme) + + ms := newUpdateMiniService(`{"key":"v1"}`) + ms.Status.Revision = 0 + v1Data := newUpdateRenderedData(t, "workload-cm", "v1") + r.saveRenderedData(ctx, ms, v1Data) + require.NoError(t, r.persistRenderedData(ctx, ms, v1Data)) + v1Hash := ms.Status.RenderDetails.Hash + + // Simulate prepareUpdateIfNeeded followed by a new render. + r.forgetRenderedData(ms) + ms.Status.RenderDetails = nil + ms.Status.Revision = 1 + ms.Spec.HelmChartConfig.Values = []byte(`{"key":"v2"}`) + _, found, err := r.getRenderedData(ctx, ms) + require.NoError(t, err) + require.False(t, found) + + v2Data := newUpdateRenderedData(t, "workload-cm", "v2") + r.saveRenderedData(ctx, ms, v2Data) + require.NoError(t, r.persistRenderedData(ctx, ms, v2Data)) + require.NotEqual(t, v1Hash, ms.Status.RenderDetails.Hash) + + secrets := &corev1.SecretList{} + require.NoError(t, c.List(ctx, secrets, client.InNamespace(updateTestNamespace))) + require.Len(t, secrets.Items, 1, "the rendered Secret is overwritten, not duplicated") + secret := secrets.Items[0] + assert.Equal(t, "1", secret.Labels[revisionLabel]) + assert.Equal(t, ms.Status.RenderDetails.Hash, secret.Annotations[renderedSecretOutputHashAnnotation]) + data, err := gunzipBytes(secret.Data[renderedSecretDataKey]) + require.NoError(t, err) + assert.JSONEq(t, string(v2Data), string(data)) +} + +func TestRenderedSecret_ReplacesSecretOfDifferentType(t *testing.T) { + ctx := newTestContext() + stale := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: RenderedSecretName, Namespace: updateTestNamespace}, + Type: corev1.SecretTypeOpaque, + Data: map[string][]byte{"old": []byte("format")}, + } + c, _ := newFakeClient(mgrScheme, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: updateTestNamespace}}, stale) + r := newUpdateTestReconciler(t, c, mgrScheme) + ms := newUpdateMiniService(`{"key":"value"}`) + rendered := newUpdateRenderedData(t, "workload-cm", "v1") + + r.saveRenderedData(ctx, ms, rendered) + require.NoError(t, r.persistRenderedData(ctx, ms, rendered)) + + secret := getRenderedSecret(t, c, updateTestNamespace) + assert.Equal(t, renderedSecretType, secret.Type) + assert.NotContains(t, secret.Data, "old") + data, err := gunzipBytes(secret.Data[renderedSecretDataKey]) + require.NoError(t, err) + assert.JSONEq(t, string(rendered), string(data)) +} + +func TestRenderedSecret_TooLargeIsSkipped(t *testing.T) { + ctx := newTestContext() + c, _ := newFakeClient(mgrScheme, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: updateTestNamespace}}) + r := newUpdateTestReconciler(t, c, mgrScheme) + ms := newUpdateMiniService(`{"key":"value"}`) + + // Random bytes do not compress, so this exceeds the Secret size guard. + large := make([]byte, renderedSecretMaxCompressedBytes+64<<10) + _, err := rand.Read(large) + require.NoError(t, err) + + r.saveRenderedData(ctx, ms, large) + require.NoError(t, r.persistRenderedData(ctx, ms, large), "oversize renders are skipped, not failed") + + secrets := &corev1.SecretList{} + require.NoError(t, c.List(ctx, secrets, client.InNamespace(updateTestNamespace))) + assert.Empty(t, secrets.Items) + + // Still served from memory in this process, but a fresh reconciler falls back to rendering. + got, found, err := r.getRenderedData(ctx, ms) + require.NoError(t, err) + require.True(t, found) + assert.Equal(t, large, got) + + r2 := newUpdateTestReconciler(t, c, mgrScheme) + _, found, err = r2.getRenderedData(ctx, ms) + require.NoError(t, err) + assert.False(t, found) +} + +func TestRenderedSecret_PersistErrorsAreReturned(t *testing.T) { + ctx := newTestContext() + c, _ := newFakeClientWithInterceptors(mgrScheme, interceptor.Funcs{ + Create: func(_ context.Context, _ client.WithWatch, obj client.Object, _ ...client.CreateOption) error { + return apierrors.NewInternalError(fmt.Errorf("injected create failure for %s", obj.GetName())) + }, + }, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: updateTestNamespace}}) + r := newUpdateTestReconciler(t, c, mgrScheme) + ms := newUpdateMiniService(`{"key":"value"}`) + rendered := newUpdateRenderedData(t, "workload-cm", "v1") + + r.saveRenderedData(ctx, ms, rendered) + err := r.persistRenderedData(ctx, ms, rendered) + require.Error(t, err) + entry, ok := r.loadRenderedEntry(ms) + require.True(t, ok) + assert.False(t, entry.synced, "a failed persist must be retried on the next reconcile") +} + +func TestDoStatus_UsesPersistedRenderInsteadOfReVal(t *testing.T) { + ctx := newTestContext() + c, _ := newFakeClient(mgrScheme, + &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: updateTestNamespace}}, + newReadyUtilsPod(), + ) + + ms := newUpdateMiniService(`{"key":"value"}`) + ms.Status.Phase = v1alpha1.MiniServiceRunning + icmsReq := newUpdateICMSRequest(true) + + // First process renders and persists. + r1 := newUpdateTestReconciler(t, c, mgrScheme) + r1.saveRenderedData(ctx, ms, []byte("[]")) + require.NoError(t, r1.persistRenderedData(ctx, ms, []byte("[]"))) + + for _, workerReadiness := range []bool{false, true} { + t.Run(map[bool]string{false: "aggressive status", true: "worker readiness status"}[workerReadiness], func(t *testing.T) { + ms := ms.DeepCopy() + if workerReadiness { + ms.Spec.WorkloadConfig = &v1alpha1.WorkloadConfig{ + FeatureFlags: map[string]bool{featureflag.StatusByWorkerReadiness: true}, + } + } + // Second process (after an agent restart) has an empty memory cache and a broken ReVal. + rv := &outputReValClient{err: errors.New("reval unavailable")} + r2 := newUpdateTestReconciler(t, c, mgrScheme) + r2.ReValClient = rv + r2.statusCheckers = r2.makeStatusCheckers() + + _, err := r2.doStatus(ctx, ms, icmsReq) + require.NoError(t, err) + assert.Equal(t, 0, rv.calls, "status checks must not call ReVal when a persisted render exists") + assert.Equal(t, v1alpha1.MiniServiceRunning, ms.Status.Phase) + }) + } +} + +func TestDoStatus_RenderFailureDoesNotFailRunningInstance(t *testing.T) { + ctx := newTestContext() + icmsReq := newUpdateICMSRequest(true) + + tests := []struct { + name string + newClient func() *outputReValClient + }{ + { + name: "reval returns invalid chart", + newClient: func() *outputReValClient { + return &outputReValClient{output: HelmReValRenderOutput{Valid: newBool(false), ValidationErrors: []string{"bad"}}} + }, + }, + { + name: "reval returns non-retryable error", + newClient: func() *outputReValClient { + return &outputReValClient{err: reconcile.TerminalError(errors.New("400 bad request"))} + }, + }, + { + name: "reval returns transient error", + newClient: func() *outputReValClient { + return &outputReValClient{err: errors.New("503 unavailable")} + }, + }, + } + for _, tt := range tests { + for _, workerReadiness := range []bool{false, true} { + name := tt.name + map[bool]string{false: " (aggressive status)", true: " (worker readiness status)"}[workerReadiness] + t.Run(name, func(t *testing.T) { + c, _ := newFakeClient(mgrScheme, + &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: updateTestNamespace}}, + newReadyUtilsPod(), + ) + ms := newUpdateMiniService(`{"key":"value"}`) + ms.Status.Phase = v1alpha1.MiniServiceRunning + // The instance was rendered in the past, but the persisted render is gone. + ms.Status.RenderDetails = &v1alpha1.RenderDetailsStatus{Hash: "sha256:previous"} + meta.SetStatusCondition(&ms.Status.Conditions, metav1.Condition{ + Type: v1alpha1.MiniServiceConditionInstallSuccessful, + Status: metav1.ConditionTrue, + Reason: "Installed", + }) + if workerReadiness { + ms.Spec.WorkloadConfig = &v1alpha1.WorkloadConfig{ + FeatureFlags: map[string]bool{featureflag.StatusByWorkerReadiness: true}, + } + } + + rv := tt.newClient() + r := newUpdateTestReconciler(t, c, mgrScheme) + r.ReValClient = rv + r.statusCheckers = r.makeStatusCheckers() + + _, err := r.doStatus(ctx, ms, icmsReq) + require.Error(t, err) + assert.False(t, isTerminal(err), "re-render failures during status must be retryable: %v", err) + assert.Equal(t, 1, rv.calls) + assert.Equal(t, v1alpha1.MiniServiceRunning, ms.Status.Phase) + cond := meta.FindStatusCondition(ms.Status.Conditions, v1alpha1.MiniServiceConditionInstallSuccessful) + require.NotNil(t, cond) + assert.Equal(t, metav1.ConditionTrue, cond.Status, "install condition must not be rewritten by a status re-render") + }) + } + } +} + +func TestRenderInputHash(t *testing.T) { + base := newUpdateMiniService(`{"a": 1}`) + same := newUpdateMiniService(`{"a": 1}`) + assert.Equal(t, renderInputHash(base), renderInputHash(same)) + + for name, mutate := range map[string]func(*v1alpha1.MiniService){ + "values": func(ms *v1alpha1.MiniService) { ms.Spec.HelmChartConfig.Values = []byte(`{"a": 2}`) }, + "chart url": func(ms *v1alpha1.MiniService) { ms.Spec.HelmChartConfig.URL = "https://example.test/other.tgz" }, + "service name": func(ms *v1alpha1.MiniService) { ms.Spec.HelmChartConfig.ServiceName = "svc" }, + "service port": func(ms *v1alpha1.MiniService) { p := int32(8080); ms.Spec.HelmChartConfig.ServicePort = &p }, + "namespace": func(ms *v1alpha1.MiniService) { ms.Spec.Namespace = "other" }, + } { + t.Run(name, func(t *testing.T) { + ms := newUpdateMiniService(`{"a": 1}`) + mutate(ms) + assert.NotEqual(t, renderInputHash(base), renderInputHash(ms)) + }) + } + + // Unrelated spec fields do not affect the hash. + other := newUpdateMiniService(`{"a": 1}`) + other.Spec.ICMSRequestName = "other-request" + assert.Equal(t, renderInputHash(base), renderInputHash(other)) +} diff --git a/src/compute-plane-services/nvca/internal/miniservice/rendered_secret_update_test.go b/src/compute-plane-services/nvca/internal/miniservice/rendered_secret_update_test.go new file mode 100644 index 0000000000..02bb8832b0 --- /dev/null +++ b/src/compute-plane-services/nvca/internal/miniservice/rendered_secret_update_test.go @@ -0,0 +1,249 @@ +/* +SPDX-FileCopyrightText: Copyright (c) NVIDIA CORPORATION & AFFILIATES. All rights reserved. +SPDX-License-Identifier: Apache-2.0 + +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. +*/ + +package mscontroller + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/interceptor" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/NVIDIA/nvcf/src/compute-plane-services/nvca/pkg/apis/nvca/v1alpha1" + "github.com/NVIDIA/nvcf/src/compute-plane-services/nvca/pkg/featureflag" + featureflagmock "github.com/NVIDIA/nvcf/src/compute-plane-services/nvca/pkg/featureflag/mock" +) + +const updateWorkloadObjectName = "updated-workload-cm" + +// newValidReValClient returns a ReVal client that renders the given objects successfully. +func newValidReValClient(rendered []byte) *outputReValClient { + return &outputReValClient{output: HelmReValRenderOutput{Valid: newBool(true), Output: rendered}} +} + +// newRevisionConfigMap builds a revision history ConfigMap as saveRevisionHistory would. +func newRevisionConfigMap(revision int64, values string) *corev1.ConfigMap { + return &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s%d", revisionConfigMapPrefix, revision), + Namespace: updateTestNamespace, + Labels: map[string]string{ + managedByLabel: managedByValue, + miniserviceNameLabel: updateMSName, + revisionLabel: fmt.Sprint(revision), + }, + }, + Data: map[string]string{ + revisionDataKeyValues: values, + revisionDataKeyChartURL: newUpdateMiniService("").Spec.HelmChartConfig.URL, + }, + } +} + +func updateTestObjects(values string) []client.Object { + return []client.Object{ + newUpdateMiniService(values), + newUpdateICMSRequest(true), + newReadyUtilsPod(), + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{Name: updateWorkloadObjectName, Namespace: updateTestNamespace}, + Data: map[string]string{"key": "existing"}, + }, + &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: updateTestNamespace}}, + } +} + +func enableRevisionHistory(r *Reconciler) { + r.FeatureFlagFetcher = &featureflagmock.Fetcher{ + EnabledFFs: []*featureflag.FeatureFlag{featureflag.MiniServiceRevisionHistory}, + } +} + +func getWorkloadConfigMapValue(t *testing.T, c client.Client) string { + t.Helper() + cm := &corev1.ConfigMap{} + require.NoError(t, c.Get(context.Background(), client.ObjectKey{Namespace: updateTestNamespace, Name: updateWorkloadObjectName}, cm)) + return cm.Data["key"] +} + +// A Secret write failure after a successful render must not attempt the apply and must be retried +// on the next reconcile without calling ReVal again. +func TestReconcile_UpdateSecretWriteFailureIsRetried(t *testing.T) { + ctx := newTestContext() + + failSecretCreate := true + c, _ := newFakeClientWithInterceptors(mgrScheme, + interceptor.Funcs{ + Create: func(ctx context.Context, c client.WithWatch, obj client.Object, opts ...client.CreateOption) error { + if s, ok := obj.(*corev1.Secret); ok && s.Name == RenderedSecretName && failSecretCreate { + return apierrors.NewInternalError(errors.New("injected secret create failure")) + } + return c.Create(ctx, obj, opts...) + }, + }, + updateTestObjects(`{"key":"value-v2"}`)..., + ) + r := newUpdateTestReconciler(t, c, mgrScheme) + enableRevisionHistory(r) + rendered := newUpdateRenderedData(t, updateWorkloadObjectName, "v2") + rv := newValidReValClient(rendered) + r.ReValClient = rv + + req := reconcile.Request{NamespacedName: client.ObjectKey{Name: updateMSName}} + _, err := r.Reconcile(ctx, req) + require.Error(t, err) + assert.ErrorContains(t, err, "injected secret create failure") + assert.Equal(t, 1, rv.calls) + + ms := &v1alpha1.MiniService{} + require.NoError(t, c.Get(ctx, client.ObjectKey{Name: updateMSName}, ms)) + assert.Equal(t, v1alpha1.MiniServiceInstalling, ms.Status.Phase) + require.NotNil(t, ms.Status.RenderDetails, "render hash is recorded even though the Secret write failed") + assert.Equal(t, "existing", getWorkloadConfigMapValue(t, c), "objects must not be applied before the render is stored") + err = c.Get(ctx, client.ObjectKey{Namespace: updateTestNamespace, Name: RenderedSecretName}, &corev1.Secret{}) + assert.True(t, apierrors.IsNotFound(err)) + + // Retry: the in-memory render is reused, the Secret write is retried, then the apply proceeds. + failSecretCreate = false + _, err = r.Reconcile(ctx, req) + require.NoError(t, err) + assert.Equal(t, 1, rv.calls, "retry must not call ReVal again") + + require.NoError(t, c.Get(ctx, client.ObjectKey{Name: updateMSName}, ms)) + assert.Equal(t, v1alpha1.MiniServiceInstalled, ms.Status.Phase) + assert.Equal(t, "v2", getWorkloadConfigMapValue(t, c)) + + secret := getRenderedSecret(t, c, updateTestNamespace) + assert.Equal(t, "2", secret.Labels[revisionLabel]) + assert.Equal(t, ms.Status.RenderDetails.Hash, secret.Annotations[renderedSecretOutputHashAnnotation]) + assert.Equal(t, renderInputHash(ms), secret.Annotations[renderedSecretInputHashAnnotation]) + + revCM := &corev1.ConfigMap{} + require.NoError(t, c.Get(ctx, client.ObjectKey{Namespace: updateTestNamespace, Name: revisionConfigMapPrefix + "2"}, revCM)) + assert.Equal(t, secret.Annotations[renderedSecretOutputHashAnnotation], revCM.Data[revisionDataKeyRenderHash]) + assert.JSONEq(t, `{"key":"value-v2"}`, revCM.Data[revisionDataKeyValues]) +} + +// If the agent died after writing the Secret but before the status patch landed, status has no render +// hash. The stored render for identical inputs must be reused rather than rendered again. +func TestReconcile_UpdateReusesSecretWhenStatusHashIsMissing(t *testing.T) { + ctx := newTestContext() + c, _ := newFakeClient(mgrScheme, updateTestObjects(`{"key":"value-v2"}`)...) + rendered := newUpdateRenderedData(t, updateWorkloadObjectName, "v2") + + // Previous process: rendered and stored, but the status patch never happened. + previous := newUpdateTestReconciler(t, c, mgrScheme) + scratch := newUpdateMiniService(`{"key":"value-v2"}`) + previous.saveRenderedData(ctx, scratch, rendered) + require.NoError(t, previous.persistRenderedData(ctx, scratch, rendered)) + storedHash := getRenderedSecret(t, c, updateTestNamespace).Annotations[renderedSecretOutputHashAnnotation] + + ms := &v1alpha1.MiniService{} + require.NoError(t, c.Get(ctx, client.ObjectKey{Name: updateMSName}, ms)) + require.Nil(t, ms.Status.RenderDetails) + + // New process with a broken ReVal. + r := newUpdateTestReconciler(t, c, mgrScheme) + rv := &outputReValClient{err: errors.New("reval unavailable")} + r.ReValClient = rv + + _, err := r.Reconcile(ctx, reconcile.Request{NamespacedName: client.ObjectKey{Name: updateMSName}}) + require.NoError(t, err) + assert.Equal(t, 0, rv.calls, "stored render for identical inputs must be reused") + + require.NoError(t, c.Get(ctx, client.ObjectKey{Name: updateMSName}, ms)) + assert.Equal(t, v1alpha1.MiniServiceInstalled, ms.Status.Phase) + require.NotNil(t, ms.Status.RenderDetails) + assert.Equal(t, storedHash, ms.Status.RenderDetails.Hash, "render hash is restored from the Secret") + assert.Equal(t, "v2", getWorkloadConfigMapValue(t, c)) +} + +// After a render and Secret write, a failed apply leaves the Secret at the new revision without a +// revision ConfigMap. Reverting the values to the last recorded revision must re-render for the +// reverted values instead of reusing the stored render. +func TestReconcile_ApplyFailureThenValuesRevertRerenders(t *testing.T) { + ctx := newTestContext() + + failWorkloadPatch := true + objs := append(updateTestObjects(`{"key":"value-v2"}`), newRevisionConfigMap(1, `{"key":"value-v1"}`)) + c, _ := newFakeClientWithInterceptors(mgrScheme, + interceptor.Funcs{ + Patch: func(ctx context.Context, c client.WithWatch, obj client.Object, patch client.Patch, opts ...client.PatchOption) error { + if cm, ok := obj.(*corev1.ConfigMap); ok && cm.Name == updateWorkloadObjectName && failWorkloadPatch { + return apierrors.NewForbidden(schema.GroupResource{Resource: "configmaps"}, cm.Name, errors.New("forbidden by test")) + } + return c.Patch(ctx, obj, patch, opts...) + }, + }, + objs..., + ) + r := newUpdateTestReconciler(t, c, mgrScheme) + enableRevisionHistory(r) + r.ReValClient = newValidReValClient(newUpdateRenderedData(t, updateWorkloadObjectName, "v2")) + + req := reconcile.Request{NamespacedName: client.ObjectKey{Name: updateMSName}} + _, err := r.Reconcile(ctx, req) + require.Error(t, err) + assert.ErrorContains(t, err, "forbidden") + + ms := &v1alpha1.MiniService{} + require.NoError(t, c.Get(ctx, client.ObjectKey{Name: updateMSName}, ms)) + assert.Equal(t, v1alpha1.MiniServiceInstalling, ms.Status.Phase) + assert.Equal(t, int64(2), ms.Status.Revision) + v2InputHash := renderInputHash(ms) + secret := getRenderedSecret(t, c, updateTestNamespace) + assert.Equal(t, "2", secret.Labels[revisionLabel]) + assert.Equal(t, v2InputHash, secret.Annotations[renderedSecretInputHashAnnotation]) + err = c.Get(ctx, client.ObjectKey{Namespace: updateTestNamespace, Name: revisionConfigMapPrefix + "2"}, &corev1.ConfigMap{}) + assert.True(t, apierrors.IsNotFound(err), "revision history is only recorded after a successful apply") + + // User reverts the values to the last recorded revision. + ms.Spec.HelmChartConfig.Values = []byte(`{"key":"value-v1"}`) + ms.Generation = 3 + require.NoError(t, c.Update(ctx, ms)) + rv := newValidReValClient(newUpdateRenderedData(t, updateWorkloadObjectName, "v1")) + r.ReValClient = rv + failWorkloadPatch = false + + _, err = r.Reconcile(ctx, req) + require.NoError(t, err) + assert.Equal(t, 1, rv.calls, "reverted values must be rendered, not served from the revision 2 render") + + require.NoError(t, c.Get(ctx, client.ObjectKey{Name: updateMSName}, ms)) + assert.Equal(t, v1alpha1.MiniServiceInstalled, ms.Status.Phase) + assert.Equal(t, int64(2), ms.Status.Revision, "values unchanged from history keeps the pending revision") + assert.Equal(t, "v1", getWorkloadConfigMapValue(t, c)) + + secret = getRenderedSecret(t, c, updateTestNamespace) + assert.Equal(t, renderInputHash(ms), secret.Annotations[renderedSecretInputHashAnnotation]) + assert.NotEqual(t, v2InputHash, secret.Annotations[renderedSecretInputHashAnnotation]) + + revCM := &corev1.ConfigMap{} + require.NoError(t, c.Get(ctx, client.ObjectKey{Namespace: updateTestNamespace, Name: revisionConfigMapPrefix + "2"}, revCM)) + assert.JSONEq(t, `{"key":"value-v1"}`, revCM.Data[revisionDataKeyValues]) + assert.Equal(t, secret.Annotations[renderedSecretOutputHashAnnotation], revCM.Data[revisionDataKeyRenderHash]) +} diff --git a/src/compute-plane-services/nvca/internal/miniservice/revision_test.go b/src/compute-plane-services/nvca/internal/miniservice/revision_test.go index c633a562a9..43e8854a9f 100644 --- a/src/compute-plane-services/nvca/internal/miniservice/revision_test.go +++ b/src/compute-plane-services/nvca/internal/miniservice/revision_test.go @@ -34,7 +34,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/interceptor" - "github.com/NVIDIA/nvcf/src/compute-plane-services/nvca/internal/miniservice/chartcache" "github.com/NVIDIA/nvcf/src/compute-plane-services/nvca/pkg/apis/nvca/v1alpha1" "github.com/NVIDIA/nvcf/src/compute-plane-services/nvca/pkg/featureflag" featureflagmock "github.com/NVIDIA/nvcf/src/compute-plane-services/nvca/pkg/featureflag/mock" @@ -195,9 +194,6 @@ func TestPrepareUpgradeIfNeeded(t *testing.T) { } c, _ := newFakeClient(testScheme, objs...) - cc := chartcache.New(t.TempDir()) - require.NoError(t, cc.Start(ctx)) - r := &Reconciler{ ControllerOptions: ControllerOptions{ FeatureFlagFetcher: &featureflagmock.Fetcher{ @@ -206,9 +202,8 @@ func TestPrepareUpgradeIfNeeded(t *testing.T) { }, }, }, - Client: c, - chartCache: cc, - now: func() time.Time { return fixedTime }, + Client: c, + now: func() time.Time { return fixedTime }, } err := r.prepareUpdateIfNeeded(ctx, ms) @@ -485,8 +480,6 @@ func TestRevisionUpgradeCycle(t *testing.T) { } c, _ := newFakeClient(testScheme, ms, ns) - cc := chartcache.New(t.TempDir()) - require.NoError(t, cc.Start(ctx)) r := &Reconciler{ ControllerOptions: ControllerOptions{ @@ -496,9 +489,8 @@ func TestRevisionUpgradeCycle(t *testing.T) { }, }, }, - Client: c, - chartCache: cc, - now: func() time.Time { return fixedTime }, + Client: c, + now: func() time.Time { return fixedTime }, } // Step 1: save revision 0 after initial install. diff --git a/src/compute-plane-services/nvca/internal/miniservice/status.go b/src/compute-plane-services/nvca/internal/miniservice/status.go index b5bc61f97e..0a72b44b3e 100644 --- a/src/compute-plane-services/nvca/internal/miniservice/status.go +++ b/src/compute-plane-services/nvca/internal/miniservice/status.go @@ -135,12 +135,25 @@ func (r *Reconciler) collectObjectStatuses( return nil, nil, err } if !isRendered { + // The persisted render is missing (e.g. too large to store), so ReVal is the last resort. + // A running instance must never be torn down because a re-render failed or came back + // invalid, so terminal render errors are downgraded to retryable errors here and the + // install condition set by render is left untouched. + conditions := slices.Clone(ms.Status.Conditions) if objsData, err = r.render(ctx, ms, icmsReq); err != nil { + if isTerminal(err) { + err = unwrapTerminalError(err) + ms.Status.Conditions = conditions + log.Error(err, "Failed to re-render Helm Chart for status checks, will retry without failing the MiniService") + } return nil, nil, err } - if err := r.saveRenderedData(ctx, ms, objsData); err != nil { - return nil, nil, err - } + r.saveRenderedData(ctx, ms, objsData) + } + // Keep the rendered Secret in sync (no-op when already stored). A Secret write problem must not + // block health checks, so it is logged and retried on the next status reconcile. + if err := r.persistRenderedData(ctx, ms, objsData); err != nil { + log.Error(err, "Failed to persist rendered Helm Chart data, will retry on next status reconcile") } objs, resources, _, err := decodeObjects(ctx, r.Decoder, objsData) diff --git a/src/compute-plane-services/nvca/internal/miniservice/status_byoo_test.go b/src/compute-plane-services/nvca/internal/miniservice/status_byoo_test.go index 5d5e177c4d..71d1fcb3be 100644 --- a/src/compute-plane-services/nvca/internal/miniservice/status_byoo_test.go +++ b/src/compute-plane-services/nvca/internal/miniservice/status_byoo_test.go @@ -30,7 +30,6 @@ import ( "k8s.io/apimachinery/pkg/runtime/serializer" "sigs.k8s.io/controller-runtime/pkg/reconcile" - "github.com/NVIDIA/nvcf/src/compute-plane-services/nvca/internal/miniservice/chartcache" nvcak8sutil "github.com/NVIDIA/nvcf/src/compute-plane-services/nvca/internal/util/k8sutil" "github.com/NVIDIA/nvcf/src/compute-plane-services/nvca/pkg/apis/nvca/v1alpha1" nvcav2beta1 "github.com/NVIDIA/nvcf/src/compute-plane-services/nvca/pkg/apis/nvca/v2beta1" @@ -96,15 +95,15 @@ func TestDoStatus_BYOOSidecarUnhealthy(t *testing.T) { } tests := []struct { - name string - workloadConfig *v1alpha1.WorkloadConfig - wantPhase v1alpha1.MiniServicePhase - wantConditionType string - wantConditionStatus metav1.ConditionStatus - wantConditionReason string - wantWorkersHealthyStatus metav1.ConditionStatus - wantWorkersHealthyReason string - wantErr bool + name string + workloadConfig *v1alpha1.WorkloadConfig + wantPhase v1alpha1.MiniServicePhase + wantConditionType string + wantConditionStatus metav1.ConditionStatus + wantConditionReason string + wantWorkersHealthyStatus metav1.ConditionStatus + wantWorkersHealthyReason string + wantErr bool }{ { // StatusByWorkerReadiness absent (default): doStatusAggressive runs. @@ -165,16 +164,12 @@ func TestDoStatus_BYOOSidecarUnhealthy(t *testing.T) { degradedUtilsPod, ) - cache := chartcache.New(t.TempDir()) - require.NoError(t, cache.Start(ctx)) - r := &Reconciler{ ControllerOptions: ControllerOptions{ K8sTimeConfig: (&nvcak8sutil.TimeConfig{}).Complete(), }, Client: c, Decoder: serializer.NewCodecFactory(mgrScheme).UniversalDeserializer(), - chartCache: cache, newPermissionsChecker: newFakePermissionsChecker, now: time.Now, } @@ -182,7 +177,7 @@ func TestDoStatus_BYOOSidecarUnhealthy(t *testing.T) { // Pre-save empty rendered objects so collectObjectStatuses does not // attempt a full Helm render. The BYOO sidecar lives in the utils pod // (not as a Helm-rendered object), so no Helm objects are needed. - require.NoError(t, r.saveRenderedData(ctx, ms, []byte("[]"))) + r.saveRenderedData(ctx, ms, []byte("[]")) _, err := r.doStatus(ctx, ms, icmsReq) diff --git a/src/compute-plane-services/nvca/pkg/apis/nvcf/v1/nvcfbackend_types.go b/src/compute-plane-services/nvca/pkg/apis/nvcf/v1/nvcfbackend_types.go index afed44f308..03ea6d52e6 100644 --- a/src/compute-plane-services/nvca/pkg/apis/nvcf/v1/nvcfbackend_types.go +++ b/src/compute-plane-services/nvca/pkg/apis/nvcf/v1/nvcfbackend_types.go @@ -211,9 +211,12 @@ const ( // +k8s:openapi-gen=true type MiniServiceConfig struct { - HelmReValServiceURL string `json:"helmReValServiceURL"` - HelmReValServiceHostHeaderOverride string `json:"helmReValServiceHostHeaderOverride,omitempty"` - CacheDirSize *resource.Quantity `json:"cacheDirSize"` + HelmReValServiceURL string `json:"helmReValServiceURL"` + HelmReValServiceHostHeaderOverride string `json:"helmReValServiceHostHeaderOverride,omitempty"` + // CacheDirSize is deprecated and ignored. Rendered Helm Charts are persisted in a Secret + // per MiniService instance instead of a local emptyDir cache on the agent. The field is + // retained and defaulted so existing NVCFBackend objects continue to validate. + CacheDirSize *resource.Quantity `json:"cacheDirSize"` } const ( diff --git a/src/compute-plane-services/nvca/pkg/operator/reconcile/nvcaagent_reconcile.go b/src/compute-plane-services/nvca/pkg/operator/reconcile/nvcaagent_reconcile.go index 94e91988a1..3d8cd820c4 100644 --- a/src/compute-plane-services/nvca/pkg/operator/reconcile/nvcaagent_reconcile.go +++ b/src/compute-plane-services/nvca/pkg/operator/reconcile/nvcaagent_reconcile.go @@ -130,10 +130,6 @@ const ( agentConfigVolumeName = "agent-config" legacyFirstClassConfigAnnotation = "nvcf.nvidia.com/legacy-first-class-config" - // ReVal config. - ReValCacheVolumeName = "reval-rendered-helmcharts" - ReValCacheDir = agentConfigDir + "/" + ReValCacheVolumeName - // New preferred secret names for OAuth2/OIDC authentication //nolint:gosec OAuthClientKeySecretName = "oauth-client-secret-key" @@ -1996,20 +1992,6 @@ func (bc *BackendK8sCache) setupNVCADeployment(ctx context.Context, original *nv }, ) - msCfg := nb.Spec.ClusterConfig.MiniService.Complete(bc.envType) - volumes = append(volumes, corev1.Volume{ - Name: ReValCacheVolumeName, - VolumeSource: corev1.VolumeSource{ - EmptyDir: &corev1.EmptyDirVolumeSource{ - SizeLimit: msCfg.CacheDirSize, - }, - }, - }) - nvcaContainer.VolumeMounts = append(nvcaContainer.VolumeMounts, corev1.VolumeMount{ - Name: ReValCacheVolumeName, - MountPath: ReValCacheDir, - }) - // Tell the agent which namespace the cluster-validator writes its // summary ConfigMap to (the operator/validator namespace), so the // agent's metrics reconciler watches the right namespace. The agent diff --git a/src/compute-plane-services/nvca/pkg/operator/reconcile/nvcaagent_reconcile_test.go b/src/compute-plane-services/nvca/pkg/operator/reconcile/nvcaagent_reconcile_test.go index 6b0e40de76..a917897739 100644 --- a/src/compute-plane-services/nvca/pkg/operator/reconcile/nvcaagent_reconcile_test.go +++ b/src/compute-plane-services/nvca/pkg/operator/reconcile/nvcaagent_reconcile_test.go @@ -408,14 +408,6 @@ func TestSetupNVCADeployment(t *testing.T) { }, }, }, - { - Name: ReValCacheVolumeName, - VolumeSource: corev1.VolumeSource{ - EmptyDir: &corev1.EmptyDirVolumeSource{ - SizeLimit: resource.NewQuantity(50*1<<30, resource.BinarySI), - }, - }, - }, }, gotDep.Spec.Template.Spec.Volumes) // Check containers. @@ -481,10 +473,6 @@ func TestSetupNVCADeployment(t *testing.T) { Name: agentConfigVolumeName, MountPath: agentConfigDir, }, - { - Name: ReValCacheVolumeName, - MountPath: ReValCacheDir, - }, }, nvcaContainer.VolumeMounts) assert.Empty(t, webhookContainer.Command) @@ -824,14 +812,6 @@ func TestSetupNVCADeployment_Vault(t *testing.T) { }, }, }, - { - Name: ReValCacheVolumeName, - VolumeSource: corev1.VolumeSource{ - EmptyDir: &corev1.EmptyDirVolumeSource{ - SizeLimit: resource.NewQuantity(50*1<<30, resource.BinarySI), - }, - }, - }, { Name: "token", VolumeSource: corev1.VolumeSource{ @@ -892,10 +872,6 @@ func TestSetupNVCADeployment_Vault(t *testing.T) { Name: agentConfigVolumeName, MountPath: agentConfigDir, }, - { - Name: ReValCacheVolumeName, - MountPath: ReValCacheDir, - }, { Name: "token", MountPath: "/var/run/secrets/kubernetes.io/serviceaccount-vault", @@ -1134,14 +1110,6 @@ func TestSetupNVCADeployment_SelfHosted(t *testing.T) { }, }, }, - { - Name: ReValCacheVolumeName, - VolumeSource: corev1.VolumeSource{ - EmptyDir: &corev1.EmptyDirVolumeSource{ - SizeLimit: resource.NewQuantity(50*1<<30, resource.BinarySI), - }, - }, - }, { Name: "token", VolumeSource: corev1.VolumeSource{ @@ -1215,10 +1183,6 @@ func TestSetupNVCADeployment_SelfHosted(t *testing.T) { Name: agentConfigVolumeName, MountPath: agentConfigDir, }, - { - Name: ReValCacheVolumeName, - MountPath: ReValCacheDir, - }, { Name: "token", MountPath: "/var/run/secrets/kubernetes.io/serviceaccount-vault", diff --git a/src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/.gitignore b/src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/.gitignore deleted file mode 100644 index 836562412f..0000000000 --- a/src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/.gitignore +++ /dev/null @@ -1,23 +0,0 @@ -# Compiled Object files, Static and Dynamic libs (Shared Objects) -*.o -*.a -*.so - -# Folders -_obj -_test - -# Architecture specific extensions/prefixes -*.[568vq] -[568vq].out - -*.cgo1.go -*.cgo2.c -_cgo_defun.c -_cgo_gotypes.go -_cgo_export.* - -_testmain.go - -*.exe -*.test diff --git a/src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/.golangci.yml b/src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/.golangci.yml deleted file mode 100644 index 7e7b8a9627..0000000000 --- a/src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/.golangci.yml +++ /dev/null @@ -1,46 +0,0 @@ -# Copyright (c) HashiCorp, Inc. -# SPDX-License-Identifier: MPL-2.0 - -linters: - fast: false - disable-all: true - enable: - - revive - - megacheck - - govet - - unconvert - - gas - - gocyclo - - dupl - - misspell - - unparam - - unused - - typecheck - - ineffassign - # - stylecheck - - exportloopref - - gocritic - - nakedret - - gosimple - - prealloc - -# golangci-lint configuration file -linters-settings: - revive: - ignore-generated-header: true - severity: warning - rules: - - name: package-comments - severity: warning - disabled: true - - name: exported - severity: warning - disabled: false - arguments: ["checkPrivateReceivers", "disableStutteringCheck"] - -issues: - exclude-use-default: false - exclude-rules: - - path: _test\.go - linters: - - dupl diff --git a/src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/2q.go b/src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/2q.go deleted file mode 100644 index 8c95252b6f..0000000000 --- a/src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/2q.go +++ /dev/null @@ -1,267 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package lru - -import ( - "errors" - "sync" - - "github.com/hashicorp/golang-lru/v2/simplelru" -) - -const ( - // Default2QRecentRatio is the ratio of the 2Q cache dedicated - // to recently added entries that have only been accessed once. - Default2QRecentRatio = 0.25 - - // Default2QGhostEntries is the default ratio of ghost - // entries kept to track entries recently evicted - Default2QGhostEntries = 0.50 -) - -// TwoQueueCache is a thread-safe fixed size 2Q cache. -// 2Q is an enhancement over the standard LRU cache -// in that it tracks both frequently and recently used -// entries separately. This avoids a burst in access to new -// entries from evicting frequently used entries. It adds some -// additional tracking overhead to the standard LRU cache, and is -// computationally about 2x the cost, and adds some metadata over -// head. The ARCCache is similar, but does not require setting any -// parameters. -type TwoQueueCache[K comparable, V any] struct { - size int - recentSize int - recentRatio float64 - ghostRatio float64 - - recent simplelru.LRUCache[K, V] - frequent simplelru.LRUCache[K, V] - recentEvict simplelru.LRUCache[K, struct{}] - lock sync.RWMutex -} - -// New2Q creates a new TwoQueueCache using the default -// values for the parameters. -func New2Q[K comparable, V any](size int) (*TwoQueueCache[K, V], error) { - return New2QParams[K, V](size, Default2QRecentRatio, Default2QGhostEntries) -} - -// New2QParams creates a new TwoQueueCache using the provided -// parameter values. -func New2QParams[K comparable, V any](size int, recentRatio, ghostRatio float64) (*TwoQueueCache[K, V], error) { - if size <= 0 { - return nil, errors.New("invalid size") - } - if recentRatio < 0.0 || recentRatio > 1.0 { - return nil, errors.New("invalid recent ratio") - } - if ghostRatio < 0.0 || ghostRatio > 1.0 { - return nil, errors.New("invalid ghost ratio") - } - - // Determine the sub-sizes - recentSize := int(float64(size) * recentRatio) - evictSize := int(float64(size) * ghostRatio) - - // Allocate the LRUs - recent, err := simplelru.NewLRU[K, V](size, nil) - if err != nil { - return nil, err - } - frequent, err := simplelru.NewLRU[K, V](size, nil) - if err != nil { - return nil, err - } - recentEvict, err := simplelru.NewLRU[K, struct{}](evictSize, nil) - if err != nil { - return nil, err - } - - // Initialize the cache - c := &TwoQueueCache[K, V]{ - size: size, - recentSize: recentSize, - recentRatio: recentRatio, - ghostRatio: ghostRatio, - recent: recent, - frequent: frequent, - recentEvict: recentEvict, - } - return c, nil -} - -// Get looks up a key's value from the cache. -func (c *TwoQueueCache[K, V]) Get(key K) (value V, ok bool) { - c.lock.Lock() - defer c.lock.Unlock() - - // Check if this is a frequent value - if val, ok := c.frequent.Get(key); ok { - return val, ok - } - - // If the value is contained in recent, then we - // promote it to frequent - if val, ok := c.recent.Peek(key); ok { - c.recent.Remove(key) - c.frequent.Add(key, val) - return val, ok - } - - // No hit - return -} - -// Add adds a value to the cache. -func (c *TwoQueueCache[K, V]) Add(key K, value V) { - c.lock.Lock() - defer c.lock.Unlock() - - // Check if the value is frequently used already, - // and just update the value - if c.frequent.Contains(key) { - c.frequent.Add(key, value) - return - } - - // Check if the value is recently used, and promote - // the value into the frequent list - if c.recent.Contains(key) { - c.recent.Remove(key) - c.frequent.Add(key, value) - return - } - - // If the value was recently evicted, add it to the - // frequently used list - if c.recentEvict.Contains(key) { - c.ensureSpace(true) - c.recentEvict.Remove(key) - c.frequent.Add(key, value) - return - } - - // Add to the recently seen list - c.ensureSpace(false) - c.recent.Add(key, value) -} - -// ensureSpace is used to ensure we have space in the cache -func (c *TwoQueueCache[K, V]) ensureSpace(recentEvict bool) { - // If we have space, nothing to do - recentLen := c.recent.Len() - freqLen := c.frequent.Len() - if recentLen+freqLen < c.size { - return - } - - // If the recent buffer is larger than - // the target, evict from there - if recentLen > 0 && (recentLen > c.recentSize || (recentLen == c.recentSize && !recentEvict)) { - k, _, _ := c.recent.RemoveOldest() - c.recentEvict.Add(k, struct{}{}) - return - } - - // Remove from the frequent list otherwise - c.frequent.RemoveOldest() -} - -// Len returns the number of items in the cache. -func (c *TwoQueueCache[K, V]) Len() int { - c.lock.RLock() - defer c.lock.RUnlock() - return c.recent.Len() + c.frequent.Len() -} - -// Resize changes the cache size. -func (c *TwoQueueCache[K, V]) Resize(size int) (evicted int) { - c.lock.Lock() - defer c.lock.Unlock() - - // Recalculate the sub-sizes - recentSize := int(float64(size) * c.recentRatio) - evictSize := int(float64(size) * c.ghostRatio) - c.size = size - c.recentSize = recentSize - - // ensureSpace - diff := c.recent.Len() + c.frequent.Len() - size - if diff < 0 { - diff = 0 - } - for i := 0; i < diff; i++ { - c.ensureSpace(true) - } - - // Reallocate the LRUs - c.recent.Resize(size) - c.frequent.Resize(size) - c.recentEvict.Resize(evictSize) - - return diff -} - -// Keys returns a slice of the keys in the cache. -// The frequently used keys are first in the returned slice. -func (c *TwoQueueCache[K, V]) Keys() []K { - c.lock.RLock() - defer c.lock.RUnlock() - k1 := c.frequent.Keys() - k2 := c.recent.Keys() - return append(k1, k2...) -} - -// Values returns a slice of the values in the cache. -// The frequently used values are first in the returned slice. -func (c *TwoQueueCache[K, V]) Values() []V { - c.lock.RLock() - defer c.lock.RUnlock() - v1 := c.frequent.Values() - v2 := c.recent.Values() - return append(v1, v2...) -} - -// Remove removes the provided key from the cache. -func (c *TwoQueueCache[K, V]) Remove(key K) { - c.lock.Lock() - defer c.lock.Unlock() - if c.frequent.Remove(key) { - return - } - if c.recent.Remove(key) { - return - } - if c.recentEvict.Remove(key) { - return - } -} - -// Purge is used to completely clear the cache. -func (c *TwoQueueCache[K, V]) Purge() { - c.lock.Lock() - defer c.lock.Unlock() - c.recent.Purge() - c.frequent.Purge() - c.recentEvict.Purge() -} - -// Contains is used to check if the cache contains a key -// without updating recency or frequency. -func (c *TwoQueueCache[K, V]) Contains(key K) bool { - c.lock.RLock() - defer c.lock.RUnlock() - return c.frequent.Contains(key) || c.recent.Contains(key) -} - -// Peek is used to inspect the cache value of a key -// without updating recency or frequency. -func (c *TwoQueueCache[K, V]) Peek(key K) (value V, ok bool) { - c.lock.RLock() - defer c.lock.RUnlock() - if val, ok := c.frequent.Peek(key); ok { - return val, ok - } - return c.recent.Peek(key) -} diff --git a/src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/BUILD.bazel b/src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/BUILD.bazel deleted file mode 100644 index 88dd1a5313..0000000000 --- a/src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/BUILD.bazel +++ /dev/null @@ -1,20 +0,0 @@ -load("@rules_go//go:def.bzl", "go_library") - -go_library( - name = "golang-lru", - srcs = [ - "2q.go", - "doc.go", - "lru.go", - ], - importmap = "github.com/NVIDIA/nvcf/src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2", - importpath = "github.com/hashicorp/golang-lru/v2", - visibility = ["//visibility:public"], - deps = ["//src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/simplelru"], -) - -alias( - name = "go_default_library", - actual = ":golang-lru", - visibility = ["//visibility:public"], -) diff --git a/src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/LICENSE b/src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/LICENSE deleted file mode 100644 index 0e5d580e0e..0000000000 --- a/src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/LICENSE +++ /dev/null @@ -1,364 +0,0 @@ -Copyright (c) 2014 HashiCorp, Inc. - -Mozilla Public License, version 2.0 - -1. Definitions - -1.1. "Contributor" - - means each individual or legal entity that creates, contributes to the - creation of, or owns Covered Software. - -1.2. "Contributor Version" - - means the combination of the Contributions of others (if any) used by a - Contributor and that particular Contributor's Contribution. - -1.3. "Contribution" - - means Covered Software of a particular Contributor. - -1.4. "Covered Software" - - means Source Code Form to which the initial Contributor has attached the - notice in Exhibit A, the Executable Form of such Source Code Form, and - Modifications of such Source Code Form, in each case including portions - thereof. - -1.5. "Incompatible With Secondary Licenses" - means - - a. that the initial Contributor has attached the notice described in - Exhibit B to the Covered Software; or - - b. that the Covered Software was made available under the terms of - version 1.1 or earlier of the License, but not also under the terms of - a Secondary License. - -1.6. "Executable Form" - - means any form of the work other than Source Code Form. - -1.7. "Larger Work" - - means a work that combines Covered Software with other material, in a - separate file or files, that is not Covered Software. - -1.8. "License" - - means this document. - -1.9. "Licensable" - - means having the right to grant, to the maximum extent possible, whether - at the time of the initial grant or subsequently, any and all of the - rights conveyed by this License. - -1.10. "Modifications" - - means any of the following: - - a. any file in Source Code Form that results from an addition to, - deletion from, or modification of the contents of Covered Software; or - - b. any new file in Source Code Form that contains any Covered Software. - -1.11. "Patent Claims" of a Contributor - - means any patent claim(s), including without limitation, method, - process, and apparatus claims, in any patent Licensable by such - Contributor that would be infringed, but for the grant of the License, - by the making, using, selling, offering for sale, having made, import, - or transfer of either its Contributions or its Contributor Version. - -1.12. "Secondary License" - - means either the GNU General Public License, Version 2.0, the GNU Lesser - General Public License, Version 2.1, the GNU Affero General Public - License, Version 3.0, or any later versions of those licenses. - -1.13. "Source Code Form" - - means the form of the work preferred for making modifications. - -1.14. "You" (or "Your") - - means an individual or a legal entity exercising rights under this - License. For legal entities, "You" includes any entity that controls, is - controlled by, or is under common control with You. For purposes of this - definition, "control" means (a) the power, direct or indirect, to cause - the direction or management of such entity, whether by contract or - otherwise, or (b) ownership of more than fifty percent (50%) of the - outstanding shares or beneficial ownership of such entity. - - -2. License Grants and Conditions - -2.1. Grants - - Each Contributor hereby grants You a world-wide, royalty-free, - non-exclusive license: - - a. under intellectual property rights (other than patent or trademark) - Licensable by such Contributor to use, reproduce, make available, - modify, display, perform, distribute, and otherwise exploit its - Contributions, either on an unmodified basis, with Modifications, or - as part of a Larger Work; and - - b. under Patent Claims of such Contributor to make, use, sell, offer for - sale, have made, import, and otherwise transfer either its - Contributions or its Contributor Version. - -2.2. Effective Date - - The licenses granted in Section 2.1 with respect to any Contribution - become effective for each Contribution on the date the Contributor first - distributes such Contribution. - -2.3. Limitations on Grant Scope - - The licenses granted in this Section 2 are the only rights granted under - this License. No additional rights or licenses will be implied from the - distribution or licensing of Covered Software under this License. - Notwithstanding Section 2.1(b) above, no patent license is granted by a - Contributor: - - a. for any code that a Contributor has removed from Covered Software; or - - b. for infringements caused by: (i) Your and any other third party's - modifications of Covered Software, or (ii) the combination of its - Contributions with other software (except as part of its Contributor - Version); or - - c. under Patent Claims infringed by Covered Software in the absence of - its Contributions. - - This License does not grant any rights in the trademarks, service marks, - or logos of any Contributor (except as may be necessary to comply with - the notice requirements in Section 3.4). - -2.4. Subsequent Licenses - - No Contributor makes additional grants as a result of Your choice to - distribute the Covered Software under a subsequent version of this - License (see Section 10.2) or under the terms of a Secondary License (if - permitted under the terms of Section 3.3). - -2.5. Representation - - Each Contributor represents that the Contributor believes its - Contributions are its original creation(s) or it has sufficient rights to - grant the rights to its Contributions conveyed by this License. - -2.6. Fair Use - - This License is not intended to limit any rights You have under - applicable copyright doctrines of fair use, fair dealing, or other - equivalents. - -2.7. Conditions - - Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in - Section 2.1. - - -3. Responsibilities - -3.1. Distribution of Source Form - - All distribution of Covered Software in Source Code Form, including any - Modifications that You create or to which You contribute, must be under - the terms of this License. You must inform recipients that the Source - Code Form of the Covered Software is governed by the terms of this - License, and how they can obtain a copy of this License. You may not - attempt to alter or restrict the recipients' rights in the Source Code - Form. - -3.2. Distribution of Executable Form - - If You distribute Covered Software in Executable Form then: - - a. such Covered Software must also be made available in Source Code Form, - as described in Section 3.1, and You must inform recipients of the - Executable Form how they can obtain a copy of such Source Code Form by - reasonable means in a timely manner, at a charge no more than the cost - of distribution to the recipient; and - - b. You may distribute such Executable Form under the terms of this - License, or sublicense it under different terms, provided that the - license for the Executable Form does not attempt to limit or alter the - recipients' rights in the Source Code Form under this License. - -3.3. Distribution of a Larger Work - - You may create and distribute a Larger Work under terms of Your choice, - provided that You also comply with the requirements of this License for - the Covered Software. If the Larger Work is a combination of Covered - Software with a work governed by one or more Secondary Licenses, and the - Covered Software is not Incompatible With Secondary Licenses, this - License permits You to additionally distribute such Covered Software - under the terms of such Secondary License(s), so that the recipient of - the Larger Work may, at their option, further distribute the Covered - Software under the terms of either this License or such Secondary - License(s). - -3.4. Notices - - You may not remove or alter the substance of any license notices - (including copyright notices, patent notices, disclaimers of warranty, or - limitations of liability) contained within the Source Code Form of the - Covered Software, except that You may alter any license notices to the - extent required to remedy known factual inaccuracies. - -3.5. Application of Additional Terms - - You may choose to offer, and to charge a fee for, warranty, support, - indemnity or liability obligations to one or more recipients of Covered - Software. However, You may do so only on Your own behalf, and not on - behalf of any Contributor. You must make it absolutely clear that any - such warranty, support, indemnity, or liability obligation is offered by - You alone, and You hereby agree to indemnify every Contributor for any - liability incurred by such Contributor as a result of warranty, support, - indemnity or liability terms You offer. You may include additional - disclaimers of warranty and limitations of liability specific to any - jurisdiction. - -4. Inability to Comply Due to Statute or Regulation - - If it is impossible for You to comply with any of the terms of this License - with respect to some or all of the Covered Software due to statute, - judicial order, or regulation then You must: (a) comply with the terms of - this License to the maximum extent possible; and (b) describe the - limitations and the code they affect. Such description must be placed in a - text file included with all distributions of the Covered Software under - this License. Except to the extent prohibited by statute or regulation, - such description must be sufficiently detailed for a recipient of ordinary - skill to be able to understand it. - -5. Termination - -5.1. The rights granted under this License will terminate automatically if You - fail to comply with any of its terms. However, if You become compliant, - then the rights granted under this License from a particular Contributor - are reinstated (a) provisionally, unless and until such Contributor - explicitly and finally terminates Your grants, and (b) on an ongoing - basis, if such Contributor fails to notify You of the non-compliance by - some reasonable means prior to 60 days after You have come back into - compliance. Moreover, Your grants from a particular Contributor are - reinstated on an ongoing basis if such Contributor notifies You of the - non-compliance by some reasonable means, this is the first time You have - received notice of non-compliance with this License from such - Contributor, and You become compliant prior to 30 days after Your receipt - of the notice. - -5.2. If You initiate litigation against any entity by asserting a patent - infringement claim (excluding declaratory judgment actions, - counter-claims, and cross-claims) alleging that a Contributor Version - directly or indirectly infringes any patent, then the rights granted to - You by any and all Contributors for the Covered Software under Section - 2.1 of this License shall terminate. - -5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user - license agreements (excluding distributors and resellers) which have been - validly granted by You or Your distributors under this License prior to - termination shall survive termination. - -6. Disclaimer of Warranty - - Covered Software is provided under this License on an "as is" basis, - without warranty of any kind, either expressed, implied, or statutory, - including, without limitation, warranties that the Covered Software is free - of defects, merchantable, fit for a particular purpose or non-infringing. - The entire risk as to the quality and performance of the Covered Software - is with You. Should any Covered Software prove defective in any respect, - You (not any Contributor) assume the cost of any necessary servicing, - repair, or correction. This disclaimer of warranty constitutes an essential - part of this License. No use of any Covered Software is authorized under - this License except under this disclaimer. - -7. Limitation of Liability - - Under no circumstances and under no legal theory, whether tort (including - negligence), contract, or otherwise, shall any Contributor, or anyone who - distributes Covered Software as permitted above, be liable to You for any - direct, indirect, special, incidental, or consequential damages of any - character including, without limitation, damages for lost profits, loss of - goodwill, work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses, even if such party shall have been - informed of the possibility of such damages. This limitation of liability - shall not apply to liability for death or personal injury resulting from - such party's negligence to the extent applicable law prohibits such - limitation. Some jurisdictions do not allow the exclusion or limitation of - incidental or consequential damages, so this exclusion and limitation may - not apply to You. - -8. Litigation - - Any litigation relating to this License may be brought only in the courts - of a jurisdiction where the defendant maintains its principal place of - business and such litigation shall be governed by laws of that - jurisdiction, without reference to its conflict-of-law provisions. Nothing - in this Section shall prevent a party's ability to bring cross-claims or - counter-claims. - -9. Miscellaneous - - This License represents the complete agreement concerning the subject - matter hereof. If any provision of this License is held to be - unenforceable, such provision shall be reformed only to the extent - necessary to make it enforceable. Any law or regulation which provides that - the language of a contract shall be construed against the drafter shall not - be used to construe this License against a Contributor. - - -10. Versions of the License - -10.1. New Versions - - Mozilla Foundation is the license steward. Except as provided in Section - 10.3, no one other than the license steward has the right to modify or - publish new versions of this License. Each version will be given a - distinguishing version number. - -10.2. Effect of New Versions - - You may distribute the Covered Software under the terms of the version - of the License under which You originally received the Covered Software, - or under the terms of any subsequent version published by the license - steward. - -10.3. Modified Versions - - If you create software not governed by this License, and you want to - create a new license for such software, you may create and use a - modified version of this License if you rename the license and remove - any references to the name of the license steward (except to note that - such modified license differs from this License). - -10.4. Distributing Source Code Form that is Incompatible With Secondary - Licenses If You choose to distribute Source Code Form that is - Incompatible With Secondary Licenses under the terms of this version of - the License, the notice described in Exhibit B of this License must be - attached. - -Exhibit A - Source Code Form License Notice - - This Source Code Form is subject to the - terms of the Mozilla Public License, v. - 2.0. If a copy of the MPL was not - distributed with this file, You can - obtain one at - http://mozilla.org/MPL/2.0/. - -If it is not possible or desirable to put the notice in a particular file, -then You may include the notice in a location (such as a LICENSE file in a -relevant directory) where a recipient would be likely to look for such a -notice. - -You may add additional accurate notices of copyright ownership. - -Exhibit B - "Incompatible With Secondary Licenses" Notice - - This Source Code Form is "Incompatible - With Secondary Licenses", as defined by - the Mozilla Public License, v. 2.0. diff --git a/src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/README.md b/src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/README.md deleted file mode 100644 index a942eb5397..0000000000 --- a/src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/README.md +++ /dev/null @@ -1,79 +0,0 @@ -golang-lru -========== - -This provides the `lru` package which implements a fixed-size -thread safe LRU cache. It is based on the cache in Groupcache. - -Documentation -============= - -Full docs are available on [Go Packages](https://pkg.go.dev/github.com/hashicorp/golang-lru/v2) - -LRU cache example -================= - -```go -package main - -import ( - "fmt" - "github.com/hashicorp/golang-lru/v2" -) - -func main() { - l, _ := lru.New[int, any](128) - for i := 0; i < 256; i++ { - l.Add(i, nil) - } - if l.Len() != 128 { - panic(fmt.Sprintf("bad len: %v", l.Len())) - } -} -``` - -Expirable LRU cache example -=========================== - -```go -package main - -import ( - "fmt" - "time" - - "github.com/hashicorp/golang-lru/v2/expirable" -) - -func main() { - // make cache with 10ms TTL and 5 max keys - cache := expirable.NewLRU[string, string](5, nil, time.Millisecond*10) - - - // set value under key1. - cache.Add("key1", "val1") - - // get value under key1 - r, ok := cache.Get("key1") - - // check for OK value - if ok { - fmt.Printf("value before expiration is found: %v, value: %q\n", ok, r) - } - - // wait for cache to expire - time.Sleep(time.Millisecond * 12) - - // get value under key1 after key expiration - r, ok = cache.Get("key1") - fmt.Printf("value after expiration is found: %v, value: %q\n", ok, r) - - // set value under key2, would evict old entry because it is already expired. - cache.Add("key2", "val2") - - fmt.Printf("Cache len: %d\n", cache.Len()) - // Output: - // value before expiration is found: true, value: "val1" - // value after expiration is found: false, value: "" - // Cache len: 1 -} -``` diff --git a/src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/doc.go b/src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/doc.go deleted file mode 100644 index 24107ee0ed..0000000000 --- a/src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/doc.go +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -// Package lru provides three different LRU caches of varying sophistication. -// -// Cache is a simple LRU cache. It is based on the LRU implementation in -// groupcache: https://github.com/golang/groupcache/tree/master/lru -// -// TwoQueueCache tracks frequently used and recently used entries separately. -// This avoids a burst of accesses from taking out frequently used entries, at -// the cost of about 2x computational overhead and some extra bookkeeping. -// -// ARCCache is an adaptive replacement cache. It tracks recent evictions as well -// as recent usage in both the frequent and recent caches. Its computational -// overhead is comparable to TwoQueueCache, but the memory overhead is linear -// with the size of the cache. -// -// ARC has been patented by IBM, so do not use it if that is problematic for -// your program. For this reason, it is in a separate go module contained within -// this repository. -// -// All caches in this package take locks while operating, and are therefore -// thread-safe for consumers. -package lru diff --git a/src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/internal/BUILD.bazel b/src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/internal/BUILD.bazel deleted file mode 100644 index bed01bd4c6..0000000000 --- a/src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/internal/BUILD.bazel +++ /dev/null @@ -1,15 +0,0 @@ -load("@rules_go//go:def.bzl", "go_library") - -go_library( - name = "internal", - srcs = ["list.go"], - importmap = "github.com/NVIDIA/nvcf/src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/internal", - importpath = "github.com/hashicorp/golang-lru/v2/internal", - visibility = ["//src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2:__subpackages__"], -) - -alias( - name = "go_default_library", - actual = ":internal", - visibility = ["//src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2:__subpackages__"], -) diff --git a/src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/internal/list.go b/src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/internal/list.go deleted file mode 100644 index 46a82d86fc..0000000000 --- a/src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/internal/list.go +++ /dev/null @@ -1,145 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -// Copyright 2009 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE_list file. - -package internal - -import "time" - -// Entry is an LRU Entry -type Entry[K comparable, V any] struct { - // Next and previous pointers in the doubly-linked list of elements. - // To simplify the implementation, internally a list l is implemented - // as a ring, such that &l.root is both the next element of the last - // list element (l.Back()) and the previous element of the first list - // element (l.Front()). - next, prev *Entry[K, V] - - // The list to which this element belongs. - list *LruList[K, V] - - // The LRU Key of this element. - Key K - - // The Value stored with this element. - Value V - - // The time this element would be cleaned up, optional - ExpiresAt time.Time - - // The expiry bucket item was put in, optional - ExpireBucket uint8 -} - -// PrevEntry returns the previous list element or nil. -func (e *Entry[K, V]) PrevEntry() *Entry[K, V] { - if p := e.prev; e.list != nil && p != &e.list.root { - return p - } - return nil -} - -// LruList represents a doubly linked list. -// The zero Value for LruList is an empty list ready to use. -type LruList[K comparable, V any] struct { - root Entry[K, V] // sentinel list element, only &root, root.prev, and root.next are used - len int // current list Length excluding (this) sentinel element -} - -// Init initializes or clears list l. -func (l *LruList[K, V]) Init() *LruList[K, V] { - l.root.next = &l.root - l.root.prev = &l.root - l.len = 0 - return l -} - -// NewList returns an initialized list. -func NewList[K comparable, V any]() *LruList[K, V] { return new(LruList[K, V]).Init() } - -// Length returns the number of elements of list l. -// The complexity is O(1). -func (l *LruList[K, V]) Length() int { return l.len } - -// Back returns the last element of list l or nil if the list is empty. -func (l *LruList[K, V]) Back() *Entry[K, V] { - if l.len == 0 { - return nil - } - return l.root.prev -} - -// lazyInit lazily initializes a zero List Value. -func (l *LruList[K, V]) lazyInit() { - if l.root.next == nil { - l.Init() - } -} - -// insert inserts e after at, increments l.len, and returns e. -func (l *LruList[K, V]) insert(e, at *Entry[K, V]) *Entry[K, V] { - e.prev = at - e.next = at.next - e.prev.next = e - e.next.prev = e - e.list = l - l.len++ - return e -} - -// insertValue is a convenience wrapper for insert(&Entry{Value: v, ExpiresAt: ExpiresAt}, at). -func (l *LruList[K, V]) insertValue(k K, v V, expiresAt time.Time, at *Entry[K, V]) *Entry[K, V] { - return l.insert(&Entry[K, V]{Value: v, Key: k, ExpiresAt: expiresAt}, at) -} - -// Remove removes e from its list, decrements l.len -func (l *LruList[K, V]) Remove(e *Entry[K, V]) V { - e.prev.next = e.next - e.next.prev = e.prev - e.next = nil // avoid memory leaks - e.prev = nil // avoid memory leaks - e.list = nil - l.len-- - - return e.Value -} - -// move moves e to next to at. -func (l *LruList[K, V]) move(e, at *Entry[K, V]) { - if e == at { - return - } - e.prev.next = e.next - e.next.prev = e.prev - - e.prev = at - e.next = at.next - e.prev.next = e - e.next.prev = e -} - -// PushFront inserts a new element e with value v at the front of list l and returns e. -func (l *LruList[K, V]) PushFront(k K, v V) *Entry[K, V] { - l.lazyInit() - return l.insertValue(k, v, time.Time{}, &l.root) -} - -// PushFrontExpirable inserts a new expirable element e with Value v at the front of list l and returns e. -func (l *LruList[K, V]) PushFrontExpirable(k K, v V, expiresAt time.Time) *Entry[K, V] { - l.lazyInit() - return l.insertValue(k, v, expiresAt, &l.root) -} - -// MoveToFront moves element e to the front of list l. -// If e is not an element of l, the list is not modified. -// The element must not be nil. -func (l *LruList[K, V]) MoveToFront(e *Entry[K, V]) { - if e.list != l || l.root.next == e { - return - } - // see comment in List.Remove about initialization of l - l.move(e, &l.root) -} diff --git a/src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/lru.go b/src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/lru.go deleted file mode 100644 index a2655f1f31..0000000000 --- a/src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/lru.go +++ /dev/null @@ -1,250 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package lru - -import ( - "sync" - - "github.com/hashicorp/golang-lru/v2/simplelru" -) - -const ( - // DefaultEvictedBufferSize defines the default buffer size to store evicted key/val - DefaultEvictedBufferSize = 16 -) - -// Cache is a thread-safe fixed size LRU cache. -type Cache[K comparable, V any] struct { - lru *simplelru.LRU[K, V] - evictedKeys []K - evictedVals []V - onEvictedCB func(k K, v V) - lock sync.RWMutex -} - -// New creates an LRU of the given size. -func New[K comparable, V any](size int) (*Cache[K, V], error) { - return NewWithEvict[K, V](size, nil) -} - -// NewWithEvict constructs a fixed size cache with the given eviction -// callback. -func NewWithEvict[K comparable, V any](size int, onEvicted func(key K, value V)) (c *Cache[K, V], err error) { - // create a cache with default settings - c = &Cache[K, V]{ - onEvictedCB: onEvicted, - } - if onEvicted != nil { - c.initEvictBuffers() - onEvicted = c.onEvicted - } - c.lru, err = simplelru.NewLRU(size, onEvicted) - return -} - -func (c *Cache[K, V]) initEvictBuffers() { - c.evictedKeys = make([]K, 0, DefaultEvictedBufferSize) - c.evictedVals = make([]V, 0, DefaultEvictedBufferSize) -} - -// onEvicted save evicted key/val and sent in externally registered callback -// outside of critical section -func (c *Cache[K, V]) onEvicted(k K, v V) { - c.evictedKeys = append(c.evictedKeys, k) - c.evictedVals = append(c.evictedVals, v) -} - -// Purge is used to completely clear the cache. -func (c *Cache[K, V]) Purge() { - var ks []K - var vs []V - c.lock.Lock() - c.lru.Purge() - if c.onEvictedCB != nil && len(c.evictedKeys) > 0 { - ks, vs = c.evictedKeys, c.evictedVals - c.initEvictBuffers() - } - c.lock.Unlock() - // invoke callback outside of critical section - if c.onEvictedCB != nil { - for i := 0; i < len(ks); i++ { - c.onEvictedCB(ks[i], vs[i]) - } - } -} - -// Add adds a value to the cache. Returns true if an eviction occurred. -func (c *Cache[K, V]) Add(key K, value V) (evicted bool) { - var k K - var v V - c.lock.Lock() - evicted = c.lru.Add(key, value) - if c.onEvictedCB != nil && evicted { - k, v = c.evictedKeys[0], c.evictedVals[0] - c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0] - } - c.lock.Unlock() - if c.onEvictedCB != nil && evicted { - c.onEvictedCB(k, v) - } - return -} - -// Get looks up a key's value from the cache. -func (c *Cache[K, V]) Get(key K) (value V, ok bool) { - c.lock.Lock() - value, ok = c.lru.Get(key) - c.lock.Unlock() - return value, ok -} - -// Contains checks if a key is in the cache, without updating the -// recent-ness or deleting it for being stale. -func (c *Cache[K, V]) Contains(key K) bool { - c.lock.RLock() - containKey := c.lru.Contains(key) - c.lock.RUnlock() - return containKey -} - -// Peek returns the key value (or undefined if not found) without updating -// the "recently used"-ness of the key. -func (c *Cache[K, V]) Peek(key K) (value V, ok bool) { - c.lock.RLock() - value, ok = c.lru.Peek(key) - c.lock.RUnlock() - return value, ok -} - -// ContainsOrAdd checks if a key is in the cache without updating the -// recent-ness or deleting it for being stale, and if not, adds the value. -// Returns whether found and whether an eviction occurred. -func (c *Cache[K, V]) ContainsOrAdd(key K, value V) (ok, evicted bool) { - var k K - var v V - c.lock.Lock() - if c.lru.Contains(key) { - c.lock.Unlock() - return true, false - } - evicted = c.lru.Add(key, value) - if c.onEvictedCB != nil && evicted { - k, v = c.evictedKeys[0], c.evictedVals[0] - c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0] - } - c.lock.Unlock() - if c.onEvictedCB != nil && evicted { - c.onEvictedCB(k, v) - } - return false, evicted -} - -// PeekOrAdd checks if a key is in the cache without updating the -// recent-ness or deleting it for being stale, and if not, adds the value. -// Returns whether found and whether an eviction occurred. -func (c *Cache[K, V]) PeekOrAdd(key K, value V) (previous V, ok, evicted bool) { - var k K - var v V - c.lock.Lock() - previous, ok = c.lru.Peek(key) - if ok { - c.lock.Unlock() - return previous, true, false - } - evicted = c.lru.Add(key, value) - if c.onEvictedCB != nil && evicted { - k, v = c.evictedKeys[0], c.evictedVals[0] - c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0] - } - c.lock.Unlock() - if c.onEvictedCB != nil && evicted { - c.onEvictedCB(k, v) - } - return -} - -// Remove removes the provided key from the cache. -func (c *Cache[K, V]) Remove(key K) (present bool) { - var k K - var v V - c.lock.Lock() - present = c.lru.Remove(key) - if c.onEvictedCB != nil && present { - k, v = c.evictedKeys[0], c.evictedVals[0] - c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0] - } - c.lock.Unlock() - if c.onEvictedCB != nil && present { - c.onEvictedCB(k, v) - } - return -} - -// Resize changes the cache size. -func (c *Cache[K, V]) Resize(size int) (evicted int) { - var ks []K - var vs []V - c.lock.Lock() - evicted = c.lru.Resize(size) - if c.onEvictedCB != nil && evicted > 0 { - ks, vs = c.evictedKeys, c.evictedVals - c.initEvictBuffers() - } - c.lock.Unlock() - if c.onEvictedCB != nil && evicted > 0 { - for i := 0; i < len(ks); i++ { - c.onEvictedCB(ks[i], vs[i]) - } - } - return evicted -} - -// RemoveOldest removes the oldest item from the cache. -func (c *Cache[K, V]) RemoveOldest() (key K, value V, ok bool) { - var k K - var v V - c.lock.Lock() - key, value, ok = c.lru.RemoveOldest() - if c.onEvictedCB != nil && ok { - k, v = c.evictedKeys[0], c.evictedVals[0] - c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0] - } - c.lock.Unlock() - if c.onEvictedCB != nil && ok { - c.onEvictedCB(k, v) - } - return -} - -// GetOldest returns the oldest entry -func (c *Cache[K, V]) GetOldest() (key K, value V, ok bool) { - c.lock.RLock() - key, value, ok = c.lru.GetOldest() - c.lock.RUnlock() - return -} - -// Keys returns a slice of the keys in the cache, from oldest to newest. -func (c *Cache[K, V]) Keys() []K { - c.lock.RLock() - keys := c.lru.Keys() - c.lock.RUnlock() - return keys -} - -// Values returns a slice of the values in the cache, from oldest to newest. -func (c *Cache[K, V]) Values() []V { - c.lock.RLock() - values := c.lru.Values() - c.lock.RUnlock() - return values -} - -// Len returns the number of items in the cache. -func (c *Cache[K, V]) Len() int { - c.lock.RLock() - length := c.lru.Len() - c.lock.RUnlock() - return length -} diff --git a/src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/simplelru/BUILD.bazel b/src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/simplelru/BUILD.bazel deleted file mode 100644 index 0288eab557..0000000000 --- a/src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/simplelru/BUILD.bazel +++ /dev/null @@ -1,19 +0,0 @@ -load("@rules_go//go:def.bzl", "go_library") - -go_library( - name = "simplelru", - srcs = [ - "lru.go", - "lru_interface.go", - ], - importmap = "github.com/NVIDIA/nvcf/src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/simplelru", - importpath = "github.com/hashicorp/golang-lru/v2/simplelru", - visibility = ["//visibility:public"], - deps = ["//src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/internal"], -) - -alias( - name = "go_default_library", - actual = ":simplelru", - visibility = ["//visibility:public"], -) diff --git a/src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/simplelru/LICENSE_list b/src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/simplelru/LICENSE_list deleted file mode 100644 index c4764e6b2f..0000000000 --- a/src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/simplelru/LICENSE_list +++ /dev/null @@ -1,29 +0,0 @@ -This license applies to simplelru/list.go - -Copyright (c) 2009 The Go Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/simplelru/lru.go b/src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/simplelru/lru.go deleted file mode 100644 index f69792388c..0000000000 --- a/src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/simplelru/lru.go +++ /dev/null @@ -1,177 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package simplelru - -import ( - "errors" - - "github.com/hashicorp/golang-lru/v2/internal" -) - -// EvictCallback is used to get a callback when a cache entry is evicted -type EvictCallback[K comparable, V any] func(key K, value V) - -// LRU implements a non-thread safe fixed size LRU cache -type LRU[K comparable, V any] struct { - size int - evictList *internal.LruList[K, V] - items map[K]*internal.Entry[K, V] - onEvict EvictCallback[K, V] -} - -// NewLRU constructs an LRU of the given size -func NewLRU[K comparable, V any](size int, onEvict EvictCallback[K, V]) (*LRU[K, V], error) { - if size <= 0 { - return nil, errors.New("must provide a positive size") - } - - c := &LRU[K, V]{ - size: size, - evictList: internal.NewList[K, V](), - items: make(map[K]*internal.Entry[K, V]), - onEvict: onEvict, - } - return c, nil -} - -// Purge is used to completely clear the cache. -func (c *LRU[K, V]) Purge() { - for k, v := range c.items { - if c.onEvict != nil { - c.onEvict(k, v.Value) - } - delete(c.items, k) - } - c.evictList.Init() -} - -// Add adds a value to the cache. Returns true if an eviction occurred. -func (c *LRU[K, V]) Add(key K, value V) (evicted bool) { - // Check for existing item - if ent, ok := c.items[key]; ok { - c.evictList.MoveToFront(ent) - ent.Value = value - return false - } - - // Add new item - ent := c.evictList.PushFront(key, value) - c.items[key] = ent - - evict := c.evictList.Length() > c.size - // Verify size not exceeded - if evict { - c.removeOldest() - } - return evict -} - -// Get looks up a key's value from the cache. -func (c *LRU[K, V]) Get(key K) (value V, ok bool) { - if ent, ok := c.items[key]; ok { - c.evictList.MoveToFront(ent) - return ent.Value, true - } - return -} - -// Contains checks if a key is in the cache, without updating the recent-ness -// or deleting it for being stale. -func (c *LRU[K, V]) Contains(key K) (ok bool) { - _, ok = c.items[key] - return ok -} - -// Peek returns the key value (or undefined if not found) without updating -// the "recently used"-ness of the key. -func (c *LRU[K, V]) Peek(key K) (value V, ok bool) { - var ent *internal.Entry[K, V] - if ent, ok = c.items[key]; ok { - return ent.Value, true - } - return -} - -// Remove removes the provided key from the cache, returning if the -// key was contained. -func (c *LRU[K, V]) Remove(key K) (present bool) { - if ent, ok := c.items[key]; ok { - c.removeElement(ent) - return true - } - return false -} - -// RemoveOldest removes the oldest item from the cache. -func (c *LRU[K, V]) RemoveOldest() (key K, value V, ok bool) { - if ent := c.evictList.Back(); ent != nil { - c.removeElement(ent) - return ent.Key, ent.Value, true - } - return -} - -// GetOldest returns the oldest entry -func (c *LRU[K, V]) GetOldest() (key K, value V, ok bool) { - if ent := c.evictList.Back(); ent != nil { - return ent.Key, ent.Value, true - } - return -} - -// Keys returns a slice of the keys in the cache, from oldest to newest. -func (c *LRU[K, V]) Keys() []K { - keys := make([]K, c.evictList.Length()) - i := 0 - for ent := c.evictList.Back(); ent != nil; ent = ent.PrevEntry() { - keys[i] = ent.Key - i++ - } - return keys -} - -// Values returns a slice of the values in the cache, from oldest to newest. -func (c *LRU[K, V]) Values() []V { - values := make([]V, len(c.items)) - i := 0 - for ent := c.evictList.Back(); ent != nil; ent = ent.PrevEntry() { - values[i] = ent.Value - i++ - } - return values -} - -// Len returns the number of items in the cache. -func (c *LRU[K, V]) Len() int { - return c.evictList.Length() -} - -// Resize changes the cache size. -func (c *LRU[K, V]) Resize(size int) (evicted int) { - diff := c.Len() - size - if diff < 0 { - diff = 0 - } - for i := 0; i < diff; i++ { - c.removeOldest() - } - c.size = size - return diff -} - -// removeOldest removes the oldest item from the cache. -func (c *LRU[K, V]) removeOldest() { - if ent := c.evictList.Back(); ent != nil { - c.removeElement(ent) - } -} - -// removeElement is used to remove a given list element from the cache -func (c *LRU[K, V]) removeElement(e *internal.Entry[K, V]) { - c.evictList.Remove(e) - delete(c.items, e.Key) - if c.onEvict != nil { - c.onEvict(e.Key, e.Value) - } -} diff --git a/src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/simplelru/lru_interface.go b/src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/simplelru/lru_interface.go deleted file mode 100644 index 043b8bcc3f..0000000000 --- a/src/compute-plane-services/nvca/vendor/github.com/hashicorp/golang-lru/v2/simplelru/lru_interface.go +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -// Package simplelru provides simple LRU implementation based on build-in container/list. -package simplelru - -// LRUCache is the interface for simple LRU cache. -type LRUCache[K comparable, V any] interface { - // Adds a value to the cache, returns true if an eviction occurred and - // updates the "recently used"-ness of the key. - Add(key K, value V) bool - - // Returns key's value from the cache and - // updates the "recently used"-ness of the key. #value, isFound - Get(key K) (value V, ok bool) - - // Checks if a key exists in cache without updating the recent-ness. - Contains(key K) (ok bool) - - // Returns key's value without updating the "recently used"-ness of the key. - Peek(key K) (value V, ok bool) - - // Removes a key from the cache. - Remove(key K) bool - - // Removes the oldest entry from cache. - RemoveOldest() (K, V, bool) - - // Returns the oldest entry from the cache. #key, value, isFound - GetOldest() (K, V, bool) - - // Returns a slice of the keys in the cache, from oldest to newest. - Keys() []K - - // Values returns a slice of the values in the cache, from oldest to newest. - Values() []V - - // Returns the number of items in the cache. - Len() int - - // Clears all cache entries. - Purge() - - // Resizes cache, returning number evicted - Resize(int) int -} diff --git a/src/compute-plane-services/nvca/vendor/modules.txt b/src/compute-plane-services/nvca/vendor/modules.txt index 1086650937..27d3142ac7 100644 --- a/src/compute-plane-services/nvca/vendor/modules.txt +++ b/src/compute-plane-services/nvca/vendor/modules.txt @@ -414,11 +414,6 @@ github.com/hashicorp/go-cleanhttp # github.com/hashicorp/go-retryablehttp v0.7.8 ## explicit; go 1.23 github.com/hashicorp/go-retryablehttp -# github.com/hashicorp/golang-lru/v2 v2.0.7 -## explicit; go 1.18 -github.com/hashicorp/golang-lru/v2 -github.com/hashicorp/golang-lru/v2/internal -github.com/hashicorp/golang-lru/v2/simplelru # github.com/imdario/mergo v0.3.16 ## explicit; go 1.13 github.com/imdario/mergo