move cluster/gce/gci/credential-provider to test/e2e_node/plugins/gcp-credential-provider

Signed-off-by: Andrew Sy Kim <andrewsy@google.com>
This commit is contained in:
Andrew Sy Kim
2022-03-17 21:36:20 -04:00
parent 3bd37e6deb
commit a4b79590eb
7 changed files with 5 additions and 5 deletions

View File

@@ -35,7 +35,7 @@ var buildTargets = []string{
"test/e2e_node/e2e_node.test",
"vendor/github.com/onsi/ginkgo/ginkgo",
"cluster/gce/gci/mounter",
"cluster/gce/gci/credential-provider",
"test/e2e_node/plugins/gcp-credential-provider",
}
// BuildGo builds k8s binaries.

View File

@@ -0,0 +1,35 @@
# GCP credential provider for e2e testing
This package contains a barebones implementation of the [kubelet GCP credential
provider](https://github.com/kubernetes/cloud-provider-gcp/tree/master/cmd/auth-provider-gcp)
for testing purposes only. This plugin SHOULD NOT be used in production.
This credential provider is installed and configured in the node e2e tests by:
1. Building the gcp-credential-provider binary and including it in the test archive
uploaded to the GCE remote node.
2. Writing the credential provider config into the temporary workspace consumed
by the kubelet. The contents of the config should be something like this:
```yaml
kind: CredentialProviderConfig
apiVersion: kubelet.config.k8s.io/v1alpha1
providers:
- name: gcp-credential-provider
apiVersion: credentialprovider.kubelet.k8s.io/v1alpha1
matchImages:
- "gcr.io"
- "*.gcr.io"
- "container.cloud.google.com"
- "*.pkg.dev"
defaultCacheDuration: 1m`
```
3. Configuring the following additional flags on the kubelet:
```
--feature-gates=DisableKubeletCloudCredentialProviders=true,KubeletCredentialProviders=true
--image-credential-provider-config=/tmp/node-e2e-123456/credential-provider.yaml
--image-credential-provider-bin-dir=/tmp/node-e2e-12345
```

View File

@@ -0,0 +1,80 @@
/*
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"
)
const metadataTokenEndpoint = "http://metadata.google.internal./computeMetadata/v1/instance/service-accounts/default/token"
func main() {
if err := getCredentials(metadataTokenEndpoint, os.Stdin, os.Stdout); err != nil {
klog.Fatalf("failed to get credentials: %v", err)
}
}
func getCredentials(tokenEndpoint string, r io.Reader, w io.Writer) error {
provider := &provider{
client: &http.Client{
Timeout: 10 * time.Second,
},
tokenEndpoint: tokenEndpoint,
}
data, err := ioutil.ReadAll(r)
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
}

View File

@@ -0,0 +1,55 @@
/*
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 (
"bytes"
"fmt"
"net/http"
"net/http/httptest"
"testing"
)
type fakeTokenServer struct {
token string
}
func (f *fakeTokenServer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
fmt.Fprintf(w, `{"access_token": "%s"}`, f.token)
}
func Test_getCredentials(t *testing.T) {
server := httptest.NewServer(&fakeTokenServer{token: "abc123"})
defer server.Close()
in := bytes.NewBuffer([]byte(`{"kind":"CredentialProviderRequest","apiVersion":"credentialprovider.kubelet.k8s.io/v1alpha1","image":"gcr.io/foobar"}`))
out := bytes.NewBuffer(nil)
err := getCredentials(server.URL, in, out)
if err != nil {
t.Fatalf("unexpected error running getCredentials: %v", err)
}
expected := `{"kind":"CredentialProviderResponse","apiVersion":"credentialprovider.kubelet.k8s.io/v1alpha1","cacheKeyType":"Registry","auth":{"*.gcr.io":{"username":"_token","password":"abc123"},"*.pkg.dev":{"username":"_token","password":"abc123"},"container.cloud.google.com":{"username":"_token","password":"abc123"},"gcr.io":{"username":"_token","password":"abc123"}}}
`
if out.String() != expected {
t.Logf("actual response: %v", out)
t.Logf("expected response: %v", expected)
t.Errorf("unexpected credential provider response")
}
}

View File

@@ -0,0 +1,121 @@
/*
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 (
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
tokenEndpoint string
}
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
}
req.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
}

View File

@@ -60,7 +60,7 @@ func (n *NodeE2ERemote) SetupTestPackage(tardir, systemSpecName string) error {
}
// Copy binaries
requiredBins := []string{"kubelet", "e2e_node.test", "ginkgo", "mounter", "credential-provider"}
requiredBins := []string{"kubelet", "e2e_node.test", "ginkgo", "mounter", "gcp-credential-provider"}
for _, bin := range requiredBins {
source := filepath.Join(buildOutputDir, bin)
if _, err := os.Stat(source); err != nil {

View File

@@ -51,7 +51,7 @@ const cniConfig = `{
const credentialProviderConfig = `kind: CredentialProviderConfig
apiVersion: kubelet.config.k8s.io/v1alpha1
providers:
- name: credential-provider
- name: gcp-credential-provider
apiVersion: credentialprovider.kubelet.k8s.io/v1alpha1
matchImages:
- "gcr.io"