diff --git a/LICENSES/k8s.io/kubelet/LICENSE b/LICENSES/k8s.io/kubelet/LICENSE
new file mode 100644
index 0000000000..d645695673
--- /dev/null
+++ b/LICENSES/k8s.io/kubelet/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ 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.
diff --git a/cmd/atelet/imagecreds.go b/cmd/atelet/imagecreds.go
new file mode 100644
index 0000000000..c8171a0c23
--- /dev/null
+++ b/cmd/atelet/imagecreds.go
@@ -0,0 +1,59 @@
+// Copyright 2026 Google LLC
+//
+// 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 main
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/agent-substrate/substrate/internal/credentialprovider"
+ "github.com/google/go-containerregistry/pkg/authn"
+ googlecontainerauth "github.com/google/go-containerregistry/pkg/v1/google"
+)
+
+// newImagePullCredentials picks how atelet authenticates image pulls. Exactly
+// one of the two results is non-nil (or neither, for anonymous-only pulls):
+//
+// - keychain: kubelet image credential provider plugins, mounted from the
+// host. Registry-agnostic — the node's plugins decide what they can
+// authenticate — so atelet needs no cloud-specific code.
+// - gcpAuth: the legacy path, GCP application default credentials applied to
+// gcr.io and pkg.dev only.
+//
+// The credential-provider path wins when configured; it is the direction we
+// want atelet to move in, and running both would only obscure which one
+// produced a credential.
+func newImagePullCredentials(ctx context.Context) (keychain authn.Keychain, gcpAuth authn.Authenticator, err error) {
+ if *imageCredentialProviderConfig != "" {
+ if *imageCredentialProviderBinDir == "" {
+ return nil, nil, fmt.Errorf("--image-credential-provider-bin-dir is required when --image-credential-provider-config is set")
+ }
+ kc, err := credentialprovider.New(*imageCredentialProviderConfig, *imageCredentialProviderBinDir)
+ if err != nil {
+ return nil, nil, err
+ }
+ return kc, nil, nil
+ }
+
+ if *gcpAuthForImagePulls {
+ gcpAuth, err := googlecontainerauth.NewEnvAuthenticator(ctx)
+ if err != nil {
+ return nil, nil, fmt.Errorf("while creating GCP registry authenticator: %w", err)
+ }
+ return nil, gcpAuth, nil
+ }
+
+ return nil, nil, nil
+}
diff --git a/cmd/atelet/main.go b/cmd/atelet/main.go
index 0fcd67c029..5288656da6 100644
--- a/cmd/atelet/main.go
+++ b/cmd/atelet/main.go
@@ -58,8 +58,6 @@ import (
"github.com/agent-substrate/substrate/pkg/proto/ateapipb"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/s3"
- "github.com/google/go-containerregistry/pkg/authn"
- googlecontainerauth "github.com/google/go-containerregistry/pkg/v1/google"
"github.com/spf13/pflag"
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
"go.opentelemetry.io/otel"
@@ -91,7 +89,14 @@ var (
ateapiCAFile = pflag.String("ateapi-ca-file", "/run/servicedns.podcert.ate.dev/trust-bundle.pem", "CA bundle used to verify ateapi.")
ateapiServerName = pflag.String("ateapi-server-name", "api.ate-system.svc", "DNS name expected on the ateapi certificate.")
- gcpAuthForImagePulls = pflag.Bool("gcp-auth-for-image-pulls", true, "Use GCP application default credentials mechanism.")
+ gcpAuthForImagePulls = pflag.Bool("gcp-auth-for-image-pulls", true, "Use GCP application default credentials mechanism. Ignored when --image-credential-provider-config is set.")
+ // The kubelet already knows how to authenticate to its node's cloud
+ // registry, via an exec plugin the node ships. Pointing atelet at the same
+ // config and bin dir (mounted read-only from the host) lets it pull with
+ // no cloud SDK compiled in, on any cloud whose nodes configure a provider.
+ imageCredentialProviderConfig = pflag.String("image-credential-provider-config", "", "Path to a kubelet CredentialProviderConfig. When set, image pull credentials come from its exec plugins instead of GCP application default credentials.")
+ imageCredentialProviderBinDir = pflag.String("image-credential-provider-bin-dir", "", "Directory holding the credential provider executables named by --image-credential-provider-config. Required when that flag is set.")
+
localhostRegistryReplacement = pflag.String("localhost-registry-replacement", "", "The replacement registry endpoint for localhost and/or loopback IP addresses, useful for local development. for example kind-registry:5000")
imageCacheDir = pflag.String("image-cache-dir", ateompath.ImageCacheDir, "Directory for the node-local OCI image layer cache. Must be on the volume shared with the ateom pods (the cached layers are their overlay lowerdirs), and on a disk sized for both capacity and IOPS: unpack throughput is gated by the volume's IOPS.")
@@ -157,18 +162,16 @@ func main() {
conns: lru.New(256),
}
- var gcpRegistryAuthn authn.Authenticator
- if *gcpAuthForImagePulls {
- gcpRegistryAuthn, err = googlecontainerauth.NewEnvAuthenticator(ctx)
- if err != nil {
- serverboot.Fatal(ctx, "Failed to create GCP registry authenticator", err)
- }
+ imageCredsKeychain, gcpRegistryAuthn, err := newImagePullCredentials(ctx)
+ if err != nil {
+ serverboot.Fatal(ctx, "Failed to configure image pull credentials", err)
}
if err := validateImageCacheGCFlags(); err != nil {
serverboot.Fatal(ctx, "Invalid image cache GC flags", err)
}
imageCache, err := imagecache.New(*imageCacheDir,
+ imagecache.WithKeychain(imageCredsKeychain),
imagecache.WithAuthenticator(gcpRegistryAuthn),
imagecache.WithLocalhostRegistryReplacement(*localhostRegistryReplacement),
imagecache.WithActorsDir(ateompath.ActorsDir),
diff --git a/go.mod b/go.mod
index 17665f0077..94984ee377 100644
--- a/go.mod
+++ b/go.mod
@@ -60,6 +60,7 @@ require (
k8s.io/apiextensions-apiserver v0.36.1
k8s.io/apimachinery v0.36.1
k8s.io/client-go v0.36.1
+ k8s.io/kubelet v0.36.1
k8s.io/metrics v0.36.1
k8s.io/utils v0.0.0-20260319190234-28399d86e0b5
sigs.k8s.io/controller-runtime v0.24.1
diff --git a/go.sum b/go.sum
index 82a1dace31..26a8ddd47f 100644
--- a/go.sum
+++ b/go.sum
@@ -510,10 +510,14 @@ k8s.io/apimachinery v0.36.1 h1:G63Gjx2W+q0YD+72Vo8oY0nDnePVwnuzTmmy5ENrVSA=
k8s.io/apimachinery v0.36.1/go.mod h1:ibYOR00vW/I1kzvi5SF0dRuJ52BvKtfvRdOn35GPQ+8=
k8s.io/client-go v0.36.1 h1:FN/K8QIT2CEDt+2WB2HnWrUANZ50AP5GII43/SP2JR0=
k8s.io/client-go v0.36.1/go.mod h1:s6rAnCtTGYDQnpNjEhSaISV+2O8jwruZ6m3QOYBFbtU=
+k8s.io/component-base v0.36.1 h1:iG6GsELftXqTNG9HG6kiVjatSgAw1sf5pJ6R5a6N0kA=
+k8s.io/component-base v0.36.1/go.mod h1:nf9XPlntRdqO6WMeEWAA5F93Y4ICZQdeT9GeqLDB3JI=
k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=
k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a h1:xCeOEAOoGYl2jnJoHkC3hkbPJgdATINPMAxaynU2Ovg=
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0=
+k8s.io/kubelet v0.36.1 h1:FcHiG9wv92xerRPNxztuhYWqwS4IilOQNPxTPQewYgo=
+k8s.io/kubelet v0.36.1/go.mod h1:e6IeoCwqc2TbneCKu6P8HjmWLi7U6SOh3Pocs32iGFM=
k8s.io/metrics v0.36.1 h1:MQPb+G4RhrKEpt8NETPssbW8QgGUc4Jbqu1jx+kPqGk=
k8s.io/metrics v0.36.1/go.mod h1:xqS8XcWLjDzo6E7DJm/GfjKpRKdN5/MtJAQFuV6nLUc=
k8s.io/streaming v0.36.1 h1:L+K68n4Gg940BGNNYtUBvL1WTLL0YnKT3s+P1MNAmR4=
diff --git a/internal/credentialprovider/config.go b/internal/credentialprovider/config.go
new file mode 100644
index 0000000000..6d2acbc44a
--- /dev/null
+++ b/internal/credentialprovider/config.go
@@ -0,0 +1,143 @@
+// Copyright 2026 Google LLC
+//
+// 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 credentialprovider
+
+import (
+ "fmt"
+ "log/slog"
+ "os"
+ "path/filepath"
+ "strings"
+
+ kubeletconfigv1 "k8s.io/kubelet/config/v1"
+ credentialproviderv1 "k8s.io/kubelet/pkg/apis/credentialprovider/v1"
+ "sigs.k8s.io/yaml"
+)
+
+// supportedAPIVersion is the only CredentialProviderRequest/Response encoding
+// this package speaks. A provider configured for anything else is rejected
+// rather than silently sent a request it cannot parse.
+var supportedAPIVersion = credentialproviderv1.SchemeGroupVersion.String()
+
+// configKind is the apiVersion/kind a credential provider config file must
+// declare.
+const (
+ configAPIVersion = "kubelet.config.k8s.io/v1"
+ configKind = "CredentialProviderConfig"
+)
+
+// loadConfig reads a kubelet CredentialProviderConfig and resolves each
+// provider's executable inside binDir, applying the same per-provider
+// validation the kubelet does.
+//
+// Errors are returned only for problems with the config as a whole -- missing,
+// unparseable, or not a CredentialProviderConfig -- which mean atelet was
+// pointed at the wrong file. Individual providers that fail validation are
+// skipped (see the loop below), so the returned slice may be shorter than the
+// config, or empty.
+func loadConfig(configPath, binDir string) ([]*plugin, error) {
+ raw, err := os.ReadFile(configPath)
+ if err != nil {
+ return nil, fmt.Errorf("while reading credential provider config: %w", err)
+ }
+ var cfg kubeletconfigv1.CredentialProviderConfig
+ if err := yaml.UnmarshalStrict(raw, &cfg); err != nil {
+ return nil, fmt.Errorf("while parsing credential provider config %q: %w", configPath, err)
+ }
+ if cfg.Kind != configKind || cfg.APIVersion != configAPIVersion {
+ return nil, fmt.Errorf("credential provider config %q must be %s %s, got %q %q", configPath, configAPIVersion, configKind, cfg.APIVersion, cfg.Kind)
+ }
+ // A single unusable provider is skipped rather than fatal. The config is
+ // the node's, shared with its kubelet, and may name providers this atelet
+ // does not implement (tokenAttributes) or does not need; refusing to start
+ // would turn one unsupported entry into a node-wide outage, and would take
+ // the other providers -- which may cover every registry the actors use --
+ // down with it. Pulls that needed a skipped provider fail at pull time with
+ // the registry's own 401, next to this warning.
+ seen := make(map[string]struct{}, len(cfg.Providers))
+ plugins := make([]*plugin, 0, len(cfg.Providers))
+ for i := range cfg.Providers {
+ p, err := newPlugin(&cfg.Providers[i], binDir)
+ if err != nil {
+ slog.Warn("Skipping unusable image credential provider",
+ slog.String("config", configPath), slog.Any("err", err))
+ continue
+ }
+ if _, dup := seen[p.name]; dup {
+ slog.Warn("Skipping duplicate image credential provider",
+ slog.String("config", configPath), slog.String("provider", p.name))
+ continue
+ }
+ seen[p.name] = struct{}{}
+ plugins = append(plugins, p)
+ }
+ // Also not fatal: an empty keychain simply makes every pull anonymous,
+ // which is correct for public registries and fails loudly for private ones.
+ if len(plugins) == 0 {
+ slog.Error("No usable image credential providers; image pulls will be anonymous",
+ slog.String("config", configPath))
+ }
+ return plugins, nil
+}
+
+// newPlugin validates one provider entry and binds it to its executable.
+func newPlugin(cfg *kubeletconfigv1.CredentialProvider, binDir string) (*plugin, error) {
+ if cfg.Name == "" {
+ return nil, fmt.Errorf("provider name is required")
+ }
+ // The name is joined onto binDir, so it must be a bare file name: a
+ // provider called "../../bin/sh" would otherwise escape the mounted dir.
+ if strings.ContainsRune(cfg.Name, filepath.Separator) {
+ return nil, fmt.Errorf("provider name %q must not contain %q", cfg.Name, string(filepath.Separator))
+ }
+ if len(cfg.MatchImages) == 0 {
+ return nil, fmt.Errorf("provider %q: matchImages is required", cfg.Name)
+ }
+ for _, m := range cfg.MatchImages {
+ if _, err := matchesImage(m, "example.registry.io/image"); err != nil {
+ return nil, fmt.Errorf("provider %q: invalid matchImages entry: %w", cfg.Name, err)
+ }
+ }
+ if cfg.DefaultCacheDuration == nil {
+ return nil, fmt.Errorf("provider %q: defaultCacheDuration is required", cfg.Name)
+ }
+ if cfg.DefaultCacheDuration.Duration < 0 {
+ return nil, fmt.Errorf("provider %q: defaultCacheDuration must not be negative", cfg.Name)
+ }
+ if cfg.APIVersion != supportedAPIVersion {
+ return nil, fmt.Errorf("provider %q: apiVersion %q is not supported (want %q)", cfg.Name, cfg.APIVersion, supportedAPIVersion)
+ }
+ // Service-account-bound image pull credentials require minting a token for
+ // the pulling pod's service account, which atelet does not do: it pulls on
+ // behalf of actors, which have no Kubernetes service account.
+ if cfg.TokenAttributes != nil {
+ return nil, fmt.Errorf("provider %q: tokenAttributes is not supported", cfg.Name)
+ }
+
+ env := make([]string, 0, len(cfg.Env))
+ for _, e := range cfg.Env {
+ env = append(env, e.Name+"="+e.Value)
+ }
+
+ return &plugin{
+ name: cfg.Name,
+ path: filepath.Join(binDir, cfg.Name),
+ args: cfg.Args,
+ env: env,
+ matchImages: cfg.MatchImages,
+ defaultCacheDuration: cfg.DefaultCacheDuration.Duration,
+ cache: map[string]cacheEntry{},
+ }, nil
+}
diff --git a/internal/credentialprovider/config_test.go b/internal/credentialprovider/config_test.go
new file mode 100644
index 0000000000..14f0dd1830
--- /dev/null
+++ b/internal/credentialprovider/config_test.go
@@ -0,0 +1,281 @@
+// Copyright 2026 Google LLC
+//
+// 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 credentialprovider
+
+import (
+ "path/filepath"
+ "testing"
+ "time"
+)
+
+// gkeConfig is verbatim /etc/srv/kubernetes/cri_auth_config.yaml from a GKE
+// node, the config this package exists to consume.
+const gkeConfig = `kind: CredentialProviderConfig
+apiVersion: kubelet.config.k8s.io/v1
+providers:
+ - name: auth-provider-gcp
+ apiVersion: credentialprovider.kubelet.k8s.io/v1
+ matchImages:
+ - "container.cloud.google.com"
+ - "gcr.io"
+ - "*.gcr.io"
+ - "*.pkg.dev"
+ args:
+ - get-credentials
+ - --v=3
+ defaultCacheDuration: 1m
+`
+
+func TestLoadConfigGKE(t *testing.T) {
+ t.Parallel()
+ dir := t.TempDir()
+ plugins, err := loadConfig(writeConfig(t, dir, gkeConfig), "/home/kubernetes/bin")
+ if err != nil {
+ t.Fatalf("loadConfig returned unexpected error: %v", err)
+ }
+ if len(plugins) != 1 {
+ t.Fatalf("loadConfig returned %d plugins, want 1", len(plugins))
+ }
+ p := plugins[0]
+ if want := filepath.Join("/home/kubernetes/bin", "auth-provider-gcp"); p.path != want {
+ t.Errorf("plugin path = %q, want %q", p.path, want)
+ }
+ if p.defaultCacheDuration != time.Minute {
+ t.Errorf("plugin defaultCacheDuration = %v, want 1m", p.defaultCacheDuration)
+ }
+ if len(p.args) != 2 || p.args[0] != "get-credentials" {
+ t.Errorf("plugin args = %v, want [get-credentials --v=3]", p.args)
+ }
+
+ // The provider must claim Artifact Registry and GCR but nothing else.
+ for _, tc := range []struct {
+ image string
+ want bool
+ }{
+ {image: "gcr.io/proj/img", want: true},
+ {image: "us.gcr.io/proj/img", want: true},
+ {image: "us-central1-docker.pkg.dev/proj/repo/img", want: true},
+ {image: "docker.io/library/busybox", want: false},
+ {image: "quay.io/proj/img", want: false},
+ } {
+ got, err := p.claims(tc.image)
+ if err != nil {
+ t.Fatalf("claims(%q) returned unexpected error: %v", tc.image, err)
+ }
+ if got != tc.want {
+ t.Errorf("claims(%q) = %v, want %v", tc.image, got, tc.want)
+ }
+ }
+}
+
+// Config-level problems mean atelet was pointed at the wrong file, and are
+// the only thing loadConfig treats as an error.
+func TestLoadConfigRejectsInvalidConfig(t *testing.T) {
+ t.Parallel()
+ for _, tc := range []struct {
+ name string
+ config string
+ }{
+ {
+ name: "wrong kind",
+ config: `kind: KubeletConfiguration
+apiVersion: kubelet.config.k8s.io/v1
+providers:
+ - name: p
+ apiVersion: credentialprovider.kubelet.k8s.io/v1
+ matchImages: ["gcr.io"]
+ defaultCacheDuration: 1m
+`,
+ },
+ {
+ name: "wrong config apiVersion",
+ config: `kind: CredentialProviderConfig
+apiVersion: kubelet.config.k8s.io/v1beta1
+providers:
+ - name: p
+ apiVersion: credentialprovider.kubelet.k8s.io/v1
+ matchImages: ["gcr.io"]
+ defaultCacheDuration: 1m
+`,
+ },
+ {
+ name: "not yaml",
+ config: "this: is: not: valid: yaml:\n",
+ },
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+ dir := t.TempDir()
+ if _, err := loadConfig(writeConfig(t, dir, tc.config), dir); err == nil {
+ t.Error("loadConfig accepted an invalid config, want an error")
+ }
+ })
+ }
+}
+
+// A provider entry atelet cannot use is skipped, not fatal: the config belongs
+// to the node and is shared with its kubelet, so one unsupported entry must not
+// cost us the others (or the whole atelet).
+func TestLoadConfigSkipsUnusableProviders(t *testing.T) {
+ t.Parallel()
+ // Each case pairs one unusable provider with one good "keeper", so the
+ // assertion covers both that the bad one is dropped and that the rest of
+ // the config still loads.
+ for _, tc := range []struct {
+ name string
+ bad string
+ }{
+ {
+ name: "missing name",
+ bad: ` - apiVersion: credentialprovider.kubelet.k8s.io/v1
+ matchImages: ["quay.io"]
+ defaultCacheDuration: 1m
+`,
+ },
+ {
+ name: "name escapes the bin dir",
+ bad: ` - name: ../../bin/sh
+ apiVersion: credentialprovider.kubelet.k8s.io/v1
+ matchImages: ["quay.io"]
+ defaultCacheDuration: 1m
+`,
+ },
+ {
+ name: "no matchImages",
+ bad: ` - name: bad
+ apiVersion: credentialprovider.kubelet.k8s.io/v1
+ defaultCacheDuration: 1m
+`,
+ },
+ {
+ name: "missing defaultCacheDuration",
+ bad: ` - name: bad
+ apiVersion: credentialprovider.kubelet.k8s.io/v1
+ matchImages: ["quay.io"]
+`,
+ },
+ {
+ name: "unsupported exec apiVersion",
+ bad: ` - name: bad
+ apiVersion: credentialprovider.kubelet.k8s.io/v1beta1
+ matchImages: ["quay.io"]
+ defaultCacheDuration: 1m
+`,
+ },
+ {
+ name: "service account token attributes",
+ bad: ` - name: bad
+ apiVersion: credentialprovider.kubelet.k8s.io/v1
+ matchImages: ["quay.io"]
+ defaultCacheDuration: 1m
+ tokenAttributes:
+ serviceAccountTokenAudience: aud
+ cacheType: Token
+ requireServiceAccount: true
+`,
+ },
+ {
+ name: "duplicate of the keeper",
+ bad: ` - name: keeper
+ apiVersion: credentialprovider.kubelet.k8s.io/v1
+ matchImages: ["quay.io"]
+ defaultCacheDuration: 1m
+`,
+ },
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+ dir := t.TempDir()
+ config := "kind: CredentialProviderConfig\napiVersion: kubelet.config.k8s.io/v1\nproviders:\n" +
+ ` - name: keeper
+ apiVersion: credentialprovider.kubelet.k8s.io/v1
+ matchImages: ["gcr.io"]
+ defaultCacheDuration: 1m
+` + tc.bad
+
+ plugins, err := loadConfig(writeConfig(t, dir, config), dir)
+ if err != nil {
+ t.Fatalf("loadConfig returned an error for an unusable provider, want it skipped: %v", err)
+ }
+ if len(plugins) != 1 {
+ names := make([]string, 0, len(plugins))
+ for _, p := range plugins {
+ names = append(names, p.name)
+ }
+ t.Fatalf("loadConfig returned providers %v, want only [keeper]", names)
+ }
+ if plugins[0].name != "keeper" {
+ t.Errorf("surviving provider is %q, want %q", plugins[0].name, "keeper")
+ }
+ // The keeper must be intact, not just present.
+ if got, err := plugins[0].claims("gcr.io/proj/img"); err != nil || !got {
+ t.Errorf("keeper.claims(gcr.io/proj/img) = %v, %v; want true, nil", got, err)
+ }
+ })
+ }
+}
+
+// An unusable config is degraded to "everything pulls anonymously" rather than
+// killing atelet, so a node whose providers we cannot run still serves actors
+// whose images are public or already cached.
+func TestLoadConfigEmptyWhenNoProviderUsable(t *testing.T) {
+ t.Parallel()
+ for _, tc := range []struct {
+ name string
+ config string
+ }{
+ {
+ name: "no providers declared",
+ config: `kind: CredentialProviderConfig
+apiVersion: kubelet.config.k8s.io/v1
+providers: []
+`,
+ },
+ {
+ name: "only provider is unusable",
+ config: `kind: CredentialProviderConfig
+apiVersion: kubelet.config.k8s.io/v1
+providers:
+ - name: bad
+ apiVersion: credentialprovider.kubelet.k8s.io/v1
+ matchImages: ["gcr.io"]
+ defaultCacheDuration: 1m
+ tokenAttributes:
+ serviceAccountTokenAudience: aud
+ cacheType: Token
+ requireServiceAccount: true
+`,
+ },
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+ dir := t.TempDir()
+ plugins, err := loadConfig(writeConfig(t, dir, tc.config), dir)
+ if err != nil {
+ t.Fatalf("loadConfig returned an error, want an empty provider set: %v", err)
+ }
+ if len(plugins) != 0 {
+ t.Errorf("loadConfig returned %d providers, want 0", len(plugins))
+ }
+ })
+ }
+}
+
+func TestLoadConfigMissingFile(t *testing.T) {
+ t.Parallel()
+ if _, err := loadConfig(filepath.Join(t.TempDir(), "absent.yaml"), "/bin"); err == nil {
+ t.Error("loadConfig accepted a missing config file, want an error")
+ }
+}
diff --git a/internal/credentialprovider/credentialprovider.go b/internal/credentialprovider/credentialprovider.go
new file mode 100644
index 0000000000..5b0fd68151
--- /dev/null
+++ b/internal/credentialprovider/credentialprovider.go
@@ -0,0 +1,245 @@
+// Copyright 2026 Google LLC
+//
+// 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 credentialprovider resolves container registry credentials by
+// invoking kubelet image credential provider plugins
+// (https://kubernetes.io/docs/tasks/administer-cluster/kubelet-credential-provider/).
+//
+// The kubelet already solves "which cloud's credentials does this registry
+// want" on every managed node: the node ships a provider executable and a
+// CredentialProviderConfig naming the registries it handles. Reusing that
+// contract lets atelet pull from a cloud's registry with no cloud SDK compiled
+// in — the same atelet binary authenticates to Artifact Registry on GKE and to
+// ECR on EKS, given each node's own provider plugin.
+//
+// The provider binary and config are supplied to atelet as read-only host
+// mounts; see manifests/ate-install/atelet.yaml.
+package credentialprovider
+
+import (
+ "context"
+ "log/slog"
+ "sync"
+ "time"
+
+ "github.com/google/go-containerregistry/pkg/authn"
+ credentialproviderv1 "k8s.io/kubelet/pkg/apis/credentialprovider/v1"
+)
+
+// Keychain is a go-containerregistry authn.Keychain backed by kubelet image
+// credential provider plugins. It is safe for concurrent use.
+type Keychain struct {
+ plugins []*plugin
+}
+
+var (
+ _ authn.Keychain = (*Keychain)(nil)
+ _ authn.ContextKeychain = (*Keychain)(nil)
+)
+
+// New loads the CredentialProviderConfig at configPath and resolves each
+// provider's executable inside binDir. Both correspond to the kubelet flags of
+// the same name (--image-credential-provider-config and
+// --image-credential-provider-bin-dir); pointing atelet at the node's own
+// values makes it authenticate exactly like the kubelet on that node.
+func New(configPath, binDir string) (*Keychain, error) {
+ plugins, err := loadConfig(configPath, binDir)
+ if err != nil {
+ return nil, err
+ }
+ names := make([]string, 0, len(plugins))
+ for _, p := range plugins {
+ names = append(names, p.name)
+ }
+ slog.Info("Loaded image credential providers",
+ slog.String("config", configPath),
+ slog.String("binDir", binDir),
+ slog.Any("providers", names),
+ )
+ return &Keychain{plugins: plugins}, nil
+}
+
+// Resolve implements authn.Keychain.
+func (k *Keychain) Resolve(target authn.Resource) (authn.Authenticator, error) {
+ return k.ResolveContext(context.Background(), target)
+}
+
+// ResolveContext implements authn.ContextKeychain. It returns the credentials
+// of the first configured provider that both claims the image (via
+// matchImages) and returns an auth entry for it, and authn.Anonymous when no
+// provider does — an unauthenticated pull, which is what public registries
+// want.
+func (k *Keychain) ResolveContext(ctx context.Context, target authn.Resource) (authn.Authenticator, error) {
+ // authn.Resource.String() is the registry and repository with no tag or
+ // digest ("us-docker.pkg.dev/proj/repo/image"), which is exactly the
+ // granularity the plugin protocol's cache keys are defined at.
+ image := target.String()
+
+ for _, p := range k.plugins {
+ claims, err := p.claims(image)
+ if err != nil {
+ return nil, err
+ }
+ if !claims {
+ continue
+ }
+ auth, err := p.provide(ctx, image)
+ if err != nil {
+ return nil, err
+ }
+ if auth == nil {
+ slog.Debug("Image credential provider returned no credentials",
+ slog.String("provider", p.name), slog.String("image", image))
+ continue
+ }
+ // Info, not Debug: this is the record of which provider authenticated a
+ // pull, which is the first thing anyone debugging a 401 in the field
+ // wants. Digest-pinned images (the API requires them) only reach a
+ // credential resolution on a cache miss, so this is once per image per
+ // node, not once per actor.
+ slog.InfoContext(ctx, "Resolved image credentials from credential provider",
+ slog.String("provider", p.name), slog.String("image", image))
+ return authn.FromConfig(authn.AuthConfig{
+ Username: auth.Username,
+ Password: auth.Password,
+ }), nil
+ }
+ return authn.Anonymous, nil
+}
+
+// cacheEntry is one plugin response held until expiry. A nil auth is cached
+// too: "this plugin has no credentials for you" is an answer worth not
+// re-execing a subprocess for.
+type cacheEntry struct {
+ auth *credentialproviderv1.AuthConfig
+ expiresAt time.Time
+}
+
+// plugin is one configured provider executable and its response cache.
+type plugin struct {
+ name string
+ path string
+ args []string
+ env []string
+ matchImages []string
+ defaultCacheDuration time.Duration
+
+ mu sync.Mutex
+ cache map[string]cacheEntry
+ // now is time.Now, overridden in tests.
+ now func() time.Time
+}
+
+// claims reports whether this plugin is configured to handle image.
+func (p *plugin) claims(image string) (bool, error) {
+ for _, m := range p.matchImages {
+ matched, err := matchesImage(m, image)
+ if err != nil {
+ return false, err
+ }
+ if matched {
+ return true, nil
+ }
+ }
+ return false, nil
+}
+
+// globalCacheKey is the cache key for plugins that answer identically for
+// every image they claim (cacheKeyType: Global).
+const globalCacheKey = "global"
+
+// provide returns cached credentials for image, or execs the plugin and caches
+// what it returns. A nil AuthConfig with a nil error means the plugin has no
+// credentials for this image.
+func (p *plugin) provide(ctx context.Context, image string) (*credentialproviderv1.AuthConfig, error) {
+ if entry, ok := p.lookup(image); ok {
+ return entry, nil
+ }
+
+ resp, err := p.exec(ctx, image)
+ if err != nil {
+ return nil, err
+ }
+
+ key, err := bestAuthKey(resp.Auth, image)
+ if err != nil {
+ return nil, err
+ }
+ var auth *credentialproviderv1.AuthConfig
+ if key != "" {
+ matched := resp.Auth[key]
+ auth = &matched
+ }
+
+ p.store(image, resp, auth)
+ return auth, nil
+}
+
+// lookup checks the cache under each key type the plugin might have stored
+// this image's credentials at, most specific first — mirroring the kubelet,
+// which cannot know a plugin's cacheKeyType until it has answered once.
+func (p *plugin) lookup(image string) (*credentialproviderv1.AuthConfig, bool) {
+ p.mu.Lock()
+ defer p.mu.Unlock()
+ for _, key := range []string{image, registryOf(image), globalCacheKey} {
+ entry, ok := p.cache[key]
+ if !ok {
+ continue
+ }
+ if !p.timeNow().Before(entry.expiresAt) {
+ delete(p.cache, key)
+ continue
+ }
+ return entry.auth, true
+ }
+ return nil, false
+}
+
+// store caches auth under the key type the plugin asked for. A response
+// carrying an unrecognized cacheKeyType, or an explicit zero duration, is not
+// cached at all.
+func (p *plugin) store(image string, resp *credentialproviderv1.CredentialProviderResponse, auth *credentialproviderv1.AuthConfig) {
+ var key string
+ switch resp.CacheKeyType {
+ case credentialproviderv1.ImagePluginCacheKeyType:
+ key = image
+ case credentialproviderv1.RegistryPluginCacheKeyType:
+ key = registryOf(image)
+ case credentialproviderv1.GlobalPluginCacheKeyType:
+ key = globalCacheKey
+ default:
+ slog.Warn("Image credential provider returned an unknown cacheKeyType; not caching",
+ slog.String("provider", p.name), slog.String("cacheKeyType", string(resp.CacheKeyType)))
+ return
+ }
+
+ duration := p.defaultCacheDuration
+ if resp.CacheDuration != nil {
+ duration = resp.CacheDuration.Duration
+ }
+ if duration <= 0 {
+ return
+ }
+
+ p.mu.Lock()
+ defer p.mu.Unlock()
+ p.cache[key] = cacheEntry{auth: auth, expiresAt: p.timeNow().Add(duration)}
+}
+
+func (p *plugin) timeNow() time.Time {
+ if p.now != nil {
+ return p.now()
+ }
+ return time.Now()
+}
diff --git a/internal/credentialprovider/credentialprovider_test.go b/internal/credentialprovider/credentialprovider_test.go
new file mode 100644
index 0000000000..c8838b0946
--- /dev/null
+++ b/internal/credentialprovider/credentialprovider_test.go
@@ -0,0 +1,547 @@
+// Copyright 2026 Google LLC
+//
+// 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 credentialprovider
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/google/go-containerregistry/pkg/authn"
+ "github.com/google/go-containerregistry/pkg/name"
+)
+
+// fakePlugin writes an executable named name into dir that records each
+// request it is handed (one JSON object per line in
/.requests) and
+// replies with response. It returns the requests file's path.
+func fakePlugin(t *testing.T, dir, pluginName, response string) string {
+ t.Helper()
+ requests := filepath.Join(dir, pluginName+".requests")
+ script := fmt.Sprintf(`#!/bin/sh
+cat >> %q
+echo >> %q
+cat <<'RESPONSE'
+%s
+RESPONSE
+`, requests, requests, response)
+ if err := os.WriteFile(filepath.Join(dir, pluginName), []byte(script), 0o700); err != nil {
+ t.Fatalf("Failed to write fake plugin: %v", err)
+ }
+ return requests
+}
+
+// failingPlugin writes an executable that prints message to stderr and exits
+// non-zero.
+func failingPlugin(t *testing.T, dir, pluginName, message string) {
+ t.Helper()
+ script := fmt.Sprintf("#!/bin/sh\necho %q >&2\nexit 7\n", message)
+ if err := os.WriteFile(filepath.Join(dir, pluginName), []byte(script), 0o700); err != nil {
+ t.Fatalf("Failed to write failing plugin: %v", err)
+ }
+}
+
+// writeConfig writes a CredentialProviderConfig into dir and returns its path.
+func writeConfig(t *testing.T, dir, contents string) string {
+ t.Helper()
+ path := filepath.Join(dir, "config.yaml")
+ if err := os.WriteFile(path, []byte(contents), 0o600); err != nil {
+ t.Fatalf("Failed to write credential provider config: %v", err)
+ }
+ return path
+}
+
+// gcpLikeConfig mirrors the CredentialProviderConfig a GKE node ships.
+const gcpLikeConfig = `kind: CredentialProviderConfig
+apiVersion: kubelet.config.k8s.io/v1
+providers:
+ - name: fake-provider
+ apiVersion: credentialprovider.kubelet.k8s.io/v1
+ matchImages:
+ - "gcr.io"
+ - "*.gcr.io"
+ - "*.pkg.dev"
+ args:
+ - get-credentials
+ defaultCacheDuration: 1m
+`
+
+// repo turns an image reference into the authn.Resource a pull would resolve
+// against.
+func repo(t *testing.T, ref string) authn.Resource {
+ t.Helper()
+ parsed, err := name.ParseReference(ref)
+ if err != nil {
+ t.Fatalf("Failed to parse reference %q: %v", ref, err)
+ }
+ return parsed.Context()
+}
+
+// resolvedAuth resolves ref through kc and returns the resulting basic auth.
+func resolvedAuth(t *testing.T, kc *Keychain, ref string) *authn.AuthConfig {
+ t.Helper()
+ authenticator, err := kc.Resolve(repo(t, ref))
+ if err != nil {
+ t.Fatalf("Resolve(%q) returned unexpected error: %v", ref, err)
+ }
+ cfg, err := authenticator.Authorization()
+ if err != nil {
+ t.Fatalf("Authorization() returned unexpected error: %v", err)
+ }
+ return cfg
+}
+
+func TestKeychainResolvesMatchingImage(t *testing.T) {
+ t.Parallel()
+ dir := t.TempDir()
+ requests := fakePlugin(t, dir, "fake-provider", `{
+ "kind": "CredentialProviderResponse",
+ "apiVersion": "credentialprovider.kubelet.k8s.io/v1",
+ "cacheKeyType": "Registry",
+ "auth": {"*.pkg.dev": {"username": "_token", "password": "ya29.fake"}}
+}`)
+
+ kc, err := New(writeConfig(t, dir, gcpLikeConfig), dir)
+ if err != nil {
+ t.Fatalf("New returned unexpected error: %v", err)
+ }
+
+ got := resolvedAuth(t, kc, "us-central1-docker.pkg.dev/proj/repo/img:latest")
+ if got.Username != "_token" || got.Password != "ya29.fake" {
+ t.Errorf("Resolve returned username %q password %q, want %q / %q", got.Username, got.Password, "_token", "ya29.fake")
+ }
+
+ // The plugin must be handed a well-formed request naming the repository
+ // without its tag, which is the granularity the protocol's cache keys use.
+ raw, err := os.ReadFile(requests)
+ if err != nil {
+ t.Fatalf("Failed to read recorded requests: %v", err)
+ }
+ var req struct {
+ Kind string `json:"kind"`
+ APIVersion string `json:"apiVersion"`
+ Image string `json:"image"`
+ }
+ if err := json.Unmarshal([]byte(strings.TrimSpace(string(raw))), &req); err != nil {
+ t.Fatalf("Failed to parse recorded request %q: %v", raw, err)
+ }
+ if req.Kind != "CredentialProviderRequest" || req.APIVersion != supportedAPIVersion {
+ t.Errorf("Plugin received kind %q apiVersion %q, want %q / %q", req.Kind, req.APIVersion, "CredentialProviderRequest", supportedAPIVersion)
+ }
+ if want := "us-central1-docker.pkg.dev/proj/repo/img"; req.Image != want {
+ t.Errorf("Plugin received image %q, want %q", req.Image, want)
+ }
+}
+
+func TestKeychainSkipsUnmatchedImage(t *testing.T) {
+ t.Parallel()
+ dir := t.TempDir()
+ // An image no provider claims must not exec anything, so point the config
+ // at a plugin that would fail loudly if it ever ran.
+ failingPlugin(t, dir, "fake-provider", "plugin must not be invoked")
+
+ kc, err := New(writeConfig(t, dir, gcpLikeConfig), dir)
+ if err != nil {
+ t.Fatalf("New returned unexpected error: %v", err)
+ }
+
+ authenticator, err := kc.Resolve(repo(t, "docker.io/library/busybox:latest"))
+ if err != nil {
+ t.Fatalf("Resolve returned unexpected error: %v", err)
+ }
+ if authenticator != authn.Anonymous {
+ t.Errorf("Resolve returned %v for an unclaimed image, want authn.Anonymous", authenticator)
+ }
+}
+
+func TestKeychainAnonymousWhenPluginReturnsNoAuth(t *testing.T) {
+ t.Parallel()
+ dir := t.TempDir()
+ fakePlugin(t, dir, "fake-provider", `{
+ "kind": "CredentialProviderResponse",
+ "apiVersion": "credentialprovider.kubelet.k8s.io/v1",
+ "cacheKeyType": "Registry"
+}`)
+
+ kc, err := New(writeConfig(t, dir, gcpLikeConfig), dir)
+ if err != nil {
+ t.Fatalf("New returned unexpected error: %v", err)
+ }
+
+ authenticator, err := kc.Resolve(repo(t, "gcr.io/proj/img:latest"))
+ if err != nil {
+ t.Fatalf("Resolve returned unexpected error: %v", err)
+ }
+ if authenticator != authn.Anonymous {
+ t.Errorf("Resolve returned %v when the plugin gave no credentials, want authn.Anonymous", authenticator)
+ }
+}
+
+func TestKeychainSurfacesPluginFailure(t *testing.T) {
+ t.Parallel()
+ dir := t.TempDir()
+ failingPlugin(t, dir, "fake-provider", "metadata server unreachable")
+
+ kc, err := New(writeConfig(t, dir, gcpLikeConfig), dir)
+ if err != nil {
+ t.Fatalf("New returned unexpected error: %v", err)
+ }
+
+ _, err = kc.Resolve(repo(t, "gcr.io/proj/img:latest"))
+ if err == nil {
+ t.Fatal("Resolve returned no error for a failing plugin, want one")
+ }
+ // The plugin's own diagnostics are the only clue to why a pull lost its
+ // credentials, so they must reach the error.
+ if !strings.Contains(err.Error(), "metadata server unreachable") {
+ t.Errorf("Resolve error %q does not carry the plugin's stderr", err)
+ }
+}
+
+func TestKeychainRejectsMismatchedResponseVersion(t *testing.T) {
+ t.Parallel()
+ dir := t.TempDir()
+ fakePlugin(t, dir, "fake-provider", `{
+ "kind": "CredentialProviderResponse",
+ "apiVersion": "credentialprovider.kubelet.k8s.io/v1beta1",
+ "cacheKeyType": "Registry",
+ "auth": {"gcr.io": {"username": "u", "password": "p"}}
+}`)
+
+ kc, err := New(writeConfig(t, dir, gcpLikeConfig), dir)
+ if err != nil {
+ t.Fatalf("New returned unexpected error: %v", err)
+ }
+
+ if _, err := kc.Resolve(repo(t, "gcr.io/proj/img:latest")); err == nil {
+ t.Fatal("Resolve accepted a response encoded at an unrequested apiVersion, want an error")
+ }
+}
+
+func TestKeychainFallsThroughToSecondProvider(t *testing.T) {
+ t.Parallel()
+ dir := t.TempDir()
+ // first-provider claims the image but has nothing for it; the keychain
+ // must go on to ask second-provider rather than give up.
+ fakePlugin(t, dir, "first-provider", `{
+ "kind": "CredentialProviderResponse",
+ "apiVersion": "credentialprovider.kubelet.k8s.io/v1",
+ "cacheKeyType": "Registry",
+ "auth": {}
+}`)
+ fakePlugin(t, dir, "second-provider", `{
+ "kind": "CredentialProviderResponse",
+ "apiVersion": "credentialprovider.kubelet.k8s.io/v1",
+ "cacheKeyType": "Registry",
+ "auth": {"gcr.io": {"username": "second", "password": "creds"}}
+}`)
+
+ kc, err := New(writeConfig(t, dir, `kind: CredentialProviderConfig
+apiVersion: kubelet.config.k8s.io/v1
+providers:
+ - name: first-provider
+ apiVersion: credentialprovider.kubelet.k8s.io/v1
+ matchImages: ["gcr.io"]
+ defaultCacheDuration: 1m
+ - name: second-provider
+ apiVersion: credentialprovider.kubelet.k8s.io/v1
+ matchImages: ["gcr.io"]
+ defaultCacheDuration: 1m
+`), dir)
+ if err != nil {
+ t.Fatalf("New returned unexpected error: %v", err)
+ }
+
+ got := resolvedAuth(t, kc, "gcr.io/proj/img:latest")
+ if got.Username != "second" {
+ t.Errorf("Resolve returned username %q, want %q from the second provider", got.Username, "second")
+ }
+}
+
+// countRequests returns how many requests the fake plugin has recorded.
+func countRequests(t *testing.T, requests string) int {
+ t.Helper()
+ raw, err := os.ReadFile(requests)
+ if err != nil {
+ t.Fatalf("Failed to read recorded requests: %v", err)
+ }
+ return len(strings.Fields(strings.TrimSpace(string(raw))))
+}
+
+func TestKeychainCaching(t *testing.T) {
+ t.Parallel()
+ for _, tc := range []struct {
+ name string
+ // cacheKeyType and cacheDuration as the plugin reports them.
+ cacheKeyType string
+ cacheDuration string
+ // second is resolved after the first image; wantExecs is how many
+ // times the plugin should have run in total by then.
+ first, second string
+ wantExecs int
+ }{
+ {
+ name: "registry key reuses across repositories",
+ cacheKeyType: "Registry",
+ first: "gcr.io/proj/one:latest",
+ second: "gcr.io/proj/two:latest",
+ wantExecs: 1,
+ },
+ {
+ name: "registry key does not span registries",
+ cacheKeyType: "Registry",
+ first: "gcr.io/proj/one:latest",
+ second: "us.gcr.io/proj/one:latest",
+ wantExecs: 2,
+ },
+ {
+ name: "image key does not span repositories",
+ cacheKeyType: "Image",
+ first: "gcr.io/proj/one:latest",
+ second: "gcr.io/proj/two:latest",
+ wantExecs: 2,
+ },
+ {
+ name: "image key reuses for the same repository",
+ cacheKeyType: "Image",
+ first: "gcr.io/proj/one:latest",
+ second: "gcr.io/proj/one:other",
+ wantExecs: 1,
+ },
+ {
+ name: "global key spans registries",
+ cacheKeyType: "Global",
+ first: "gcr.io/proj/one:latest",
+ second: "us.gcr.io/other/two:latest",
+ wantExecs: 1,
+ },
+ {
+ name: "zero duration disables caching",
+ cacheKeyType: "Registry",
+ cacheDuration: `"cacheDuration": "0s",`,
+ first: "gcr.io/proj/one:latest",
+ second: "gcr.io/proj/one:latest",
+ wantExecs: 2,
+ },
+ {
+ name: "unknown key type disables caching",
+ cacheKeyType: "Nonsense",
+ first: "gcr.io/proj/one:latest",
+ second: "gcr.io/proj/one:latest",
+ wantExecs: 2,
+ },
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+ dir := t.TempDir()
+ requests := fakePlugin(t, dir, "fake-provider", fmt.Sprintf(`{
+ "kind": "CredentialProviderResponse",
+ "apiVersion": "credentialprovider.kubelet.k8s.io/v1",
+ "cacheKeyType": %q,
+ %s
+ "auth": {"*.gcr.io": {"username": "u", "password": "p"}, "gcr.io": {"username": "u", "password": "p"}}
+}`, tc.cacheKeyType, tc.cacheDuration))
+
+ kc, err := New(writeConfig(t, dir, gcpLikeConfig), dir)
+ if err != nil {
+ t.Fatalf("New returned unexpected error: %v", err)
+ }
+
+ for _, ref := range []string{tc.first, tc.second} {
+ if _, err := kc.Resolve(repo(t, ref)); err != nil {
+ t.Fatalf("Resolve(%q) returned unexpected error: %v", ref, err)
+ }
+ }
+ if got := countRequests(t, requests); got != tc.wantExecs {
+ t.Errorf("Plugin ran %d times, want %d", got, tc.wantExecs)
+ }
+ })
+ }
+}
+
+func TestKeychainCacheExpires(t *testing.T) {
+ t.Parallel()
+ dir := t.TempDir()
+ requests := fakePlugin(t, dir, "fake-provider", `{
+ "kind": "CredentialProviderResponse",
+ "apiVersion": "credentialprovider.kubelet.k8s.io/v1",
+ "cacheKeyType": "Registry",
+ "auth": {"gcr.io": {"username": "u", "password": "p"}}
+}`)
+
+ kc, err := New(writeConfig(t, dir, gcpLikeConfig), dir)
+ if err != nil {
+ t.Fatalf("New returned unexpected error: %v", err)
+ }
+ clock := time.Now()
+ kc.plugins[0].now = func() time.Time { return clock }
+
+ ref := repo(t, "gcr.io/proj/img:latest")
+ if _, err := kc.Resolve(ref); err != nil {
+ t.Fatalf("Resolve returned unexpected error: %v", err)
+ }
+ // The config's defaultCacheDuration is 1m; just short of it still hits.
+ clock = clock.Add(59 * time.Second)
+ if _, err := kc.Resolve(ref); err != nil {
+ t.Fatalf("Resolve returned unexpected error: %v", err)
+ }
+ if got := countRequests(t, requests); got != 1 {
+ t.Fatalf("Plugin ran %d times before the cache expired, want 1", got)
+ }
+
+ clock = clock.Add(2 * time.Second)
+ if _, err := kc.Resolve(ref); err != nil {
+ t.Fatalf("Resolve returned unexpected error: %v", err)
+ }
+ if got := countRequests(t, requests); got != 2 {
+ t.Errorf("Plugin ran %d times after the cache expired, want 2", got)
+ }
+}
+
+// Not parallel: t.Setenv mutates the process environment the plugin inherits.
+func TestKeychainPassesConfiguredEnv(t *testing.T) {
+ dir := t.TempDir()
+ // The plugin echoes an env var back as the username, so the assertion
+ // covers both the configured env and the inherited process env.
+ script := `#!/bin/sh
+cat > /dev/null
+cat < /dev/null
+cat < maxResponseBytes {
+ return nil, fmt.Errorf("credential provider %q returned %d bytes, over the %d byte limit", p.name, stdout.Len(), maxResponseBytes)
+ }
+
+ var resp credentialproviderv1.CredentialProviderResponse
+ if err := json.Unmarshal(stdout.Bytes(), &resp); err != nil {
+ return nil, fmt.Errorf("while decoding response from credential provider %q: %w (stderr: %s)", p.name, err, truncate(stderr.String()))
+ }
+ // A response encoded at a version we did not ask for may have different
+ // field semantics, so refuse it rather than misread the credentials.
+ if resp.APIVersion != supportedAPIVersion {
+ return nil, fmt.Errorf("credential provider %q responded with apiVersion %q, want %q", p.name, resp.APIVersion, supportedAPIVersion)
+ }
+ return &resp, nil
+}
+
+// truncate bounds plugin stderr so a chatty provider cannot flood atelet's
+// logs through an error message.
+func truncate(s string) string {
+ const limit = 2048
+ s = strings.TrimSpace(s)
+ if len(s) <= limit {
+ return s
+ }
+ return s[:limit] + "... (truncated)"
+}
diff --git a/internal/credentialprovider/match.go b/internal/credentialprovider/match.go
new file mode 100644
index 0000000000..9b34088d85
--- /dev/null
+++ b/internal/credentialprovider/match.go
@@ -0,0 +1,120 @@
+// Copyright 2026 Google LLC
+//
+// 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 credentialprovider
+
+import (
+ "fmt"
+ "net"
+ "net/url"
+ "path/filepath"
+ "sort"
+ "strings"
+)
+
+// matchesImage reports whether glob matches image, using the kubelet's
+// credential-provider matching rules (k8s.io/kubernetes/pkg/credentialprovider
+// urlsMatch): the domain is split on "." and the path on "/", each resulting
+// segment is matched with filepath.Match (so a glob never spans a segment,
+// i.e. "*.io" does not match "k8s.gcr.io"), the glob's segments must be a
+// prefix of the image's, and ports must be equal.
+//
+// Both arguments are scheme-less: "*.pkg.dev", "registry.io:8080/path",
+// "us-central1-docker.pkg.dev/proj/repo/img".
+func matchesImage(glob, image string) (bool, error) {
+ globURL, err := parseSchemelessURL(glob)
+ if err != nil {
+ return false, fmt.Errorf("while parsing match pattern %q: %w", glob, err)
+ }
+ imageURL, err := parseSchemelessURL(image)
+ if err != nil {
+ return false, fmt.Errorf("while parsing image %q: %w", image, err)
+ }
+
+ globParts, globPort := splitURL(globURL)
+ imageParts, imagePort := splitURL(imageURL)
+ if globPort != imagePort {
+ return false, nil
+ }
+ // The pattern may be less specific than the image (a bare registry matches
+ // every repository under it), but never more.
+ if len(globParts) > len(imageParts) {
+ return false, nil
+ }
+ for i, globPart := range globParts {
+ matched, err := filepath.Match(globPart, imageParts[i])
+ if err != nil {
+ return false, fmt.Errorf("while matching pattern %q against image %q: %w", glob, image, err)
+ }
+ if !matched {
+ return false, nil
+ }
+ }
+ return true, nil
+}
+
+// parseSchemelessURL parses a registry/repository string that carries no
+// scheme by lending it one, so net/url splits host, port and path for us.
+func parseSchemelessURL(schemeless string) (*url.URL, error) {
+ parsed, err := url.Parse("https://" + schemeless)
+ if err != nil {
+ return nil, err
+ }
+ parsed.Scheme = ""
+ return parsed, nil
+}
+
+// splitURL flattens a URL into the segment list matchesImage compares: the
+// host split on "." followed by the path split on "/", with the port returned
+// separately (globs are not allowed in ports).
+func splitURL(u *url.URL) (parts []string, port string) {
+ host, port, err := net.SplitHostPort(u.Host)
+ if err != nil {
+ host, port = u.Host, ""
+ }
+ return append(strings.Split(host, "."), strings.Split(u.Path, "/")...), port
+}
+
+// bestAuthKey picks the key of auth that best matches image, or "" when none
+// does. Ties are broken the way the kubelet's docker keyring breaks them —
+// reverse lexical order — which puts longer keys ahead of shorter keys sharing
+// their prefix, and concrete keys ahead of wildcards (since "*" sorts below
+// every character a registry name can start with).
+func bestAuthKey[V any](auth map[string]V, image string) (string, error) {
+ var matches []string
+ for key := range auth {
+ matched, err := matchesImage(key, image)
+ if err != nil {
+ return "", err
+ }
+ if matched {
+ matches = append(matches, key)
+ }
+ }
+ if len(matches) == 0 {
+ return "", nil
+ }
+ sort.Sort(sort.Reverse(sort.StringSlice(matches)))
+ return matches[0], nil
+}
+
+// registryOf returns the domain (with port, if any) of an image reference,
+// which is the cache key for the "Registry" plugin cache-key type.
+func registryOf(image string) string {
+ parsed, err := parseSchemelessURL(image)
+ if err != nil {
+ return image
+ }
+ return parsed.Host
+}
diff --git a/internal/credentialprovider/match_test.go b/internal/credentialprovider/match_test.go
new file mode 100644
index 0000000000..083bae699a
--- /dev/null
+++ b/internal/credentialprovider/match_test.go
@@ -0,0 +1,127 @@
+// Copyright 2026 Google LLC
+//
+// 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 credentialprovider
+
+import "testing"
+
+func TestMatchesImage(t *testing.T) {
+ t.Parallel()
+ for _, tc := range []struct {
+ name string
+ glob string
+ image string
+ want bool
+ }{
+ {name: "exact registry", glob: "gcr.io", image: "gcr.io/proj/img", want: true},
+ {name: "registry only, no repo", glob: "gcr.io", image: "gcr.io", want: true},
+ {name: "different registry", glob: "gcr.io", image: "quay.io/proj/img"},
+ {name: "wildcard subdomain", glob: "*.gcr.io", image: "us.gcr.io/proj/img", want: true},
+ {name: "wildcard does not match bare domain", glob: "*.gcr.io", image: "gcr.io/proj/img"},
+ {name: "wildcard spans one segment only", glob: "*.gcr.io", image: "a.b.gcr.io/proj/img"},
+ {name: "pkg.dev regional", glob: "*.pkg.dev", image: "us-central1-docker.pkg.dev/proj/repo/img", want: true},
+ {name: "partial subdomain glob", glob: "app*.k8s.io", image: "appfoo.k8s.io/img", want: true},
+ {name: "top level domain glob", glob: "k8s.*", image: "k8s.io/img", want: true},
+ {name: "multiple globs", glob: "*.*.registry.io", image: "a.b.registry.io/img", want: true},
+ {name: "path prefix matches", glob: "registry.io/path", image: "registry.io/path/deeper/img", want: true},
+ {name: "path prefix must match exactly", glob: "registry.io/path", image: "registry.io/other/img"},
+ {name: "image shallower than pattern", glob: "registry.io/a/b/c", image: "registry.io/a"},
+ {name: "matching port", glob: "registry.io:8080/path", image: "registry.io:8080/path/img", want: true},
+ {name: "mismatched port", glob: "registry.io:8080/path", image: "registry.io:9090/path/img"},
+ {name: "pattern port, image none", glob: "registry.io:8080", image: "registry.io/img"},
+ {name: "image port, pattern none", glob: "registry.io", image: "registry.io:8080/img"},
+ {name: "localhost registry", glob: "localhost:5000", image: "localhost:5000/img", want: true},
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+ got, err := matchesImage(tc.glob, tc.image)
+ if err != nil {
+ t.Fatalf("matchesImage(%q, %q) returned unexpected error: %v", tc.glob, tc.image, err)
+ }
+ if got != tc.want {
+ t.Errorf("matchesImage(%q, %q) = %v, want %v", tc.glob, tc.image, got, tc.want)
+ }
+ })
+ }
+}
+
+func TestBestAuthKey(t *testing.T) {
+ t.Parallel()
+ for _, tc := range []struct {
+ name string
+ auth map[string]string
+ image string
+ want string
+ }{
+ {
+ name: "no keys",
+ auth: map[string]string{},
+ image: "gcr.io/proj/img",
+ },
+ {
+ name: "no matching key",
+ auth: map[string]string{"quay.io": "a"},
+ image: "gcr.io/proj/img",
+ },
+ {
+ name: "single match",
+ auth: map[string]string{"*.pkg.dev": "a"},
+ image: "us-docker.pkg.dev/proj/repo/img",
+ want: "*.pkg.dev",
+ },
+ {
+ name: "concrete key beats wildcard",
+ auth: map[string]string{"*.pkg.dev": "a", "us-docker.pkg.dev": "b"},
+ image: "us-docker.pkg.dev/proj/repo/img",
+ want: "us-docker.pkg.dev",
+ },
+ {
+ name: "longer path beats shorter",
+ auth: map[string]string{"gcr.io": "a", "gcr.io/proj": "b"},
+ image: "gcr.io/proj/img",
+ want: "gcr.io/proj",
+ },
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+ got, err := bestAuthKey(tc.auth, tc.image)
+ if err != nil {
+ t.Fatalf("bestAuthKey returned unexpected error: %v", err)
+ }
+ if got != tc.want {
+ t.Errorf("bestAuthKey(%v, %q) = %q, want %q", tc.auth, tc.image, got, tc.want)
+ }
+ })
+ }
+}
+
+func TestRegistryOf(t *testing.T) {
+ t.Parallel()
+ for _, tc := range []struct {
+ image string
+ want string
+ }{
+ {image: "gcr.io/proj/img", want: "gcr.io"},
+ {image: "us-docker.pkg.dev/proj/repo/img", want: "us-docker.pkg.dev"},
+ {image: "localhost:5000/img", want: "localhost:5000"},
+ {image: "gcr.io", want: "gcr.io"},
+ } {
+ t.Run(tc.image, func(t *testing.T) {
+ t.Parallel()
+ if got := registryOf(tc.image); got != tc.want {
+ t.Errorf("registryOf(%q) = %q, want %q", tc.image, got, tc.want)
+ }
+ })
+ }
+}
diff --git a/internal/imagecache/imagecache.go b/internal/imagecache/imagecache.go
index 5ac985fd81..c15bf1a802 100644
--- a/internal/imagecache/imagecache.go
+++ b/internal/imagecache/imagecache.go
@@ -106,6 +106,10 @@ type Store struct {
// GCP credentials (gcr.io / pkg.dev). See remoteOpts.
authenticator authn.Authenticator
+ // keychain, when set, resolves credentials per registry and takes
+ // precedence over authenticator. See remoteOpts.
+ keychain authn.Keychain
+
localhostRegistryReplacement string
// platform overrides the default pull platform (linux/GOARCH), for
@@ -151,6 +155,13 @@ func WithAuthenticator(a authn.Authenticator) Option {
return func(s *Store) { s.authenticator = a }
}
+// WithKeychain attaches a keychain consulted for every pull, whatever the
+// registry — the keychain itself decides which registries it has credentials
+// for. It takes precedence over WithAuthenticator. A nil keychain is ignored.
+func WithKeychain(k authn.Keychain) Option {
+ return func(s *Store) { s.keychain = k }
+}
+
// WithLocalhostRegistryReplacement rewrites localhost/loopback registry refs
// to the given endpoint, mirroring the containerd mirror config used by kind
// local registries (https://kind.sigs.k8s.io/docs/user/local-registry/).
@@ -679,8 +690,10 @@ func (s *Store) remoteOpts(ctx context.Context, parsedRef name.Reference) []remo
remote.WithContext(ctx),
remote.WithPlatform(platform),
}
- registry := parsedRef.Context().Registry.RegistryStr()
- if s.authenticator != nil && registryUsesGCPAuth(registry) {
+ switch registry := parsedRef.Context().Registry.RegistryStr(); {
+ case s.keychain != nil:
+ opts = append(opts, remote.WithAuthFromKeychain(s.keychain))
+ case s.authenticator != nil && registryUsesGCPAuth(registry):
opts = append(opts, remote.WithAuth(s.authenticator))
}
return opts
diff --git a/internal/imagecache/keychain_test.go b/internal/imagecache/keychain_test.go
new file mode 100644
index 0000000000..ae41327645
--- /dev/null
+++ b/internal/imagecache/keychain_test.go
@@ -0,0 +1,132 @@
+// Copyright 2026 Google LLC
+//
+// 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 imagecache
+
+import (
+ "archive/tar"
+ "context"
+ "io"
+ "log"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "sync/atomic"
+ "testing"
+
+ "github.com/google/go-containerregistry/pkg/authn"
+ "github.com/google/go-containerregistry/pkg/registry"
+ v1 "github.com/google/go-containerregistry/pkg/v1"
+)
+
+// newAuthedRegistry starts an in-memory registry that is open until
+// requireAuth is called, after which every request needs the given basic
+// credentials. Tests push their fixtures while it is open, then close it, so
+// any later success is attributable to the credentials under test.
+func newAuthedRegistry(t *testing.T, username, password string) (host string, requireAuth func()) {
+ t.Helper()
+ var authRequired atomic.Bool
+ inner := registry.New(registry.Logger(log.New(io.Discard, "", 0)))
+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if authRequired.Load() {
+ if u, p, ok := r.BasicAuth(); !ok || u != username || p != password {
+ w.Header().Set("WWW-Authenticate", `Basic realm="test"`)
+ w.WriteHeader(http.StatusUnauthorized)
+ return
+ }
+ }
+ inner.ServeHTTP(w, r)
+ }))
+ t.Cleanup(srv.Close)
+ u, err := url.Parse(srv.URL)
+ if err != nil {
+ t.Fatalf("parsing registry URL: %v", err)
+ }
+ return u.Host, func() { authRequired.Store(true) }
+}
+
+// staticKeychain hands out the same credentials for every resource, recording
+// what it was asked about.
+type staticKeychain struct {
+ auth authn.Authenticator
+ resolved []string
+}
+
+func (k *staticKeychain) Resolve(target authn.Resource) (authn.Authenticator, error) {
+ k.resolved = append(k.resolved, target.String())
+ return k.auth, nil
+}
+
+func TestEnsureImage_KeychainAuthenticatesPull(t *testing.T) {
+ host, requireAuth := newAuthedRegistry(t, "robot", "s3cret")
+ ref := host + "/test/app:latest"
+ layer := layerFromEntries(t, []tarEntry{
+ {name: "app/", typeflag: tar.TypeDir},
+ {name: "app/main", typeflag: tar.TypeReg, mode: 0o755, body: "main"},
+ })
+ pushImage(t, ref, v1.Config{}, layer)
+ requireAuth()
+
+ // Without credentials the registry rejects the pull, so a success below is
+ // attributable to the keychain and nothing else.
+ if _, err := newTestStore(t).EnsureImage(context.Background(), ref); err == nil {
+ t.Fatal("EnsureImage succeeded against an authenticated registry with no keychain, want an error")
+ }
+
+ kc := &staticKeychain{auth: authn.FromConfig(authn.AuthConfig{Username: "robot", Password: "s3cret"})}
+ if _, err := newTestStore(t, WithKeychain(kc)).EnsureImage(context.Background(), ref); err != nil {
+ t.Fatalf("EnsureImage with a keychain: %v", err)
+ }
+ if len(kc.resolved) == 0 {
+ t.Error("keychain was never consulted")
+ }
+ for _, got := range kc.resolved {
+ if want := host + "/test/app"; got != want {
+ t.Errorf("keychain resolved %q, want %q", got, want)
+ }
+ }
+}
+
+func TestRemoteOptsKeychainTakesPrecedence(t *testing.T) {
+ kc := &staticKeychain{auth: authn.Anonymous}
+ auth := authn.FromConfig(authn.AuthConfig{Username: "u"})
+
+ // remote.Option values are opaque, so assert on how many the store
+ // attaches: two unconditional ones (context, platform) plus at most one
+ // credential option.
+ const base = 2
+ for _, tc := range []struct {
+ name string
+ opts []Option
+ ref string
+ want int
+ }{
+ {name: "no credentials", ref: "gcr.io/proj/img", want: base},
+ {name: "gcp authenticator, gcp registry", opts: []Option{WithAuthenticator(auth)}, ref: "gcr.io/proj/img", want: base + 1},
+ {name: "gcp authenticator, other registry", opts: []Option{WithAuthenticator(auth)}, ref: "quay.io/proj/img", want: base},
+ {name: "keychain, any registry", opts: []Option{WithKeychain(kc)}, ref: "quay.io/proj/img", want: base + 1},
+ {name: "keychain wins over authenticator", opts: []Option{WithKeychain(kc), WithAuthenticator(auth)}, ref: "quay.io/proj/img", want: base + 1},
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ s := newTestStore(t, tc.opts...)
+ parsed, err := s.parseRef(tc.ref)
+ if err != nil {
+ t.Fatalf("parseRef(%q): %v", tc.ref, err)
+ }
+ if got := len(s.remoteOpts(context.Background(), parsed)); got != tc.want {
+ t.Errorf("remoteOpts returned %d options, want %d", got, tc.want)
+ }
+ })
+ }
+}
diff --git a/manifests/ate-install/atelet.yaml b/manifests/ate-install/atelet.yaml
index 8f28579a35..784b36adb5 100644
--- a/manifests/ate-install/atelet.yaml
+++ b/manifests/ate-install/atelet.yaml
@@ -112,7 +112,14 @@ spec:
- name: atelet
image: ko://github.com/agent-substrate/substrate/cmd/atelet
args:
- - --gcp-auth-for-image-pulls=true
+ # Image pull credentials come from the node's own kubelet image
+ # credential provider plugin, mounted read-only below, rather than from
+ # a cloud SDK compiled into atelet. The plugin and its config are
+ # whatever the node distribution ships, so the same atelet
+ # authenticates to Artifact Registry on GKE and to ECR on EKS.
+ - --image-credential-provider-config=/run/image-credential-provider/config.yaml
+ - --image-credential-provider-bin-dir=/run/image-credential-provider/bin
+ - --gcp-auth-for-image-pulls=false
- --grpc-server-cred-bundle=/run/podidentity.podcert.ate.dev/credential-bundle.pem
- --client-ca-certs=/run/podidentity.podcert.ate.dev/trust-bundle.pem
- --ateapi-ca-file=/run/servicedns.podcert.ate.dev/trust-bundle.pem
@@ -190,6 +197,12 @@ spec:
readOnly: true
- name: kubelet-plugins
mountPath: /var/lib/kubelet/plugins
+ - name: image-credential-provider-bin
+ mountPath: /run/image-credential-provider/bin
+ readOnly: true
+ - name: image-credential-provider-config
+ mountPath: /run/image-credential-provider/config.yaml
+ readOnly: true
volumes:
- name: run-ateom
hostPath:
@@ -200,6 +213,21 @@ spec:
hostPath:
path: /var/lib/kubelet/plugins
type: DirectoryOrCreate
+ # The kubelet's own image credential provider plugin directory and
+ # config, i.e. the values of its --image-credential-provider-bin-dir and
+ # --image-credential-provider-config flags. Paths below are GKE's; other
+ # node distributions place them elsewhere. Read-only, but note the bin
+ # dir also holds the node's other kubelet-installed binaries: the plugin
+ # contract is directory-scoped, so a tighter mount would have to hard-code
+ # the provider's name and give up being cloud-agnostic.
+ - name: image-credential-provider-bin
+ hostPath:
+ path: /home/kubernetes/bin
+ type: DirectoryOrCreate
+ - name: image-credential-provider-config
+ hostPath:
+ path: /etc/srv/kubernetes/cri_auth_config.yaml
+ type: FileOrCreate
# Identity for mutual TLS with the ate-apiserver. atelet is not behind a
# Service, so it uses a podidentity (SPIFFE) cert rather than a servicedns
# serving cert (which requires DNS SANs it would not have); the
diff --git a/manifests/ate-install/kind/atelet/kustomization.yaml b/manifests/ate-install/kind/atelet/kustomization.yaml
index 2a0cfa4113..767a651f4d 100644
--- a/manifests/ate-install/kind/atelet/kustomization.yaml
+++ b/manifests/ate-install/kind/atelet/kustomization.yaml
@@ -56,3 +56,19 @@ patches:
value: rustfsadmin
- name: AWS_SECRET_ACCESS_KEY
value: rustfsadmin
+ # kind nodes ship no image credential provider plugin (the args
+ # above leave atelet's --image-credential-provider-* flags
+ # unset), so drop the host mounts the base manifest adds for
+ # them rather than fabricating empty ones on the node.
+ # volumeMounts merges on mountPath, not name -- keying these by
+ # name matches nothing and silently empties the whole list.
+ volumeMounts:
+ - mountPath: /run/image-credential-provider/bin
+ $patch: delete
+ - mountPath: /run/image-credential-provider/config.yaml
+ $patch: delete
+ volumes:
+ - name: image-credential-provider-bin
+ $patch: delete
+ - name: image-credential-provider-config
+ $patch: delete
diff --git a/vendor/k8s.io/kubelet/LICENSE b/vendor/k8s.io/kubelet/LICENSE
new file mode 100644
index 0000000000..d645695673
--- /dev/null
+++ b/vendor/k8s.io/kubelet/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ 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.
diff --git a/vendor/k8s.io/kubelet/config/v1/doc.go b/vendor/k8s.io/kubelet/config/v1/doc.go
new file mode 100644
index 0000000000..2b8cbe19fe
--- /dev/null
+++ b/vendor/k8s.io/kubelet/config/v1/doc.go
@@ -0,0 +1,23 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+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.
+*/
+
+// +k8s:deepcopy-gen=package
+// +k8s:openapi-gen=true
+// +k8s:openapi-model-package=io.k8s.kubelet.config.v1
+
+// +groupName=kubelet.config.k8s.io
+
+package v1
diff --git a/vendor/k8s.io/kubelet/config/v1/register.go b/vendor/k8s.io/kubelet/config/v1/register.go
new file mode 100644
index 0000000000..effb5b3817
--- /dev/null
+++ b/vendor/k8s.io/kubelet/config/v1/register.go
@@ -0,0 +1,43 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+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 v1
+
+import (
+ "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/apimachinery/pkg/runtime/schema"
+)
+
+// GroupName is the group name used in this package
+const GroupName = "kubelet.config.k8s.io"
+
+// SchemeGroupVersion is group version used to register these objects
+var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1"}
+
+var (
+ // SchemeBuilder is the scheme builder with scheme init functions to run for this API package
+ SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
+ // AddToScheme is a global function that registers this API group & version to a scheme
+ AddToScheme = SchemeBuilder.AddToScheme
+)
+
+// addKnownTypes registers known types to the given scheme
+func addKnownTypes(scheme *runtime.Scheme) error {
+ scheme.AddKnownTypes(SchemeGroupVersion,
+ &CredentialProviderConfig{},
+ )
+ return nil
+}
diff --git a/vendor/k8s.io/kubelet/config/v1/types.go b/vendor/k8s.io/kubelet/config/v1/types.go
new file mode 100644
index 0000000000..6ff4d5541f
--- /dev/null
+++ b/vendor/k8s.io/kubelet/config/v1/types.go
@@ -0,0 +1,189 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+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 v1
+
+import (
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+// ServiceAccountTokenCacheType is the type of cache key used for caching credentials returned by the plugin
+// when the service account token is used.
+type ServiceAccountTokenCacheType string
+
+const (
+ // TokenServiceAccountTokenCacheType means the kubelet will cache returned credentials
+ // on a per-token basis. This should be set if the returned credential's lifetime is limited
+ // to the input service account token's lifetime.
+ // For example, this must be used when returning the input service account token directly as a pull credential.
+ TokenServiceAccountTokenCacheType ServiceAccountTokenCacheType = "Token"
+ // ServiceAccountServiceAccountTokenCacheType means the kubelet will cache returned credentials
+ // on a per-serviceaccount basis. This should be set if the plugin's credential retrieval logic
+ // depends only on the service account and not on pod-specific claims.
+ // Use this when the returned credential is valid for all pods using the same service account.
+ ServiceAccountServiceAccountTokenCacheType ServiceAccountTokenCacheType = "ServiceAccount"
+)
+
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+
+// CredentialProviderConfig is the configuration containing information about
+// each exec credential provider. Kubelet reads this configuration from disk and enables
+// each provider as specified by the CredentialProvider type.
+type CredentialProviderConfig struct {
+ metav1.TypeMeta `json:",inline"`
+
+ // providers is a list of credential provider plugins that will be enabled by the kubelet.
+ // Multiple providers may match against a single image, in which case credentials
+ // from all providers will be returned to the kubelet. If multiple providers are called
+ // for a single image, the results are combined. If providers return overlapping
+ // auth keys, the value from the provider earlier in this list is attempted first.
+ Providers []CredentialProvider `json:"providers"`
+}
+
+// CredentialProvider represents an exec plugin to be invoked by the kubelet. The plugin is only
+// invoked when an image being pulled matches the images handled by the plugin (see matchImages).
+type CredentialProvider struct {
+ // name is the required name of the credential provider. It must match the name of the
+ // provider executable as seen by the kubelet. The executable must be in the kubelet's
+ // bin directory (set by the --image-credential-provider-bin-dir flag).
+ // Required to be unique across all providers.
+ Name string `json:"name"`
+
+ // matchImages is a required list of strings used to match against images in order to
+ // determine if this provider should be invoked. If one of the strings matches the
+ // requested image from the kubelet, the plugin will be invoked and given a chance
+ // to provide credentials. Images are expected to contain the registry domain
+ // and URL path.
+ //
+ // Each entry in matchImages is a pattern which can optionally contain a port and a path.
+ // Globs can be used in the domain, but not in the port or the path. Globs are supported
+ // as subdomains like '*.k8s.io' or 'k8s.*.io', and top-level-domains such as 'k8s.*'.
+ // Matching partial subdomains like 'app*.k8s.io' is also supported. Each glob can only match
+ // a single subdomain segment, so *.io does not match *.k8s.io.
+ //
+ // A match exists between an image and a matchImage when all of the below are true:
+ // - Both contain the same number of domain parts and each part matches.
+ // - The URL path of an imageMatch must be a prefix of the target image URL path.
+ // - If the imageMatch contains a port, then the port must match in the image as well.
+ //
+ // Example values of matchImages:
+ // - 123456789.dkr.ecr.us-east-1.amazonaws.com
+ // - *.azurecr.io
+ // - gcr.io
+ // - *.*.registry.io
+ // - registry.io:8080/path
+ MatchImages []string `json:"matchImages"`
+
+ // defaultCacheDuration is the default duration the plugin will cache credentials in-memory
+ // if a cache duration is not provided in the plugin response. This field is required.
+ DefaultCacheDuration *metav1.Duration `json:"defaultCacheDuration"`
+
+ // Required input version of the exec CredentialProviderRequest. The returned CredentialProviderResponse
+ // MUST use the same encoding version as the input. Current supported values are:
+ // - credentialprovider.kubelet.k8s.io/v1
+ APIVersion string `json:"apiVersion"`
+
+ // Arguments to pass to the command when executing it.
+ // +optional
+ Args []string `json:"args,omitempty"`
+
+ // Env defines additional environment variables to expose to the process. These
+ // are unioned with the host's environment, as well as variables client-go uses
+ // to pass argument to the plugin.
+ // +optional
+ Env []ExecEnvVar `json:"env,omitempty"`
+
+ // tokenAttributes is the configuration for the service account token that will be passed to the plugin.
+ // The credential provider opts in to using service account tokens for image pull by setting this field.
+ // When this field is set, kubelet will generate a service account token bound to the pod for which the
+ // image is being pulled and pass to the plugin as part of CredentialProviderRequest along with other
+ // attributes required by the plugin.
+ //
+ // The service account metadata and token attributes will be used as a dimension to cache
+ // the credentials in kubelet. The cache key is generated by combining the service account metadata
+ // (namespace, name, UID, and annotations key+value for the keys defined in
+ // serviceAccountTokenAttribute.requiredServiceAccountAnnotationKeys and serviceAccountTokenAttribute.optionalServiceAccountAnnotationKeys).
+ // The pod metadata (namespace, name, UID) that are in the service account token are not used as a dimension
+ // to cache the credentials in kubelet. This means workloads that are using the same service account
+ // could end up using the same credentials for image pull. For plugins that don't want this behavior, or
+ // plugins that operate in pass-through mode; i.e., they return the service account token as-is, they
+ // can set the credentialProviderResponse.cacheDuration to 0. This will disable the caching of
+ // credentials in kubelet and the plugin will be invoked for every image pull. This does result in
+ // token generation overhead for every image pull, but it is the only way to ensure that the
+ // credentials are not shared across pods (even if they are using the same service account).
+ // +optional
+ TokenAttributes *ServiceAccountTokenAttributes `json:"tokenAttributes,omitempty"`
+}
+
+// ServiceAccountTokenAttributes is the configuration for the service account token that will be passed to the plugin.
+type ServiceAccountTokenAttributes struct {
+ // serviceAccountTokenAudience is the intended audience for the projected service account token.
+ // +required
+ ServiceAccountTokenAudience string `json:"serviceAccountTokenAudience"`
+
+ // cacheType indicates the type of cache key use for caching the credentials returned by the plugin
+ // when the service account token is used.
+ // The most conservative option is to set this to "Token", which means the kubelet will cache returned credentials
+ // on a per-token basis. This should be set if the returned credential's lifetime is limited to the service account
+ // token's lifetime.
+ // If the plugin's credential retrieval logic depends only on the service account and not on pod-specific claims,
+ // then the plugin can set this to "ServiceAccount". In this case, the kubelet will cache returned credentials
+ // on a per-serviceaccount basis. Use this when the returned credential is valid for all pods using the same service account.
+ // +required
+ CacheType ServiceAccountTokenCacheType `json:"cacheType"`
+
+ // requireServiceAccount indicates whether the plugin requires the pod to have a service account.
+ // If set to true, kubelet will only invoke the plugin if the pod has a service account.
+ // If set to false, kubelet will invoke the plugin even if the pod does not have a service account
+ // and will not include a token in the CredentialProviderRequest in that scenario. This is useful for plugins that
+ // are used to pull images for pods without service accounts (e.g., static pods).
+ // +required
+ RequireServiceAccount *bool `json:"requireServiceAccount"`
+
+ // requiredServiceAccountAnnotationKeys is the list of annotation keys that the plugin is interested in
+ // and that are required to be present in the service account.
+ // The keys defined in this list will be extracted from the corresponding service account and passed
+ // to the plugin as part of the CredentialProviderRequest. If any of the keys defined in this list
+ // are not present in the service account, kubelet will not invoke the plugin and will return an error.
+ // This field is optional and may be empty. Plugins may use this field to extract
+ // additional information required to fetch credentials or allow workloads to opt in to
+ // using service account tokens for image pull.
+ // If non-empty, requireServiceAccount must be set to true.
+ // Keys in this list must be unique.
+ // This list needs to be mutually exclusive with optionalServiceAccountAnnotationKeys.
+ // +optional
+ // +listType=set
+ RequiredServiceAccountAnnotationKeys []string `json:"requiredServiceAccountAnnotationKeys,omitempty"`
+
+ // optionalServiceAccountAnnotationKeys is the list of annotation keys that the plugin is interested in
+ // and that are optional to be present in the service account.
+ // The keys defined in this list will be extracted from the corresponding service account and passed
+ // to the plugin as part of the CredentialProviderRequest. The plugin is responsible for validating
+ // the existence of annotations and their values.
+ // This field is optional and may be empty. Plugins may use this field to extract
+ // additional information required to fetch credentials.
+ // Keys in this list must be unique.
+ // +optional
+ // +listType=set
+ OptionalServiceAccountAnnotationKeys []string `json:"optionalServiceAccountAnnotationKeys,omitempty"`
+}
+
+// ExecEnvVar is used for setting environment variables when executing an exec-based
+// credential plugin.
+type ExecEnvVar struct {
+ Name string `json:"name"`
+ Value string `json:"value"`
+}
diff --git a/vendor/k8s.io/kubelet/config/v1/zz_generated.deepcopy.go b/vendor/k8s.io/kubelet/config/v1/zz_generated.deepcopy.go
new file mode 100644
index 0000000000..481dd988a5
--- /dev/null
+++ b/vendor/k8s.io/kubelet/config/v1/zz_generated.deepcopy.go
@@ -0,0 +1,147 @@
+//go:build !ignore_autogenerated
+// +build !ignore_autogenerated
+
+/*
+Copyright The Kubernetes Authors.
+
+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.
+*/
+
+// Code generated by deepcopy-gen. DO NOT EDIT.
+
+package v1
+
+import (
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ runtime "k8s.io/apimachinery/pkg/runtime"
+)
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *CredentialProvider) DeepCopyInto(out *CredentialProvider) {
+ *out = *in
+ if in.MatchImages != nil {
+ in, out := &in.MatchImages, &out.MatchImages
+ *out = make([]string, len(*in))
+ copy(*out, *in)
+ }
+ if in.DefaultCacheDuration != nil {
+ in, out := &in.DefaultCacheDuration, &out.DefaultCacheDuration
+ *out = new(metav1.Duration)
+ **out = **in
+ }
+ if in.Args != nil {
+ in, out := &in.Args, &out.Args
+ *out = make([]string, len(*in))
+ copy(*out, *in)
+ }
+ if in.Env != nil {
+ in, out := &in.Env, &out.Env
+ *out = make([]ExecEnvVar, len(*in))
+ copy(*out, *in)
+ }
+ if in.TokenAttributes != nil {
+ in, out := &in.TokenAttributes, &out.TokenAttributes
+ *out = new(ServiceAccountTokenAttributes)
+ (*in).DeepCopyInto(*out)
+ }
+ return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CredentialProvider.
+func (in *CredentialProvider) DeepCopy() *CredentialProvider {
+ if in == nil {
+ return nil
+ }
+ out := new(CredentialProvider)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *CredentialProviderConfig) DeepCopyInto(out *CredentialProviderConfig) {
+ *out = *in
+ out.TypeMeta = in.TypeMeta
+ if in.Providers != nil {
+ in, out := &in.Providers, &out.Providers
+ *out = make([]CredentialProvider, len(*in))
+ for i := range *in {
+ (*in)[i].DeepCopyInto(&(*out)[i])
+ }
+ }
+ return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CredentialProviderConfig.
+func (in *CredentialProviderConfig) DeepCopy() *CredentialProviderConfig {
+ if in == nil {
+ return nil
+ }
+ out := new(CredentialProviderConfig)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *CredentialProviderConfig) DeepCopyObject() runtime.Object {
+ if c := in.DeepCopy(); c != nil {
+ return c
+ }
+ return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ExecEnvVar) DeepCopyInto(out *ExecEnvVar) {
+ *out = *in
+ return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExecEnvVar.
+func (in *ExecEnvVar) DeepCopy() *ExecEnvVar {
+ if in == nil {
+ return nil
+ }
+ out := new(ExecEnvVar)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ServiceAccountTokenAttributes) DeepCopyInto(out *ServiceAccountTokenAttributes) {
+ *out = *in
+ if in.RequireServiceAccount != nil {
+ in, out := &in.RequireServiceAccount, &out.RequireServiceAccount
+ *out = new(bool)
+ **out = **in
+ }
+ if in.RequiredServiceAccountAnnotationKeys != nil {
+ in, out := &in.RequiredServiceAccountAnnotationKeys, &out.RequiredServiceAccountAnnotationKeys
+ *out = make([]string, len(*in))
+ copy(*out, *in)
+ }
+ if in.OptionalServiceAccountAnnotationKeys != nil {
+ in, out := &in.OptionalServiceAccountAnnotationKeys, &out.OptionalServiceAccountAnnotationKeys
+ *out = make([]string, len(*in))
+ copy(*out, *in)
+ }
+ return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceAccountTokenAttributes.
+func (in *ServiceAccountTokenAttributes) DeepCopy() *ServiceAccountTokenAttributes {
+ if in == nil {
+ return nil
+ }
+ out := new(ServiceAccountTokenAttributes)
+ in.DeepCopyInto(out)
+ return out
+}
diff --git a/vendor/k8s.io/kubelet/config/v1/zz_generated.model_name.go b/vendor/k8s.io/kubelet/config/v1/zz_generated.model_name.go
new file mode 100644
index 0000000000..bc69a465d6
--- /dev/null
+++ b/vendor/k8s.io/kubelet/config/v1/zz_generated.model_name.go
@@ -0,0 +1,42 @@
+//go:build !ignore_autogenerated
+// +build !ignore_autogenerated
+
+/*
+Copyright The Kubernetes Authors.
+
+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.
+*/
+
+// Code generated by openapi-gen. DO NOT EDIT.
+
+package v1
+
+// OpenAPIModelName returns the OpenAPI model name for this type.
+func (in CredentialProvider) OpenAPIModelName() string {
+ return "io.k8s.kubelet.config.v1.CredentialProvider"
+}
+
+// OpenAPIModelName returns the OpenAPI model name for this type.
+func (in CredentialProviderConfig) OpenAPIModelName() string {
+ return "io.k8s.kubelet.config.v1.CredentialProviderConfig"
+}
+
+// OpenAPIModelName returns the OpenAPI model name for this type.
+func (in ExecEnvVar) OpenAPIModelName() string {
+ return "io.k8s.kubelet.config.v1.ExecEnvVar"
+}
+
+// OpenAPIModelName returns the OpenAPI model name for this type.
+func (in ServiceAccountTokenAttributes) OpenAPIModelName() string {
+ return "io.k8s.kubelet.config.v1.ServiceAccountTokenAttributes"
+}
diff --git a/vendor/k8s.io/kubelet/pkg/apis/credentialprovider/OWNERS b/vendor/k8s.io/kubelet/pkg/apis/credentialprovider/OWNERS
new file mode 100644
index 0000000000..b9a5d28459
--- /dev/null
+++ b/vendor/k8s.io/kubelet/pkg/apis/credentialprovider/OWNERS
@@ -0,0 +1,5 @@
+# See the OWNERS docs at https://go.k8s.io/owners
+
+reviewers:
+ - sig-node-api-reviewers
+ - sig-auth-api-reviewers
diff --git a/vendor/k8s.io/kubelet/pkg/apis/credentialprovider/doc.go b/vendor/k8s.io/kubelet/pkg/apis/credentialprovider/doc.go
new file mode 100644
index 0000000000..7f90183fa4
--- /dev/null
+++ b/vendor/k8s.io/kubelet/pkg/apis/credentialprovider/doc.go
@@ -0,0 +1,20 @@
+/*
+Copyright 2020 The Kubernetes Authors.
+
+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.
+*/
+
+// +k8s:deepcopy-gen=package
+// +groupName=credentialprovider.kubelet.k8s.io
+
+package credentialprovider
diff --git a/vendor/k8s.io/kubelet/pkg/apis/credentialprovider/register.go b/vendor/k8s.io/kubelet/pkg/apis/credentialprovider/register.go
new file mode 100644
index 0000000000..9319c21fe6
--- /dev/null
+++ b/vendor/k8s.io/kubelet/pkg/apis/credentialprovider/register.go
@@ -0,0 +1,41 @@
+/*
+Copyright 2020 The Kubernetes Authors.
+
+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 credentialprovider
+
+import (
+ "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/apimachinery/pkg/runtime/schema"
+)
+
+// GroupName is the group name use in this package
+const GroupName = "credentialprovider.kubelet.k8s.io"
+
+// SchemeGroupVersion is group version used to register these objects
+var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: runtime.APIVersionInternal}
+
+var (
+ SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
+ AddToScheme = SchemeBuilder.AddToScheme
+)
+
+func addKnownTypes(scheme *runtime.Scheme) error {
+ scheme.AddKnownTypes(SchemeGroupVersion,
+ &CredentialProviderRequest{},
+ &CredentialProviderResponse{},
+ )
+ return nil
+}
diff --git a/vendor/k8s.io/kubelet/pkg/apis/credentialprovider/types.go b/vendor/k8s.io/kubelet/pkg/apis/credentialprovider/types.go
new file mode 100644
index 0000000000..fabd318e0d
--- /dev/null
+++ b/vendor/k8s.io/kubelet/pkg/apis/credentialprovider/types.go
@@ -0,0 +1,128 @@
+/*
+Copyright 2020 The Kubernetes Authors.
+
+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 credentialprovider
+
+import (
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+
+// CredentialProviderRequest includes the image that the kubelet requires authentication for.
+// Kubelet will pass this request object to the plugin via stdin. In general, plugins should
+// prefer responding with the same apiVersion they were sent.
+type CredentialProviderRequest struct {
+ metav1.TypeMeta
+
+ // image is the container image that is being pulled as part of the
+ // credential provider plugin request. Plugins may optionally parse the image
+ // to extract any information required to fetch credentials.
+ Image string
+
+ // serviceAccountToken is the service account token bound to the pod for which
+ // the image is being pulled. This token is only sent to the plugin if the
+ // tokenAttributes.serviceAccountTokenAudience field is configured in the kubelet's credential provider configuration.
+ ServiceAccountToken string
+
+ // serviceAccountAnnotations is a map of annotations on the service account bound to the
+ // pod for which the image is being pulled. The list of annotations in the service account
+ // that need to be passed to the plugin is configured in the kubelet's credential provider
+ // configuration.
+ ServiceAccountAnnotations map[string]string
+}
+
+type PluginCacheKeyType string
+
+const (
+ // ImagePluginCacheKeyType means the kubelet will cache credentials on a per-image basis,
+ // using the image passed from the kubelet directly as the cache key. This includes
+ // the registry domain, port (if specified), and path but does not include tags or SHAs.
+ ImagePluginCacheKeyType PluginCacheKeyType = "Image"
+ // RegistryPluginCacheKeyType means the kubelet will cache credentials on a per-registry basis.
+ // The cache key will be based on the registry domain and port (if present) parsed from the requested image.
+ RegistryPluginCacheKeyType PluginCacheKeyType = "Registry"
+ // GlobalPluginCacheKeyType means the kubelet will cache credentials for all images that
+ // match for a given plugin. This cache key should only be returned by plugins that do not use
+ // the image input at all.
+ GlobalPluginCacheKeyType PluginCacheKeyType = "Global"
+)
+
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+
+// CredentialProviderResponse holds credentials that the kubelet should use for the specified
+// image provided in the original request. Kubelet will read the response from the plugin via stdout.
+// This response should be set to the same apiVersion as CredentialProviderRequest.
+type CredentialProviderResponse struct {
+ metav1.TypeMeta
+
+ // cacheKeyType indiciates the type of caching key to use based on the image provided
+ // in the request. There are three valid values for the cache key type: Image, Registry, and
+ // Global. If an invalid value is specified, the response will NOT be used by the kubelet.
+ CacheKeyType PluginCacheKeyType
+
+ // cacheDuration indicates the duration the provided credentials should be cached for.
+ // The kubelet will use this field to set the in-memory cache duration for credentials
+ // in the AuthConfig. If null, the kubelet will use defaultCacheDuration provided in
+ // CredentialProviderConfig. If set to 0, the kubelet will not cache the provided AuthConfig.
+ // +optional
+ CacheDuration *metav1.Duration
+
+ // auth is a map containing authentication information passed into the kubelet.
+ // Each key is a match image string (more on this below). The corresponding authConfig value
+ // should be valid for all images that match against this key. A plugin should set
+ // this field to null if no valid credentials can be returned for the requested image.
+ //
+ // Each key in the map is a pattern which can optionally contain a port and a path.
+ // Globs can be used in the domain, but not in the port or the path. Globs are supported
+ // as subdomains like '*.k8s.io' or 'k8s.*.io', and top-level-domains such as 'k8s.*'.
+ // Matching partial subdomains like 'app*.k8s.io' is also supported. Each glob can only match
+ // a single subdomain segment, so *.io does not match *.k8s.io.
+ //
+ // The kubelet will match images against the key when all of the below are true:
+ // - Both contain the same number of domain parts and each part matches.
+ // - The URL path of an imageMatch must be a prefix of the target image URL path.
+ // - If the imageMatch contains a port, then the port must match in the image as well.
+ //
+ // When multiple keys are returned, the kubelet will traverse all keys in reverse order so that:
+ // - longer keys come before shorter keys with the same prefix
+ // - non-wildcard keys come before wildcard keys with the same prefix.
+ //
+ // For any given match, the kubelet will attempt an image pull with the provided credentials,
+ // stopping after the first successfully authenticated pull.
+ //
+ // Example keys:
+ // - 123456789.dkr.ecr.us-east-1.amazonaws.com
+ // - *.azurecr.io
+ // - gcr.io
+ // - *.*.registry.io
+ // - registry.io:8080/path
+ // +optional
+ Auth map[string]AuthConfig
+}
+
+// AuthConfig contains authentication information for a container registry.
+// Only username/password based authentication is supported today, but more authentication
+// mechanisms may be added in the future.
+type AuthConfig struct {
+ // username is the username used for authenticating to the container registry
+ // An empty username is valid.
+ Username string
+
+ // password is the password used for authenticating to the container registry
+ // An empty password is valid.
+ Password string
+}
diff --git a/vendor/k8s.io/kubelet/pkg/apis/credentialprovider/v1/doc.go b/vendor/k8s.io/kubelet/pkg/apis/credentialprovider/v1/doc.go
new file mode 100644
index 0000000000..871ccc86f6
--- /dev/null
+++ b/vendor/k8s.io/kubelet/pkg/apis/credentialprovider/v1/doc.go
@@ -0,0 +1,22 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+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.
+*/
+
+// +k8s:deepcopy-gen=package
+// +k8s:conversion-gen=k8s.io/kubelet/pkg/apis/credentialprovider
+// +k8s:defaulter-gen=TypeMeta
+// +groupName=credentialprovider.kubelet.k8s.io
+
+package v1
diff --git a/vendor/k8s.io/kubelet/pkg/apis/credentialprovider/v1/register.go b/vendor/k8s.io/kubelet/pkg/apis/credentialprovider/v1/register.go
new file mode 100644
index 0000000000..0f8177bd8c
--- /dev/null
+++ b/vendor/k8s.io/kubelet/pkg/apis/credentialprovider/v1/register.go
@@ -0,0 +1,46 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+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 v1
+
+import (
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/apimachinery/pkg/runtime/schema"
+)
+
+// GroupName is the group name use in this package
+const GroupName = "credentialprovider.kubelet.k8s.io"
+
+// SchemeGroupVersion is group version used to register these objects
+var (
+ SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1"}
+ localSchemeBuilder = &SchemeBuilder
+)
+
+var (
+ SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
+ AddToScheme = SchemeBuilder.AddToScheme
+)
+
+func addKnownTypes(scheme *runtime.Scheme) error {
+ scheme.AddKnownTypes(SchemeGroupVersion,
+ &CredentialProviderRequest{},
+ &CredentialProviderResponse{},
+ )
+ metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
+ return nil
+}
diff --git a/vendor/k8s.io/kubelet/pkg/apis/credentialprovider/v1/types.go b/vendor/k8s.io/kubelet/pkg/apis/credentialprovider/v1/types.go
new file mode 100644
index 0000000000..994f34610a
--- /dev/null
+++ b/vendor/k8s.io/kubelet/pkg/apis/credentialprovider/v1/types.go
@@ -0,0 +1,129 @@
+/*
+Copyright 2022 The Kubernetes Authors.
+
+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 v1
+
+import (
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+
+// CredentialProviderRequest includes the image that the kubelet requires authentication for.
+// Kubelet will pass this request object to the plugin via stdin. In general, plugins should
+// prefer responding with the same apiVersion they were sent.
+type CredentialProviderRequest struct {
+ metav1.TypeMeta `json:",inline"`
+
+ // image is the container image that is being pulled as part of the
+ // credential provider plugin request. Plugins may optionally parse the image
+ // to extract any information required to fetch credentials.
+ Image string `json:"image"`
+
+ // serviceAccountToken is the service account token bound to the pod for which
+ // the image is being pulled. This token is only sent to the plugin if the
+ // tokenAttributes.serviceAccountTokenAudience field is configured in the kubelet's credential
+ // provider configuration.
+ ServiceAccountToken string `json:"serviceAccountToken,omitempty" datapolicy:"token"`
+
+ // serviceAccountAnnotations is a map of annotations on the service account bound to the
+ // pod for which the image is being pulled. The list of annotations in the service account
+ // that need to be passed to the plugin is configured in the kubelet's credential provider
+ // configuration.
+ ServiceAccountAnnotations map[string]string `json:"serviceAccountAnnotations,omitempty"`
+}
+
+type PluginCacheKeyType string
+
+const (
+ // ImagePluginCacheKeyType means the kubelet will cache credentials on a per-image basis,
+ // using the image passed from the kubelet directly as the cache key. This includes
+ // the registry domain, port (if specified), and path but does not include tags or SHAs.
+ ImagePluginCacheKeyType PluginCacheKeyType = "Image"
+ // RegistryPluginCacheKeyType means the kubelet will cache credentials on a per-registry basis.
+ // The cache key will be based on the registry domain and port (if present) parsed from the requested image.
+ RegistryPluginCacheKeyType PluginCacheKeyType = "Registry"
+ // GlobalPluginCacheKeyType means the kubelet will cache credentials for all images that
+ // match for a given plugin. This cache key should only be returned by plugins that do not use
+ // the image input at all.
+ GlobalPluginCacheKeyType PluginCacheKeyType = "Global"
+)
+
+// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
+
+// CredentialProviderResponse holds credentials that the kubelet should use for the specified
+// image provided in the original request. Kubelet will read the response from the plugin via stdout.
+// This response should be set to the same apiVersion as CredentialProviderRequest.
+type CredentialProviderResponse struct {
+ metav1.TypeMeta `json:",inline"`
+
+ // cacheKeyType indiciates the type of caching key to use based on the image provided
+ // in the request. There are three valid values for the cache key type: Image, Registry, and
+ // Global. If an invalid value is specified, the response will NOT be used by the kubelet.
+ CacheKeyType PluginCacheKeyType `json:"cacheKeyType"`
+
+ // cacheDuration indicates the duration the provided credentials should be cached for.
+ // The kubelet will use this field to set the in-memory cache duration for credentials
+ // in the AuthConfig. If null, the kubelet will use defaultCacheDuration provided in
+ // CredentialProviderConfig. If set to 0, the kubelet will not cache the provided AuthConfig.
+ // +optional
+ CacheDuration *metav1.Duration `json:"cacheDuration,omitempty"`
+
+ // auth is a map containing authentication information passed into the kubelet.
+ // Each key is a match image string (more on this below). The corresponding authConfig value
+ // should be valid for all images that match against this key. A plugin should set
+ // this field to null if no valid credentials can be returned for the requested image.
+ //
+ // Each key in the map is a pattern which can optionally contain a port and a path.
+ // Globs can be used in the domain, but not in the port or the path. Globs are supported
+ // as subdomains like '*.k8s.io' or 'k8s.*.io', and top-level-domains such as 'k8s.*'.
+ // Matching partial subdomains like 'app*.k8s.io' is also supported. Each glob can only match
+ // a single subdomain segment, so *.io does not match *.k8s.io.
+ //
+ // The kubelet will match images against the key when all of the below are true:
+ // - Both contain the same number of domain parts and each part matches.
+ // - The URL path of an imageMatch must be a prefix of the target image URL path.
+ // - If the imageMatch contains a port, then the port must match in the image as well.
+ //
+ // When multiple keys are returned, the kubelet will traverse all keys in reverse order so that:
+ // - longer keys come before shorter keys with the same prefix
+ // - non-wildcard keys come before wildcard keys with the same prefix.
+ //
+ // For any given match, the kubelet will attempt an image pull with the provided credentials,
+ // stopping after the first successfully authenticated pull.
+ //
+ // Example keys:
+ // - 123456789.dkr.ecr.us-east-1.amazonaws.com
+ // - *.azurecr.io
+ // - gcr.io
+ // - *.*.registry.io
+ // - registry.io:8080/path
+ // +optional
+ Auth map[string]AuthConfig `json:"auth,omitempty"`
+}
+
+// AuthConfig contains authentication information for a container registry.
+// Only username/password based authentication is supported today, but more authentication
+// mechanisms may be added in the future.
+type AuthConfig struct {
+ // username is the username used for authenticating to the container registry
+ // An empty username is valid.
+ Username string `json:"username"`
+
+ // password is the password used for authenticating to the container registry
+ // An empty password is valid.
+ Password string `json:"password"`
+}
diff --git a/vendor/k8s.io/kubelet/pkg/apis/credentialprovider/v1/zz_generated.conversion.go b/vendor/k8s.io/kubelet/pkg/apis/credentialprovider/v1/zz_generated.conversion.go
new file mode 100644
index 0000000000..77692a7497
--- /dev/null
+++ b/vendor/k8s.io/kubelet/pkg/apis/credentialprovider/v1/zz_generated.conversion.go
@@ -0,0 +1,141 @@
+//go:build !ignore_autogenerated
+// +build !ignore_autogenerated
+
+/*
+Copyright The Kubernetes Authors.
+
+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.
+*/
+
+// Code generated by conversion-gen. DO NOT EDIT.
+
+package v1
+
+import (
+ unsafe "unsafe"
+
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ conversion "k8s.io/apimachinery/pkg/conversion"
+ runtime "k8s.io/apimachinery/pkg/runtime"
+ credentialprovider "k8s.io/kubelet/pkg/apis/credentialprovider"
+)
+
+func init() {
+ localSchemeBuilder.Register(RegisterConversions)
+}
+
+// RegisterConversions adds conversion functions to the given scheme.
+// Public to allow building arbitrary schemes.
+func RegisterConversions(s *runtime.Scheme) error {
+ if err := s.AddGeneratedConversionFunc((*AuthConfig)(nil), (*credentialprovider.AuthConfig)(nil), func(a, b interface{}, scope conversion.Scope) error {
+ return Convert_v1_AuthConfig_To_credentialprovider_AuthConfig(a.(*AuthConfig), b.(*credentialprovider.AuthConfig), scope)
+ }); err != nil {
+ return err
+ }
+ if err := s.AddGeneratedConversionFunc((*credentialprovider.AuthConfig)(nil), (*AuthConfig)(nil), func(a, b interface{}, scope conversion.Scope) error {
+ return Convert_credentialprovider_AuthConfig_To_v1_AuthConfig(a.(*credentialprovider.AuthConfig), b.(*AuthConfig), scope)
+ }); err != nil {
+ return err
+ }
+ if err := s.AddGeneratedConversionFunc((*CredentialProviderRequest)(nil), (*credentialprovider.CredentialProviderRequest)(nil), func(a, b interface{}, scope conversion.Scope) error {
+ return Convert_v1_CredentialProviderRequest_To_credentialprovider_CredentialProviderRequest(a.(*CredentialProviderRequest), b.(*credentialprovider.CredentialProviderRequest), scope)
+ }); err != nil {
+ return err
+ }
+ if err := s.AddGeneratedConversionFunc((*credentialprovider.CredentialProviderRequest)(nil), (*CredentialProviderRequest)(nil), func(a, b interface{}, scope conversion.Scope) error {
+ return Convert_credentialprovider_CredentialProviderRequest_To_v1_CredentialProviderRequest(a.(*credentialprovider.CredentialProviderRequest), b.(*CredentialProviderRequest), scope)
+ }); err != nil {
+ return err
+ }
+ if err := s.AddGeneratedConversionFunc((*CredentialProviderResponse)(nil), (*credentialprovider.CredentialProviderResponse)(nil), func(a, b interface{}, scope conversion.Scope) error {
+ return Convert_v1_CredentialProviderResponse_To_credentialprovider_CredentialProviderResponse(a.(*CredentialProviderResponse), b.(*credentialprovider.CredentialProviderResponse), scope)
+ }); err != nil {
+ return err
+ }
+ if err := s.AddGeneratedConversionFunc((*credentialprovider.CredentialProviderResponse)(nil), (*CredentialProviderResponse)(nil), func(a, b interface{}, scope conversion.Scope) error {
+ return Convert_credentialprovider_CredentialProviderResponse_To_v1_CredentialProviderResponse(a.(*credentialprovider.CredentialProviderResponse), b.(*CredentialProviderResponse), scope)
+ }); err != nil {
+ return err
+ }
+ return nil
+}
+
+func autoConvert_v1_AuthConfig_To_credentialprovider_AuthConfig(in *AuthConfig, out *credentialprovider.AuthConfig, s conversion.Scope) error {
+ out.Username = in.Username
+ out.Password = in.Password
+ return nil
+}
+
+// Convert_v1_AuthConfig_To_credentialprovider_AuthConfig is an autogenerated conversion function.
+func Convert_v1_AuthConfig_To_credentialprovider_AuthConfig(in *AuthConfig, out *credentialprovider.AuthConfig, s conversion.Scope) error {
+ return autoConvert_v1_AuthConfig_To_credentialprovider_AuthConfig(in, out, s)
+}
+
+func autoConvert_credentialprovider_AuthConfig_To_v1_AuthConfig(in *credentialprovider.AuthConfig, out *AuthConfig, s conversion.Scope) error {
+ out.Username = in.Username
+ out.Password = in.Password
+ return nil
+}
+
+// Convert_credentialprovider_AuthConfig_To_v1_AuthConfig is an autogenerated conversion function.
+func Convert_credentialprovider_AuthConfig_To_v1_AuthConfig(in *credentialprovider.AuthConfig, out *AuthConfig, s conversion.Scope) error {
+ return autoConvert_credentialprovider_AuthConfig_To_v1_AuthConfig(in, out, s)
+}
+
+func autoConvert_v1_CredentialProviderRequest_To_credentialprovider_CredentialProviderRequest(in *CredentialProviderRequest, out *credentialprovider.CredentialProviderRequest, s conversion.Scope) error {
+ out.Image = in.Image
+ out.ServiceAccountToken = in.ServiceAccountToken
+ out.ServiceAccountAnnotations = *(*map[string]string)(unsafe.Pointer(&in.ServiceAccountAnnotations))
+ return nil
+}
+
+// Convert_v1_CredentialProviderRequest_To_credentialprovider_CredentialProviderRequest is an autogenerated conversion function.
+func Convert_v1_CredentialProviderRequest_To_credentialprovider_CredentialProviderRequest(in *CredentialProviderRequest, out *credentialprovider.CredentialProviderRequest, s conversion.Scope) error {
+ return autoConvert_v1_CredentialProviderRequest_To_credentialprovider_CredentialProviderRequest(in, out, s)
+}
+
+func autoConvert_credentialprovider_CredentialProviderRequest_To_v1_CredentialProviderRequest(in *credentialprovider.CredentialProviderRequest, out *CredentialProviderRequest, s conversion.Scope) error {
+ out.Image = in.Image
+ out.ServiceAccountToken = in.ServiceAccountToken
+ out.ServiceAccountAnnotations = *(*map[string]string)(unsafe.Pointer(&in.ServiceAccountAnnotations))
+ return nil
+}
+
+// Convert_credentialprovider_CredentialProviderRequest_To_v1_CredentialProviderRequest is an autogenerated conversion function.
+func Convert_credentialprovider_CredentialProviderRequest_To_v1_CredentialProviderRequest(in *credentialprovider.CredentialProviderRequest, out *CredentialProviderRequest, s conversion.Scope) error {
+ return autoConvert_credentialprovider_CredentialProviderRequest_To_v1_CredentialProviderRequest(in, out, s)
+}
+
+func autoConvert_v1_CredentialProviderResponse_To_credentialprovider_CredentialProviderResponse(in *CredentialProviderResponse, out *credentialprovider.CredentialProviderResponse, s conversion.Scope) error {
+ out.CacheKeyType = credentialprovider.PluginCacheKeyType(in.CacheKeyType)
+ out.CacheDuration = (*metav1.Duration)(unsafe.Pointer(in.CacheDuration))
+ out.Auth = *(*map[string]credentialprovider.AuthConfig)(unsafe.Pointer(&in.Auth))
+ return nil
+}
+
+// Convert_v1_CredentialProviderResponse_To_credentialprovider_CredentialProviderResponse is an autogenerated conversion function.
+func Convert_v1_CredentialProviderResponse_To_credentialprovider_CredentialProviderResponse(in *CredentialProviderResponse, out *credentialprovider.CredentialProviderResponse, s conversion.Scope) error {
+ return autoConvert_v1_CredentialProviderResponse_To_credentialprovider_CredentialProviderResponse(in, out, s)
+}
+
+func autoConvert_credentialprovider_CredentialProviderResponse_To_v1_CredentialProviderResponse(in *credentialprovider.CredentialProviderResponse, out *CredentialProviderResponse, s conversion.Scope) error {
+ out.CacheKeyType = PluginCacheKeyType(in.CacheKeyType)
+ out.CacheDuration = (*metav1.Duration)(unsafe.Pointer(in.CacheDuration))
+ out.Auth = *(*map[string]AuthConfig)(unsafe.Pointer(&in.Auth))
+ return nil
+}
+
+// Convert_credentialprovider_CredentialProviderResponse_To_v1_CredentialProviderResponse is an autogenerated conversion function.
+func Convert_credentialprovider_CredentialProviderResponse_To_v1_CredentialProviderResponse(in *credentialprovider.CredentialProviderResponse, out *CredentialProviderResponse, s conversion.Scope) error {
+ return autoConvert_credentialprovider_CredentialProviderResponse_To_v1_CredentialProviderResponse(in, out, s)
+}
diff --git a/vendor/k8s.io/kubelet/pkg/apis/credentialprovider/v1/zz_generated.deepcopy.go b/vendor/k8s.io/kubelet/pkg/apis/credentialprovider/v1/zz_generated.deepcopy.go
new file mode 100644
index 0000000000..348574fe73
--- /dev/null
+++ b/vendor/k8s.io/kubelet/pkg/apis/credentialprovider/v1/zz_generated.deepcopy.go
@@ -0,0 +1,112 @@
+//go:build !ignore_autogenerated
+// +build !ignore_autogenerated
+
+/*
+Copyright The Kubernetes Authors.
+
+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.
+*/
+
+// Code generated by deepcopy-gen. DO NOT EDIT.
+
+package v1
+
+import (
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ runtime "k8s.io/apimachinery/pkg/runtime"
+)
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *AuthConfig) DeepCopyInto(out *AuthConfig) {
+ *out = *in
+ return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthConfig.
+func (in *AuthConfig) DeepCopy() *AuthConfig {
+ if in == nil {
+ return nil
+ }
+ out := new(AuthConfig)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *CredentialProviderRequest) DeepCopyInto(out *CredentialProviderRequest) {
+ *out = *in
+ out.TypeMeta = in.TypeMeta
+ if in.ServiceAccountAnnotations != nil {
+ in, out := &in.ServiceAccountAnnotations, &out.ServiceAccountAnnotations
+ *out = make(map[string]string, len(*in))
+ for key, val := range *in {
+ (*out)[key] = val
+ }
+ }
+ return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CredentialProviderRequest.
+func (in *CredentialProviderRequest) DeepCopy() *CredentialProviderRequest {
+ if in == nil {
+ return nil
+ }
+ out := new(CredentialProviderRequest)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *CredentialProviderRequest) DeepCopyObject() runtime.Object {
+ if c := in.DeepCopy(); c != nil {
+ return c
+ }
+ return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *CredentialProviderResponse) DeepCopyInto(out *CredentialProviderResponse) {
+ *out = *in
+ out.TypeMeta = in.TypeMeta
+ if in.CacheDuration != nil {
+ in, out := &in.CacheDuration, &out.CacheDuration
+ *out = new(metav1.Duration)
+ **out = **in
+ }
+ if in.Auth != nil {
+ in, out := &in.Auth, &out.Auth
+ *out = make(map[string]AuthConfig, len(*in))
+ for key, val := range *in {
+ (*out)[key] = val
+ }
+ }
+ return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CredentialProviderResponse.
+func (in *CredentialProviderResponse) DeepCopy() *CredentialProviderResponse {
+ if in == nil {
+ return nil
+ }
+ out := new(CredentialProviderResponse)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *CredentialProviderResponse) DeepCopyObject() runtime.Object {
+ if c := in.DeepCopy(); c != nil {
+ return c
+ }
+ return nil
+}
diff --git a/vendor/k8s.io/kubelet/pkg/apis/credentialprovider/v1/zz_generated.defaults.go b/vendor/k8s.io/kubelet/pkg/apis/credentialprovider/v1/zz_generated.defaults.go
new file mode 100644
index 0000000000..dac177e93b
--- /dev/null
+++ b/vendor/k8s.io/kubelet/pkg/apis/credentialprovider/v1/zz_generated.defaults.go
@@ -0,0 +1,33 @@
+//go:build !ignore_autogenerated
+// +build !ignore_autogenerated
+
+/*
+Copyright The Kubernetes Authors.
+
+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.
+*/
+
+// Code generated by defaulter-gen. DO NOT EDIT.
+
+package v1
+
+import (
+ runtime "k8s.io/apimachinery/pkg/runtime"
+)
+
+// RegisterDefaults adds defaulters functions to the given scheme.
+// Public to allow building arbitrary schemes.
+// All generated defaulters are covering - they call all nested defaulters.
+func RegisterDefaults(scheme *runtime.Scheme) error {
+ return nil
+}
diff --git a/vendor/k8s.io/kubelet/pkg/apis/credentialprovider/zz_generated.deepcopy.go b/vendor/k8s.io/kubelet/pkg/apis/credentialprovider/zz_generated.deepcopy.go
new file mode 100644
index 0000000000..a5a2432534
--- /dev/null
+++ b/vendor/k8s.io/kubelet/pkg/apis/credentialprovider/zz_generated.deepcopy.go
@@ -0,0 +1,112 @@
+//go:build !ignore_autogenerated
+// +build !ignore_autogenerated
+
+/*
+Copyright The Kubernetes Authors.
+
+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.
+*/
+
+// Code generated by deepcopy-gen. DO NOT EDIT.
+
+package credentialprovider
+
+import (
+ v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ runtime "k8s.io/apimachinery/pkg/runtime"
+)
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *AuthConfig) DeepCopyInto(out *AuthConfig) {
+ *out = *in
+ return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthConfig.
+func (in *AuthConfig) DeepCopy() *AuthConfig {
+ if in == nil {
+ return nil
+ }
+ out := new(AuthConfig)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *CredentialProviderRequest) DeepCopyInto(out *CredentialProviderRequest) {
+ *out = *in
+ out.TypeMeta = in.TypeMeta
+ if in.ServiceAccountAnnotations != nil {
+ in, out := &in.ServiceAccountAnnotations, &out.ServiceAccountAnnotations
+ *out = make(map[string]string, len(*in))
+ for key, val := range *in {
+ (*out)[key] = val
+ }
+ }
+ return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CredentialProviderRequest.
+func (in *CredentialProviderRequest) DeepCopy() *CredentialProviderRequest {
+ if in == nil {
+ return nil
+ }
+ out := new(CredentialProviderRequest)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *CredentialProviderRequest) DeepCopyObject() runtime.Object {
+ if c := in.DeepCopy(); c != nil {
+ return c
+ }
+ return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *CredentialProviderResponse) DeepCopyInto(out *CredentialProviderResponse) {
+ *out = *in
+ out.TypeMeta = in.TypeMeta
+ if in.CacheDuration != nil {
+ in, out := &in.CacheDuration, &out.CacheDuration
+ *out = new(v1.Duration)
+ **out = **in
+ }
+ if in.Auth != nil {
+ in, out := &in.Auth, &out.Auth
+ *out = make(map[string]AuthConfig, len(*in))
+ for key, val := range *in {
+ (*out)[key] = val
+ }
+ }
+ return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CredentialProviderResponse.
+func (in *CredentialProviderResponse) DeepCopy() *CredentialProviderResponse {
+ if in == nil {
+ return nil
+ }
+ out := new(CredentialProviderResponse)
+ in.DeepCopyInto(out)
+ return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *CredentialProviderResponse) DeepCopyObject() runtime.Object {
+ if c := in.DeepCopy(); c != nil {
+ return c
+ }
+ return nil
+}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index d3d7ded2c4..b44dafdf7c 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -1849,6 +1849,11 @@ k8s.io/kube-openapi/pkg/spec3
k8s.io/kube-openapi/pkg/util
k8s.io/kube-openapi/pkg/util/proto
k8s.io/kube-openapi/pkg/validation/spec
+# k8s.io/kubelet v0.36.1
+## explicit; go 1.26.0
+k8s.io/kubelet/config/v1
+k8s.io/kubelet/pkg/apis/credentialprovider
+k8s.io/kubelet/pkg/apis/credentialprovider/v1
# k8s.io/metrics v0.36.1
## explicit; go 1.26.0
k8s.io/metrics/pkg/apis/metrics