diff --git a/cluster/gce/gci/credential-provider/main.go b/cluster/gce/gci/credential-provider/main.go new file mode 100644 index 00000000000..29b1c89db39 --- /dev/null +++ b/cluster/gce/gci/credential-provider/main.go @@ -0,0 +1,77 @@ +/* +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 main + +import ( + "encoding/json" + "errors" + "io" + "io/ioutil" + "net/http" + "os" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/klog/v2" + credentialproviderv1alpha1 "k8s.io/kubelet/pkg/apis/credentialprovider/v1alpha1" +) + +func main() { + if err := getCredentials(os.Stdout); err != nil { + klog.Fatalf("failed to get credentials: %v", err) + } +} + +func getCredentials(w io.Writer) error { + provider := &provider{ + client: &http.Client{ + Timeout: 10 * time.Second, + }, + } + + data, err := ioutil.ReadAll(os.Stdin) + if err != nil { + return err + } + + var authRequest credentialproviderv1alpha1.CredentialProviderRequest + err = json.Unmarshal(data, &authRequest) + if err != nil { + return err + } + + auth, err := provider.Provide(authRequest.Image) + if err != nil { + return err + } + + response := &credentialproviderv1alpha1.CredentialProviderResponse{ + TypeMeta: metav1.TypeMeta{ + Kind: "CredentialProviderResponse", + APIVersion: "credentialprovider.kubelet.k8s.io/v1alpha1", + }, + CacheKeyType: credentialproviderv1alpha1.RegistryPluginCacheKeyType, + Auth: auth, + } + + if err := json.NewEncoder(w).Encode(response); err != nil { + // The error from json.Marshal is intentionally not included so as to not leak credentials into the logs + return errors.New("error marshaling response") + } + + return nil +} diff --git a/cluster/gce/gci/credential-provider/provider.go b/cluster/gce/gci/credential-provider/provider.go new file mode 100644 index 00000000000..3ba61e0557a --- /dev/null +++ b/cluster/gce/gci/credential-provider/provider.go @@ -0,0 +1,122 @@ +/* +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. +*/ + +// Originally copied from pkg/credentialproviders/gcp +package main + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "net/http" + + credentialproviderv1alpha1 "k8s.io/kubelet/pkg/apis/credentialprovider/v1alpha1" +) + +const ( + metadataToken = "http://metadata.google.internal./computeMetadata/v1/instance/service-accounts/default/token" + metadataEmail = "http://metadata.google.internal./computeMetadata/v1/instance/service-accounts/default/email" + maxReadLength = 10 * 1 << 20 // 10MB +) + +var containerRegistryUrls = []string{"container.cloud.google.com", "gcr.io", "*.gcr.io", "*.pkg.dev"} + +// HTTPError wraps a non-StatusOK error code as an error. +type HTTPError struct { + StatusCode int + URL string +} + +var _ error = &HTTPError{} + +// Error implements error +func (h *HTTPError) Error() string { + return fmt.Sprintf("http status code: %d while fetching url %s", + h.StatusCode, h.URL) +} + +// TokenBlob is used to decode the JSON blob containing an access token +// that is returned by GCE metadata. +type TokenBlob struct { + AccessToken string `json:"access_token"` +} + +type provider struct { + client *http.Client +} + +func (p *provider) Provide(image string) (map[string]credentialproviderv1alpha1.AuthConfig, error) { + cfg := map[string]credentialproviderv1alpha1.AuthConfig{} + + tokenJSONBlob, err := readURL(p.tokenEndpoint, p.client) + if err != nil { + return cfg, err + } + + var parsedBlob TokenBlob + if err := json.Unmarshal(tokenJSONBlob, &parsedBlob); err != nil { + return cfg, err + } + + authConfig := credentialproviderv1alpha1.AuthConfig{ + Username: "_token", + Password: parsedBlob.AccessToken, + } + + // Add our entry for each of the supported container registry URLs + for _, k := range containerRegistryUrls { + cfg[k] = authConfig + } + return cfg, nil +} + +func readURL(url string, client *http.Client) (body []byte, err error) { + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + + rea.Header = &http.Header{ + "Metadata-Flavor": []string{"Google"}, + } + + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, &HTTPError{ + StatusCode: resp.StatusCode, + URL: url, + } + } + + limitedReader := &io.LimitedReader{R: resp.Body, N: maxReadLength} + contents, err := ioutil.ReadAll(limitedReader) + if err != nil { + return nil, err + } + + if limitedReader.N <= 0 { + return nil, errors.New("the read limit is reached") + } + + return contents, nil +} diff --git a/test/e2e_node/remote/node_e2e.go b/test/e2e_node/remote/node_e2e.go index ab8f40beeb0..4d7b7732578 100644 --- a/test/e2e_node/remote/node_e2e.go +++ b/test/e2e_node/remote/node_e2e.go @@ -106,10 +106,10 @@ func prependMemcgNotificationFlag(args string) string { // a credential provider plugin. func prependGCPCredentialProviderFlag(args, workspace string) string { credentialProviderConfig := filepath.Join(workspace, "credential-provider.yaml") - disableIntreeCredentialProviderFlag := "--kubelet-flags=--feature-gates=DisableKubeletCloudCredentialProviders=true" + featureGateFlag := "--kubelet-flags=--feature-gates=DisableKubeletCloudCredentialProviders=true,KubeletCredentialProviders=true" configFlag := fmt.Sprintf("--kubelet-flags=--image-credential-provider-config=%s", credentialProviderConfig) binFlag := fmt.Sprintf("--kubelet-flags=--image-credential-provider-bin-dir=%s", workspace) - return fmt.Sprintf("%s %s %s %s", disableIntreeCredentialProviderFlag, configFlag, binFlag, args) + return fmt.Sprintf("%s %s %s %s", featureGateFlag, configFlag, binFlag, args) } // osSpecificActions takes OS specific actions required for the node tests diff --git a/test/e2e_node/remote/utils.go b/test/e2e_node/remote/utils.go index 48408929574..7751ad9bb2c 100644 --- a/test/e2e_node/remote/utils.go +++ b/test/e2e_node/remote/utils.go @@ -56,6 +56,8 @@ providers: matchImages: - "gcr.io" - "*.gcr.io" + - "container.cloud.google.com" + - "*.pkg.dev" defaultCacheDuration: 1m` // Install the cni plugin and add basic bridge configuration to the