Merge branch 'main' into initial_ldap

This commit is contained in:
Ryan Richard 2021-04-14 17:47:26 -07:00
commit 8d75825635
32 changed files with 1248 additions and 91 deletions

View File

@ -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 != "" {

View File

@ -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

View File

@ -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
}

View File

@ -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)
}

View File

@ -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",

View File

@ -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)
}

View File

@ -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

18
go.mod
View File

@ -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
)

89
go.sum
View File

@ -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=

View File

@ -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"

View File

@ -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 <<EOF >/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

View File

@ -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(),

View File

@ -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
}

View File

@ -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())
})
}

View File

@ -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))
}
}

View File

@ -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") }

View File

@ -0,0 +1 @@
invalid YAML

View File

@ -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"

View File

@ -0,0 +1,3 @@
apiVersion: config.supervisor.pinniped.dev/v2alpha6
kind: NotACredentialCache
credentials: []

View File

@ -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))

View File

@ -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))
},

View File

@ -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

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>Categories on </title>
<link>/categories/</link>
<description>Recent content in Categories on </description>
<generator>Hugo -- gohugo.io</generator><atom:link href="/categories/index.xml" rel="self" type="application/rss+xml" />
</channel>
</rss>

9
public/index.xml Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title></title>
<link>/</link>
<description>Recent content on </description>
<generator>Hugo -- gohugo.io</generator><atom:link href="/index.xml" rel="self" type="application/rss+xml" />
</channel>
</rss>

11
public/sitemap.xml Normal file
View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xhtml="http://www.w3.org/1999/xhtml">
<url>
<loc>/</loc>
</url><url>
<loc>/categories/</loc>
</url><url>
<loc>/tags/</loc>
</url>
</urlset>

9
public/tags/index.xml Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>Tags on </title>
<link>/tags/</link>
<description>Recent content in Tags on </description>
<generator>Hugo -- gohugo.io</generator><atom:link href="/tags/index.xml" rel="self" type="application/rss+xml" />
</channel>
</rss>

View File

@ -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"

View File

@ -15,10 +15,19 @@
{{ with .OutputFormats.Get "RSS" -}}
{{ printf `<link rel="%s" type="%s" href="%s" title="%s">` .Rel .MediaType.Type .RelPermalink $.Site.Title | safeHTML }}
{{- end }}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.css" />
</head>
<body>
{{ partial "header" . }}
{{ block "main" . }}{{ end }}
{{ partial "footer" . }}
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.js"></script>
<script type="text/javascript"> docsearch({
apiKey: '8f37841a145124eb42a0e3249d49a3c5',
indexName: 'pinniped',
inputSelector: '.docsearch-input',
debug: false // Set debug to true if you want to inspect the dropdown
});
</script>
</body>
</html>

View File

