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.go b/cmd/pinniped/cmd/login.go index e27442ee..95e2541d 100644 --- a/cmd/pinniped/cmd/login.go +++ b/cmd/pinniped/cmd/login.go @@ -1,10 +1,12 @@ -// Copyright 2020 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 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 a3125475..34ead8f8 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"), "Path to cluster-specific credentials cache (\"\" disables the cache)") mustMarkHidden(cmd, "debug-session-cache") mustMarkRequired(cmd, "issuer") @@ -164,6 +167,22 @@ 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 and the cluster info. + cacheKey := struct { + Args []string `json:"args"` + ClusterInfo *clientauthv1beta1.Cluster `json:"cluster"` + }{ + Args: os.Args[1:], + ClusterInfo: loadClusterInfo(), + } + 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 +200,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..8472f6af 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 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 @@ -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..4b9ac2fd 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"), "Path to cluster-specific credentials cache (\"\" disables the cache)") cmd.RunE = func(cmd *cobra.Command, args []string) error { return runStaticLogin(cmd.OutOrStdout(), deps, flags) } @@ -113,6 +117,24 @@ 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, the current token value, and the cluster info. + cacheKey := struct { + Args []string `json:"args"` + Token string `json:"token"` + ClusterInfo *clientauthv1beta1.Cluster `json:"cluster"` + }{ + Args: os.Args[1:], + Token: token, + ClusterInfo: loadClusterInfo(), + } + 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 +146,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..c2e6d3ea 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 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 diff --git a/go.mod b/go.mod index 550cb6dd..3e09dce0 100644 --- a/go.mod +++ b/go.mod @@ -27,20 +27,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 50e700d8..b5ac283a 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= @@ -62,8 +60,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= @@ -149,6 +148,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= @@ -163,8 +163,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= @@ -201,7 +199,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-asn1-ber/asn1-ber v1.5.1 h1:pDbRAunXzIUXfx4CB2QJFv5IuPiuoW+sWvr/Us009o8= github.com/go-asn1-ber/asn1-ber v1.5.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= @@ -230,6 +227,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= @@ -673,7 +671,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= @@ -762,7 +759,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= @@ -951,8 +950,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= @@ -1104,8 +1104,8 @@ golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/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= @@ -1141,8 +1141,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= @@ -1189,8 +1190,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= @@ -1277,13 +1278,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= @@ -1296,8 +1300,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= @@ -1377,8 +1381,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= @@ -1519,6 +1524,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= @@ -1526,31 +1532,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= @@ -1565,8 +1568,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= 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 62c6e09a..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}' @@ -318,7 +323,7 @@ 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_HOST=ldap.tools.svc.cluster.local -export PINNIPED_TEST_LDAP_LDAPS_CA_BUNDLE="${test_ca_bundle_pem}" +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" @@ -335,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 @@ -361,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/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(), 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: [] 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)) }, 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 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/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 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 @@
+
+ + + +