| // Licensed to the Apache Software Foundation (ASF) under one or more |
| // contributor license agreements. See the NOTICE file distributed with |
| // this work for additional information regarding copyright ownership. |
| // The ASF licenses this file to You 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 creds_test |
| |
| import ( |
| "context" |
| "crypto/ed25519" |
| "crypto/rand" |
| "crypto/tls" |
| "crypto/x509" |
| "crypto/x509/pkix" |
| "encoding/base64" |
| "encoding/json" |
| "errors" |
| "fmt" |
| "io" |
| "math/big" |
| "net" |
| "net/http" |
| "os" |
| "path/filepath" |
| "runtime" |
| "strings" |
| "sync" |
| "testing" |
| "time" |
| ) |
| |
| import ( |
| "github.com/docker/docker-credential-helpers/credentials" |
| ) |
| |
| import ( |
| "github.com/apache/dubbo-kubernetes/app/dubboctl/internal/docker" |
| "github.com/apache/dubbo-kubernetes/app/dubboctl/internal/docker/creds" |
| . "github.com/apache/dubbo-kubernetes/app/dubboctl/internal/testing" |
| ) |
| |
| func Test_registryEquals(t *testing.T) { |
| tests := []struct { |
| name string |
| urlA string |
| urlB string |
| want bool |
| }{ |
| {"no port matching host", "quay.io", "quay.io", true}, |
| {"non-matching host added sub-domain", "sub.quay.io", "quay.io", false}, |
| {"non-matching host different sub-domain", "sub.quay.io", "sub3.quay.io", false}, |
| {"localhost", "localhost", "localhost", true}, |
| {"localhost with standard ports", "localhost:80", "localhost:443", false}, |
| {"localhost with matching port", "https://localhost:1234", "http://localhost:1234", true}, |
| {"localhost with match by default port 80", "http://localhost", "localhost:80", true}, |
| {"localhost with match by default port 443", "https://localhost", "localhost:443", true}, |
| {"localhost with mismatch by non-default port 5000", "https://localhost", "localhost:5000", false}, |
| {"localhost with match by empty ports", "https://localhost", "http://localhost", true}, |
| {"docker.io matching host https", "https://docker.io", "docker.io", true}, |
| {"docker.io matching host http", "http://docker.io", "docker.io", true}, |
| {"docker.io with path", "docker.io/v1/", "docker.io", true}, |
| {"docker.io with protocol and path", "https://docker.io/v1/", "docker.io", true}, |
| {"docker.io with subdomain index.", "https://index.docker.io/v1/", "docker.io", true}, |
| } |
| for _, tt := range tests { |
| t.Run(tt.name, func(t *testing.T) { |
| if got := creds.RegistryEquals(tt.urlA, tt.urlB); got != tt.want { |
| t.Errorf("to2ndLevelDomain() = %v, want %v", got, tt.want) |
| } |
| }) |
| } |
| } |
| |
| func TestCheckAuth(t *testing.T) { |
| const ( |
| uname = "testuser" |
| pwd = "testpwd" |
| incorrectPwd = "badpwd" |
| ) |
| |
| localhost, localhostTLS := startServer(t, uname, pwd) |
| |
| _, portTLS, err := net.SplitHostPort(localhostTLS) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| nonLocalhostTLS := "test.io:" + portTLS |
| |
| type args struct { |
| ctx context.Context |
| username string |
| password string |
| registry string |
| } |
| tests := []struct { |
| name string |
| args args |
| wantErr bool |
| }{ |
| { |
| name: "correct credentials localhost no-TLS", |
| args: args{ |
| ctx: context.Background(), |
| username: uname, |
| password: pwd, |
| registry: localhost, |
| }, |
| wantErr: false, |
| }, |
| { |
| name: "correct credentials localhost", |
| args: args{ |
| ctx: context.Background(), |
| username: uname, |
| password: pwd, |
| registry: localhostTLS, |
| }, |
| wantErr: false, |
| }, |
| |
| { |
| name: "correct credentials non-localhost", |
| args: args{ |
| ctx: context.Background(), |
| username: uname, |
| password: pwd, |
| registry: nonLocalhostTLS, |
| }, |
| wantErr: false, |
| }, |
| { |
| name: "incorrect credentials localhost no-TLS", |
| args: args{ |
| ctx: context.Background(), |
| username: uname, |
| password: incorrectPwd, |
| registry: localhost, |
| }, |
| wantErr: true, |
| }, |
| { |
| name: "incorrect credentials localhost", |
| args: args{ |
| ctx: context.Background(), |
| username: uname, |
| password: incorrectPwd, |
| registry: localhostTLS, |
| }, |
| wantErr: true, |
| }, |
| } |
| for _, tt := range tests { |
| t.Run(tt.name, func(t *testing.T) { |
| c := docker.Credentials{ |
| Username: tt.args.username, |
| Password: tt.args.password, |
| } |
| if err := creds.CheckAuth(tt.args.ctx, tt.args.registry+"/someorg/someimage:sometag", c, http.DefaultTransport); (err != nil) != tt.wantErr { |
| t.Errorf("CheckAuth() error = %v, wantErr %v", err, tt.wantErr) |
| } |
| }) |
| } |
| } |
| |
| func TestCheckAuthEmptyCreds(t *testing.T) { |
| localhost, _ := startServer(t, "", "") |
| err := creds.CheckAuth(context.Background(), localhost+"/someorg/someimage:sometag", docker.Credentials{}, http.DefaultTransport) |
| if err != nil { |
| t.Error(err) |
| } |
| } |
| |
| func startServer(t *testing.T, uname, pwd string) (addr, addrTLS string) { |
| // TODO: this should be refactored to use OS-chosen ports so as not to |
| // fail when a user is running a function on the default port.) |
| listener, err := net.Listen("tcp", "localhost:0") |
| if err != nil { |
| t.Fatal(err) |
| } |
| addr = listener.Addr().String() |
| |
| listenerTLS, err := net.Listen("tcp", "localhost:0") |
| if err != nil { |
| t.Fatal(err) |
| } |
| addrTLS = listenerTLS.Addr().String() |
| |
| handler := http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { |
| if uname == "" || pwd == "" { |
| if req.Method == http.MethodPost { |
| resp.WriteHeader(http.StatusCreated) |
| } else { |
| resp.WriteHeader(http.StatusOK) |
| } |
| return |
| } |
| // TODO add also test for token based auth |
| resp.Header().Add("WWW-Authenticate", "basic") |
| if u, p, ok := req.BasicAuth(); ok { |
| if u == uname && p == pwd { |
| if req.Method == http.MethodPost { |
| resp.WriteHeader(http.StatusCreated) |
| } else { |
| resp.WriteHeader(http.StatusOK) |
| } |
| return |
| } |
| } |
| resp.WriteHeader(http.StatusUnauthorized) |
| }) |
| |
| randReader := rand.Reader |
| |
| caPublicKey, caPrivateKey, err := ed25519.GenerateKey(randReader) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| ca := &x509.Certificate{ |
| SerialNumber: big.NewInt(1), |
| Subject: pkix.Name{ |
| CommonName: "localhost", |
| }, |
| IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback}, |
| DNSNames: []string{"localhost", "test.io"}, |
| NotBefore: time.Now(), |
| NotAfter: time.Now().AddDate(10, 0, 0), |
| IsCA: true, |
| ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, |
| ExtraExtensions: []pkix.Extension{}, |
| KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, |
| BasicConstraintsValid: true, |
| } |
| |
| caBytes, err := x509.CreateCertificate(randReader, ca, ca, caPublicKey, caPrivateKey) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| ca, err = x509.ParseCertificate(caBytes) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| cert := tls.Certificate{ |
| Certificate: [][]byte{caBytes}, |
| PrivateKey: caPrivateKey, |
| Leaf: ca, |
| } |
| |
| server := http.Server{ |
| Handler: handler, |
| TLSConfig: &tls.Config{ |
| ServerName: "localhost", |
| Certificates: []tls.Certificate{cert}, |
| }, |
| } |
| |
| go func() { |
| err := server.ServeTLS(listenerTLS, "", "") |
| if err != nil && !strings.Contains(err.Error(), "Server closed") { |
| panic(err) |
| } |
| }() |
| |
| go func() { |
| err := server.Serve(listener) |
| if err != nil && !strings.Contains(err.Error(), "Server closed") { |
| panic(err) |
| } |
| }() |
| |
| // make the testing CA trusted by default HTTP transport/client |
| oldDefaultTransport := http.DefaultTransport |
| newDefaultTransport := http.DefaultTransport.(*http.Transport).Clone() |
| http.DefaultTransport = newDefaultTransport |
| caPool := x509.NewCertPool() |
| caPool.AddCert(ca) |
| newDefaultTransport.TLSClientConfig.RootCAs = caPool |
| dc := newDefaultTransport.DialContext |
| newDefaultTransport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { |
| h, p, err := net.SplitHostPort(addr) |
| if err != nil { |
| return nil, err |
| } |
| if h == "test.io" { |
| h = "localhost" |
| } |
| addr = net.JoinHostPort(h, p) |
| return dc(ctx, network, addr) |
| } |
| |
| t.Cleanup(func() { |
| err := server.Shutdown(context.Background()) |
| if err != nil { |
| t.Fatal(err) |
| } |
| http.DefaultTransport = oldDefaultTransport |
| }) |
| |
| return addr, addrTLS |
| } |
| |
| const ( |
| dockerIoUser = "testUser1" |
| dockerIoUserPwd = "goodPwd1" |
| quayIoUser = "testUser2" |
| quayIoUserPwd = "goodPwd2" |
| ) |
| |
| type Credentials = docker.Credentials |
| |
| func TestNewCredentialsProvider(t *testing.T) { |
| withCleanHome(t) |
| |
| helperWithQuayIO := newInMemoryHelper() |
| |
| err := helperWithQuayIO.Add(&credentials.Credentials{ |
| ServerURL: "quay.io", |
| Username: quayIoUser, |
| Secret: quayIoUserPwd, |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| type args struct { |
| promptUser creds.CredentialsCallback |
| verifyCredentials creds.VerifyCredentialsCallback |
| additionalLoaders []creds.CredentialsCallback |
| registry string |
| setUpEnv setUpEnv |
| } |
| tests := []struct { |
| name string |
| args args |
| want Credentials |
| }{ |
| { |
| name: "test user callback correct password on first try", |
| args: args{ |
| promptUser: correctPwdCallback, |
| verifyCredentials: correctVerifyCbk, |
| registry: "docker.io", |
| }, |
| want: Credentials{Username: dockerIoUser, Password: dockerIoUserPwd}, |
| }, |
| { |
| name: "test user callback correct password on second try", |
| args: args{ |
| promptUser: pwdCbkFirstWrongThenCorrect(t), |
| verifyCredentials: correctVerifyCbk, |
| registry: "docker.io", |
| }, |
| want: Credentials{Username: dockerIoUser, Password: dockerIoUserPwd}, |
| }, |
| { |
| name: "get quay-io credentials with func config populated", |
| args: args{ |
| promptUser: pwdCbkThatShallNotBeCalled(t), |
| verifyCredentials: correctVerifyCbk, |
| registry: "quay.io", |
| setUpEnv: withPopulatedFuncAuthConfig, |
| }, |
| want: Credentials{Username: quayIoUser, Password: quayIoUserPwd}, |
| }, |
| { |
| name: "get docker-io credentials with func config populated", |
| args: args{ |
| promptUser: pwdCbkThatShallNotBeCalled(t), |
| verifyCredentials: correctVerifyCbk, |
| registry: "docker.io", |
| setUpEnv: withPopulatedFuncAuthConfig, |
| }, |
| want: Credentials{Username: dockerIoUser, Password: dockerIoUserPwd}, |
| }, |
| { |
| name: "get quay-io credentials with docker config populated", |
| args: args{ |
| promptUser: pwdCbkThatShallNotBeCalled(t), |
| verifyCredentials: correctVerifyCbk, |
| registry: "quay.io", |
| setUpEnv: all( |
| withPopulatedDockerAuthConfig, |
| setUpMockHelper("docker-credential-mock", helperWithQuayIO)), |
| }, |
| want: Credentials{Username: quayIoUser, Password: quayIoUserPwd}, |
| }, |
| { |
| name: "get docker-io credentials from custom loader", |
| args: args{ |
| promptUser: pwdCbkThatShallNotBeCalled(t), |
| verifyCredentials: correctVerifyCbk, |
| registry: "docker.io", |
| additionalLoaders: []creds.CredentialsCallback{correctPwdCallback}, |
| }, |
| want: Credentials{Username: dockerIoUser, Password: dockerIoUserPwd}, |
| }, |
| } |
| for _, tt := range tests { |
| t.Run(tt.name, func(t *testing.T) { |
| defer cleanUpConfigs(t) |
| |
| if tt.args.setUpEnv != nil { |
| tt.args.setUpEnv(t) |
| } |
| |
| credentialsProvider := creds.NewCredentialsProvider( |
| testConfigPath(t), |
| creds.WithPromptForCredentials(tt.args.promptUser), |
| creds.WithVerifyCredentials(tt.args.verifyCredentials), |
| creds.WithAdditionalCredentialLoaders(tt.args.additionalLoaders...)) |
| got, err := credentialsProvider(context.Background(), tt.args.registry+"/someorg/someimage:sometag") |
| if err != nil { |
| t.Errorf("unexpected error: %v", err) |
| return |
| } |
| if got != tt.want { |
| t.Errorf("credentialsProvider() = %v, want %v", got, tt.want) |
| } |
| }) |
| } |
| } |
| |
| func TestNewCredentialsProviderEmptyCreds(t *testing.T) { |
| withCleanHome(t) |
| credentialsProvider := creds.NewCredentialsProvider(testConfigPath(t), creds.WithVerifyCredentials(func(ctx context.Context, image string, credentials docker.Credentials) error { |
| if image == "localhost:5555/someorg/someimage:sometag" && credentials == (docker.Credentials{}) { |
| return nil |
| } |
| t.Fatal("unreachable") |
| return nil |
| })) |
| c, err := credentialsProvider(context.Background(), "localhost:5555/someorg/someimage:sometag") |
| if err != nil { |
| t.Error(err) |
| } |
| if c != (docker.Credentials{}) { |
| t.Error("unexpected credentials") |
| } |
| } |
| |
| func TestCredentialsProviderSavingFromUserInput(t *testing.T) { |
| withCleanHome(t) |
| |
| helper := newInMemoryHelper() |
| setUpMockHelper("docker-credential-mock", helper)(t) |
| |
| var pwdCbkInvocations int |
| pwdCbk := func(r string) (Credentials, error) { |
| pwdCbkInvocations++ |
| return correctPwdCallback(r) |
| } |
| |
| chooseNoStore := func(available []string) (string, error) { |
| if len(available) < 1 { |
| t.Errorf("this should have been invoked with non empty list") |
| } |
| return "", nil |
| } |
| chooseMockStore := func(available []string) (string, error) { |
| if len(available) < 1 { |
| t.Errorf("this should have been invoked with non empty list") |
| } |
| return "docker-credential-mock", nil |
| } |
| shallNotBeInvoked := func(available []string) (string, error) { |
| t.Fatal("this choose helper callback shall not be invoked") |
| return "", errors.New("this callback shall not be invoked") |
| } |
| |
| credentialsProvider := creds.NewCredentialsProvider( |
| testConfigPath(t), |
| creds.WithPromptForCredentials(pwdCbk), |
| creds.WithVerifyCredentials(correctVerifyCbk), |
| creds.WithPromptForCredentialStore(chooseNoStore)) |
| _, err := credentialsProvider(context.Background(), "docker.io/someorg/someimage:sometag") |
| if err != nil { |
| t.Errorf("unexpected error: %v", err) |
| return |
| } |
| |
| // now credentials should not be saved because no helper was provided |
| l, err := helper.List() |
| if err != nil { |
| t.Fatal(err) |
| } |
| credsInStore := len(l) |
| if credsInStore != 0 { |
| t.Errorf("expected to have zero credentials in store, but has: %d", credsInStore) |
| } |
| credentialsProvider = creds.NewCredentialsProvider( |
| testConfigPath(t), |
| creds.WithPromptForCredentials(pwdCbk), |
| creds.WithVerifyCredentials(correctVerifyCbk), |
| creds.WithPromptForCredentialStore(chooseMockStore)) |
| _, err = credentialsProvider(context.Background(), "docker.io/someorg/someimage:sometag") |
| if err != nil { |
| t.Errorf("unexpected error: %v", err) |
| return |
| } |
| if pwdCbkInvocations != 2 { |
| t.Errorf("the pwd callback should have been invoked exactly twice but was invoked %d time", pwdCbkInvocations) |
| } |
| |
| // now credentials should be saved in the mock secure store |
| l, err = helper.List() |
| if err != nil { |
| t.Fatal(err) |
| } |
| credsInStore = len(l) |
| if len(l) != 1 { |
| t.Errorf("expected to have exactly one credentials in store, but has: %d", credsInStore) |
| } |
| credentialsProvider = creds.NewCredentialsProvider( |
| testConfigPath(t), |
| creds.WithPromptForCredentials(pwdCbkThatShallNotBeCalled(t)), |
| creds.WithVerifyCredentials(correctVerifyCbk), |
| creds.WithPromptForCredentialStore(shallNotBeInvoked)) |
| _, err = credentialsProvider(context.Background(), "docker.io/someorg/someimage:sometag") |
| if err != nil { |
| t.Errorf("unexpected error: %v", err) |
| return |
| } |
| } |
| |
| func cleanUpConfigs(t *testing.T) { |
| home, err := os.UserHomeDir() |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| os.RemoveAll(filepath.Join(home, ".docker")) |
| } |
| |
| type setUpEnv = func(t *testing.T) |
| |
| func withPopulatedDockerAuthConfig(t *testing.T) { |
| t.Helper() |
| home, err := os.UserHomeDir() |
| if err != nil { |
| t.Fatal(err) |
| } |
| dockerConfigDir := filepath.Join(home, ".docker") |
| dockerConfigPath := filepath.Join(dockerConfigDir, "config.json") |
| err = os.MkdirAll(filepath.Dir(dockerConfigPath), 0o700) |
| if err != nil { |
| t.Fatal(err) |
| } |
| t.Cleanup(func() { _ = os.RemoveAll(dockerConfigDir) }) |
| |
| configJSON := `{ |
| "auths": { |
| "docker.io": { "auth": "%s" }, |
| "quay.io": {} |
| }, |
| "credsStore": "mock" |
| }` |
| configJSON = fmt.Sprintf(configJSON, base64.StdEncoding.EncodeToString([]byte(dockerIoUser+":"+dockerIoUserPwd))) |
| |
| err = os.WriteFile(dockerConfigPath, []byte(configJSON), 0o600) |
| if err != nil { |
| t.Fatal(err) |
| } |
| } |
| |
| func withPopulatedFuncAuthConfig(t *testing.T) { |
| t.Helper() |
| |
| var err error |
| |
| authConfig := filepath.Join(testConfigPath(t), "auth.json") |
| err = os.MkdirAll(filepath.Dir(authConfig), 0o700) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| t.Cleanup(func() { _ = os.RemoveAll(authConfig) }) |
| |
| authJSON := `{ |
| "auths": { |
| "docker.io": { "auth": "%s" }, |
| "quay.io": { "auth": "%s" } |
| } |
| }` |
| authJSON = fmt.Sprintf(authJSON, |
| base64.StdEncoding.EncodeToString([]byte(dockerIoUser+":"+dockerIoUserPwd)), |
| base64.StdEncoding.EncodeToString([]byte(quayIoUser+":"+quayIoUserPwd))) |
| |
| err = os.WriteFile(authConfig, []byte(authJSON), 0o600) |
| if err != nil { |
| t.Fatal(err) |
| } |
| } |
| |
| func pwdCbkThatShallNotBeCalled(t *testing.T) creds.CredentialsCallback { |
| t.Helper() |
| return func(registry string) (Credentials, error) { |
| return Credentials{}, errors.New("this pwd cbk code shall not be called") |
| } |
| } |
| |
| func pwdCbkFirstWrongThenCorrect(t *testing.T) func(registry string) (Credentials, error) { |
| t.Helper() |
| var firstInvocation bool |
| return func(registry string) (Credentials, error) { |
| if registry != "index.docker.io" && registry != "quay.io" { |
| return Credentials{}, fmt.Errorf("unexpected registry: %s", registry) |
| } |
| if firstInvocation { |
| firstInvocation = false |
| return Credentials{Username: dockerIoUser, Password: "badPwd"}, nil |
| } |
| return correctPwdCallback(registry) |
| } |
| } |
| |
| func correctPwdCallback(registry string) (Credentials, error) { |
| if registry == "index.docker.io" { |
| return Credentials{Username: dockerIoUser, Password: dockerIoUserPwd}, nil |
| } |
| if registry == "quay.io" { |
| return Credentials{Username: quayIoUser, Password: quayIoUserPwd}, nil |
| } |
| return Credentials{}, errors.New("this cbk don't know the pwd") |
| } |
| |
| func correctVerifyCbk(ctx context.Context, image string, credentials Credentials) error { |
| username, password := credentials.Username, credentials.Password |
| if username == dockerIoUser && password == dockerIoUserPwd && image == "docker.io/someorg/someimage:sometag" { |
| return nil |
| } |
| if username == quayIoUser && password == quayIoUserPwd && image == "quay.io/someorg/someimage:sometag" { |
| return nil |
| } |
| return creds.ErrUnauthorized |
| } |
| |
| func testHomeEnvName(t *testing.T) string { |
| t.Helper() |
| if runtime.GOOS == "windows" { |
| return "USERPROFILE" |
| } |
| return "HOME" |
| } |
| |
| func withCleanHome(t *testing.T) { |
| t.Helper() |
| tmpHome := t.TempDir() |
| t.Setenv(testHomeEnvName(t), tmpHome) |
| |
| if runtime.GOOS == "linux" { |
| t.Setenv("XDG_CONFIG_HOME", filepath.Join(tmpHome, ".config")) |
| } |
| } |
| |
| func testConfigPath(t *testing.T) string { |
| t.Helper() |
| home := os.Getenv(testHomeEnvName(t)) |
| configPath := filepath.Join(home, ".config", "func") |
| if err := os.MkdirAll(configPath, os.ModePerm); err != nil { |
| t.Fatal(err) |
| } |
| return configPath |
| } |
| |
| func handlerForCredHelper(t *testing.T, credHelper credentials.Helper) http.Handler { |
| return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { |
| defer request.Body.Close() |
| |
| var err error |
| var outBody interface{} |
| |
| uri := strings.Trim(request.RequestURI, "/") |
| |
| var serverURL string |
| if uri == "get" || uri == "erase" { |
| data, err := io.ReadAll(request.Body) |
| if err != nil { |
| writer.WriteHeader(http.StatusInternalServerError) |
| return |
| } |
| serverURL = string(data) |
| serverURL = strings.Trim(serverURL, "\n\r\t ") |
| } |
| |
| switch uri { |
| case "list": |
| var list map[string]string |
| list, err = credHelper.List() |
| if err == nil { |
| outBody = &list |
| } |
| case "store": |
| creds := credentials.Credentials{} |
| dec := json.NewDecoder(request.Body) |
| err = dec.Decode(&creds) |
| if err != nil { |
| break |
| } |
| err = credHelper.Add(&creds) |
| case "get": |
| var user, secret string |
| user, secret, err = credHelper.Get(serverURL) |
| if err == nil { |
| outBody = &credentials.Credentials{ServerURL: serverURL, Username: user, Secret: secret} |
| } |
| case "erase": |
| err = credHelper.Delete(serverURL) |
| default: |
| writer.WriteHeader(http.StatusNotFound) |
| return |
| } |
| |
| if err != nil { |
| if credentials.IsErrCredentialsNotFound(err) { |
| writer.WriteHeader(http.StatusNotFound) |
| } else { |
| writer.WriteHeader(http.StatusInternalServerError) |
| writer.Header().Add("Content-Type", "text/plain") |
| fmt.Fprintf(writer, "error: %+v\n", err) |
| } |
| return |
| } |
| |
| if outBody != nil { |
| var data []byte |
| data, err = json.Marshal(outBody) |
| if err != nil { |
| writer.WriteHeader(http.StatusInternalServerError) |
| return |
| } |
| writer.Header().Add("Content-Type", "application/json") |
| _, err = writer.Write(data) |
| if err != nil { |
| t.Fatal(err) |
| } |
| } |
| }) |
| } |
| |
| // Go source code of mock docker-credential-helper implementation. |
| // Its storage is backed by inMemoryHelper instantiated in test and exposed via HTTP. |
| const helperGoSrc = `package main |
| |
| import ( |
| "errors" |
| "io" |
| "log" |
| "net/http" |
| "os" |
| ) |
| |
| func main() { |
| var ( |
| baseURL = os.Getenv("HELPER_BASE_URL") |
| resp *http.Response |
| err error |
| ) |
| cmd := os.Args[1] |
| switch cmd { |
| case "list": |
| resp, err = http.Get(baseURL + "/" + cmd) |
| if err != nil { |
| log.Fatal(err) |
| } |
| io.Copy(os.Stdout, resp.Body) |
| case "get", "erase": |
| resp, err = http.Post(baseURL+ "/" + cmd, "text/plain", os.Stdin) |
| if err != nil { |
| log.Fatal(err) |
| } |
| io.Copy(os.Stdout, resp.Body) |
| case "store": |
| resp, err = http.Post(baseURL+ "/" + cmd, "application/json", os.Stdin) |
| if err != nil { |
| log.Fatal(err) |
| } |
| default: |
| log.Fatal(errors.New("unknown cmd: " + cmd)) |
| } |
| if resp.StatusCode != http.StatusOK { |
| log.Fatal(errors.New(resp.Status)) |
| } |
| return |
| } |
| ` |
| |
| // Creates executable with name determined by the helperName parameter and puts it on $PATH. |
| // |
| // The executable behaves like docker credential helper (https://github.com/docker/docker-credential-helpers). |
| // |
| // The content of the store presented by the executable is backed by the helper parameter. |
| func setUpMockHelper(helperName string, helper credentials.Helper) func(t *testing.T) { |
| return func(t *testing.T) { |
| WithExecutable(t, helperName, helperGoSrc) |
| |
| listener, err := net.Listen("tcp", "localhost:0") |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| t.Cleanup(func() { _ = listener.Close() }) |
| |
| baseURL := fmt.Sprintf("http://%s", listener.Addr().String()) |
| t.Setenv("HELPER_BASE_URL", baseURL) |
| |
| server := http.Server{Handler: handlerForCredHelper(t, helper)} |
| servErrChan := make(chan error) |
| go func() { |
| servErrChan <- server.Serve(listener) |
| }() |
| |
| t.Cleanup(func() { |
| ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) |
| defer cancel() |
| _ = server.Shutdown(ctx) |
| e := <-servErrChan |
| if !errors.Is(e, http.ErrServerClosed) { |
| t.Fatal(e) |
| } |
| }) |
| } |
| } |
| |
| // combines multiple setUp routines into one setUp routine |
| func all(fns ...setUpEnv) setUpEnv { |
| return func(t *testing.T) { |
| t.Helper() |
| for _, fn := range fns { |
| fn(t) |
| } |
| } |
| } |
| |
| func newInMemoryHelper() *inMemoryHelper { |
| return &inMemoryHelper{lock: &sync.Mutex{}, credentials: make(map[string]credentials.Credentials)} |
| } |
| |
| type inMemoryHelper struct { |
| credentials map[string]credentials.Credentials |
| lock sync.Locker |
| } |
| |
| func (i *inMemoryHelper) Add(credentials *credentials.Credentials) error { |
| i.lock.Lock() |
| defer i.lock.Unlock() |
| |
| i.credentials[credentials.ServerURL] = *credentials |
| |
| return nil |
| } |
| |
| func (i *inMemoryHelper) Get(serverURL string) (string, string, error) { |
| i.lock.Lock() |
| defer i.lock.Unlock() |
| |
| if result, ok := i.credentials[serverURL]; ok { |
| return result.Username, result.Secret, nil |
| } |
| |
| return "", "", credentials.NewErrCredentialsNotFound() |
| } |
| |
| func (i *inMemoryHelper) List() (map[string]string, error) { |
| i.lock.Lock() |
| defer i.lock.Unlock() |
| |
| result := make(map[string]string, len(i.credentials)) |
| |
| for k, v := range i.credentials { |
| result[k] = v.Username |
| } |
| |
| return result, nil |
| } |
| |
| func (i *inMemoryHelper) Delete(serverURL string) error { |
| i.lock.Lock() |
| defer i.lock.Unlock() |
| |
| if _, ok := i.credentials[serverURL]; ok { |
| delete(i.credentials, serverURL) |
| return nil |
| } |
| |
| return credentials.NewErrCredentialsNotFound() |
| } |