@ -1,4 +1,12 @@
<div class="side-nav">
<form class="d-flex align-items-center">
<span class="algolia-autocomplete" style="position: relative; display: inline-block; direction: ltr;">
<input type="search" class="form-control docsearch-input" id="search-input" placeholder="Search..."
aria-label="Search for..." autocomplete="off" spellcheck="false" role="combobox"
aria-autocomplete="list" aria-expanded="false" aria-owns="algolia-autocomplete-listbox-0"
dir="auto" style="position: relative; vertical-align: top;">
</span>
</form>
<ul>
{{- $currentPage := . }}
{{- range .Site.Menus.docs }}

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 104 44" style="enable-background:new 0 0 104 44;" xml:space="preserve" shape-rendering="geometricPrecision">
<path style="fill:#DB001B;" d="M45.9,40.8c0.4,0,0.7-0.2,1-0.3c0.3-0.1,0.6-0.3,0.9-0.6c0.3-0.2,0.5-0.5,0.7-0.8
c0.5-0.6,0.7-1.3,1-1.9c0.1-0.3,0.3-0.7,0.3-1.1l7-28.9C57,6.7,57.1,6.1,57,5.6C57,5.3,57,5,56.8,4.8c-0.1-0.3-0.3-0.5-0.4-0.7
c-0.1-0.1-0.3-0.3-0.4-0.4c-0.3-0.3-0.8-0.4-1.2-0.4L55.1,2h19.7l-0.3,1.5c-0.3,0-0.7,0.1-1,0.1c-0.3,0.1-0.6,0.2-0.9,0.4
c-0.2,0.1-0.4,0.3-0.6,0.5c-0.2,0.2-0.4,0.4-0.5,0.6c-0.1,0.2-0.3,0.4-0.4,0.7C71.1,6,71,6.2,71,6.5l-3.4,13.7L85.8,5.8
c0.2-0.1,0.3-0.3,0.5-0.5c0.1-0.2,0.3-0.3,0.4-0.5c0.2-0.3,0.2-0.6,0.1-0.9c-0.1-0.1-0.1-0.2-0.2-0.3c-0.2-0.2-0.4-0.3-0.6-0.3
c-0.1,0-0.2,0-0.3,0L85.9,2H103l-0.3,1.4c-0.4,0.1-0.9,0.2-1.3,0.3c-0.4,0.1-0.9,0.3-1.3,0.5c-0.5,0.2-1,0.5-1.4,0.8
c-0.5,0.3-0.9,0.6-1.3,0.9l-16,12.8l12.3,16.8c0.4,0.5,0.8,1.1,1.2,1.6c0.4,0.5,0.8,1,1.3,1.5c0.3,0.3,0.5,0.5,0.8,0.8
c0.3,0.2,0.5,0.5,0.8,0.7c0.2,0.2,0.4,0.3,0.6,0.4c0.2,0.1,0.4,0.2,0.7,0.3l-0.3,1.1c-2.8,0.7-5.6,1-8.4,0.9
c-1.1-0.1-2.1-0.2-3.1-0.3c-0.8-0.1-1.6-0.3-2.3-0.5c-0.7-0.3-1.5-0.6-2.2-0.9c-0.4-0.2-0.8-0.5-1.1-0.7c-0.4-0.3-0.7-0.6-1-0.9
c-0.3-0.3-0.7-0.7-1-1.1c-0.3-0.4-0.6-0.8-0.9-1.2L67.4,21l-3.9,16c-0.1,0.6-0.2,1.2-0.1,1.8c0,0.2,0,0.3,0.1,0.5
c0.1,0.2,0.2,0.3,0.3,0.4c0.1,0.1,0.2,0.3,0.3,0.3c0.2,0.2,0.5,0.3,0.7,0.4c0.3,0.1,0.5,0.3,0.8,0.3L65.4,42H45.7L45.9,40.8z"/>
<path style="fill:#DB001B;" d="M21.6,36c1.5,0.1,3.1-0.1,4.6-0.6c0.5-0.2,1-0.4,1.4-0.6c0.6-0.3,1.2-0.7,1.7-1.1
c0.5-0.4,1-0.9,1.4-1.5c0.6-0.8,1.1-1.6,1.6-2.5c0.5-0.9,0.8-1.7,1.2-2.7c0.3-1,0.6-2,0.9-3c0.3-1,0.5-2,0.7-3
c0.3-1.7,0.5-3.4,0.6-5.1c0-1.1-0.1-2.2-0.4-3.3c-0.2-0.5-0.4-1-0.6-1.5c-0.3-0.5-0.6-0.9-1-1.2c-0.5-0.5-1.1-0.9-1.7-1.2
c-0.6-0.3-1.3-0.5-2-0.7C29.3,8,28.6,8,27.8,8c-1.5,0-3,0.3-4.4,0.9c-0.7,0.3-1.3,0.6-2,1.1c-0.5,0.4-1,0.8-1.4,1.2
c-0.4,0.4-0.8,0.9-1.1,1.5c-0.5,0.8-0.9,1.6-1.3,2.5c-0.4,0.8-0.7,1.7-1,2.6c-0.3,0.9-0.6,1.9-0.8,2.8c-0.4,1.6-0.8,3.1-1,4.7
c-0.1,1.1-0.3,2.2-0.2,3.2c0.1,0.9,0.2,1.9,0.5,2.8c0.1,0.4,0.3,0.9,0.6,1.3c0.3,0.5,0.6,0.9,1,1.3c0.4,0.4,0.8,0.8,1.3,1
c0.6,0.3,1.1,0.6,1.8,0.8C20.3,35.9,20.9,36,21.6,36z M1.5,19.7c0.2-1.1,0.6-2.3,1-3.4c0.4-1,0.8-2,1.4-2.9c0.5-0.9,1.2-1.8,1.8-2.7
c0.8-0.9,1.6-1.8,2.4-2.6c0.9-0.8,1.8-1.5,2.8-2.2c0.9-0.6,1.9-1.2,2.8-1.7c1-0.5,2-1,3-1.3c1-0.4,1.9-0.7,2.9-0.9
c2.6-0.7,5.4-1,8-1c2.4,0,4.8,0.2,7.2,1c0.8,0.2,1.6,0.5,2.3,0.8c0.8,0.3,1.5,0.7,2.2,1.1c0.8,0.4,1.5,0.9,2.2,1.4
c0.7,0.5,1.4,1.1,2,1.6c0.6,0.6,1.2,1.2,1.8,1.9c0.6,0.6,1.1,1.4,1.5,2.1c0.5,0.7,0.9,1.5,1.2,2.3c0.6,1.5,1,3.1,1.2,4.7
c0.2,1.8,0.2,3.7-0.1,5.6c-0.1,0.9-0.3,1.7-0.5,2.5c-0.2,0.8-0.5,1.6-0.9,2.4c-0.5,1.2-1.2,2.4-1.9,3.5c-0.7,1.1-1.5,2.1-2.4,3.1
c-0.9,1-1.9,1.9-2.9,2.7s-2.2,1.5-3.4,2.1c-1.1,0.5-2.2,1-3.3,1.4c-2.4,0.9-4.9,1.4-7.3,1.7c-2.9,0.3-5.9,0.3-8.8-0.2
C17,42.5,16,42.3,15.1,42c-0.9-0.3-1.8-0.6-2.7-1c-0.9-0.4-1.7-0.8-2.6-1.3c-0.8-0.5-1.6-1-2.3-1.7c-0.7-0.6-1.4-1.2-2-1.9
c-0.6-0.7-1.2-1.4-1.7-2.2c-0.4-0.7-0.8-1.4-1.2-2.1c-0.6-1.2-1-2.5-1.3-3.8C1.1,26.5,1,24.9,1,23.3C1.1,22.1,1.2,20.9,1.5,19.7z"/>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -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",
)

View File

@ -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"`
@ -138,6 +140,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 {
@ -159,6 +171,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")
@ -171,7 +185,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",
@ -180,6 +193,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
@ -193,12 +207,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.CLIUpstreamOIDC = 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"),
@ -207,7 +222,7 @@ func loadEnvVars(t *testing.T, result *TestEnv) {
result.SupervisorUpstreamOIDC = 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"),