From 5501b5aa13d9995572d72dc3321baabbc8652d1e Mon Sep 17 00:00:00 2001 From: Nanci Lancaster Date: Thu, 8 Apr 2021 11:48:06 -0500 Subject: [PATCH 01/12] Added Ok amba logo for adopters file --- site/themes/pinniped/static/img/ok-amba.svg | 29 +++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 site/themes/pinniped/static/img/ok-amba.svg diff --git a/site/themes/pinniped/static/img/ok-amba.svg b/site/themes/pinniped/static/img/ok-amba.svg new file mode 100644 index 00000000..5575534a --- /dev/null +++ b/site/themes/pinniped/static/img/ok-amba.svg @@ -0,0 +1,29 @@ + + + + + \ No newline at end of file From fec24d307e2e18fd778fd071c413ed9bda3f05d8 Mon Sep 17 00:00:00 2001 From: Matt Moyer Date: Tue, 6 Apr 2021 11:18:51 -0500 Subject: [PATCH 02/12] Fix missing normalization in pkg/oidcclient/filesession. We have some nice normalization code in this package to remove expired or otherwise malformed cache entries, but we weren't calling it in the appropriate place. Added calls to normalize the cache data structure before and after each transaction, and added test cases to ensure that it's being called. Signed-off-by: Matt Moyer --- pkg/oidcclient/filesession/filesession.go | 8 ++- .../filesession/filesession_test.go | 64 ++++++++++++++++++- 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/pkg/oidcclient/filesession/filesession.go b/pkg/oidcclient/filesession/filesession.go index 151fde71..3417a123 100644 --- a/pkg/oidcclient/filesession/filesession.go +++ b/pkg/oidcclient/filesession/filesession.go @@ -1,4 +1,4 @@ -// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // Package filesession implements a simple YAML file-based login.sessionCache. @@ -137,9 +137,15 @@ func (c *Cache) withCache(transact func(*sessionCache)) { cache = emptySessionCache() } + // Normalize the cache before modifying it, to remove any entries that have already expired. + cache = cache.normalized() + // Process/mutate the session using the provided function. transact(cache) + // Normalize again to put everything into a known order. + cache = cache.normalized() + // Marshal the session back to YAML and save it to the file. if err := cache.writeTo(c.path); err != nil { c.errReporter(fmt.Errorf("could not write session cache: %w", err)) diff --git a/pkg/oidcclient/filesession/filesession_test.go b/pkg/oidcclient/filesession/filesession_test.go index 2ba7c55b..4b7f8b0b 100644 --- a/pkg/oidcclient/filesession/filesession_test.go +++ b/pkg/oidcclient/filesession/filesession_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package filesession @@ -125,6 +125,41 @@ func TestGetToken(t *testing.T) { }, wantErrors: []string{}, }, + { + name: "valid file but expired cache hit", + makeTestFile: func(t *testing.T, tmp string) { + validCache := emptySessionCache() + validCache.insert(sessionEntry{ + Key: oidcclient.SessionCacheKey{ + Issuer: "test-issuer", + ClientID: "test-client-id", + Scopes: []string{"email", "offline_access", "openid", "profile"}, + RedirectURI: "http://localhost:0/callback", + }, + CreationTimestamp: metav1.NewTime(now.Add(-2 * time.Hour)), + LastUsedTimestamp: metav1.NewTime(now.Add(-1 * time.Hour)), + Tokens: oidctypes.Token{ + AccessToken: &oidctypes.AccessToken{ + Token: "test-access-token", + Type: "Bearer", + Expiry: metav1.NewTime(now.Add(-1 * time.Hour)), + }, + IDToken: &oidctypes.IDToken{ + Token: "test-id-token", + Expiry: metav1.NewTime(now.Add(-1 * time.Hour)), + }, + }, + }) + require.NoError(t, validCache.writeTo(tmp)) + }, + key: oidcclient.SessionCacheKey{ + Issuer: "test-issuer", + ClientID: "test-client-id", + Scopes: []string{"email", "offline_access", "openid", "profile"}, + RedirectURI: "http://localhost:0/callback", + }, + wantErrors: []string{}, + }, { name: "valid file with cache hit", makeTestFile: func(t *testing.T, tmp string) { @@ -261,6 +296,33 @@ func TestPutToken(t *testing.T) { }, }, }) + + // Insert another entry that hasn't been used for over 90 days. + validCache.insert(sessionEntry{ + Key: oidcclient.SessionCacheKey{ + Issuer: "test-issuer-2", + ClientID: "test-client-id-2", + Scopes: []string{"email", "offline_access", "openid", "profile"}, + RedirectURI: "http://localhost:0/callback", + }, + CreationTimestamp: metav1.NewTime(now.Add(-95 * 24 * time.Hour)), + LastUsedTimestamp: metav1.NewTime(now.Add(-91 * 24 * time.Hour)), + Tokens: oidctypes.Token{ + AccessToken: &oidctypes.AccessToken{ + Token: "old-access-token2", + Type: "Bearer", + Expiry: metav1.NewTime(now.Add(-1 * time.Hour)), + }, + IDToken: &oidctypes.IDToken{ + Token: "old-id-token2", + Expiry: metav1.NewTime(now.Add(-1 * time.Hour)), + }, + RefreshToken: &oidctypes.RefreshToken{ + Token: "old-refresh-token2", + }, + }, + }) + require.NoError(t, os.MkdirAll(filepath.Dir(tmp), 0700)) require.NoError(t, validCache.writeTo(tmp)) }, From 2296faaeef05b04dcacd645fbd690168f6a0e017 Mon Sep 17 00:00:00 2001 From: Matt Moyer Date: Thu, 8 Apr 2021 10:48:45 -0500 Subject: [PATCH 03/12] Add CLI caching of cluster-specific credentials. Signed-off-by: Matt Moyer --- cmd/pinniped/cmd/login_oidc.go | 22 + cmd/pinniped/cmd/login_oidc_test.go | 2 + cmd/pinniped/cmd/login_static.go | 27 ++ cmd/pinniped/cmd/login_static_test.go | 3 + internal/execcredcache/cachefile.go | 127 ++++++ internal/execcredcache/cachefile_test.go | 207 ++++++++++ internal/execcredcache/execcredcache.go | 159 +++++++ internal/execcredcache/execcredcache_test.go | 389 ++++++++++++++++++ internal/execcredcache/testdata/invalid.yaml | 1 + internal/execcredcache/testdata/valid.yaml | 9 + .../execcredcache/testdata/wrong-version.yaml | 3 + 11 files changed, 949 insertions(+) create mode 100644 internal/execcredcache/cachefile.go create mode 100644 internal/execcredcache/cachefile_test.go create mode 100644 internal/execcredcache/execcredcache.go create mode 100644 internal/execcredcache/execcredcache_test.go create mode 100644 internal/execcredcache/testdata/invalid.yaml create mode 100644 internal/execcredcache/testdata/valid.yaml create mode 100644 internal/execcredcache/testdata/wrong-version.yaml diff --git a/cmd/pinniped/cmd/login_oidc.go b/cmd/pinniped/cmd/login_oidc.go index a3125475..8395e6c7 100644 --- a/cmd/pinniped/cmd/login_oidc.go +++ b/cmd/pinniped/cmd/login_oidc.go @@ -22,6 +22,7 @@ import ( clientauthv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1" "k8s.io/klog/v2/klogr" + "go.pinniped.dev/internal/execcredcache" "go.pinniped.dev/internal/groupsuffix" "go.pinniped.dev/pkg/conciergeclient" "go.pinniped.dev/pkg/oidcclient" @@ -65,6 +66,7 @@ type oidcLoginFlags struct { conciergeEndpoint string conciergeCABundle string conciergeAPIGroupSuffix string + credentialCachePath string } func oidcLoginCommand(deps oidcLoginCommandDeps) *cobra.Command { @@ -95,6 +97,7 @@ func oidcLoginCommand(deps oidcLoginCommandDeps) *cobra.Command { cmd.Flags().StringVar(&flags.conciergeEndpoint, "concierge-endpoint", "", "API base for the Concierge endpoint") cmd.Flags().StringVar(&flags.conciergeCABundle, "concierge-ca-bundle-data", "", "CA bundle to use when connecting to the Concierge") cmd.Flags().StringVar(&flags.conciergeAPIGroupSuffix, "concierge-api-group-suffix", groupsuffix.PinnipedDefaultSuffix, "Concierge API group suffix") + cmd.Flags().StringVar(&flags.credentialCachePath, "credential-cache", filepath.Join(mustGetConfigDir(), "credentials.yaml"), "Cluster-specific credentials cache path (\"\" disables the cache)") mustMarkHidden(cmd, "debug-session-cache") mustMarkRequired(cmd, "issuer") @@ -164,6 +167,20 @@ func runOIDCLogin(cmd *cobra.Command, deps oidcLoginCommandDeps, flags oidcLogin opts = append(opts, oidcclient.WithClient(client)) } + // Look up cached credentials based on a hash of all the CLI arguments. + cacheKey := struct { + Args []string `json:"args"` + }{ + Args: os.Args[1:], + } + var credCache *execcredcache.Cache + if flags.credentialCachePath != "" { + credCache = execcredcache.New(flags.credentialCachePath) + if cred := credCache.Get(cacheKey); cred != nil { + return json.NewEncoder(cmd.OutOrStdout()).Encode(cred) + } + } + // Do the basic login to get an OIDC token. token, err := deps.login(flags.issuer, flags.clientID, opts...) if err != nil { @@ -181,6 +198,11 @@ func runOIDCLogin(cmd *cobra.Command, deps oidcLoginCommandDeps, flags oidcLogin return fmt.Errorf("could not complete Concierge credential exchange: %w", err) } } + + // If there was a credential cache, save the resulting credential for future use. + if credCache != nil { + credCache.Put(cacheKey, cred) + } return json.NewEncoder(cmd.OutOrStdout()).Encode(cred) } diff --git a/cmd/pinniped/cmd/login_oidc_test.go b/cmd/pinniped/cmd/login_oidc_test.go index 2b1e8469..c26c0b46 100644 --- a/cmd/pinniped/cmd/login_oidc_test.go +++ b/cmd/pinniped/cmd/login_oidc_test.go @@ -64,6 +64,7 @@ func TestLoginOIDCCommand(t *testing.T) { --concierge-authenticator-type string Concierge authenticator type (e.g., 'webhook', 'jwt') --concierge-ca-bundle-data string CA bundle to use when connecting to the Concierge --concierge-endpoint string API base for the Concierge endpoint + --credential-cache string Cluster-specific credentials cache path ("" disables the cache) (default "` + cfgDir + `/credentials.yaml") --enable-concierge Use the Concierge to login -h, --help help for oidc --issuer string OpenID Connect issuer URL @@ -189,6 +190,7 @@ func TestLoginOIDCCommand(t *testing.T) { "--concierge-endpoint", "https://127.0.0.1:1234/", "--concierge-ca-bundle-data", base64.StdEncoding.EncodeToString(testCA.Bundle()), "--concierge-api-group-suffix", "some.suffix.com", + "--credential-cache", testutil.TempDir(t) + "/credentials.yaml", }, wantOptionsCount: 7, wantStdout: `{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{},"status":{"token":"exchanged-token"}}` + "\n", diff --git a/cmd/pinniped/cmd/login_static.go b/cmd/pinniped/cmd/login_static.go index 4aa6d5eb..18a48e9d 100644 --- a/cmd/pinniped/cmd/login_static.go +++ b/cmd/pinniped/cmd/login_static.go @@ -9,11 +9,13 @@ import ( "fmt" "io" "os" + "path/filepath" "time" "github.com/spf13/cobra" clientauthv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1" + "go.pinniped.dev/internal/execcredcache" "go.pinniped.dev/internal/groupsuffix" "go.pinniped.dev/pkg/conciergeclient" "go.pinniped.dev/pkg/oidcclient/oidctypes" @@ -47,6 +49,7 @@ type staticLoginParams struct { conciergeEndpoint string conciergeCABundle string conciergeAPIGroupSuffix string + credentialCachePath string } func staticLoginCommand(deps staticLoginDeps) *cobra.Command { @@ -69,6 +72,7 @@ func staticLoginCommand(deps staticLoginDeps) *cobra.Command { cmd.Flags().StringVar(&flags.conciergeEndpoint, "concierge-endpoint", "", "API base for the Concierge endpoint") cmd.Flags().StringVar(&flags.conciergeCABundle, "concierge-ca-bundle-data", "", "CA bundle to use when connecting to the Concierge") cmd.Flags().StringVar(&flags.conciergeAPIGroupSuffix, "concierge-api-group-suffix", groupsuffix.PinnipedDefaultSuffix, "Concierge API group suffix") + cmd.Flags().StringVar(&flags.credentialCachePath, "credential-cache", filepath.Join(mustGetConfigDir(), "credentials.yaml"), "Cluster-specific credentials cache path (\"\" disables the cache)") cmd.RunE = func(cmd *cobra.Command, args []string) error { return runStaticLogin(cmd.OutOrStdout(), deps, flags) } @@ -113,6 +117,22 @@ func runStaticLogin(out io.Writer, deps staticLoginDeps, flags staticLoginParams } cred := tokenCredential(&oidctypes.Token{IDToken: &oidctypes.IDToken{Token: token}}) + // Look up cached credentials based on a hash of all the CLI arguments and the current token value. + cacheKey := struct { + Args []string `json:"args"` + Token string `json:"token"` + }{ + Args: os.Args[1:], + Token: token, + } + var credCache *execcredcache.Cache + if flags.credentialCachePath != "" { + credCache = execcredcache.New(flags.credentialCachePath) + if cred := credCache.Get(cacheKey); cred != nil { + return json.NewEncoder(out).Encode(cred) + } + } + // If the concierge was configured, exchange the credential for a separate short-lived, cluster-specific credential. if concierge != nil { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) @@ -124,5 +144,12 @@ func runStaticLogin(out io.Writer, deps staticLoginDeps, flags staticLoginParams return fmt.Errorf("could not complete Concierge credential exchange: %w", err) } } + + // If there was a credential cache, save the resulting credential for future use. We only save to the cache if + // the credential came from the concierge, since that's the only static token case where the cache is useful. + if credCache != nil && concierge != nil { + credCache.Put(cacheKey, cred) + } + return json.NewEncoder(out).Encode(cred) } diff --git a/cmd/pinniped/cmd/login_static_test.go b/cmd/pinniped/cmd/login_static_test.go index caf41df5..53739353 100644 --- a/cmd/pinniped/cmd/login_static_test.go +++ b/cmd/pinniped/cmd/login_static_test.go @@ -23,6 +23,8 @@ import ( ) func TestLoginStaticCommand(t *testing.T) { + cfgDir := mustGetConfigDir() + testCA, err := certauthority.New("Test CA", 1*time.Hour) require.NoError(t, err) tmpdir := testutil.TempDir(t) @@ -55,6 +57,7 @@ func TestLoginStaticCommand(t *testing.T) { --concierge-authenticator-type string Concierge authenticator type (e.g., 'webhook', 'jwt') --concierge-ca-bundle-data string CA bundle to use when connecting to the Concierge --concierge-endpoint string API base for the Concierge endpoint + --credential-cache string Cluster-specific credentials cache path ("" disables the cache) (default "` + cfgDir + `/credentials.yaml") --enable-concierge Use the Concierge to login -h, --help help for static --token string Static token to present during login diff --git a/internal/execcredcache/cachefile.go b/internal/execcredcache/cachefile.go new file mode 100644 index 00000000..07bd99ad --- /dev/null +++ b/internal/execcredcache/cachefile.go @@ -0,0 +1,127 @@ +// Copyright 2021 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package execcredcache + +import ( + "errors" + "fmt" + "io/ioutil" + "os" + "sort" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1" + "sigs.k8s.io/yaml" +) + +var ( + // errUnsupportedVersion is returned (internally) when we encounter a version of the cache file that we + // don't understand how to handle (such as one produced by a future version of Pinniped). + errUnsupportedVersion = fmt.Errorf("unsupported credential cache version") +) + +const ( + // apiVersion is the Kubernetes-style API version of the credential cache file object. + apiVersion = "config.supervisor.pinniped.dev/v1alpha1" + + // apiKind is the Kubernetes-style Kind of the credential cache file object. + apiKind = "CredentialCache" + + // maxCacheDuration is how long a credential can remain in the cache even if it's still otherwise valid. + maxCacheDuration = 1 * time.Hour +) + +type ( + // credCache is the object which is YAML-serialized to form the contents of the cache file. + credCache struct { + metav1.TypeMeta + Entries []entry `json:"credentials"` + } + + // entry is a single credential in the cache file. + entry struct { + Key string `json:"key"` + CreationTimestamp metav1.Time `json:"creationTimestamp"` + LastUsedTimestamp metav1.Time `json:"lastUsedTimestamp"` + Credential *clientauthenticationv1beta1.ExecCredentialStatus `json:"credential"` + } +) + +// readCache loads a credCache from a path on disk. If the requested path does not exist, it returns an empty cache. +func readCache(path string) (*credCache, error) { + cacheYAML, err := ioutil.ReadFile(path) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + // If the file was not found, generate a freshly initialized empty cache. + return emptyCache(), nil + } + // Otherwise bubble up the error. + return nil, fmt.Errorf("could not read cache file: %w", err) + } + + // If we read the file successfully, unmarshal it from YAML. + var cache credCache + if err := yaml.Unmarshal(cacheYAML, &cache); err != nil { + return nil, fmt.Errorf("invalid cache file: %w", err) + } + + // Validate that we're reading a version of the config we understand how to parse. + if !(cache.TypeMeta.APIVersion == apiVersion && cache.TypeMeta.Kind == apiKind) { + return nil, fmt.Errorf("%w: %#v", errUnsupportedVersion, cache.TypeMeta) + } + return &cache, nil +} + +// emptyCache returns an empty, initialized credCache. +func emptyCache() *credCache { + return &credCache{ + TypeMeta: metav1.TypeMeta{APIVersion: apiVersion, Kind: apiKind}, + Entries: make([]entry, 0, 1), + } +} + +// writeTo writes the cache to the specified file path. +func (c *credCache) writeTo(path string) error { + // Marshal the cache back to YAML and save it to the file. + cacheYAML, err := yaml.Marshal(c) + if err == nil { + err = ioutil.WriteFile(path, cacheYAML, 0600) + } + return err +} + +// normalized returns a copy of the credCache with stale entries removed and entries sorted in a canonical order. +func (c *credCache) normalized() *credCache { + result := emptyCache() + + // Clean up expired/invalid tokens. + now := time.Now() + result.Entries = make([]entry, 0, len(c.Entries)) + + for _, e := range c.Entries { + // Eliminate any cache entries that are missing a credential or an expiration timestamp. + if e.Credential == nil || e.Credential.ExpirationTimestamp == nil { + continue + } + + // Eliminate any expired credentials. + if e.Credential.ExpirationTimestamp.Time.Before(time.Now()) { + continue + } + + // Eliminate any entries older than maxCacheDuration. + if e.CreationTimestamp.Time.Before(now.Add(-maxCacheDuration)) { + continue + } + result.Entries = append(result.Entries, e) + } + + // Sort the entries by creation time. + sort.SliceStable(result.Entries, func(i, j int) bool { + return result.Entries[i].CreationTimestamp.Before(&result.Entries[j].CreationTimestamp) + }) + + return result +} diff --git a/internal/execcredcache/cachefile_test.go b/internal/execcredcache/cachefile_test.go new file mode 100644 index 00000000..e0544448 --- /dev/null +++ b/internal/execcredcache/cachefile_test.go @@ -0,0 +1,207 @@ +// Copyright 2021 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package execcredcache + +import ( + "os" + "testing" + "time" + + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1" + + "go.pinniped.dev/internal/testutil" +) + +var ( + // validCache should be the same data as `testdata/valid.yaml`. + validCache = credCache{ + TypeMeta: metav1.TypeMeta{APIVersion: "config.supervisor.pinniped.dev/v1alpha1", Kind: "CredentialCache"}, + Entries: []entry{ + { + Key: "test-key", + CreationTimestamp: metav1.NewTime(time.Date(2020, 10, 20, 18, 42, 7, 0, time.UTC).Local()), + LastUsedTimestamp: metav1.NewTime(time.Date(2020, 10, 20, 18, 45, 31, 0, time.UTC).Local()), + Credential: &clientauthenticationv1beta1.ExecCredentialStatus{ + Token: "test-token", + ExpirationTimestamp: &expTime, + }, + }, + }, + } + expTime = metav1.NewTime(time.Date(2020, 10, 20, 19, 46, 30, 0, time.UTC).Local()) +) + +func TestReadCache(t *testing.T) { + t.Parallel() + tests := []struct { + name string + path string + want *credCache + wantErr string + }{ + { + name: "does not exist", + path: "./testdata/does-not-exist.yaml", + want: &credCache{ + TypeMeta: metav1.TypeMeta{APIVersion: "config.supervisor.pinniped.dev/v1alpha1", Kind: "CredentialCache"}, + Entries: []entry{}, + }, + }, + { + name: "other file error", + path: "./testdata/", + wantErr: "could not read cache file: read ./testdata/: is a directory", + }, + { + name: "invalid YAML", + path: "./testdata/invalid.yaml", + wantErr: "invalid cache file: error unmarshaling JSON: while decoding JSON: json: cannot unmarshal string into Go value of type execcredcache.credCache", + }, + { + name: "wrong version", + path: "./testdata/wrong-version.yaml", + wantErr: `unsupported credential cache version: v1.TypeMeta{Kind:"NotACredentialCache", APIVersion:"config.supervisor.pinniped.dev/v2alpha6"}`, + }, + { + name: "valid", + path: "./testdata/valid.yaml", + want: &validCache, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got, err := readCache(tt.path) + if tt.wantErr != "" { + require.EqualError(t, err, tt.wantErr) + require.Nil(t, got) + return + } + require.NoError(t, err) + require.NotNil(t, got) + require.Equal(t, tt.want, got) + }) + } +} + +func TestEmptyCache(t *testing.T) { + t.Parallel() + got := emptyCache() + require.Equal(t, metav1.TypeMeta{APIVersion: "config.supervisor.pinniped.dev/v1alpha1", Kind: "CredentialCache"}, got.TypeMeta) + require.Equal(t, 0, len(got.Entries)) + require.Equal(t, 1, cap(got.Entries)) +} + +func TestWriteTo(t *testing.T) { + t.Parallel() + t.Run("io error", func(t *testing.T) { + t.Parallel() + tmp := testutil.TempDir(t) + "/credentials.yaml" + require.NoError(t, os.Mkdir(tmp, 0700)) + err := validCache.writeTo(tmp) + require.EqualError(t, err, "open "+tmp+": is a directory") + }) + + t.Run("success", func(t *testing.T) { + t.Parallel() + require.NoError(t, validCache.writeTo(testutil.TempDir(t)+"/credentials.yaml")) + }) +} + +func TestNormalized(t *testing.T) { + t.Parallel() + + t.Run("empty", func(t *testing.T) { + t.Parallel() + require.Equal(t, emptyCache(), emptyCache().normalized()) + }) + + t.Run("nonempty", func(t *testing.T) { + t.Parallel() + input := emptyCache() + now := time.Now() + oneMinuteAgo := metav1.NewTime(now.Add(-1 * time.Minute)) + oneHourFromNow := metav1.NewTime(now.Add(1 * time.Hour)) + input.Entries = []entry{ + // Credential is nil. + { + Key: "nil-credential-key", + LastUsedTimestamp: metav1.NewTime(now), + Credential: nil, + }, + // Credential's expiration is nil. + { + Key: "nil-expiration-key", + LastUsedTimestamp: metav1.NewTime(now), + Credential: &clientauthenticationv1beta1.ExecCredentialStatus{}, + }, + // Credential is expired. + { + Key: "expired-key", + LastUsedTimestamp: metav1.NewTime(now), + Credential: &clientauthenticationv1beta1.ExecCredentialStatus{ + ExpirationTimestamp: &oneMinuteAgo, + Token: "expired-token", + }, + }, + // Credential is still valid but is older than maxCacheDuration. + { + Key: "too-old-key", + LastUsedTimestamp: metav1.NewTime(now), + CreationTimestamp: metav1.NewTime(now.Add(-3 * time.Hour)), + Credential: &clientauthenticationv1beta1.ExecCredentialStatus{ + ExpirationTimestamp: &oneHourFromNow, + Token: "too-old-token", + }, + }, + // Two entries that are still valid but are out of order. + { + Key: "key-two", + CreationTimestamp: metav1.NewTime(now.Add(-1 * time.Minute)), + LastUsedTimestamp: metav1.NewTime(now), + Credential: &clientauthenticationv1beta1.ExecCredentialStatus{ + ExpirationTimestamp: &oneHourFromNow, + Token: "token-two", + }, + }, + { + Key: "key-one", + CreationTimestamp: metav1.NewTime(now.Add(-2 * time.Minute)), + LastUsedTimestamp: metav1.NewTime(now), + Credential: &clientauthenticationv1beta1.ExecCredentialStatus{ + ExpirationTimestamp: &oneHourFromNow, + Token: "token-one", + }, + }, + } + + // Expect that all but the last two valid entries are pruned, and that they're sorted. + require.Equal(t, &credCache{ + TypeMeta: metav1.TypeMeta{APIVersion: "config.supervisor.pinniped.dev/v1alpha1", Kind: "CredentialCache"}, + Entries: []entry{ + { + Key: "key-one", + CreationTimestamp: metav1.NewTime(now.Add(-2 * time.Minute)), + LastUsedTimestamp: metav1.NewTime(now), + Credential: &clientauthenticationv1beta1.ExecCredentialStatus{ + ExpirationTimestamp: &oneHourFromNow, + Token: "token-one", + }, + }, + { + Key: "key-two", + CreationTimestamp: metav1.NewTime(now.Add(-1 * time.Minute)), + LastUsedTimestamp: metav1.NewTime(now), + Credential: &clientauthenticationv1beta1.ExecCredentialStatus{ + ExpirationTimestamp: &oneHourFromNow, + Token: "token-two", + }, + }, + }, + }, input.normalized()) + }) +} diff --git a/internal/execcredcache/execcredcache.go b/internal/execcredcache/execcredcache.go new file mode 100644 index 00000000..1ab90e0d --- /dev/null +++ b/internal/execcredcache/execcredcache.go @@ -0,0 +1,159 @@ +// Copyright 2021 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Package execcredcache implements a cache for Kubernetes ExecCredential data. +package execcredcache + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "time" + + "github.com/gofrs/flock" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1" +) + +const ( + // defaultFileLockTimeout is how long we will wait trying to acquire the file lock on the cache file before timing out. + defaultFileLockTimeout = 10 * time.Second + + // defaultFileLockRetryInterval is how often we will poll while waiting for the file lock to become available. + defaultFileLockRetryInterval = 10 * time.Millisecond +) + +type Cache struct { + path string + errReporter func(error) + trylockFunc func() error + unlockFunc func() error +} + +func New(path string) *Cache { + lock := flock.New(path + ".lock") + return &Cache{ + path: path, + trylockFunc: func() error { + ctx, cancel := context.WithTimeout(context.Background(), defaultFileLockTimeout) + defer cancel() + _, err := lock.TryLockContext(ctx, defaultFileLockRetryInterval) + return err + }, + unlockFunc: lock.Unlock, + errReporter: func(_ error) {}, + } +} + +func (c *Cache) Get(key interface{}) *clientauthenticationv1beta1.ExecCredential { + // If the cache file does not exist, exit immediately with no error log + if _, err := os.Stat(c.path); errors.Is(err, os.ErrNotExist) { + return nil + } + + // Read the cache and lookup the matching entry. If one exists, update its last used timestamp and return it. + var result *clientauthenticationv1beta1.ExecCredential + cacheKey := jsonSHA256Hex(key) + c.withCache(func(cache *credCache) { + // Find the existing entry, if one exists + for i := range cache.Entries { + if cache.Entries[i].Key == cacheKey { + result = &clientauthenticationv1beta1.ExecCredential{ + TypeMeta: metav1.TypeMeta{ + Kind: "ExecCredential", + APIVersion: "client.authentication.k8s.io/v1beta1", + }, + Status: cache.Entries[i].Credential, + } + + // Update the last-used timestamp. + cache.Entries[i].LastUsedTimestamp = metav1.Now() + break + } + } + }) + return result +} + +func (c *Cache) Put(key interface{}, cred *clientauthenticationv1beta1.ExecCredential) { + // Create the cache directory if it does not exist. + if err := os.MkdirAll(filepath.Dir(c.path), 0700); err != nil && !errors.Is(err, os.ErrExist) { + c.errReporter(fmt.Errorf("could not create credential cache directory: %w", err)) + return + } + + // Mutate the cache to upsert the new entry. + cacheKey := jsonSHA256Hex(key) + c.withCache(func(cache *credCache) { + // Find the existing entry, if one exists + for i := range cache.Entries { + if cache.Entries[i].Key == cacheKey { + // Update the stored entry and return. + cache.Entries[i].Credential = cred.Status + cache.Entries[i].LastUsedTimestamp = metav1.Now() + return + } + } + + // If there's not an entry for this key, insert one. + now := metav1.Now() + cache.Entries = append(cache.Entries, entry{ + Key: cacheKey, + CreationTimestamp: now, + LastUsedTimestamp: now, + Credential: cred.Status, + }) + }) +} + +func jsonSHA256Hex(key interface{}) string { + hash := sha256.New() + if err := json.NewEncoder(hash).Encode(key); err != nil { + panic(err) + } + return hex.EncodeToString(hash.Sum(nil)) +} + +// withCache is an internal helper which locks, reads the cache, processes/mutates it with the provided function, then +// saves it back to the file. +func (c *Cache) withCache(transact func(*credCache)) { + // Grab the file lock so we have exclusive access to read the file. + if err := c.trylockFunc(); err != nil { + c.errReporter(fmt.Errorf("could not lock cache file: %w", err)) + return + } + + // Unlock the file at the end of this call, bubbling up the error if things were otherwise successful. + defer func() { + if err := c.unlockFunc(); err != nil { + c.errReporter(fmt.Errorf("could not unlock cache file: %w", err)) + } + }() + + // Try to read the existing cache. + cache, err := readCache(c.path) + if err != nil { + // If that fails, fall back to resetting to a blank slate. + c.errReporter(fmt.Errorf("failed to read cache, resetting: %w", err)) + cache = emptyCache() + } + + // Normalize the cache before modifying it, to remove any entries that have already expired. + cache = cache.normalized() + + // Process/mutate the cache using the provided function. + transact(cache) + + // Normalize again to put everything into a known order. + cache = cache.normalized() + + // Marshal the cache back to YAML and save it to the file. + if err := cache.writeTo(c.path); err != nil { + c.errReporter(fmt.Errorf("could not write cache: %w", err)) + } +} diff --git a/internal/execcredcache/execcredcache_test.go b/internal/execcredcache/execcredcache_test.go new file mode 100644 index 00000000..eab53c8d --- /dev/null +++ b/internal/execcredcache/execcredcache_test.go @@ -0,0 +1,389 @@ +// Copyright 2021 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package execcredcache + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1" + + "go.pinniped.dev/internal/testutil" +) + +func TestNew(t *testing.T) { + t.Parallel() + tmp := testutil.TempDir(t) + "/credentials.yaml" + c := New(tmp) + require.NotNil(t, c) + require.Equal(t, tmp, c.path) + require.NotNil(t, c.errReporter) + c.errReporter(fmt.Errorf("some error")) +} + +func TestGet(t *testing.T) { + t.Parallel() + now := time.Now().Round(1 * time.Second) + oneHourFromNow := metav1.NewTime(now.Add(1 * time.Hour)) + + type testKey struct{ K1, K2 string } + + tests := []struct { + name string + makeTestFile func(t *testing.T, tmp string) + trylockFunc func(*testing.T) error + unlockFunc func(*testing.T) error + key testKey + want *clientauthenticationv1beta1.ExecCredential + wantErrors []string + wantTestFile func(t *testing.T, tmp string) + }{ + { + name: "not found", + key: testKey{}, + }, + { + name: "file lock error", + makeTestFile: func(t *testing.T, tmp string) { require.NoError(t, ioutil.WriteFile(tmp, []byte(""), 0600)) }, + trylockFunc: func(t *testing.T) error { return fmt.Errorf("some lock error") }, + unlockFunc: func(t *testing.T) error { require.Fail(t, "should not be called"); return nil }, + key: testKey{}, + wantErrors: []string{"could not lock cache file: some lock error"}, + }, + { + name: "invalid file", + makeTestFile: func(t *testing.T, tmp string) { + require.NoError(t, ioutil.WriteFile(tmp, []byte("invalid yaml"), 0600)) + }, + key: testKey{}, + wantErrors: []string{ + "failed to read cache, resetting: invalid cache file: error unmarshaling JSON: while decoding JSON: json: cannot unmarshal string into Go value of type execcredcache.credCache", + }, + }, + { + name: "invalid file, fail to unlock", + makeTestFile: func(t *testing.T, tmp string) { require.NoError(t, ioutil.WriteFile(tmp, []byte("invalid"), 0600)) }, + trylockFunc: func(t *testing.T) error { return nil }, + unlockFunc: func(t *testing.T) error { return fmt.Errorf("some unlock error") }, + key: testKey{}, + wantErrors: []string{ + "failed to read cache, resetting: invalid cache file: error unmarshaling JSON: while decoding JSON: json: cannot unmarshal string into Go value of type execcredcache.credCache", + "could not unlock cache file: some unlock error", + }, + }, + { + name: "unreadable file", + makeTestFile: func(t *testing.T, tmp string) { + require.NoError(t, os.Mkdir(tmp, 0700)) + }, + key: testKey{}, + wantErrors: []string{ + "failed to read cache, resetting: could not read cache file: read TEMPFILE: is a directory", + "could not write cache: open TEMPFILE: is a directory", + }, + }, + { + name: "valid file but cache miss", + makeTestFile: func(t *testing.T, tmp string) { + validCache := emptyCache() + validCache.Entries = []entry{{ + Key: jsonSHA256Hex(testKey{K1: "v3", K2: "v4"}), + CreationTimestamp: metav1.NewTime(now.Add(-2 * time.Minute)), + LastUsedTimestamp: metav1.NewTime(now.Add(-1 * time.Minute)), + Credential: &clientauthenticationv1beta1.ExecCredentialStatus{ + Token: "test-token", + ExpirationTimestamp: &oneHourFromNow, + }, + }} + require.NoError(t, validCache.writeTo(tmp)) + }, + key: testKey{K1: "v1", K2: "v2"}, + wantErrors: []string{}, + }, + { + name: "valid file but expired cache hit", + makeTestFile: func(t *testing.T, tmp string) { + validCache := emptyCache() + oneMinuteAgo := metav1.NewTime(now.Add(-1 * time.Minute)) + validCache.Entries = []entry{{ + Key: jsonSHA256Hex(testKey{K1: "v1", K2: "v2"}), + CreationTimestamp: metav1.NewTime(now.Add(-2 * time.Minute)), + LastUsedTimestamp: metav1.NewTime(now.Add(-1 * time.Minute)), + Credential: &clientauthenticationv1beta1.ExecCredentialStatus{ + Token: "test-token", + ExpirationTimestamp: &oneMinuteAgo, + }, + }} + require.NoError(t, validCache.writeTo(tmp)) + }, + key: testKey{K1: "v1", K2: "v2"}, + wantErrors: []string{}, + }, + { + name: "valid file with cache hit", + makeTestFile: func(t *testing.T, tmp string) { + validCache := emptyCache() + + validCache.Entries = []entry{{ + Key: jsonSHA256Hex(testKey{K1: "v1", K2: "v2"}), + CreationTimestamp: metav1.NewTime(now.Add(-2 * time.Minute)), + LastUsedTimestamp: metav1.NewTime(now.Add(-1 * time.Minute)), + Credential: &clientauthenticationv1beta1.ExecCredentialStatus{ + Token: "test-token", + ExpirationTimestamp: &oneHourFromNow, + }, + }} + require.NoError(t, validCache.writeTo(tmp)) + }, + key: testKey{K1: "v1", K2: "v2"}, + wantErrors: []string{}, + want: &clientauthenticationv1beta1.ExecCredential{ + TypeMeta: metav1.TypeMeta{ + Kind: "ExecCredential", + APIVersion: "client.authentication.k8s.io/v1beta1", + }, + Spec: clientauthenticationv1beta1.ExecCredentialSpec{}, + Status: &clientauthenticationv1beta1.ExecCredentialStatus{ + Token: "test-token", + ExpirationTimestamp: &oneHourFromNow, + }, + }, + wantTestFile: func(t *testing.T, tmp string) { + cache, err := readCache(tmp) + require.NoError(t, err) + require.Len(t, cache.Entries, 1) + require.Less(t, time.Since(cache.Entries[0].LastUsedTimestamp.Time).Nanoseconds(), (5 * time.Second).Nanoseconds()) + }, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + tmp := testutil.TempDir(t) + "/sessions.yaml" + if tt.makeTestFile != nil { + tt.makeTestFile(t, tmp) + } + + // Initialize a cache with a reporter that collects errors + errors := errorCollector{t: t} + c := New(tmp) + c.errReporter = errors.report + if tt.trylockFunc != nil { + c.trylockFunc = func() error { return tt.trylockFunc(t) } + } + if tt.unlockFunc != nil { + c.unlockFunc = func() error { return tt.unlockFunc(t) } + } + + got := c.Get(tt.key) + require.Equal(t, tt.want, got) + errors.require(tt.wantErrors, "TEMPFILE", tmp) + if tt.wantTestFile != nil { + tt.wantTestFile(t, tmp) + } + }) + } +} + +func TestPutToken(t *testing.T) { + t.Parallel() + now := time.Now().Round(1 * time.Second) + + type testKey struct{ K1, K2 string } + + tests := []struct { + name string + makeTestFile func(t *testing.T, tmp string) + key testKey + cred *clientauthenticationv1beta1.ExecCredential + wantErrors []string + wantTestFile func(t *testing.T, tmp string) + }{ + { + name: "fail to create directory", + makeTestFile: func(t *testing.T, tmp string) { + require.NoError(t, ioutil.WriteFile(filepath.Dir(tmp), []byte{}, 0600)) + }, + wantErrors: []string{ + "could not create credential cache directory: mkdir TEMPDIR: not a directory", + }, + }, + { + name: "update to existing entry", + makeTestFile: func(t *testing.T, tmp string) { + validCache := emptyCache() + validCache.Entries = []entry{ + { + Key: jsonSHA256Hex(testKey{K1: "v1", K2: "v2"}), + CreationTimestamp: metav1.NewTime(now.Add(-2 * time.Minute)), + LastUsedTimestamp: metav1.NewTime(now.Add(-1 * time.Minute)), + Credential: &clientauthenticationv1beta1.ExecCredentialStatus{ + ExpirationTimestamp: timePtr(now.Add(1 * time.Hour)), + Token: "token-one", + }, + }, + + // A second entry that was created over a day ago. + { + Key: jsonSHA256Hex(testKey{K1: "v3", K2: "v4"}), + CreationTimestamp: metav1.NewTime(now.Add(-2 * time.Hour)), + LastUsedTimestamp: metav1.NewTime(now.Add(-1 * time.Hour)), + Credential: &clientauthenticationv1beta1.ExecCredentialStatus{ + ExpirationTimestamp: timePtr(now.Add(1 * time.Hour)), + Token: "token-two", + }, + }, + } + require.NoError(t, os.MkdirAll(filepath.Dir(tmp), 0700)) + require.NoError(t, validCache.writeTo(tmp)) + }, + key: testKey{K1: "v1", K2: "v2"}, + cred: &clientauthenticationv1beta1.ExecCredential{ + TypeMeta: metav1.TypeMeta{ + Kind: "ExecCredential", + APIVersion: "client.authentication.k8s.io/v1beta1", + }, + Status: &clientauthenticationv1beta1.ExecCredentialStatus{ + ExpirationTimestamp: timePtr(now.Add(1 * time.Hour)), + Token: "token-one", + }, + }, + wantTestFile: func(t *testing.T, tmp string) { + cache, err := readCache(tmp) + require.NoError(t, err) + require.Len(t, cache.Entries, 1) + require.Less(t, time.Since(cache.Entries[0].LastUsedTimestamp.Time).Nanoseconds(), (5 * time.Second).Nanoseconds()) + require.Equal(t, &clientauthenticationv1beta1.ExecCredentialStatus{ + ExpirationTimestamp: timePtr(now.Add(1 * time.Hour).Local()), + Token: "token-one", + }, cache.Entries[0].Credential) + }, + }, + { + name: "new entry", + makeTestFile: func(t *testing.T, tmp string) { + validCache := emptyCache() + validCache.Entries = []entry{ + { + Key: jsonSHA256Hex(testKey{K1: "v3", K2: "v4"}), + CreationTimestamp: metav1.NewTime(now.Add(-2 * time.Minute)), + LastUsedTimestamp: metav1.NewTime(now.Add(-1 * time.Minute)), + Credential: &clientauthenticationv1beta1.ExecCredentialStatus{ + ExpirationTimestamp: timePtr(now.Add(1 * time.Hour)), + Token: "other-token", + }, + }, + } + require.NoError(t, os.MkdirAll(filepath.Dir(tmp), 0700)) + require.NoError(t, validCache.writeTo(tmp)) + }, + key: testKey{K1: "v1", K2: "v2"}, + cred: &clientauthenticationv1beta1.ExecCredential{ + TypeMeta: metav1.TypeMeta{ + Kind: "ExecCredential", + APIVersion: "client.authentication.k8s.io/v1beta1", + }, + Status: &clientauthenticationv1beta1.ExecCredentialStatus{ + ExpirationTimestamp: timePtr(now.Add(1 * time.Hour)), + Token: "token-one", + }, + }, + wantTestFile: func(t *testing.T, tmp string) { + cache, err := readCache(tmp) + require.NoError(t, err) + require.Len(t, cache.Entries, 2) + require.Less(t, time.Since(cache.Entries[1].LastUsedTimestamp.Time).Nanoseconds(), (5 * time.Second).Nanoseconds()) + require.Equal(t, &clientauthenticationv1beta1.ExecCredentialStatus{ + ExpirationTimestamp: timePtr(now.Add(1 * time.Hour).Local()), + Token: "token-one", + }, cache.Entries[1].Credential) + }, + }, + { + name: "error writing cache", + makeTestFile: func(t *testing.T, tmp string) { + require.NoError(t, os.MkdirAll(tmp, 0700)) + }, + key: testKey{K1: "v1", K2: "v2"}, + cred: &clientauthenticationv1beta1.ExecCredential{ + TypeMeta: metav1.TypeMeta{ + Kind: "ExecCredential", + APIVersion: "client.authentication.k8s.io/v1beta1", + }, + Status: &clientauthenticationv1beta1.ExecCredentialStatus{ + ExpirationTimestamp: timePtr(now.Add(1 * time.Hour)), + Token: "token-one", + }, + }, + wantErrors: []string{ + "failed to read cache, resetting: could not read cache file: read TEMPFILE: is a directory", + "could not write cache: open TEMPFILE: is a directory", + }, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + tmp := testutil.TempDir(t) + "/cachedir/credentials.yaml" + if tt.makeTestFile != nil { + tt.makeTestFile(t, tmp) + } + // Initialize a cache with a reporter that collects errors + errors := errorCollector{t: t} + c := New(tmp) + c.errReporter = errors.report + c.Put(tt.key, tt.cred) + errors.require(tt.wantErrors, "TEMPFILE", tmp, "TEMPDIR", filepath.Dir(tmp)) + if tt.wantTestFile != nil { + tt.wantTestFile(t, tmp) + } + }) + } +} + +func TestHashing(t *testing.T) { + type testKey struct{ K1, K2 string } + require.Equal(t, "38e0b9de817f645c4bec37c0d4a3e58baecccb040f5718dc069a72c7385a0bed", jsonSHA256Hex(nil)) + require.Equal(t, "625bb1f93dc90a1bda400fdaceb8c96328e567a0c6aaf81e7fccc68958b4565d", jsonSHA256Hex([]string{"k1", "k2"})) + require.Equal(t, "8fb659f5dd266ffd8d0c96116db1d96fe10e3879f9cb6f7e9ace016696ff69f6", jsonSHA256Hex(testKey{K1: "v1", K2: "v2"})) + require.Equal(t, "42c783a2c29f91127b064df368bda61788181d2dd1709b417f9506102ea8da67", jsonSHA256Hex(testKey{K1: "v3", K2: "v4"})) + require.Panics(t, func() { jsonSHA256Hex(&unmarshalable{}) }) +} + +type errorCollector struct { + t *testing.T + saw []error +} + +func (e *errorCollector) report(err error) { + e.saw = append(e.saw, err) +} + +func (e *errorCollector) require(want []string, subs ...string) { + require.Len(e.t, e.saw, len(want)) + for i, w := range want { + for i := 0; i < len(subs); i += 2 { + w = strings.ReplaceAll(w, subs[i], subs[i+1]) + } + require.EqualError(e.t, e.saw[i], w) + } +} + +func timePtr(from time.Time) *metav1.Time { + t := metav1.NewTime(from) + return &t +} + +type unmarshalable struct{} + +func (*unmarshalable) MarshalJSON() ([]byte, error) { return nil, fmt.Errorf("some MarshalJSON error") } diff --git a/internal/execcredcache/testdata/invalid.yaml b/internal/execcredcache/testdata/invalid.yaml new file mode 100644 index 00000000..85e638a6 --- /dev/null +++ b/internal/execcredcache/testdata/invalid.yaml @@ -0,0 +1 @@ +invalid YAML diff --git a/internal/execcredcache/testdata/valid.yaml b/internal/execcredcache/testdata/valid.yaml new file mode 100644 index 00000000..d7c2fcb0 --- /dev/null +++ b/internal/execcredcache/testdata/valid.yaml @@ -0,0 +1,9 @@ +apiVersion: config.supervisor.pinniped.dev/v1alpha1 +kind: CredentialCache +credentials: + - key: "test-key" + creationTimestamp: "2020-10-20T18:42:07Z" + lastUsedTimestamp: "2020-10-20T18:45:31Z" + credential: + expirationTimestamp: "2020-10-20T19:46:30Z" + token: "test-token" diff --git a/internal/execcredcache/testdata/wrong-version.yaml b/internal/execcredcache/testdata/wrong-version.yaml new file mode 100644 index 00000000..62421a39 --- /dev/null +++ b/internal/execcredcache/testdata/wrong-version.yaml @@ -0,0 +1,3 @@ +apiVersion: config.supervisor.pinniped.dev/v2alpha6 +kind: NotACredentialCache +credentials: [] From 271c006b6c773fa72bbf030bded01fdd446c3b2e Mon Sep 17 00:00:00 2001 From: Matt Moyer Date: Thu, 8 Apr 2021 16:00:21 -0500 Subject: [PATCH 04/12] Add --credential-cache flag to "pinniped get kubeconfig" and tweak usage messages. Signed-off-by: Matt Moyer --- cmd/pinniped/cmd/kubeconfig.go | 10 +++++++++- cmd/pinniped/cmd/kubeconfig_test.go | 5 +++++ cmd/pinniped/cmd/login_oidc.go | 2 +- cmd/pinniped/cmd/login_oidc_test.go | 2 +- cmd/pinniped/cmd/login_static.go | 2 +- cmd/pinniped/cmd/login_static_test.go | 2 +- 6 files changed, 18 insertions(+), 5 deletions(-) diff --git a/cmd/pinniped/cmd/kubeconfig.go b/cmd/pinniped/cmd/kubeconfig.go index 8e0051b6..50aee669 100644 --- a/cmd/pinniped/cmd/kubeconfig.go +++ b/cmd/pinniped/cmd/kubeconfig.go @@ -87,6 +87,8 @@ type getKubeconfigParams struct { oidc getKubeconfigOIDCParams concierge getKubeconfigConciergeParams generatedNameSuffix string + credentialCachePath string + credentialCachePathSet bool } func kubeconfigCommand(deps kubeconfigDeps) *cobra.Command { @@ -132,7 +134,7 @@ func kubeconfigCommand(deps kubeconfigDeps) *cobra.Command { f.DurationVar(&flags.timeout, "timeout", 10*time.Minute, "Timeout for autodiscovery and validation") f.StringVarP(&flags.outputPath, "output", "o", "", "Output file path (default: stdout)") f.StringVar(&flags.generatedNameSuffix, "generated-name-suffix", "-pinniped", "Suffix to append to generated cluster, context, user kubeconfig entries") - + f.StringVar(&flags.credentialCachePath, "credential-cache", "", "Path to cluster-specific credentials cache") mustMarkHidden(cmd, "oidc-debug-session-cache") mustMarkDeprecated(cmd, "concierge-namespace", "not needed anymore") @@ -147,6 +149,7 @@ func kubeconfigCommand(deps kubeconfigDeps) *cobra.Command { defer func() { _ = out.Close() }() cmd.SetOut(out) } + flags.credentialCachePathSet = cmd.Flags().Changed("credential-cache") return runGetKubeconfig(cmd.Context(), cmd.OutOrStdout(), deps, flags) } return cmd @@ -233,6 +236,11 @@ func runGetKubeconfig(ctx context.Context, out io.Writer, deps kubeconfigDeps, f cluster.CertificateAuthorityData = flags.concierge.caBundle } + // If --credential-cache is set, pass it through. + if flags.credentialCachePathSet { + execConfig.Args = append(execConfig.Args, "--credential-cache="+flags.credentialCachePath) + } + // If one of the --static-* flags was passed, output a config that runs `pinniped login static`. if flags.staticToken != "" || flags.staticTokenEnvName != "" { if flags.staticToken != "" && flags.staticTokenEnvName != "" { diff --git a/cmd/pinniped/cmd/kubeconfig_test.go b/cmd/pinniped/cmd/kubeconfig_test.go index c7342a54..52384bb4 100644 --- a/cmd/pinniped/cmd/kubeconfig_test.go +++ b/cmd/pinniped/cmd/kubeconfig_test.go @@ -73,6 +73,7 @@ func TestGetKubeconfig(t *testing.T) { --concierge-endpoint string API base for the Concierge endpoint --concierge-mode mode Concierge mode of operation (default TokenCredentialRequestAPI) --concierge-skip-wait Skip waiting for any pending Concierge strategies to become ready (default: false) + --credential-cache string Path to cluster-specific credentials cache --generated-name-suffix string Suffix to append to generated cluster, context, user kubeconfig entries (default "-pinniped") -h, --help help for kubeconfig --kubeconfig string Path to kubeconfig file @@ -642,6 +643,7 @@ func TestGetKubeconfig(t *testing.T) { "--kubeconfig", "./testdata/kubeconfig.yaml", "--static-token-env", "TEST_TOKEN", "--skip-validation", + "--credential-cache", "", }, conciergeObjects: []runtime.Object{ &configv1alpha1.CredentialIssuer{ @@ -699,6 +701,7 @@ func TestGetKubeconfig(t *testing.T) { - --concierge-authenticator-type=webhook - --concierge-endpoint=https://fake-server-url-value - --concierge-ca-bundle-data=ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ== + - --credential-cache= - --token-env=TEST_TOKEN command: '.../path/to/pinniped' env: [] @@ -809,6 +812,7 @@ func TestGetKubeconfig(t *testing.T) { "--oidc-request-audience", "test-audience", "--skip-validation", "--generated-name-suffix", "-sso", + "--credential-cache", "/path/to/cache/dir/credentials.yaml", }, conciergeObjects: []runtime.Object{ &configv1alpha1.CredentialIssuer{ @@ -862,6 +866,7 @@ func TestGetKubeconfig(t *testing.T) { - --concierge-authenticator-type=webhook - --concierge-endpoint=https://explicit-concierge-endpoint.example.com - --concierge-ca-bundle-data=%s + - --credential-cache=/path/to/cache/dir/credentials.yaml - --issuer=https://example.com/issuer - --client-id=pinniped-cli - --scopes=offline_access,openid,pinniped:request-audience diff --git a/cmd/pinniped/cmd/login_oidc.go b/cmd/pinniped/cmd/login_oidc.go index 8395e6c7..4d751d35 100644 --- a/cmd/pinniped/cmd/login_oidc.go +++ b/cmd/pinniped/cmd/login_oidc.go @@ -97,7 +97,7 @@ func oidcLoginCommand(deps oidcLoginCommandDeps) *cobra.Command { cmd.Flags().StringVar(&flags.conciergeEndpoint, "concierge-endpoint", "", "API base for the Concierge endpoint") cmd.Flags().StringVar(&flags.conciergeCABundle, "concierge-ca-bundle-data", "", "CA bundle to use when connecting to the Concierge") cmd.Flags().StringVar(&flags.conciergeAPIGroupSuffix, "concierge-api-group-suffix", groupsuffix.PinnipedDefaultSuffix, "Concierge API group suffix") - cmd.Flags().StringVar(&flags.credentialCachePath, "credential-cache", filepath.Join(mustGetConfigDir(), "credentials.yaml"), "Cluster-specific credentials cache path (\"\" disables the cache)") + cmd.Flags().StringVar(&flags.credentialCachePath, "credential-cache", filepath.Join(mustGetConfigDir(), "credentials.yaml"), "Path to cluster-specific credentials cache (\"\" disables the cache)") mustMarkHidden(cmd, "debug-session-cache") mustMarkRequired(cmd, "issuer") diff --git a/cmd/pinniped/cmd/login_oidc_test.go b/cmd/pinniped/cmd/login_oidc_test.go index c26c0b46..8472f6af 100644 --- a/cmd/pinniped/cmd/login_oidc_test.go +++ b/cmd/pinniped/cmd/login_oidc_test.go @@ -64,7 +64,7 @@ func TestLoginOIDCCommand(t *testing.T) { --concierge-authenticator-type string Concierge authenticator type (e.g., 'webhook', 'jwt') --concierge-ca-bundle-data string CA bundle to use when connecting to the Concierge --concierge-endpoint string API base for the Concierge endpoint - --credential-cache string Cluster-specific credentials cache path ("" disables the cache) (default "` + cfgDir + `/credentials.yaml") + --credential-cache string Path to cluster-specific credentials cache ("" disables the cache) (default "` + cfgDir + `/credentials.yaml") --enable-concierge Use the Concierge to login -h, --help help for oidc --issuer string OpenID Connect issuer URL diff --git a/cmd/pinniped/cmd/login_static.go b/cmd/pinniped/cmd/login_static.go index 18a48e9d..da7ff8e6 100644 --- a/cmd/pinniped/cmd/login_static.go +++ b/cmd/pinniped/cmd/login_static.go @@ -72,7 +72,7 @@ func staticLoginCommand(deps staticLoginDeps) *cobra.Command { cmd.Flags().StringVar(&flags.conciergeEndpoint, "concierge-endpoint", "", "API base for the Concierge endpoint") cmd.Flags().StringVar(&flags.conciergeCABundle, "concierge-ca-bundle-data", "", "CA bundle to use when connecting to the Concierge") cmd.Flags().StringVar(&flags.conciergeAPIGroupSuffix, "concierge-api-group-suffix", groupsuffix.PinnipedDefaultSuffix, "Concierge API group suffix") - cmd.Flags().StringVar(&flags.credentialCachePath, "credential-cache", filepath.Join(mustGetConfigDir(), "credentials.yaml"), "Cluster-specific credentials cache path (\"\" disables the cache)") + cmd.Flags().StringVar(&flags.credentialCachePath, "credential-cache", filepath.Join(mustGetConfigDir(), "credentials.yaml"), "Path to cluster-specific credentials cache (\"\" disables the cache)") cmd.RunE = func(cmd *cobra.Command, args []string) error { return runStaticLogin(cmd.OutOrStdout(), deps, flags) } diff --git a/cmd/pinniped/cmd/login_static_test.go b/cmd/pinniped/cmd/login_static_test.go index 53739353..c2e6d3ea 100644 --- a/cmd/pinniped/cmd/login_static_test.go +++ b/cmd/pinniped/cmd/login_static_test.go @@ -57,7 +57,7 @@ func TestLoginStaticCommand(t *testing.T) { --concierge-authenticator-type string Concierge authenticator type (e.g., 'webhook', 'jwt') --concierge-ca-bundle-data string CA bundle to use when connecting to the Concierge --concierge-endpoint string API base for the Concierge endpoint - --credential-cache string Cluster-specific credentials cache path ("" disables the cache) (default "` + cfgDir + `/credentials.yaml") + --credential-cache string Path to cluster-specific credentials cache ("" disables the cache) (default "` + cfgDir + `/credentials.yaml") --enable-concierge Use the Concierge to login -h, --help help for static --token string Static token to present during login From 3b461572eae544c183426748a34a6cd4d6ac35b5 Mon Sep 17 00:00:00 2001 From: Matt Moyer Date: Thu, 8 Apr 2021 17:00:14 -0500 Subject: [PATCH 05/12] Add cluster info to cache key for cluster-specific credential cache. This isn't strictly necessary because we currently always have the concierge endpoint and CA as CLI flags, but it doesn't hurt and it's better to err on the side of _not_ reusing a cache entry. Signed-off-by: Matt Moyer --- cmd/pinniped/cmd/login.go | 14 ++++++++++++++ cmd/pinniped/cmd/login_oidc.go | 8 +++++--- cmd/pinniped/cmd/login_static.go | 12 +++++++----- 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/cmd/pinniped/cmd/login.go b/cmd/pinniped/cmd/login.go index e27442ee..d1d1d151 100644 --- a/cmd/pinniped/cmd/login.go +++ b/cmd/pinniped/cmd/login.go @@ -5,6 +5,8 @@ package cmd import ( "github.com/spf13/cobra" + clientauthv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1" + "k8s.io/client-go/tools/auth/exec" ) //nolint: gochecknoglobals @@ -20,3 +22,15 @@ var loginCmd = &cobra.Command{ func init() { rootCmd.AddCommand(loginCmd) } + +func loadClusterInfo() *clientauthv1beta1.Cluster { + obj, _, err := exec.LoadExecCredentialFromEnv() + if err != nil { + return nil + } + cred, ok := obj.(*clientauthv1beta1.ExecCredential) + if !ok { + return nil + } + return cred.Spec.Cluster +} diff --git a/cmd/pinniped/cmd/login_oidc.go b/cmd/pinniped/cmd/login_oidc.go index 4d751d35..34ead8f8 100644 --- a/cmd/pinniped/cmd/login_oidc.go +++ b/cmd/pinniped/cmd/login_oidc.go @@ -167,11 +167,13 @@ func runOIDCLogin(cmd *cobra.Command, deps oidcLoginCommandDeps, flags oidcLogin opts = append(opts, oidcclient.WithClient(client)) } - // Look up cached credentials based on a hash of all the CLI arguments. + // Look up cached credentials based on a hash of all the CLI arguments and the cluster info. cacheKey := struct { - Args []string `json:"args"` + Args []string `json:"args"` + ClusterInfo *clientauthv1beta1.Cluster `json:"cluster"` }{ - Args: os.Args[1:], + Args: os.Args[1:], + ClusterInfo: loadClusterInfo(), } var credCache *execcredcache.Cache if flags.credentialCachePath != "" { diff --git a/cmd/pinniped/cmd/login_static.go b/cmd/pinniped/cmd/login_static.go index da7ff8e6..4b9ac2fd 100644 --- a/cmd/pinniped/cmd/login_static.go +++ b/cmd/pinniped/cmd/login_static.go @@ -117,13 +117,15 @@ func runStaticLogin(out io.Writer, deps staticLoginDeps, flags staticLoginParams } cred := tokenCredential(&oidctypes.Token{IDToken: &oidctypes.IDToken{Token: token}}) - // Look up cached credentials based on a hash of all the CLI arguments and the current token value. + // Look up cached credentials based on a hash of all the CLI arguments, the current token value, and the cluster info. cacheKey := struct { - Args []string `json:"args"` - Token string `json:"token"` + Args []string `json:"args"` + Token string `json:"token"` + ClusterInfo *clientauthv1beta1.Cluster `json:"cluster"` }{ - Args: os.Args[1:], - Token: token, + Args: os.Args[1:], + Token: token, + ClusterInfo: loadClusterInfo(), } var credCache *execcredcache.Cache if flags.credentialCachePath != "" { From b59a4f3fec3a038a0563e1d59ccc04bd50f58a57 Mon Sep 17 00:00:00 2001 From: Matt Moyer Date: Thu, 8 Apr 2021 18:14:21 -0500 Subject: [PATCH 06/12] Use a temporary directory for credential cache in CLI tests. This avoids polluting the main cache directory on developer machines. Signed-off-by: Matt Moyer --- test/integration/cli_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/integration/cli_test.go b/test/integration/cli_test.go index 1221a604..3bf37437 100644 --- a/test/integration/cli_test.go +++ b/test/integration/cli_test.go @@ -49,11 +49,13 @@ func TestCLIGetKubeconfigStaticToken(t *testing.T) { // Build pinniped CLI. pinnipedExe := library.PinnipedCLIPath(t) + credCacheDir := testutil.TempDir(t) stdout, stderr := runPinnipedCLI(t, nil, pinnipedExe, "get", "kubeconfig", "--static-token", env.TestUser.Token, "--concierge-api-group-suffix", env.APIGroupSuffix, "--concierge-authenticator-type", "webhook", "--concierge-authenticator-name", authenticator.Name, + "--credential-cache", credCacheDir+"/credentials.yaml", ) assert.Contains(t, stderr, "discovered CredentialIssuer") assert.Contains(t, stderr, "discovered Concierge endpoint") @@ -383,6 +385,7 @@ func oidcLoginCommand(ctx context.Context, t *testing.T, pinnipedExe string, ses "--scopes", "offline_access,openid,email,profile", "--listen-port", callbackURL.Port(), "--session-cache", sessionCachePath, + "--credential-cache", testutil.TempDir(t)+"/credentials.yaml", "--skip-browser", ) From e5314164c5a8605f09a0d3deaa499a6c0eb2d422 Mon Sep 17 00:00:00 2001 From: Nanci Lancaster Date: Tue, 6 Apr 2021 12:20:17 -0500 Subject: [PATCH 07/12] added search functionality to docs on Pinniped.dev Signed-off-by: Nanci Lancaster --- public/categories/index.xml | 9 +++++++++ public/index.xml | 9 +++++++++ public/sitemap.xml | 11 +++++++++++ public/tags/index.xml | 9 +++++++++ site/themes/pinniped/layouts/_default/baseof.html | 9 +++++++++ .../pinniped/layouts/partials/docs-sidebar.html | 8 ++++++++ 6 files changed, 55 insertions(+) create mode 100644 public/categories/index.xml create mode 100644 public/index.xml create mode 100644 public/sitemap.xml create mode 100644 public/tags/index.xml diff --git a/public/categories/index.xml b/public/categories/index.xml new file mode 100644 index 00000000..1edd3cb5 --- /dev/null +++ b/public/categories/index.xml @@ -0,0 +1,9 @@ + + + + Categories on + /categories/ + Recent content in Categories on + Hugo -- gohugo.io + + diff --git a/public/index.xml b/public/index.xml new file mode 100644 index 00000000..e61b979e --- /dev/null +++ b/public/index.xml @@ -0,0 +1,9 @@ + + + + + / + Recent content on + Hugo -- gohugo.io + + diff --git a/public/sitemap.xml b/public/sitemap.xml new file mode 100644 index 00000000..cd5ab70f --- /dev/null +++ b/public/sitemap.xml @@ -0,0 +1,11 @@ + + + + / + + /categories/ + + /tags/ + + diff --git a/public/tags/index.xml b/public/tags/index.xml new file mode 100644 index 00000000..1c4e357b --- /dev/null +++ b/public/tags/index.xml @@ -0,0 +1,9 @@ + + + + Tags on + /tags/ + Recent content in Tags on + Hugo -- gohugo.io + + diff --git a/site/themes/pinniped/layouts/_default/baseof.html b/site/themes/pinniped/layouts/_default/baseof.html index 2977d22e..e9a0e599 100644 --- a/site/themes/pinniped/layouts/_default/baseof.html +++ b/site/themes/pinniped/layouts/_default/baseof.html @@ -15,10 +15,19 @@ {{ with .OutputFormats.Get "RSS" -}} {{ printf `` .Rel .MediaType.Type .RelPermalink $.Site.Title | safeHTML }} {{- end }} + {{ partial "header" . }} {{ block "main" . }}{{ end }} {{ partial "footer" . }} + + diff --git a/site/themes/pinniped/layouts/partials/docs-sidebar.html b/site/themes/pinniped/layouts/partials/docs-sidebar.html index cdd79d4b..00d68b75 100644 --- a/site/themes/pinniped/layouts/partials/docs-sidebar.html +++ b/site/themes/pinniped/layouts/partials/docs-sidebar.html @@ -1,4 +1,12 @@
+
+ + + +
    {{- $currentPage := . }} {{- range .Site.Menus.docs }} From 63816aa3ba62b7c7af1644564667b3ea886afc55 Mon Sep 17 00:00:00 2001 From: Matt Moyer Date: Fri, 9 Apr 2021 10:57:54 -0500 Subject: [PATCH 08/12] Disable Content-Security-Policy for now. Signed-off-by: Matt Moyer --- site/netlify.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/site/netlify.toml b/site/netlify.toml index 3118ada5..8e45c395 100644 --- a/site/netlify.toml +++ b/site/netlify.toml @@ -37,7 +37,8 @@ HUGO_ENABLEGITINFO = "true" [[headers]] for = "/*" [headers.values] - Content-Security-Policy = "default-src 'self'; img-src *" + # disabled to support docsearch until https://github.com/algolia/instantsearch.js/issues/2868 is fixed. + # Content-Security-Policy = "default-src 'self'; img-src *" X-Content-Type-Options = "nosniff" X-Frame-Options = "DENY" X-XSS-Protection = "1; mode=block" \ No newline at end of file From 11d820be06b1d18448fb259abb1b6ab469599f8c Mon Sep 17 00:00:00 2001 From: Matt Moyer Date: Fri, 9 Apr 2021 09:55:27 -0500 Subject: [PATCH 09/12] Remove proxy-kubeconfig.yaml. I don't believe this is used by any tests or docs. I think it was for some initial local testing of the impersonation proxy? Signed-off-by: Matt Moyer --- proxy-kubeconfig.yaml | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 proxy-kubeconfig.yaml diff --git a/proxy-kubeconfig.yaml b/proxy-kubeconfig.yaml deleted file mode 100644 index 37c60516..00000000 --- a/proxy-kubeconfig.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: v1 -clusters: - - cluster: - server: https://127.0.0.1:8444 - insecure-skip-tls-verify: true - name: kind-pinniped -contexts: - - context: - cluster: kind-pinniped - user: kind-pinniped - name: kind-pinniped -current-context: kind-pinniped -kind: Config -preferences: {} -users: - - name: kind-pinniped From 38f3ea3f2f264ce95603e61137bae3e82d861e97 Mon Sep 17 00:00:00 2001 From: Matt Moyer Date: Fri, 9 Apr 2021 10:40:23 -0500 Subject: [PATCH 10/12] Upgrade to client-go and apimachinery from Kubernetes 1.21. Signed-off-by: Matt Moyer --- go.mod | 18 ++++++------ go.sum | 89 ++++++++++++++++++++++++++++++---------------------------- 2 files changed, 55 insertions(+), 52 deletions(-) diff --git a/go.mod b/go.mod index 9541d8da..9e54e61c 100644 --- a/go.mod +++ b/go.mod @@ -26,20 +26,20 @@ require ( github.com/spf13/cobra v1.1.3 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.7.0 - golang.org/x/crypto v0.0.0-20201217014255-9d1352758620 + golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d golang.org/x/sync v0.0.0-20201207232520-09787c993a3a gopkg.in/square/go-jose.v2 v2.5.1 - k8s.io/api v0.0.0-20210329192759-4cbcd86ea749 - k8s.io/apimachinery v0.21.0-alpha.0.0.20210329192153-640a6275d2b0 - k8s.io/apiserver v0.0.0-20210330222258-23775f4efbdf - k8s.io/client-go v0.0.0-20210329194426-720ea497dc06 - k8s.io/component-base v0.0.0-20210329195309-e1576f54c4ca - k8s.io/gengo v0.0.0-20201113003025-83324d819ded + k8s.io/api v0.21.0 + k8s.io/apimachinery v0.21.0 + k8s.io/apiserver v0.21.0 + k8s.io/client-go v0.21.0 + k8s.io/component-base v0.21.0 + k8s.io/gengo v0.0.0-20201214224949-b6c5ce23f027 k8s.io/klog/v2 v2.8.0 - k8s.io/kube-aggregator v0.0.0-20210329201137-c9d5b747f33b - k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd + k8s.io/kube-aggregator v0.21.0 + k8s.io/kube-openapi v0.0.0-20210305001622-591a79e4bda7 k8s.io/utils v0.0.0-20201110183641-67b214c5f920 sigs.k8s.io/yaml v1.2.0 ) diff --git a/go.sum b/go.sum index 919c1ecc..550f8a33 100644 --- a/go.sum +++ b/go.sum @@ -35,14 +35,12 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7 github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= -github.com/Azure/go-autorest/autorest v0.11.1 h1:eVvIXUKiTgv++6YnWb42DUA1YL7qDugnKP0HljexdnQ= -github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw= -github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg= +github.com/Azure/go-autorest/autorest v0.11.12 h1:gI8ytXbxMfI+IVbI9mP2JGCTXIuhHLgRlvQ9X4PsnHE= +github.com/Azure/go-autorest/autorest v0.11.12/go.mod h1:eipySxLmqSyC5s5k1CLupqet0PSENBEDP93LQ9a8QYw= github.com/Azure/go-autorest/autorest/adal v0.9.5 h1:Y3bBUV4rTuxenJJs41HU3qmqsb+auo+a3Lz+PlJPpL0= github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= -github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= github.com/Azure/go-autorest/autorest/mocks v0.4.1 h1:K0laFcLE6VLTOwNgSxaGbUcLPuGXlNkbVvq4cW4nIHk= github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= github.com/Azure/go-autorest/logger v0.2.0 h1:e4RVHVZKC5p6UANLJHkM4OfR1UKZPj8Wt8Pcx+3oqrE= @@ -60,8 +58,9 @@ github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF0 github.com/Masterminds/semver/v3 v3.0.3/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= -github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46 h1:lsxEuwrXEAokXB9qhlbKWPpo3KMLZQ5WB5WLQRW1uq0= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= +github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= @@ -147,6 +146,7 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cucumber/godog v0.8.1/go.mod h1:vSh3r/lM+psC1BPXvdkSEuNjmXfpVqrMGYAElF6hxnA= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -161,8 +161,6 @@ github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8 github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96 h1:cenwrSVm+Z7QLSV/BsnenAOcDXdX4cMv4wP0B/5QbPg= -github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v0.0.0-20180713052910-9f541cc9db5d/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= @@ -199,7 +197,6 @@ github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoD github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-bindata/go-bindata v3.1.1+incompatible/go.mod h1:xK8Dsgwmeed+BBsSy2XTopBn/8uK2HWuGSnA11C3Joo= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= @@ -224,6 +221,7 @@ github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL9 github.com/go-openapi/jsonreference v0.19.5 h1:1WJP/wi4OjB4iV8KVbH73rQaoialJrqv8gitZLxGLtM= github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg= github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= +github.com/go-openapi/spec v0.19.5/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk= github.com/go-openapi/spec v0.20.3 h1:uH9RQ6vdyPSs2pSy9fL8QPspDF2AMIMPtmK5coSSjtQ= github.com/go-openapi/spec v0.20.3/go.mod h1:gG4F8wdEDN+YPBMVnzE85Rbhf+Th2DTvA9nFPQ5AYEg= github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= @@ -667,7 +665,6 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/konsorten/go-windows-terminal-sequences v0.0.0-20180402223658-b729f2633dfe/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= @@ -756,7 +753,9 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh github.com/mitchellh/mapstructure v1.2.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.3.2 h1:mRS76wmkOn3KkKAyXDu42V+6ebnXWIztFSYGN7GeoRg= github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo= +github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= +github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= +github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -945,8 +944,9 @@ github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo= -github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= @@ -1097,8 +1097,8 @@ golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20201217014255-9d1352758620 h1:3wPMTskHO3+O6jqTEXyFcsnuxMQOqYSaHsDxcbUXpqA= -golang.org/x/crypto v0.0.0-20201217014255-9d1352758620/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g= +golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1134,8 +1134,9 @@ golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449 h1:xUIPaMhvROX9dhPvRCenIJtU78+lbEenGbgqB5hfHCQ= +golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180816102801-aaf60122140d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1182,8 +1183,8 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210224082022-3d97a244fca7/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -1270,13 +1271,16 @@ golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200720211630-cb9d2d5c5666/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073 h1:8qxJSnu+7dRq6upnbntrmriWByIakBuct5OM/MdQC1M= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE= +golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1289,8 +1293,8 @@ golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e h1:EHBhcS0mlXEAVwNyO2dLfjToGsyY4j24pTs2ScHnX7s= -golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba h1:O8mE0/t419eoIwhTFpKVkHiTs/Igowgfkj25AcZrtiE= +golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -1370,8 +1374,9 @@ golang.org/x/tools v0.0.0-20200522201501-cb1345f3a375/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200626171337-aa94e735be7f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200721223218-6123e77877b2/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a h1:CB3a9Nez8M13wwlr/E2YtwoU+qYHKfC+JrDa45RXXoQ= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1512,6 +1517,7 @@ gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclp gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= +gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -1519,31 +1525,28 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= -k8s.io/api v0.0.0-20210329192759-4cbcd86ea749 h1:FhxDmQX91mo1Xsh6quWX769L65eJPP8kNMTeIw6d6po= -k8s.io/api v0.0.0-20210329192759-4cbcd86ea749/go.mod h1:cgILOv2D1tPmboo8I/DGUMNkV0C3JtN5aAEYvcTyKQc= -k8s.io/apimachinery v0.0.0-20210329192153-640a6275d2b0/go.mod h1:ejZXtW1Ra6V1O5H8xPBGz+T3+4gfkTCeExAHKU57MAc= -k8s.io/apimachinery v0.21.0-alpha.0.0.20210329192153-640a6275d2b0 h1:ikCWZMv/0K5O7JrQzU2r3UwSfas+7jPk/ADWV+SYPFk= -k8s.io/apimachinery v0.21.0-alpha.0.0.20210329192153-640a6275d2b0/go.mod h1:ejZXtW1Ra6V1O5H8xPBGz+T3+4gfkTCeExAHKU57MAc= -k8s.io/apiserver v0.0.0-20210329200458-395cee214d8e/go.mod h1:OORuX3x2IIO92Bj0kYiJ5RUkxHQHRbh0zT/dc9SXxdM= -k8s.io/apiserver v0.0.0-20210330222258-23775f4efbdf h1:g9BX+PSd1dGlB3IsRrvN1ntgHWeYsqewoLUB5+PqyFk= -k8s.io/apiserver v0.0.0-20210330222258-23775f4efbdf/go.mod h1:OORuX3x2IIO92Bj0kYiJ5RUkxHQHRbh0zT/dc9SXxdM= -k8s.io/client-go v0.0.0-20210329194426-720ea497dc06 h1:ig00mgkRCia3Lfr2l1uHTESYtuRFEV6Fl1NWWwvfd6E= -k8s.io/client-go v0.0.0-20210329194426-720ea497dc06/go.mod h1:1C1ztLCJQP6JaCIcN/gJ4tjQI5EDBsuD3fQ6wQCY17I= -k8s.io/code-generator v0.0.0-20210329191617-48c1e31cd8b3/go.mod h1:i6FmG+QxaLxvJsezvZp0q/gAEzzOz3U53KFibghWToU= -k8s.io/component-base v0.0.0-20210329195309-e1576f54c4ca h1:vXQs8TtiZCMlRO1AfYgSIgvoHKvqjP72WO90rzu3JNg= -k8s.io/component-base v0.0.0-20210329195309-e1576f54c4ca/go.mod h1:0GA/S/qw95GXEDv164YZl1I0s52DVkqcWMi9gskopDo= +k8s.io/api v0.21.0 h1:gu5iGF4V6tfVCQ/R+8Hc0h7H1JuEhzyEi9S4R5LM8+Y= +k8s.io/api v0.21.0/go.mod h1:+YbrhBBGgsxbF6o6Kj4KJPJnBmAKuXDeS3E18bgHNVU= +k8s.io/apimachinery v0.21.0 h1:3Fx+41if+IRavNcKOz09FwEXDBG6ORh6iMsTSelhkMA= +k8s.io/apimachinery v0.21.0/go.mod h1:jbreFvJo3ov9rj7eWT7+sYiRx+qZuCYXwWT1bcDswPY= +k8s.io/apiserver v0.21.0 h1:1hWMfsz+cXxB77k6/y0XxWxwl6l9OF26PC9QneUVn1Q= +k8s.io/apiserver v0.21.0/go.mod h1:w2YSn4/WIwYuxG5zJmcqtRdtqgW/J2JRgFAqps3bBpg= +k8s.io/client-go v0.21.0 h1:n0zzzJsAQmJngpC0IhgFcApZyoGXPrDIAD601HD09ag= +k8s.io/client-go v0.21.0/go.mod h1:nNBytTF9qPFDEhoqgEPaarobC8QPae13bElIVHzIglA= +k8s.io/code-generator v0.21.0/go.mod h1:hUlps5+9QaTrKx+jiM4rmq7YmH8wPOIko64uZCHDh6Q= +k8s.io/component-base v0.21.0 h1:tLLGp4BBjQaCpS/KiuWh7m2xqvAdsxLm4ATxHSe5Zpg= +k8s.io/component-base v0.21.0/go.mod h1:qvtjz6X0USWXbgmbfXR+Agik4RZ3jv2Bgr5QnZzdPYw= k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= -k8s.io/gengo v0.0.0-20201113003025-83324d819ded h1:JApXBKYyB7l9xx+DK7/+mFjC7A9Bt5A93FPvFD0HIFE= -k8s.io/gengo v0.0.0-20201113003025-83324d819ded/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= +k8s.io/gengo v0.0.0-20201214224949-b6c5ce23f027 h1:Uusb3oh8XcdzDF/ndlI4ToKTYVlkCSJP39SRY2mfRAw= +k8s.io/gengo v0.0.0-20201214224949-b6c5ce23f027/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= -k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= k8s.io/klog/v2 v2.8.0 h1:Q3gmuM9hKEjefWFFYF0Mat+YyFJvsUyYuwyNNJ5C9Ts= k8s.io/klog/v2 v2.8.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= -k8s.io/kube-aggregator v0.0.0-20210329201137-c9d5b747f33b h1:HDslh4s73TWDjm0BxVMOXtut7Q/o9Sz+xHSf2nmR8uw= -k8s.io/kube-aggregator v0.0.0-20210329201137-c9d5b747f33b/go.mod h1:DUuPy83ZThcPzC5SGxS3t67M5VDd85+GyCOKhvIxbqA= -k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd h1:sOHNzJIkytDF6qadMNKhhDRpc6ODik8lVC6nOur7B2c= -k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM= +k8s.io/kube-aggregator v0.21.0 h1:my2WYu8RJcj/ZzWAjPPnmxNRELk/iCdPjMaOmsZOeBU= +k8s.io/kube-aggregator v0.21.0/go.mod h1:sIaa9L4QCBo9gjPyoGJns4cBjYVLq3s49FxF7m/1A0A= +k8s.io/kube-openapi v0.0.0-20210305001622-591a79e4bda7 h1:vEx13qjvaZ4yfObSSXW7BrMc/KQBBT/Jyee8XtLf4x0= +k8s.io/kube-openapi v0.0.0-20210305001622-591a79e4bda7/go.mod h1:wXW5VT87nVfh/iLV8FpR2uDvrFyomxbtb1KivDbvPTE= k8s.io/utils v0.0.0-20201110183641-67b214c5f920 h1:CbnUZsM497iRC5QMVkHwyl8s2tB3g7yaSHkYPkpgelw= k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= modernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw= @@ -1558,8 +1561,8 @@ rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.15 h1:4uqm9Mv+w2MmBYD+F4qf/v6tDFUdPOk29C095RbU5mY= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.15/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= -sigs.k8s.io/structured-merge-diff/v4 v4.0.3 h1:4oyYo8NREp49LBBhKxEqCulFjg26rawYKrnCmg+Sr6c= -sigs.k8s.io/structured-merge-diff/v4 v4.0.3/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/structured-merge-diff/v4 v4.1.0 h1:C4r9BgJ98vrKnnVCjwCSXcWjWe0NKcUQkmzDXZXGwH8= +sigs.k8s.io/structured-merge-diff/v4 v4.1.0/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= From 599c537d24e3df759ce7169e9f5a41216e14abef Mon Sep 17 00:00:00 2001 From: Matt Moyer Date: Fri, 9 Apr 2021 11:11:06 -0500 Subject: [PATCH 11/12] Remove metav1.ExportOptions from scheme tests. This type was removed in Kubernetes v1.21.0 (see https://github.com/kubernetes/kubernetes/pull/98312). Signed-off-by: Matt Moyer --- internal/concierge/scheme/scheme_test.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/internal/concierge/scheme/scheme_test.go b/internal/concierge/scheme/scheme_test.go index dc50c235..5627ada7 100644 --- a/internal/concierge/scheme/scheme_test.go +++ b/internal/concierge/scheme/scheme_test.go @@ -90,7 +90,6 @@ func TestNew(t *testing.T) { regularLoginGV.WithKind("CreateOptions"): reflect.TypeOf(&metav1.CreateOptions{}).Elem(), regularLoginGV.WithKind("DeleteOptions"): reflect.TypeOf(&metav1.DeleteOptions{}).Elem(), - regularLoginGV.WithKind("ExportOptions"): reflect.TypeOf(&metav1.ExportOptions{}).Elem(), regularLoginGV.WithKind("GetOptions"): reflect.TypeOf(&metav1.GetOptions{}).Elem(), regularLoginGV.WithKind("ListOptions"): reflect.TypeOf(&metav1.ListOptions{}).Elem(), regularLoginGV.WithKind("PatchOptions"): reflect.TypeOf(&metav1.PatchOptions{}).Elem(), @@ -99,7 +98,6 @@ func TestNew(t *testing.T) { regularIdentityGV.WithKind("CreateOptions"): reflect.TypeOf(&metav1.CreateOptions{}).Elem(), regularIdentityGV.WithKind("DeleteOptions"): reflect.TypeOf(&metav1.DeleteOptions{}).Elem(), - regularIdentityGV.WithKind("ExportOptions"): reflect.TypeOf(&metav1.ExportOptions{}).Elem(), regularIdentityGV.WithKind("GetOptions"): reflect.TypeOf(&metav1.GetOptions{}).Elem(), regularIdentityGV.WithKind("ListOptions"): reflect.TypeOf(&metav1.ListOptions{}).Elem(), regularIdentityGV.WithKind("PatchOptions"): reflect.TypeOf(&metav1.PatchOptions{}).Elem(), @@ -120,7 +118,6 @@ func TestNew(t *testing.T) { metav1.Unversioned.WithKind("APIVersions"): reflect.TypeOf(&metav1.APIVersions{}).Elem(), metav1.Unversioned.WithKind("CreateOptions"): reflect.TypeOf(&metav1.CreateOptions{}).Elem(), metav1.Unversioned.WithKind("DeleteOptions"): reflect.TypeOf(&metav1.DeleteOptions{}).Elem(), - metav1.Unversioned.WithKind("ExportOptions"): reflect.TypeOf(&metav1.ExportOptions{}).Elem(), metav1.Unversioned.WithKind("GetOptions"): reflect.TypeOf(&metav1.GetOptions{}).Elem(), metav1.Unversioned.WithKind("ListOptions"): reflect.TypeOf(&metav1.ListOptions{}).Elem(), metav1.Unversioned.WithKind("PatchOptions"): reflect.TypeOf(&metav1.PatchOptions{}).Elem(), @@ -151,7 +148,6 @@ func TestNew(t *testing.T) { otherLoginGV.WithKind("CreateOptions"): reflect.TypeOf(&metav1.CreateOptions{}).Elem(), otherLoginGV.WithKind("DeleteOptions"): reflect.TypeOf(&metav1.DeleteOptions{}).Elem(), - otherLoginGV.WithKind("ExportOptions"): reflect.TypeOf(&metav1.ExportOptions{}).Elem(), otherLoginGV.WithKind("GetOptions"): reflect.TypeOf(&metav1.GetOptions{}).Elem(), otherLoginGV.WithKind("ListOptions"): reflect.TypeOf(&metav1.ListOptions{}).Elem(), otherLoginGV.WithKind("PatchOptions"): reflect.TypeOf(&metav1.PatchOptions{}).Elem(), @@ -160,7 +156,6 @@ func TestNew(t *testing.T) { otherIdentityGV.WithKind("CreateOptions"): reflect.TypeOf(&metav1.CreateOptions{}).Elem(), otherIdentityGV.WithKind("DeleteOptions"): reflect.TypeOf(&metav1.DeleteOptions{}).Elem(), - otherIdentityGV.WithKind("ExportOptions"): reflect.TypeOf(&metav1.ExportOptions{}).Elem(), otherIdentityGV.WithKind("GetOptions"): reflect.TypeOf(&metav1.GetOptions{}).Elem(), otherIdentityGV.WithKind("ListOptions"): reflect.TypeOf(&metav1.ListOptions{}).Elem(), otherIdentityGV.WithKind("PatchOptions"): reflect.TypeOf(&metav1.PatchOptions{}).Elem(), @@ -181,7 +176,6 @@ func TestNew(t *testing.T) { metav1.Unversioned.WithKind("APIVersions"): reflect.TypeOf(&metav1.APIVersions{}).Elem(), metav1.Unversioned.WithKind("CreateOptions"): reflect.TypeOf(&metav1.CreateOptions{}).Elem(), metav1.Unversioned.WithKind("DeleteOptions"): reflect.TypeOf(&metav1.DeleteOptions{}).Elem(), - metav1.Unversioned.WithKind("ExportOptions"): reflect.TypeOf(&metav1.ExportOptions{}).Elem(), metav1.Unversioned.WithKind("GetOptions"): reflect.TypeOf(&metav1.GetOptions{}).Elem(), metav1.Unversioned.WithKind("ListOptions"): reflect.TypeOf(&metav1.ListOptions{}).Elem(), metav1.Unversioned.WithKind("PatchOptions"): reflect.TypeOf(&metav1.PatchOptions{}).Elem(), From 923938ab26775e026ae4fec2280f9df09ebabbc1 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Wed, 14 Apr 2021 17:26:12 -0700 Subject: [PATCH 12/12] Avoid multi-line integration test env vars Avoid them because they can't be used in GoLand for running integration tests in the UI, like running in the debugger. Also adds optional PINNIPED_TEST_TOOLS_NAMESPACE because we need it on the LDAP feature branch where we are developing the upcoming LDAP support for the Supervisor. --- hack/integration-test-env-goland.sh | 22 ++++++++++++++++++++++ hack/prepare-for-integration-tests.sh | 26 +++++++++++++++----------- test/library/env.go | 21 ++++++++++++++++++--- 3 files changed, 55 insertions(+), 14 deletions(-) create mode 100755 hack/integration-test-env-goland.sh diff --git a/hack/integration-test-env-goland.sh b/hack/integration-test-env-goland.sh new file mode 100755 index 00000000..89885e2f --- /dev/null +++ b/hack/integration-test-env-goland.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +# Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +# +# Print the PINNIPED_TEST_* env vars from /tmp/integration-test-env in a format that can be used in GoLand. +# + +set -euo pipefail + +ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd )" + +source /tmp/integration-test-env + +printenv | grep PINNIPED_TEST_ | sed 's/=.*//g' | grep -v CLUSTER_CAPABILITY_YAML | while read -r var ; do + echo -n "${var}=" + echo -n "${!var}" | tr -d '\n' + echo -n ";" +done + +echo -n "PINNIPED_TEST_CLUSTER_CAPABILITY_FILE=${ROOT}/test/cluster_capabilities/kind.yaml" diff --git a/hack/prepare-for-integration-tests.sh b/hack/prepare-for-integration-tests.sh index c52e7776..b697d7ce 100755 --- a/hack/prepare-for-integration-tests.sh +++ b/hack/prepare-for-integration-tests.sh @@ -296,13 +296,18 @@ popd >/dev/null test_ca_bundle_pem="$(kubectl get secrets -n tools certs -o go-template='{{index .data "ca.pem" | base64decode}}')" # -# Create the environment file +# Create the environment file. +# +# Note that all values should not contains newlines, except for PINNIPED_TEST_CLUSTER_CAPABILITY_YAML, +# so that the environment can also be used in tools like GoLand. Therefore, multi-line values, +# such as PEM-formatted certificates, should be base64 encoded. # kind_capabilities_file="$pinniped_path/test/cluster_capabilities/kind.yaml" pinniped_cluster_capability_file_content=$(cat "$kind_capabilities_file") cat </tmp/integration-test-env # The following env vars should be set before running 'go test -v -count 1 -timeout 0 ./test/integration' +export PINNIPED_TEST_TOOLS_NAMESPACE="tools" export PINNIPED_TEST_CONCIERGE_NAMESPACE=${concierge_namespace} export PINNIPED_TEST_CONCIERGE_APP_NAME=${concierge_app_name} export PINNIPED_TEST_CONCIERGE_CUSTOM_LABELS='${concierge_custom_labels}' @@ -317,9 +322,8 @@ export PINNIPED_TEST_SUPERVISOR_CUSTOM_LABELS='${supervisor_custom_labels}' export PINNIPED_TEST_SUPERVISOR_HTTP_ADDRESS="127.0.0.1:12345" export PINNIPED_TEST_SUPERVISOR_HTTPS_ADDRESS="localhost:12344" export PINNIPED_TEST_PROXY=http://127.0.0.1:12346 -export PINNIPED_TEST_LDAP_LDAP_URL=ldap://ldap.tools.svc.cluster.local -export PINNIPED_TEST_LDAP_LDAPS_URL=ldaps://ldap.tools.svc.cluster.local -export PINNIPED_TEST_LDAP_LDAPS_CA_BUNDLE="${test_ca_bundle_pem}" +export PINNIPED_TEST_LDAP_HOST=ldap.tools.svc.cluster.local +export PINNIPED_TEST_LDAP_LDAPS_CA_BUNDLE=$(echo "${test_ca_bundle_pem}" | base64 ) export PINNIPED_TEST_LDAP_BIND_ACCOUNT_USERNAME="cn=admin,dc=pinniped,dc=dev" export PINNIPED_TEST_LDAP_BIND_ACCOUNT_PASSWORD=password export PINNIPED_TEST_LDAP_USERS_SEARCH_BASE="ou=users,dc=pinniped,dc=dev" @@ -327,6 +331,8 @@ export PINNIPED_TEST_LDAP_GROUPS_SEARCH_BASE="ou=groups,dc=pinniped,dc=dev" export PINNIPED_TEST_LDAP_USER_DN="cn=pinny,ou=users,dc=pinniped,dc=dev" export PINNIPED_TEST_LDAP_USER_CN="pinny" export PINNIPED_TEST_LDAP_USER_PASSWORD=${ldap_test_password} +export PINNIPED_TEST_LDAP_USER_UNIQUE_ID_ATTRIBUTE_NAME="uidNumber" +export PINNIPED_TEST_LDAP_USER_UNIQUE_ID_ATTRIBUTE_VALUE="1000" export PINNIPED_TEST_LDAP_USER_EMAIL_ATTRIBUTE_NAME="mail" export PINNIPED_TEST_LDAP_USER_EMAIL_ATTRIBUTE_VALUE="pinny.ldap@example.com" export PINNIPED_TEST_LDAP_EXPECTED_DIRECT_GROUPS_DN="cn=ball-game-players,ou=beach-groups,ou=groups,dc=pinniped,dc=dev;cn=seals,ou=groups,dc=pinniped,dc=dev" @@ -334,13 +340,13 @@ export PINNIPED_TEST_LDAP_EXPECTED_INDIRECT_GROUPS_DN="cn=pinnipeds,ou=groups,dc export PINNIPED_TEST_LDAP_EXPECTED_DIRECT_GROUPS_CN="ball-game-players;seals" export PINNIPED_TEST_LDAP_EXPECTED_INDIRECT_GROUPS_CN="pinnipeds;mammals" export PINNIPED_TEST_CLI_OIDC_ISSUER=https://dex.tools.svc.cluster.local/dex -export PINNIPED_TEST_CLI_OIDC_ISSUER_CA_BUNDLE="${test_ca_bundle_pem}" +export PINNIPED_TEST_CLI_OIDC_ISSUER_CA_BUNDLE=$(echo "${test_ca_bundle_pem}" | base64 ) export PINNIPED_TEST_CLI_OIDC_CLIENT_ID=pinniped-cli export PINNIPED_TEST_CLI_OIDC_CALLBACK_URL=http://127.0.0.1:48095/callback export PINNIPED_TEST_CLI_OIDC_USERNAME=pinny@example.com export PINNIPED_TEST_CLI_OIDC_PASSWORD=${dex_test_password} export PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_ISSUER=https://dex.tools.svc.cluster.local/dex -export PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_ISSUER_CA_BUNDLE="${test_ca_bundle_pem}" +export PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_ISSUER_CA_BUNDLE=$(echo "${test_ca_bundle_pem}" | base64 ) export PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_ADDITIONAL_SCOPES=email export PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_USERNAME_CLAIM=email export PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_GROUPS_CLAIM=groups @@ -360,17 +366,15 @@ export PINNIPED_TEST_CLUSTER_CAPABILITY_YAML EOF # -# Print instructions for next steps +# Print instructions for next steps. # -goland_vars=$(grep -v '^#' /tmp/integration-test-env | grep -E '^export .+=' | sed 's/export //g' | tr '\n' ';') - log_note log_note "🚀 Ready to run integration tests! For example..." log_note " cd $pinniped_path" log_note ' source /tmp/integration-test-env && go test -v -race -count 1 -timeout 0 ./test/integration' log_note -log_note 'Want to run integration tests in GoLand? Copy/paste this "Environment" value for GoLand run configurations:' -log_note " ${goland_vars}PINNIPED_TEST_CLUSTER_CAPABILITY_FILE=${kind_capabilities_file}" +log_note "Using GoLand? Paste the result of this command into GoLand's run configuration \"Environment\"." +log_note " hack/integration-test-env-goland.sh | pbcopy" log_note log_note "You can rerun this script to redeploy local production code changes while you are working." log_note diff --git a/test/library/env.go b/test/library/env.go index a19aaa2e..a806d5ff 100644 --- a/test/library/env.go +++ b/test/library/env.go @@ -4,6 +4,7 @@ package library import ( + "encoding/base64" "io/ioutil" "os" "strings" @@ -28,6 +29,7 @@ const ( type TestEnv struct { t *testing.T + ToolsNamespace string `json:"toolsNamespace"` ConciergeNamespace string `json:"conciergeNamespace"` SupervisorNamespace string `json:"supervisorNamespace"` ConciergeAppName string `json:"conciergeAppName"` @@ -122,6 +124,16 @@ func needEnv(t *testing.T, key string) string { return value } +func base64Decoded(t *testing.T, s string) string { + t.Helper() + if len(s) == 0 { + return s + } + bytes, err := base64.StdEncoding.DecodeString(s) + require.NoError(t, err) + return string(bytes) +} + func wantEnv(key, dephault string) string { value, ok := os.LookupEnv(key) if !ok { @@ -143,6 +155,8 @@ func filterEmpty(ss []string) []string { func loadEnvVars(t *testing.T, result *TestEnv) { t.Helper() + result.ToolsNamespace = os.Getenv("PINNIPED_TEST_TOOLS_NAMESPACE") + result.ConciergeNamespace = needEnv(t, "PINNIPED_TEST_CONCIERGE_NAMESPACE") result.ConciergeAppName = needEnv(t, "PINNIPED_TEST_CONCIERGE_APP_NAME") result.TestUser.ExpectedUsername = needEnv(t, "PINNIPED_TEST_USER_USERNAME") @@ -155,7 +169,6 @@ func loadEnvVars(t *testing.T, result *TestEnv) { result.SupervisorHTTPAddress = os.Getenv("PINNIPED_TEST_SUPERVISOR_HTTP_ADDRESS") result.SupervisorHTTPSIngressAddress = os.Getenv("PINNIPED_TEST_SUPERVISOR_HTTPS_INGRESS_ADDRESS") - result.SupervisorHTTPSIngressCABundle = os.Getenv("PINNIPED_TEST_SUPERVISOR_HTTPS_INGRESS_CA_BUNDLE") // optional require.NotEmptyf(t, result.SupervisorHTTPAddress+result.SupervisorHTTPSIngressAddress, "must specify either PINNIPED_TEST_SUPERVISOR_HTTP_ADDRESS or PINNIPED_TEST_SUPERVISOR_HTTPS_INGRESS_ADDRESS env var (or both) for integration tests", @@ -164,6 +177,7 @@ func loadEnvVars(t *testing.T, result *TestEnv) { require.NotRegexp(t, "^[0-9]", result.SupervisorHTTPSAddress, "PINNIPED_TEST_SUPERVISOR_HTTPS_ADDRESS must be a hostname with an optional port and cannot be an IP address", ) + result.SupervisorHTTPSIngressCABundle = base64Decoded(t, os.Getenv("PINNIPED_TEST_SUPERVISOR_HTTPS_INGRESS_CA_BUNDLE")) conciergeCustomLabelsYAML := needEnv(t, "PINNIPED_TEST_CONCIERGE_CUSTOM_LABELS") var conciergeCustomLabels map[string]string @@ -177,12 +191,13 @@ func loadEnvVars(t *testing.T, result *TestEnv) { require.NoErrorf(t, err, "PINNIPED_TEST_SUPERVISOR_CUSTOM_LABELS must be a YAML map of string to string") result.SupervisorCustomLabels = supervisorCustomLabels require.NotEmpty(t, result.SupervisorCustomLabels, "PINNIPED_TEST_SUPERVISOR_CUSTOM_LABELS cannot be empty") + result.Proxy = os.Getenv("PINNIPED_TEST_PROXY") result.APIGroupSuffix = wantEnv("PINNIPED_TEST_API_GROUP_SUFFIX", "pinniped.dev") result.CLITestUpstream = TestOIDCUpstream{ Issuer: needEnv(t, "PINNIPED_TEST_CLI_OIDC_ISSUER"), - CABundle: os.Getenv("PINNIPED_TEST_CLI_OIDC_ISSUER_CA_BUNDLE"), + CABundle: base64Decoded(t, os.Getenv("PINNIPED_TEST_CLI_OIDC_ISSUER_CA_BUNDLE")), ClientID: needEnv(t, "PINNIPED_TEST_CLI_OIDC_CLIENT_ID"), CallbackURL: needEnv(t, "PINNIPED_TEST_CLI_OIDC_CALLBACK_URL"), Username: needEnv(t, "PINNIPED_TEST_CLI_OIDC_USERNAME"), @@ -191,7 +206,7 @@ func loadEnvVars(t *testing.T, result *TestEnv) { result.SupervisorTestUpstream = TestOIDCUpstream{ Issuer: needEnv(t, "PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_ISSUER"), - CABundle: os.Getenv("PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_ISSUER_CA_BUNDLE"), + CABundle: base64Decoded(t, os.Getenv("PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_ISSUER_CA_BUNDLE")), AdditionalScopes: strings.Fields(os.Getenv("PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_ADDITIONAL_SCOPES")), UsernameClaim: os.Getenv("PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_USERNAME_CLAIM"), GroupsClaim: os.Getenv("PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_GROUPS_CLAIM"),