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