Merge pull request #98 from suzerain-io/get_kubeconfig_cli
Organize Pinniped CLI into subcommands; Add get-kubeconfig subcommand
This commit is contained in:
commit
db98f2810f
@ -58,6 +58,10 @@ built with the [Pinniped Go client library](generated).
|
|||||||
|
|
||||||
![implementation](doc/img/pinniped.svg)
|
![implementation](doc/img/pinniped.svg)
|
||||||
|
|
||||||
|
## Trying Pinniped
|
||||||
|
|
||||||
|
Care to kick the tires? It's easy to [install and try Pinniped](doc/demo.md).
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Currently, Pinniped supports self-hosted clusters where the Kube Controller Manager pod
|
Currently, Pinniped supports self-hosted clusters where the Kube Controller Manager pod
|
||||||
|
126
cmd/pinniped/cmd/exchange_credential.go
Normal file
126
cmd/pinniped/cmd/exchange_credential.go
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2020 VMware, Inc.
|
||||||
|
SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
||||||
|
|
||||||
|
"github.com/suzerain-io/pinniped/internal/client"
|
||||||
|
"github.com/suzerain-io/pinniped/internal/constable"
|
||||||
|
"github.com/suzerain-io/pinniped/internal/here"
|
||||||
|
)
|
||||||
|
|
||||||
|
//nolint: gochecknoinits
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(newExchangeCredentialCmd(os.Args, os.Stdout, os.Stderr).cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
type exchangeCredentialCommand struct {
|
||||||
|
// runFunc is called by the cobra.Command.Run hook. It is included here for
|
||||||
|
// testability.
|
||||||
|
runFunc func(stdout, stderr io.Writer)
|
||||||
|
|
||||||
|
// cmd is the cobra.Command for this CLI command. It is included here for
|
||||||
|
// testability.
|
||||||
|
cmd *cobra.Command
|
||||||
|
}
|
||||||
|
|
||||||
|
func newExchangeCredentialCmd(args []string, stdout, stderr io.Writer) *exchangeCredentialCommand {
|
||||||
|
c := &exchangeCredentialCommand{
|
||||||
|
runFunc: runExchangeCredential,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.cmd = &cobra.Command{
|
||||||
|
Run: func(cmd *cobra.Command, _ []string) {
|
||||||
|
c.runFunc(stdout, stderr)
|
||||||
|
},
|
||||||
|
Args: cobra.NoArgs, // do not accept positional arguments for this command
|
||||||
|
Use: "exchange-credential",
|
||||||
|
Short: "Exchange a credential for a cluster-specific access credential",
|
||||||
|
Long: here.Doc(`
|
||||||
|
Exchange a credential which proves your identity for a time-limited,
|
||||||
|
cluster-specific access credential.
|
||||||
|
|
||||||
|
Designed to be conveniently used as an credential plugin for kubectl.
|
||||||
|
See the help message for 'pinniped get-kubeconfig' for more
|
||||||
|
information about setting up a kubeconfig file using Pinniped.
|
||||||
|
|
||||||
|
Requires all of the following environment variables, which are
|
||||||
|
typically set in the kubeconfig:
|
||||||
|
- PINNIPED_TOKEN: the token to send to Pinniped for exchange
|
||||||
|
- PINNIPED_CA_BUNDLE: the CA bundle to trust when calling
|
||||||
|
Pinniped's HTTPS endpoint
|
||||||
|
- PINNIPED_K8S_API_ENDPOINT: the URL for the Pinniped credential
|
||||||
|
exchange API
|
||||||
|
|
||||||
|
For more information about credential plugins in general, see
|
||||||
|
https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins
|
||||||
|
`),
|
||||||
|
}
|
||||||
|
|
||||||
|
c.cmd.SetArgs(args)
|
||||||
|
c.cmd.SetOut(stdout)
|
||||||
|
c.cmd.SetErr(stderr)
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
type envGetter func(string) (string, bool)
|
||||||
|
type tokenExchanger func(ctx context.Context, token, caBundle, apiEndpoint string) (*clientauthenticationv1beta1.ExecCredential, error)
|
||||||
|
|
||||||
|
const ErrMissingEnvVar = constable.Error("failed to get credential: environment variable not set")
|
||||||
|
|
||||||
|
func runExchangeCredential(stdout, _ io.Writer) {
|
||||||
|
err := exchangeCredential(os.LookupEnv, client.ExchangeToken, stdout, 30*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
_, _ = fmt.Fprintf(os.Stderr, "%s\n", err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func exchangeCredential(envGetter envGetter, tokenExchanger tokenExchanger, outputWriter io.Writer, timeout time.Duration) error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
token, varExists := envGetter("PINNIPED_TOKEN")
|
||||||
|
if !varExists {
|
||||||
|
return envVarNotSetError("PINNIPED_TOKEN")
|
||||||
|
}
|
||||||
|
|
||||||
|
caBundle, varExists := envGetter("PINNIPED_CA_BUNDLE")
|
||||||
|
if !varExists {
|
||||||
|
return envVarNotSetError("PINNIPED_CA_BUNDLE")
|
||||||
|
}
|
||||||
|
|
||||||
|
apiEndpoint, varExists := envGetter("PINNIPED_K8S_API_ENDPOINT")
|
||||||
|
if !varExists {
|
||||||
|
return envVarNotSetError("PINNIPED_K8S_API_ENDPOINT")
|
||||||
|
}
|
||||||
|
|
||||||
|
cred, err := tokenExchanger(ctx, token, caBundle, apiEndpoint)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get credential: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.NewEncoder(outputWriter).Encode(cred)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal response to stdout: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func envVarNotSetError(varName string) error {
|
||||||
|
return fmt.Errorf("%w: %s", ErrMissingEnvVar, varName)
|
||||||
|
}
|
@ -3,12 +3,13 @@ Copyright 2020 VMware, Inc.
|
|||||||
SPDX-License-Identifier: Apache-2.0
|
SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package main
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -18,11 +19,107 @@ import (
|
|||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
||||||
|
|
||||||
|
"github.com/suzerain-io/pinniped/internal/here"
|
||||||
"github.com/suzerain-io/pinniped/internal/testutil"
|
"github.com/suzerain-io/pinniped/internal/testutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRun(t *testing.T) {
|
var (
|
||||||
spec.Run(t, "main.run", func(t *testing.T, when spec.G, it spec.S) {
|
knownGoodUsageForExchangeCredential = here.Doc(`
|
||||||
|
Usage:
|
||||||
|
exchange-credential [flags]
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
-h, --help help for exchange-credential
|
||||||
|
|
||||||
|
`)
|
||||||
|
|
||||||
|
knownGoodHelpForExchangeCredential = here.Doc(`
|
||||||
|
Exchange a credential which proves your identity for a time-limited,
|
||||||
|
cluster-specific access credential.
|
||||||
|
|
||||||
|
Designed to be conveniently used as an credential plugin for kubectl.
|
||||||
|
See the help message for 'pinniped get-kubeconfig' for more
|
||||||
|
information about setting up a kubeconfig file using Pinniped.
|
||||||
|
|
||||||
|
Requires all of the following environment variables, which are
|
||||||
|
typically set in the kubeconfig:
|
||||||
|
- PINNIPED_TOKEN: the token to send to Pinniped for exchange
|
||||||
|
- PINNIPED_CA_BUNDLE: the CA bundle to trust when calling
|
||||||
|
Pinniped's HTTPS endpoint
|
||||||
|
- PINNIPED_K8S_API_ENDPOINT: the URL for the Pinniped credential
|
||||||
|
exchange API
|
||||||
|
|
||||||
|
For more information about credential plugins in general, see
|
||||||
|
https://kubernetes.io/docs/reference/access-authn-authz/authentication/#client-go-credential-plugins
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
exchange-credential [flags]
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
-h, --help help for exchange-credential
|
||||||
|
`)
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewCredentialExchangeCmd(t *testing.T) {
|
||||||
|
spec.Run(t, "newCredentialExchangeCmd", func(t *testing.T, when spec.G, it spec.S) {
|
||||||
|
var r *require.Assertions
|
||||||
|
var stdout, stderr *bytes.Buffer
|
||||||
|
|
||||||
|
it.Before(func() {
|
||||||
|
r = require.New(t)
|
||||||
|
|
||||||
|
stdout, stderr = bytes.NewBuffer([]byte{}), bytes.NewBuffer([]byte{})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("calls runFunc and does not print usage or help when correct arguments and flags are used", func() {
|
||||||
|
c := newExchangeCredentialCmd([]string{}, stdout, stderr)
|
||||||
|
|
||||||
|
runFuncCalled := false
|
||||||
|
c.runFunc = func(out, err io.Writer) {
|
||||||
|
runFuncCalled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
r.NoError(c.cmd.Execute())
|
||||||
|
r.True(runFuncCalled)
|
||||||
|
r.Empty(stdout.String())
|
||||||
|
r.Empty(stderr.String())
|
||||||
|
})
|
||||||
|
|
||||||
|
it("fails when args are passed", func() {
|
||||||
|
c := newExchangeCredentialCmd([]string{"some-arg"}, stdout, stderr)
|
||||||
|
|
||||||
|
runFuncCalled := false
|
||||||
|
c.runFunc = func(out, err io.Writer) {
|
||||||
|
runFuncCalled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
errorMessage := `unknown command "some-arg" for "exchange-credential"`
|
||||||
|
r.EqualError(c.cmd.Execute(), errorMessage)
|
||||||
|
r.False(runFuncCalled)
|
||||||
|
|
||||||
|
output := "Error: " + errorMessage + "\n" + knownGoodUsageForExchangeCredential
|
||||||
|
r.Equal(output, stdout.String())
|
||||||
|
r.Empty(stderr.String())
|
||||||
|
})
|
||||||
|
|
||||||
|
it("prints a nice help message", func() {
|
||||||
|
c := newExchangeCredentialCmd([]string{"--help"}, stdout, stderr)
|
||||||
|
|
||||||
|
runFuncCalled := false
|
||||||
|
c.runFunc = func(out, err io.Writer) {
|
||||||
|
runFuncCalled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
r.NoError(c.cmd.Execute())
|
||||||
|
r.False(runFuncCalled)
|
||||||
|
r.Equal(knownGoodHelpForExchangeCredential, stdout.String())
|
||||||
|
r.Empty(stderr.String())
|
||||||
|
})
|
||||||
|
}, spec.Parallel(), spec.Report(report.Terminal{}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExchangeCredential(t *testing.T) {
|
||||||
|
spec.Run(t, "cmd.exchangeCredential", func(t *testing.T, when spec.G, it spec.S) {
|
||||||
var r *require.Assertions
|
var r *require.Assertions
|
||||||
var buffer *bytes.Buffer
|
var buffer *bytes.Buffer
|
||||||
var tokenExchanger tokenExchanger
|
var tokenExchanger tokenExchanger
|
||||||
@ -49,19 +146,19 @@ func TestRun(t *testing.T) {
|
|||||||
when("env vars are missing", func() {
|
when("env vars are missing", func() {
|
||||||
it("returns an error when PINNIPED_TOKEN is missing", func() {
|
it("returns an error when PINNIPED_TOKEN is missing", func() {
|
||||||
delete(fakeEnv, "PINNIPED_TOKEN")
|
delete(fakeEnv, "PINNIPED_TOKEN")
|
||||||
err := run(envGetter, tokenExchanger, buffer, 30*time.Second)
|
err := exchangeCredential(envGetter, tokenExchanger, buffer, 30*time.Second)
|
||||||
r.EqualError(err, "failed to get credential: environment variable not set: PINNIPED_TOKEN")
|
r.EqualError(err, "failed to get credential: environment variable not set: PINNIPED_TOKEN")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("returns an error when PINNIPED_CA_BUNDLE is missing", func() {
|
it("returns an error when PINNIPED_CA_BUNDLE is missing", func() {
|
||||||
delete(fakeEnv, "PINNIPED_CA_BUNDLE")
|
delete(fakeEnv, "PINNIPED_CA_BUNDLE")
|
||||||
err := run(envGetter, tokenExchanger, buffer, 30*time.Second)
|
err := exchangeCredential(envGetter, tokenExchanger, buffer, 30*time.Second)
|
||||||
r.EqualError(err, "failed to get credential: environment variable not set: PINNIPED_CA_BUNDLE")
|
r.EqualError(err, "failed to get credential: environment variable not set: PINNIPED_CA_BUNDLE")
|
||||||
})
|
})
|
||||||
|
|
||||||
it("returns an error when PINNIPED_K8S_API_ENDPOINT is missing", func() {
|
it("returns an error when PINNIPED_K8S_API_ENDPOINT is missing", func() {
|
||||||
delete(fakeEnv, "PINNIPED_K8S_API_ENDPOINT")
|
delete(fakeEnv, "PINNIPED_K8S_API_ENDPOINT")
|
||||||
err := run(envGetter, tokenExchanger, buffer, 30*time.Second)
|
err := exchangeCredential(envGetter, tokenExchanger, buffer, 30*time.Second)
|
||||||
r.EqualError(err, "failed to get credential: environment variable not set: PINNIPED_K8S_API_ENDPOINT")
|
r.EqualError(err, "failed to get credential: environment variable not set: PINNIPED_K8S_API_ENDPOINT")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -74,7 +171,7 @@ func TestRun(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("returns an error", func() {
|
it("returns an error", func() {
|
||||||
err := run(envGetter, tokenExchanger, buffer, 30*time.Second)
|
err := exchangeCredential(envGetter, tokenExchanger, buffer, 30*time.Second)
|
||||||
r.EqualError(err, "failed to get credential: some error")
|
r.EqualError(err, "failed to get credential: some error")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -91,7 +188,7 @@ func TestRun(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("returns an error", func() {
|
it("returns an error", func() {
|
||||||
err := run(envGetter, tokenExchanger, &testutil.ErrorWriter{ReturnError: fmt.Errorf("some IO error")}, 30*time.Second)
|
err := exchangeCredential(envGetter, tokenExchanger, &testutil.ErrorWriter{ReturnError: fmt.Errorf("some IO error")}, 30*time.Second)
|
||||||
r.EqualError(err, "failed to marshal response to stdout: some IO error")
|
r.EqualError(err, "failed to marshal response to stdout: some IO error")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -113,7 +210,7 @@ func TestRun(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("returns an error", func() {
|
it("returns an error", func() {
|
||||||
err := run(envGetter, tokenExchanger, buffer, 1*time.Millisecond)
|
err := exchangeCredential(envGetter, tokenExchanger, buffer, 1*time.Millisecond)
|
||||||
r.EqualError(err, "failed to get credential: context deadline exceeded")
|
r.EqualError(err, "failed to get credential: context deadline exceeded")
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -141,7 +238,7 @@ func TestRun(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("writes the execCredential to the given writer", func() {
|
it("writes the execCredential to the given writer", func() {
|
||||||
err := run(envGetter, tokenExchanger, buffer, 30*time.Second)
|
err := exchangeCredential(envGetter, tokenExchanger, buffer, 30*time.Second)
|
||||||
r.NoError(err)
|
r.NoError(err)
|
||||||
r.Equal(fakeEnv["PINNIPED_TOKEN"], actualToken)
|
r.Equal(fakeEnv["PINNIPED_TOKEN"], actualToken)
|
||||||
r.Equal(fakeEnv["PINNIPED_CA_BUNDLE"], actualCaBundle)
|
r.Equal(fakeEnv["PINNIPED_CA_BUNDLE"], actualCaBundle)
|
347
cmd/pinniped/cmd/get_kubeconfig.go
Normal file
347
cmd/pinniped/cmd/get_kubeconfig.go
Normal file
@ -0,0 +1,347 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2020 VMware, Inc.
|
||||||
|
SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ghodss/yaml"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
||||||
|
"k8s.io/client-go/rest"
|
||||||
|
"k8s.io/client-go/tools/clientcmd"
|
||||||
|
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||||
|
v1 "k8s.io/client-go/tools/clientcmd/api/v1"
|
||||||
|
|
||||||
|
"github.com/suzerain-io/pinniped/generated/1.19/apis/crdpinniped/v1alpha1"
|
||||||
|
pinnipedclientset "github.com/suzerain-io/pinniped/generated/1.19/client/clientset/versioned"
|
||||||
|
"github.com/suzerain-io/pinniped/internal/constable"
|
||||||
|
"github.com/suzerain-io/pinniped/internal/controller/issuerconfig"
|
||||||
|
"github.com/suzerain-io/pinniped/internal/here"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
getKubeConfigCmdTokenFlagName = "token"
|
||||||
|
getKubeConfigCmdKubeconfigFlagName = "kubeconfig"
|
||||||
|
getKubeConfigCmdKubeconfigContextFlagName = "kubeconfig-context"
|
||||||
|
getKubeConfigCmdPinnipedNamespaceFlagName = "pinniped-namespace"
|
||||||
|
)
|
||||||
|
|
||||||
|
//nolint: gochecknoinits
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(newGetKubeConfigCmd(os.Args, os.Stdout, os.Stderr).cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
type getKubeConfigCommand struct {
|
||||||
|
// runFunc is called by the cobra.Command.Run hook. It is included here for
|
||||||
|
// testability.
|
||||||
|
runFunc func(
|
||||||
|
stdout, stderr io.Writer,
|
||||||
|
token, kubeconfigPathOverride, currentContextOverride, pinnipedInstallationNamespace string,
|
||||||
|
)
|
||||||
|
|
||||||
|
// cmd is the cobra.Command for this CLI command. It is included here for
|
||||||
|
// testability.
|
||||||
|
cmd *cobra.Command
|
||||||
|
}
|
||||||
|
|
||||||
|
func newGetKubeConfigCmd(args []string, stdout, stderr io.Writer) *getKubeConfigCommand {
|
||||||
|
c := &getKubeConfigCommand{
|
||||||
|
runFunc: runGetKubeConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.cmd = &cobra.Command{
|
||||||
|
Run: func(cmd *cobra.Command, _ []string) {
|
||||||
|
token := cmd.Flag(getKubeConfigCmdTokenFlagName).Value.String()
|
||||||
|
kubeconfigPathOverride := cmd.Flag(getKubeConfigCmdKubeconfigFlagName).Value.String()
|
||||||
|
currentContextOverride := cmd.Flag(getKubeConfigCmdKubeconfigContextFlagName).Value.String()
|
||||||
|
pinnipedInstallationNamespace := cmd.Flag(getKubeConfigCmdPinnipedNamespaceFlagName).Value.String()
|
||||||
|
c.runFunc(
|
||||||
|
stdout,
|
||||||
|
stderr,
|
||||||
|
token,
|
||||||
|
kubeconfigPathOverride,
|
||||||
|
currentContextOverride,
|
||||||
|
pinnipedInstallationNamespace,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
Args: cobra.NoArgs, // do not accept positional arguments for this command
|
||||||
|
Use: "get-kubeconfig",
|
||||||
|
Short: "Print a kubeconfig for authenticating into a cluster via Pinniped",
|
||||||
|
Long: here.Doc(`
|
||||||
|
Print a kubeconfig for authenticating into a cluster via Pinniped.
|
||||||
|
|
||||||
|
Requires admin-like access to the cluster using the current
|
||||||
|
kubeconfig context in order to access Pinniped's metadata.
|
||||||
|
The current kubeconfig is found similar to how kubectl finds it:
|
||||||
|
using the value of the --kubeconfig option, or if that is not
|
||||||
|
specified then from the value of the KUBECONFIG environment
|
||||||
|
variable, or if that is not specified then it defaults to
|
||||||
|
.kube/config in your home directory.
|
||||||
|
|
||||||
|
Prints a kubeconfig which is suitable to access the cluster using
|
||||||
|
Pinniped as the authentication mechanism. This kubeconfig output
|
||||||
|
can be saved to a file and used with future kubectl commands, e.g.:
|
||||||
|
pinniped get-kubeconfig --token $MY_TOKEN > $HOME/mycluster-kubeconfig
|
||||||
|
kubectl --kubeconfig $HOME/mycluster-kubeconfig get pods
|
||||||
|
`),
|
||||||
|
}
|
||||||
|
|
||||||
|
c.cmd.SetArgs(args)
|
||||||
|
c.cmd.SetOut(stdout)
|
||||||
|
c.cmd.SetErr(stderr)
|
||||||
|
|
||||||
|
c.cmd.Flags().StringP(
|
||||||
|
getKubeConfigCmdTokenFlagName,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"Credential to include in the resulting kubeconfig output (Required)",
|
||||||
|
)
|
||||||
|
err := c.cmd.MarkFlagRequired(getKubeConfigCmdTokenFlagName)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.cmd.Flags().StringP(
|
||||||
|
getKubeConfigCmdKubeconfigFlagName,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"Path to the kubeconfig file",
|
||||||
|
)
|
||||||
|
|
||||||
|
c.cmd.Flags().StringP(
|
||||||
|
getKubeConfigCmdKubeconfigContextFlagName,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"Kubeconfig context override",
|
||||||
|
)
|
||||||
|
|
||||||
|
c.cmd.Flags().StringP(
|
||||||
|
getKubeConfigCmdPinnipedNamespaceFlagName,
|
||||||
|
"",
|
||||||
|
"pinniped",
|
||||||
|
"Namespace in which Pinniped was installed",
|
||||||
|
)
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func runGetKubeConfig(
|
||||||
|
stdout, stderr io.Writer,
|
||||||
|
token, kubeconfigPathOverride, currentContextOverride, pinnipedInstallationNamespace string,
|
||||||
|
) {
|
||||||
|
err := getKubeConfig(
|
||||||
|
stdout,
|
||||||
|
stderr,
|
||||||
|
token,
|
||||||
|
kubeconfigPathOverride,
|
||||||
|
currentContextOverride,
|
||||||
|
pinnipedInstallationNamespace,
|
||||||
|
func(restConfig *rest.Config) (pinnipedclientset.Interface, error) {
|
||||||
|
return pinnipedclientset.NewForConfig(restConfig)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
_, _ = fmt.Fprintf(os.Stderr, "error: %s\n", err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getKubeConfig(
|
||||||
|
outputWriter io.Writer,
|
||||||
|
warningsWriter io.Writer,
|
||||||
|
token string,
|
||||||
|
kubeconfigPathOverride string,
|
||||||
|
currentContextNameOverride string,
|
||||||
|
pinnipedInstallationNamespace string,
|
||||||
|
kubeClientCreator func(restConfig *rest.Config) (pinnipedclientset.Interface, error),
|
||||||
|
) error {
|
||||||
|
if token == "" {
|
||||||
|
return constable.Error("--" + getKubeConfigCmdTokenFlagName + " flag value cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
fullPathToSelf, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not find path to self: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
clientConfig := newClientConfig(kubeconfigPathOverride, currentContextNameOverride)
|
||||||
|
|
||||||
|
currentKubeConfig, err := clientConfig.RawConfig()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
credentialIssuerConfig, err := fetchPinnipedCredentialIssuerConfig(clientConfig, kubeClientCreator, pinnipedInstallationNamespace)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if credentialIssuerConfig.Status.KubeConfigInfo == nil {
|
||||||
|
return constable.Error(`CredentialIssuerConfig "pinniped-config" was missing KubeConfigInfo`)
|
||||||
|
}
|
||||||
|
|
||||||
|
v1Cluster, err := copyCurrentClusterFromExistingKubeConfig(currentKubeConfig, currentContextNameOverride)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = issueWarningForNonMatchingServerOrCA(v1Cluster, credentialIssuerConfig, warningsWriter)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
config := newPinnipedKubeconfig(v1Cluster, fullPathToSelf, token)
|
||||||
|
|
||||||
|
err = writeConfigAsYAML(outputWriter, config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func issueWarningForNonMatchingServerOrCA(v1Cluster v1.Cluster, credentialIssuerConfig *v1alpha1.CredentialIssuerConfig, warningsWriter io.Writer) error {
|
||||||
|
credentialIssuerConfigCA, err := base64.StdEncoding.DecodeString(credentialIssuerConfig.Status.KubeConfigInfo.CertificateAuthorityData)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if v1Cluster.Server != credentialIssuerConfig.Status.KubeConfigInfo.Server ||
|
||||||
|
!bytes.Equal(v1Cluster.CertificateAuthorityData, credentialIssuerConfigCA) {
|
||||||
|
_, err := warningsWriter.Write([]byte("WARNING: Server and certificate authority did not match between local kubeconfig and Pinniped's CredentialIssuerConfig on the cluster. Using local kubeconfig values.\n"))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("output write error: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchPinnipedCredentialIssuerConfig(clientConfig clientcmd.ClientConfig, kubeClientCreator func(restConfig *rest.Config) (pinnipedclientset.Interface, error), pinnipedInstallationNamespace string) (*v1alpha1.CredentialIssuerConfig, error) {
|
||||||
|
restConfig, err := clientConfig.ClientConfig()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
clientset, err := kubeClientCreator(restConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*20)
|
||||||
|
defer cancelFunc()
|
||||||
|
|
||||||
|
credentialIssuerConfig, err := clientset.CrdV1alpha1().CredentialIssuerConfigs(pinnipedInstallationNamespace).Get(ctx, issuerconfig.ConfigName, metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
if apierrors.IsNotFound(err) {
|
||||||
|
return nil, constable.Error(fmt.Sprintf(
|
||||||
|
`CredentialIssuerConfig "%s" was not found in namespace "%s". Is Pinniped installed on this cluster in namespace "%s"?`,
|
||||||
|
issuerconfig.ConfigName,
|
||||||
|
pinnipedInstallationNamespace,
|
||||||
|
pinnipedInstallationNamespace,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return credentialIssuerConfig, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newClientConfig(kubeconfigPathOverride string, currentContextName string) clientcmd.ClientConfig {
|
||||||
|
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
|
||||||
|
loadingRules.ExplicitPath = kubeconfigPathOverride
|
||||||
|
clientConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, &clientcmd.ConfigOverrides{
|
||||||
|
CurrentContext: currentContextName,
|
||||||
|
})
|
||||||
|
return clientConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeConfigAsYAML(outputWriter io.Writer, config v1.Config) error {
|
||||||
|
output, err := yaml.Marshal(&config)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("YAML serialization error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = outputWriter.Write(output)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("output write error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyCurrentClusterFromExistingKubeConfig(currentKubeConfig clientcmdapi.Config, currentContextNameOverride string) (v1.Cluster, error) {
|
||||||
|
v1Cluster := v1.Cluster{}
|
||||||
|
|
||||||
|
contextName := currentKubeConfig.CurrentContext
|
||||||
|
if currentContextNameOverride != "" {
|
||||||
|
contextName = currentContextNameOverride
|
||||||
|
}
|
||||||
|
|
||||||
|
err := v1.Convert_api_Cluster_To_v1_Cluster(
|
||||||
|
currentKubeConfig.Clusters[currentKubeConfig.Contexts[contextName].Cluster],
|
||||||
|
&v1Cluster,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return v1.Cluster{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return v1Cluster, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPinnipedKubeconfig(v1Cluster v1.Cluster, fullPathToSelf string, token string) v1.Config {
|
||||||
|
clusterName := "pinniped-cluster"
|
||||||
|
userName := "pinniped-user"
|
||||||
|
|
||||||
|
return v1.Config{
|
||||||
|
Kind: "Config",
|
||||||
|
APIVersion: v1.SchemeGroupVersion.Version,
|
||||||
|
Preferences: v1.Preferences{},
|
||||||
|
Clusters: []v1.NamedCluster{
|
||||||
|
{
|
||||||
|
Name: clusterName,
|
||||||
|
Cluster: v1Cluster,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Contexts: []v1.NamedContext{
|
||||||
|
{
|
||||||
|
Name: clusterName,
|
||||||
|
Context: v1.Context{
|
||||||
|
Cluster: clusterName,
|
||||||
|
AuthInfo: userName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
AuthInfos: []v1.NamedAuthInfo{
|
||||||
|
{
|
||||||
|
Name: userName,
|
||||||
|
AuthInfo: v1.AuthInfo{
|
||||||
|
Exec: &v1.ExecConfig{
|
||||||
|
Command: fullPathToSelf,
|
||||||
|
Args: []string{"exchange-credential"},
|
||||||
|
Env: []v1.ExecEnvVar{
|
||||||
|
{Name: "PINNIPED_K8S_API_ENDPOINT", Value: v1Cluster.Server},
|
||||||
|
{Name: "PINNIPED_CA_BUNDLE", Value: string(v1Cluster.CertificateAuthorityData)},
|
||||||
|
{Name: "PINNIPED_TOKEN", Value: token},
|
||||||
|
},
|
||||||
|
APIVersion: clientauthenticationv1beta1.SchemeGroupVersion.String(),
|
||||||
|
InstallHint: "The Pinniped CLI is required to authenticate to the current cluster.\n" +
|
||||||
|
"For more information, please visit https://pinniped.dev",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
CurrentContext: clusterName,
|
||||||
|
}
|
||||||
|
}
|
642
cmd/pinniped/cmd/get_kubeconfig_test.go
Normal file
642
cmd/pinniped/cmd/get_kubeconfig_test.go
Normal file
@ -0,0 +1,642 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2020 VMware, Inc.
|
||||||
|
SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/sclevine/spec"
|
||||||
|
"github.com/sclevine/spec/report"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/client-go/rest"
|
||||||
|
|
||||||
|
crdpinnipedv1alpha1 "github.com/suzerain-io/pinniped/generated/1.19/apis/crdpinniped/v1alpha1"
|
||||||
|
pinnipedclientset "github.com/suzerain-io/pinniped/generated/1.19/client/clientset/versioned"
|
||||||
|
pinnipedfake "github.com/suzerain-io/pinniped/generated/1.19/client/clientset/versioned/fake"
|
||||||
|
"github.com/suzerain-io/pinniped/internal/here"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
knownGoodUsageForGetKubeConfig = here.Doc(`
|
||||||
|
Usage:
|
||||||
|
get-kubeconfig [flags]
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
-h, --help help for get-kubeconfig
|
||||||
|
--kubeconfig string Path to the kubeconfig file
|
||||||
|
--kubeconfig-context string Kubeconfig context override
|
||||||
|
--pinniped-namespace string Namespace in which Pinniped was installed (default "pinniped")
|
||||||
|
--token string Credential to include in the resulting kubeconfig output (Required)
|
||||||
|
|
||||||
|
`)
|
||||||
|
|
||||||
|
knownGoodHelpForGetKubeConfig = here.Doc(`
|
||||||
|
Print a kubeconfig for authenticating into a cluster via Pinniped.
|
||||||
|
|
||||||
|
Requires admin-like access to the cluster using the current
|
||||||
|
kubeconfig context in order to access Pinniped's metadata.
|
||||||
|
The current kubeconfig is found similar to how kubectl finds it:
|
||||||
|
using the value of the --kubeconfig option, or if that is not
|
||||||
|
specified then from the value of the KUBECONFIG environment
|
||||||
|
variable, or if that is not specified then it defaults to
|
||||||
|
.kube/config in your home directory.
|
||||||
|
|
||||||
|
Prints a kubeconfig which is suitable to access the cluster using
|
||||||
|
Pinniped as the authentication mechanism. This kubeconfig output
|
||||||
|
can be saved to a file and used with future kubectl commands, e.g.:
|
||||||
|
pinniped get-kubeconfig --token $MY_TOKEN > $HOME/mycluster-kubeconfig
|
||||||
|
kubectl --kubeconfig $HOME/mycluster-kubeconfig get pods
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
get-kubeconfig [flags]
|
||||||
|
|
||||||
|
Flags:
|
||||||
|
-h, --help help for get-kubeconfig
|
||||||
|
--kubeconfig string Path to the kubeconfig file
|
||||||
|
--kubeconfig-context string Kubeconfig context override
|
||||||
|
--pinniped-namespace string Namespace in which Pinniped was installed (default "pinniped")
|
||||||
|
--token string Credential to include in the resulting kubeconfig output (Required)
|
||||||
|
`)
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewGetKubeConfigCmd(t *testing.T) {
|
||||||
|
spec.Run(t, "newGetKubeConfigCmd", func(t *testing.T, when spec.G, it spec.S) {
|
||||||
|
var r *require.Assertions
|
||||||
|
var stdout, stderr *bytes.Buffer
|
||||||
|
|
||||||
|
it.Before(func() {
|
||||||
|
r = require.New(t)
|
||||||
|
|
||||||
|
stdout, stderr = bytes.NewBuffer([]byte{}), bytes.NewBuffer([]byte{})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("passes all flags to runFunc", func() {
|
||||||
|
args := []string{
|
||||||
|
"--token", "some-token",
|
||||||
|
"--kubeconfig", "some-kubeconfig",
|
||||||
|
"--kubeconfig-context", "some-kubeconfig-context",
|
||||||
|
"--pinniped-namespace", "some-pinniped-namespace",
|
||||||
|
}
|
||||||
|
c := newGetKubeConfigCmd(args, stdout, stderr)
|
||||||
|
|
||||||
|
runFuncCalled := false
|
||||||
|
c.runFunc = func(
|
||||||
|
out, err io.Writer,
|
||||||
|
token, kubeconfigPathOverride, currentContextOverride, pinnipedInstallationNamespace string,
|
||||||
|
) {
|
||||||
|
runFuncCalled = true
|
||||||
|
r.Equal("some-token", token)
|
||||||
|
r.Equal("some-kubeconfig", kubeconfigPathOverride)
|
||||||
|
r.Equal("some-kubeconfig-context", currentContextOverride)
|
||||||
|
r.Equal("some-pinniped-namespace", pinnipedInstallationNamespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.NoError(c.cmd.Execute())
|
||||||
|
r.True(runFuncCalled)
|
||||||
|
r.Empty(stdout.String())
|
||||||
|
r.Empty(stderr.String())
|
||||||
|
})
|
||||||
|
|
||||||
|
it("requires the 'token' flag", func() {
|
||||||
|
args := []string{
|
||||||
|
"--kubeconfig", "some-kubeconfig",
|
||||||
|
"--kubeconfig-context", "some-kubeconfig-context",
|
||||||
|
"--pinniped-namespace", "some-pinniped-namespace",
|
||||||
|
}
|
||||||
|
c := newGetKubeConfigCmd(args, stdout, stderr)
|
||||||
|
|
||||||
|
runFuncCalled := false
|
||||||
|
c.runFunc = func(
|
||||||
|
out, err io.Writer,
|
||||||
|
token, kubeconfigPathOverride, currentContextOverride, pinnipedInstallationNamespace string,
|
||||||
|
) {
|
||||||
|
runFuncCalled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
errorMessage := `required flag(s) "token" not set`
|
||||||
|
r.EqualError(c.cmd.Execute(), errorMessage)
|
||||||
|
r.False(runFuncCalled)
|
||||||
|
|
||||||
|
output := "Error: " + errorMessage + "\n" + knownGoodUsageForGetKubeConfig
|
||||||
|
r.Equal(output, stdout.String())
|
||||||
|
r.Empty(stderr.String())
|
||||||
|
})
|
||||||
|
|
||||||
|
it("defaults the flags correctly", func() {
|
||||||
|
args := []string{
|
||||||
|
"--token", "some-token",
|
||||||
|
}
|
||||||
|
c := newGetKubeConfigCmd(args, stdout, stderr)
|
||||||
|
|
||||||
|
runFuncCalled := false
|
||||||
|
c.runFunc = func(
|
||||||
|
out, err io.Writer,
|
||||||
|
token, kubeconfigPathOverride, currentContextOverride, pinnipedInstallationNamespace string,
|
||||||
|
) {
|
||||||
|
runFuncCalled = true
|
||||||
|
r.Equal("some-token", token)
|
||||||
|
r.Equal("", kubeconfigPathOverride)
|
||||||
|
r.Equal("", currentContextOverride)
|
||||||
|
r.Equal("pinniped", pinnipedInstallationNamespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.NoError(c.cmd.Execute())
|
||||||
|
r.True(runFuncCalled)
|
||||||
|
r.Empty(stdout.String())
|
||||||
|
r.Empty(stderr.String())
|
||||||
|
})
|
||||||
|
|
||||||
|
it("fails when args are passed", func() {
|
||||||
|
args := []string{
|
||||||
|
"--token", "some-token",
|
||||||
|
"some-arg",
|
||||||
|
}
|
||||||
|
c := newGetKubeConfigCmd(args, stdout, stderr)
|
||||||
|
|
||||||
|
runFuncCalled := false
|
||||||
|
c.runFunc = func(
|
||||||
|
out, err io.Writer,
|
||||||
|
token, kubeconfigPathOverride, currentContextOverride, pinnipedInstallationNamespace string,
|
||||||
|
) {
|
||||||
|
runFuncCalled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
errorMessage := `unknown command "some-arg" for "get-kubeconfig"`
|
||||||
|
r.EqualError(c.cmd.Execute(), errorMessage)
|
||||||
|
r.False(runFuncCalled)
|
||||||
|
|
||||||
|
output := "Error: " + errorMessage + "\n" + knownGoodUsageForGetKubeConfig
|
||||||
|
r.Equal(output, stdout.String())
|
||||||
|
r.Empty(stderr.String())
|
||||||
|
})
|
||||||
|
|
||||||
|
it("prints a nice help message", func() {
|
||||||
|
args := []string{
|
||||||
|
"--help",
|
||||||
|
}
|
||||||
|
c := newGetKubeConfigCmd(args, stdout, stderr)
|
||||||
|
|
||||||
|
runFuncCalled := false
|
||||||
|
c.runFunc = func(
|
||||||
|
out, err io.Writer,
|
||||||
|
token, kubeconfigPathOverride, currentContextOverride, pinnipedInstallationNamespace string,
|
||||||
|
) {
|
||||||
|
runFuncCalled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
r.NoError(c.cmd.Execute())
|
||||||
|
r.False(runFuncCalled)
|
||||||
|
r.Equal(knownGoodHelpForGetKubeConfig, stdout.String())
|
||||||
|
r.Empty(stderr.String())
|
||||||
|
})
|
||||||
|
}, spec.Parallel(), spec.Report(report.Terminal{}))
|
||||||
|
}
|
||||||
|
|
||||||
|
//nolint: unparam
|
||||||
|
func expectedKubeconfigYAML(clusterCAData, clusterServer, command, token, pinnipedEndpoint, pinnipedCABundle string) string {
|
||||||
|
return here.Docf(`
|
||||||
|
apiVersion: v1
|
||||||
|
clusters:
|
||||||
|
- cluster:
|
||||||
|
certificate-authority-data: %s
|
||||||
|
server: %s
|
||||||
|
name: pinniped-cluster
|
||||||
|
contexts:
|
||||||
|
- context:
|
||||||
|
cluster: pinniped-cluster
|
||||||
|
user: pinniped-user
|
||||||
|
name: pinniped-cluster
|
||||||
|
current-context: pinniped-cluster
|
||||||
|
kind: Config
|
||||||
|
preferences: {}
|
||||||
|
users:
|
||||||
|
- name: pinniped-user
|
||||||
|
user:
|
||||||
|
exec:
|
||||||
|
apiVersion: client.authentication.k8s.io/v1beta1
|
||||||
|
args:
|
||||||
|
- exchange-credential
|
||||||
|
command: %s
|
||||||
|
env:
|
||||||
|
- name: PINNIPED_K8S_API_ENDPOINT
|
||||||
|
value: %s
|
||||||
|
- name: PINNIPED_CA_BUNDLE
|
||||||
|
value: %s
|
||||||
|
- name: PINNIPED_TOKEN
|
||||||
|
value: %s
|
||||||
|
installHint: |-
|
||||||
|
The Pinniped CLI is required to authenticate to the current cluster.
|
||||||
|
For more information, please visit https://pinniped.dev
|
||||||
|
`, clusterCAData, clusterServer, command, pinnipedEndpoint, pinnipedCABundle, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCredentialIssuerConfig(server, certificateAuthorityData string) *crdpinnipedv1alpha1.CredentialIssuerConfig {
|
||||||
|
return &crdpinnipedv1alpha1.CredentialIssuerConfig{
|
||||||
|
TypeMeta: metav1.TypeMeta{
|
||||||
|
Kind: "CredentialIssuerConfig",
|
||||||
|
APIVersion: crdpinnipedv1alpha1.SchemeGroupVersion.String(),
|
||||||
|
},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "pinniped-config",
|
||||||
|
Namespace: "some-namespace",
|
||||||
|
},
|
||||||
|
Status: crdpinnipedv1alpha1.CredentialIssuerConfigStatus{
|
||||||
|
KubeConfigInfo: &crdpinnipedv1alpha1.CredentialIssuerConfigKubeConfigInfo{
|
||||||
|
Server: server,
|
||||||
|
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(certificateAuthorityData)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetKubeConfig(t *testing.T) {
|
||||||
|
spec.Run(t, "cmd.getKubeConfig", func(t *testing.T, when spec.G, it spec.S) {
|
||||||
|
var r *require.Assertions
|
||||||
|
var outputBuffer *bytes.Buffer
|
||||||
|
var warningsBuffer *bytes.Buffer
|
||||||
|
var fullPathToSelf string
|
||||||
|
var pinnipedClient *pinnipedfake.Clientset
|
||||||
|
|
||||||
|
it.Before(func() {
|
||||||
|
r = require.New(t)
|
||||||
|
|
||||||
|
outputBuffer = new(bytes.Buffer)
|
||||||
|
warningsBuffer = new(bytes.Buffer)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
fullPathToSelf, err = os.Executable()
|
||||||
|
r.NoError(err)
|
||||||
|
|
||||||
|
pinnipedClient = pinnipedfake.NewSimpleClientset()
|
||||||
|
})
|
||||||
|
|
||||||
|
when("the CredentialIssuerConfig is found on the cluster with a configuration that matches the existing kubeconfig", func() {
|
||||||
|
it.Before(func() {
|
||||||
|
r.NoError(pinnipedClient.Tracker().Add(
|
||||||
|
newCredentialIssuerConfig("https://fake-server-url-value", "fake-certificate-authority-data-value"),
|
||||||
|
))
|
||||||
|
})
|
||||||
|
|
||||||
|
it("writes the kubeconfig to the given writer", func() {
|
||||||
|
kubeClientCreatorFuncWasCalled := false
|
||||||
|
err := getKubeConfig(outputBuffer,
|
||||||
|
warningsBuffer,
|
||||||
|
"some-token",
|
||||||
|
"./testdata/kubeconfig.yaml",
|
||||||
|
"",
|
||||||
|
"some-namespace",
|
||||||
|
func(restConfig *rest.Config) (pinnipedclientset.Interface, error) {
|
||||||
|
kubeClientCreatorFuncWasCalled = true
|
||||||
|
r.Equal("https://fake-server-url-value", restConfig.Host)
|
||||||
|
r.Equal("fake-certificate-authority-data-value", string(restConfig.CAData))
|
||||||
|
return pinnipedClient, nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
r.NoError(err)
|
||||||
|
r.True(kubeClientCreatorFuncWasCalled)
|
||||||
|
|
||||||
|
r.Empty(warningsBuffer.String())
|
||||||
|
r.Equal(expectedKubeconfigYAML(
|
||||||
|
base64.StdEncoding.EncodeToString([]byte("fake-certificate-authority-data-value")),
|
||||||
|
"https://fake-server-url-value",
|
||||||
|
fullPathToSelf,
|
||||||
|
"some-token",
|
||||||
|
"https://fake-server-url-value",
|
||||||
|
"fake-certificate-authority-data-value",
|
||||||
|
), outputBuffer.String())
|
||||||
|
})
|
||||||
|
|
||||||
|
when("the currentContextOverride is used to specify a context other than the default context", func() {
|
||||||
|
it.Before(func() {
|
||||||
|
// update the Server and CertificateAuthorityData to make them match the other kubeconfig context
|
||||||
|
r.NoError(pinnipedClient.Tracker().Update(
|
||||||
|
schema.GroupVersionResource{
|
||||||
|
Group: crdpinnipedv1alpha1.GroupName,
|
||||||
|
Version: crdpinnipedv1alpha1.SchemeGroupVersion.Version,
|
||||||
|
Resource: "credentialissuerconfigs",
|
||||||
|
},
|
||||||
|
newCredentialIssuerConfig(
|
||||||
|
"https://some-other-fake-server-url-value",
|
||||||
|
"some-other-fake-certificate-authority-data-value",
|
||||||
|
),
|
||||||
|
"some-namespace",
|
||||||
|
))
|
||||||
|
})
|
||||||
|
|
||||||
|
when("that context exists", func() {
|
||||||
|
it("writes the kubeconfig to the given writer using the specified context", func() {
|
||||||
|
kubeClientCreatorFuncWasCalled := false
|
||||||
|
err := getKubeConfig(outputBuffer,
|
||||||
|
warningsBuffer,
|
||||||
|
"some-token",
|
||||||
|
"./testdata/kubeconfig.yaml",
|
||||||
|
"some-other-context",
|
||||||
|
"some-namespace",
|
||||||
|
func(restConfig *rest.Config) (pinnipedclientset.Interface, error) {
|
||||||
|
kubeClientCreatorFuncWasCalled = true
|
||||||
|
r.Equal("https://some-other-fake-server-url-value", restConfig.Host)
|
||||||
|
r.Equal("some-other-fake-certificate-authority-data-value", string(restConfig.CAData))
|
||||||
|
return pinnipedClient, nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
r.NoError(err)
|
||||||
|
r.True(kubeClientCreatorFuncWasCalled)
|
||||||
|
|
||||||
|
r.Empty(warningsBuffer.String())
|
||||||
|
r.Equal(expectedKubeconfigYAML(
|
||||||
|
base64.StdEncoding.EncodeToString([]byte("some-other-fake-certificate-authority-data-value")),
|
||||||
|
"https://some-other-fake-server-url-value",
|
||||||
|
fullPathToSelf,
|
||||||
|
"some-token",
|
||||||
|
"https://some-other-fake-server-url-value",
|
||||||
|
"some-other-fake-certificate-authority-data-value",
|
||||||
|
), outputBuffer.String())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
when("that context does not exist the in the current kubeconfig", func() {
|
||||||
|
it("returns an error", func() {
|
||||||
|
err := getKubeConfig(outputBuffer,
|
||||||
|
warningsBuffer,
|
||||||
|
"some-token",
|
||||||
|
"./testdata/kubeconfig.yaml",
|
||||||
|
"this-context-name-does-not-exist-in-kubeconfig.yaml",
|
||||||
|
"some-namespace",
|
||||||
|
func(restConfig *rest.Config) (pinnipedclientset.Interface, error) { return pinnipedClient, nil },
|
||||||
|
)
|
||||||
|
r.EqualError(err, `context "this-context-name-does-not-exist-in-kubeconfig.yaml" does not exist`)
|
||||||
|
r.Empty(warningsBuffer.String())
|
||||||
|
r.Empty(outputBuffer.String())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
when("the token passed in is empty", func() {
|
||||||
|
it("returns an error", func() {
|
||||||
|
err := getKubeConfig(outputBuffer,
|
||||||
|
warningsBuffer,
|
||||||
|
"",
|
||||||
|
"./testdata/kubeconfig.yaml",
|
||||||
|
"",
|
||||||
|
"some-namespace",
|
||||||
|
func(restConfig *rest.Config) (pinnipedclientset.Interface, error) { return pinnipedClient, nil },
|
||||||
|
)
|
||||||
|
r.EqualError(err, "--token flag value cannot be empty")
|
||||||
|
r.Empty(warningsBuffer.String())
|
||||||
|
r.Empty(outputBuffer.String())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
when("the kubeconfig path passed refers to a file that does not exist", func() {
|
||||||
|
it("returns an error", func() {
|
||||||
|
err := getKubeConfig(outputBuffer,
|
||||||
|
warningsBuffer,
|
||||||
|
"some-token",
|
||||||
|
"./testdata/this-file-does-not-exist.yaml",
|
||||||
|
"",
|
||||||
|
"some-namespace",
|
||||||
|
func(restConfig *rest.Config) (pinnipedclientset.Interface, error) { return pinnipedClient, nil },
|
||||||
|
)
|
||||||
|
r.EqualError(err, "stat ./testdata/this-file-does-not-exist.yaml: no such file or directory")
|
||||||
|
r.Empty(warningsBuffer.String())
|
||||||
|
r.Empty(outputBuffer.String())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
when("the kubeconfig path parameter is empty", func() {
|
||||||
|
it.Before(func() {
|
||||||
|
// Note that this is technically polluting other parallel tests in this file, but other tests
|
||||||
|
// are always specifying the kubeconfigPathOverride parameter, so they're not actually looking
|
||||||
|
// at the value of this environment variable.
|
||||||
|
r.NoError(os.Setenv("KUBECONFIG", "./testdata/kubeconfig.yaml"))
|
||||||
|
})
|
||||||
|
|
||||||
|
it.After(func() {
|
||||||
|
r.NoError(os.Unsetenv("KUBECONFIG"))
|
||||||
|
})
|
||||||
|
|
||||||
|
it("falls back to using the KUBECONFIG env var to find the kubeconfig file", func() {
|
||||||
|
kubeClientCreatorFuncWasCalled := false
|
||||||
|
err := getKubeConfig(outputBuffer,
|
||||||
|
warningsBuffer,
|
||||||
|
"some-token",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"some-namespace",
|
||||||
|
func(restConfig *rest.Config) (pinnipedclientset.Interface, error) {
|
||||||
|
kubeClientCreatorFuncWasCalled = true
|
||||||
|
r.Equal("https://fake-server-url-value", restConfig.Host)
|
||||||
|
r.Equal("fake-certificate-authority-data-value", string(restConfig.CAData))
|
||||||
|
return pinnipedClient, nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
r.NoError(err)
|
||||||
|
r.True(kubeClientCreatorFuncWasCalled)
|
||||||
|
|
||||||
|
r.Empty(warningsBuffer.String())
|
||||||
|
r.Equal(expectedKubeconfigYAML(
|
||||||
|
base64.StdEncoding.EncodeToString([]byte("fake-certificate-authority-data-value")),
|
||||||
|
"https://fake-server-url-value",
|
||||||
|
fullPathToSelf,
|
||||||
|
"some-token",
|
||||||
|
"https://fake-server-url-value",
|
||||||
|
"fake-certificate-authority-data-value",
|
||||||
|
), outputBuffer.String())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
when("the wrong pinniped namespace is passed in", func() {
|
||||||
|
it("returns an error", func() {
|
||||||
|
kubeClientCreatorFuncWasCalled := false
|
||||||
|
err := getKubeConfig(outputBuffer,
|
||||||
|
warningsBuffer,
|
||||||
|
"some-token",
|
||||||
|
"./testdata/kubeconfig.yaml",
|
||||||
|
"",
|
||||||
|
"this-is-the-wrong-namespace",
|
||||||
|
func(restConfig *rest.Config) (pinnipedclientset.Interface, error) {
|
||||||
|
kubeClientCreatorFuncWasCalled = true
|
||||||
|
r.Equal("https://fake-server-url-value", restConfig.Host)
|
||||||
|
r.Equal("fake-certificate-authority-data-value", string(restConfig.CAData))
|
||||||
|
return pinnipedClient, nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
r.EqualError(err, `CredentialIssuerConfig "pinniped-config" was not found in namespace "this-is-the-wrong-namespace". Is Pinniped installed on this cluster in namespace "this-is-the-wrong-namespace"?`)
|
||||||
|
r.True(kubeClientCreatorFuncWasCalled)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
when("the CredentialIssuerConfig is found on the cluster with a configuration that does not match the existing kubeconfig", func() {
|
||||||
|
when("the Server doesn't match", func() {
|
||||||
|
it.Before(func() {
|
||||||
|
r.NoError(pinnipedClient.Tracker().Add(
|
||||||
|
newCredentialIssuerConfig("non-matching-pinniped-server-url", "fake-certificate-authority-data-value"),
|
||||||
|
))
|
||||||
|
})
|
||||||
|
|
||||||
|
it("writes the kubeconfig to the given writer using the values found in the local kubeconfig and issues a warning", func() {
|
||||||
|
kubeClientCreatorFuncWasCalled := false
|
||||||
|
err := getKubeConfig(outputBuffer,
|
||||||
|
warningsBuffer,
|
||||||
|
"some-token",
|
||||||
|
"./testdata/kubeconfig.yaml",
|
||||||
|
"",
|
||||||
|
"some-namespace",
|
||||||
|
func(restConfig *rest.Config) (pinnipedclientset.Interface, error) {
|
||||||
|
kubeClientCreatorFuncWasCalled = true
|
||||||
|
r.Equal("https://fake-server-url-value", restConfig.Host)
|
||||||
|
r.Equal("fake-certificate-authority-data-value", string(restConfig.CAData))
|
||||||
|
return pinnipedClient, nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
r.NoError(err)
|
||||||
|
r.True(kubeClientCreatorFuncWasCalled)
|
||||||
|
|
||||||
|
r.Equal(
|
||||||
|
"WARNING: Server and certificate authority did not match between local kubeconfig and Pinniped's CredentialIssuerConfig on the cluster. Using local kubeconfig values.\n",
|
||||||
|
warningsBuffer.String(),
|
||||||
|
)
|
||||||
|
r.Equal(expectedKubeconfigYAML(
|
||||||
|
base64.StdEncoding.EncodeToString([]byte("fake-certificate-authority-data-value")),
|
||||||
|
"https://fake-server-url-value",
|
||||||
|
fullPathToSelf,
|
||||||
|
"some-token",
|
||||||
|
"https://fake-server-url-value",
|
||||||
|
"fake-certificate-authority-data-value",
|
||||||
|
), outputBuffer.String())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
when("the CA doesn't match", func() {
|
||||||
|
it.Before(func() {
|
||||||
|
r.NoError(pinnipedClient.Tracker().Add(
|
||||||
|
newCredentialIssuerConfig("https://fake-server-url-value", "non-matching-certificate-authority-data-value"),
|
||||||
|
))
|
||||||
|
})
|
||||||
|
|
||||||
|
it("writes the kubeconfig to the given writer using the values found in the local kubeconfig and issues a warning", func() {
|
||||||
|
kubeClientCreatorFuncWasCalled := false
|
||||||
|
err := getKubeConfig(outputBuffer,
|
||||||
|
warningsBuffer,
|
||||||
|
"some-token",
|
||||||
|
"./testdata/kubeconfig.yaml",
|
||||||
|
"",
|
||||||
|
"some-namespace",
|
||||||
|
func(restConfig *rest.Config) (pinnipedclientset.Interface, error) {
|
||||||
|
kubeClientCreatorFuncWasCalled = true
|
||||||
|
r.Equal("https://fake-server-url-value", restConfig.Host)
|
||||||
|
r.Equal("fake-certificate-authority-data-value", string(restConfig.CAData))
|
||||||
|
return pinnipedClient, nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
r.NoError(err)
|
||||||
|
r.True(kubeClientCreatorFuncWasCalled)
|
||||||
|
|
||||||
|
r.Equal(
|
||||||
|
"WARNING: Server and certificate authority did not match between local kubeconfig and Pinniped's CredentialIssuerConfig on the cluster. Using local kubeconfig values.\n",
|
||||||
|
warningsBuffer.String(),
|
||||||
|
)
|
||||||
|
r.Equal(expectedKubeconfigYAML(
|
||||||
|
base64.StdEncoding.EncodeToString([]byte("fake-certificate-authority-data-value")),
|
||||||
|
"https://fake-server-url-value",
|
||||||
|
fullPathToSelf,
|
||||||
|
"some-token",
|
||||||
|
"https://fake-server-url-value",
|
||||||
|
"fake-certificate-authority-data-value",
|
||||||
|
), outputBuffer.String())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
when("the CredentialIssuerConfig is found on the cluster with an empty KubeConfigInfo", func() {
|
||||||
|
it.Before(func() {
|
||||||
|
r.NoError(pinnipedClient.Tracker().Add(
|
||||||
|
&crdpinnipedv1alpha1.CredentialIssuerConfig{
|
||||||
|
TypeMeta: metav1.TypeMeta{
|
||||||
|
Kind: "CredentialIssuerConfig",
|
||||||
|
APIVersion: crdpinnipedv1alpha1.SchemeGroupVersion.String(),
|
||||||
|
},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "pinniped-config",
|
||||||
|
Namespace: "some-namespace",
|
||||||
|
},
|
||||||
|
Status: crdpinnipedv1alpha1.CredentialIssuerConfigStatus{},
|
||||||
|
},
|
||||||
|
))
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns an error", func() {
|
||||||
|
kubeClientCreatorFuncWasCalled := false
|
||||||
|
err := getKubeConfig(outputBuffer,
|
||||||
|
warningsBuffer,
|
||||||
|
"some-token",
|
||||||
|
"./testdata/kubeconfig.yaml",
|
||||||
|
"",
|
||||||
|
"some-namespace",
|
||||||
|
func(restConfig *rest.Config) (pinnipedclientset.Interface, error) {
|
||||||
|
kubeClientCreatorFuncWasCalled = true
|
||||||
|
r.Equal("https://fake-server-url-value", restConfig.Host)
|
||||||
|
r.Equal("fake-certificate-authority-data-value", string(restConfig.CAData))
|
||||||
|
return pinnipedClient, nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
r.True(kubeClientCreatorFuncWasCalled)
|
||||||
|
r.EqualError(err, `CredentialIssuerConfig "pinniped-config" was missing KubeConfigInfo`)
|
||||||
|
r.Empty(warningsBuffer.String())
|
||||||
|
r.Empty(outputBuffer.String())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
when("the CredentialIssuerConfig does not exist on the cluster", func() {
|
||||||
|
it("returns an error", func() {
|
||||||
|
kubeClientCreatorFuncWasCalled := false
|
||||||
|
err := getKubeConfig(outputBuffer,
|
||||||
|
warningsBuffer,
|
||||||
|
"some-token",
|
||||||
|
"./testdata/kubeconfig.yaml",
|
||||||
|
"",
|
||||||
|
"some-namespace",
|
||||||
|
func(restConfig *rest.Config) (pinnipedclientset.Interface, error) {
|
||||||
|
kubeClientCreatorFuncWasCalled = true
|
||||||
|
r.Equal("https://fake-server-url-value", restConfig.Host)
|
||||||
|
r.Equal("fake-certificate-authority-data-value", string(restConfig.CAData))
|
||||||
|
return pinnipedClient, nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
r.True(kubeClientCreatorFuncWasCalled)
|
||||||
|
r.EqualError(err, `CredentialIssuerConfig "pinniped-config" was not found in namespace "some-namespace". Is Pinniped installed on this cluster in namespace "some-namespace"?`)
|
||||||
|
r.Empty(warningsBuffer.String())
|
||||||
|
r.Empty(outputBuffer.String())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
when("there is an error while getting the CredentialIssuerConfig from the cluster", func() {
|
||||||
|
it("returns an error", func() {
|
||||||
|
err := getKubeConfig(outputBuffer,
|
||||||
|
warningsBuffer,
|
||||||
|
"some-token",
|
||||||
|
"./testdata/kubeconfig.yaml",
|
||||||
|
"",
|
||||||
|
"some-namespace",
|
||||||
|
func(restConfig *rest.Config) (pinnipedclientset.Interface, error) {
|
||||||
|
return nil, fmt.Errorf("some error getting CredentialIssuerConfig")
|
||||||
|
},
|
||||||
|
)
|
||||||
|
r.EqualError(err, "some error getting CredentialIssuerConfig")
|
||||||
|
r.Empty(warningsBuffer.String())
|
||||||
|
r.Empty(outputBuffer.String())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}, spec.Parallel(), spec.Report(report.Terminal{}))
|
||||||
|
}
|
30
cmd/pinniped/cmd/root.go
Normal file
30
cmd/pinniped/cmd/root.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2020 VMware, Inc.
|
||||||
|
SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
//nolint: gochecknoglobals
|
||||||
|
var rootCmd = &cobra.Command{
|
||||||
|
Use: "pinniped",
|
||||||
|
Short: "pinniped",
|
||||||
|
Long: "pinniped is the client-side binary for use with Pinniped-enabled Kubernetes clusters.",
|
||||||
|
SilenceUsage: true, // do not print usage message when commands fail
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||||
|
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
||||||
|
func Execute() {
|
||||||
|
if err := rootCmd.Execute(); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
31
cmd/pinniped/cmd/testdata/kubeconfig.yaml
vendored
Normal file
31
cmd/pinniped/cmd/testdata/kubeconfig.yaml
vendored
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
clusters:
|
||||||
|
- cluster:
|
||||||
|
certificate-authority-data: ZmFrZS1jZXJ0aWZpY2F0ZS1hdXRob3JpdHktZGF0YS12YWx1ZQ== # fake-certificate-authority-data-value
|
||||||
|
server: https://fake-server-url-value
|
||||||
|
name: kind-kind
|
||||||
|
- cluster:
|
||||||
|
certificate-authority-data: c29tZS1vdGhlci1mYWtlLWNlcnRpZmljYXRlLWF1dGhvcml0eS1kYXRhLXZhbHVl # some-other-fake-certificate-authority-data-value
|
||||||
|
server: https://some-other-fake-server-url-value
|
||||||
|
name: some-other-cluster
|
||||||
|
contexts:
|
||||||
|
- context:
|
||||||
|
cluster: kind-kind
|
||||||
|
user: kind-kind
|
||||||
|
name: kind-kind
|
||||||
|
- context:
|
||||||
|
cluster: some-other-cluster
|
||||||
|
user: some-other-user
|
||||||
|
name: some-other-context
|
||||||
|
current-context: kind-kind
|
||||||
|
kind: Config
|
||||||
|
preferences: {}
|
||||||
|
users:
|
||||||
|
- name: kind-kind
|
||||||
|
user:
|
||||||
|
client-certificate-data: ZmFrZS1jbGllbnQtY2VydGlmaWNhdGUtZGF0YS12YWx1ZQ== # fake-client-certificate-data-value
|
||||||
|
client-key-data: ZmFrZS1jbGllbnQta2V5LWRhdGEtdmFsdWU= # fake-client-key-data-value
|
||||||
|
- name: some-other-user
|
||||||
|
user:
|
||||||
|
client-certificate-data: c29tZS1vdGhlci1mYWtlLWNsaWVudC1jZXJ0aWZpY2F0ZS1kYXRhLXZhbHVl # some-other-fake-client-certificate-data-value
|
||||||
|
client-key-data: c29tZS1vdGhlci1mYWtlLWNsaWVudC1rZXktZGF0YS12YWx1ZQ== # some-other-fake-client-key-data-value
|
@ -5,65 +5,8 @@ SPDX-License-Identifier: Apache-2.0
|
|||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import "github.com/suzerain-io/pinniped/cmd/pinniped/cmd"
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
|
||||||
|
|
||||||
"github.com/suzerain-io/pinniped/internal/client"
|
|
||||||
"github.com/suzerain-io/pinniped/internal/constable"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
err := run(os.LookupEnv, client.ExchangeToken, os.Stdout, 30*time.Second)
|
cmd.Execute()
|
||||||
if err != nil {
|
|
||||||
_, _ = fmt.Fprintf(os.Stderr, "%s\n", err.Error())
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type envGetter func(string) (string, bool)
|
|
||||||
type tokenExchanger func(ctx context.Context, token, caBundle, apiEndpoint string) (*clientauthenticationv1beta1.ExecCredential, error)
|
|
||||||
|
|
||||||
const ErrMissingEnvVar = constable.Error("failed to get credential: environment variable not set")
|
|
||||||
|
|
||||||
func run(envGetter envGetter, tokenExchanger tokenExchanger, outputWriter io.Writer, timeout time.Duration) error {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
token, varExists := envGetter("PINNIPED_TOKEN")
|
|
||||||
if !varExists {
|
|
||||||
return envVarNotSetError("PINNIPED_TOKEN")
|
|
||||||
}
|
|
||||||
|
|
||||||
caBundle, varExists := envGetter("PINNIPED_CA_BUNDLE")
|
|
||||||
if !varExists {
|
|
||||||
return envVarNotSetError("PINNIPED_CA_BUNDLE")
|
|
||||||
}
|
|
||||||
|
|
||||||
apiEndpoint, varExists := envGetter("PINNIPED_K8S_API_ENDPOINT")
|
|
||||||
if !varExists {
|
|
||||||
return envVarNotSetError("PINNIPED_K8S_API_ENDPOINT")
|
|
||||||
}
|
|
||||||
|
|
||||||
cred, err := tokenExchanger(ctx, token, caBundle, apiEndpoint)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to get credential: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.NewEncoder(outputWriter).Encode(cred)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to marshal response to stdout: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func envVarNotSetError(varName string) error {
|
|
||||||
return fmt.Errorf("%w: %s", ErrMissingEnvVar, varName)
|
|
||||||
}
|
}
|
||||||
|
@ -14,8 +14,9 @@ User accounts can be created and edited dynamically using `kubectl` commands (se
|
|||||||
|
|
||||||
## Tools
|
## Tools
|
||||||
|
|
||||||
This example deployment uses `ytt` from [Carvel](https://carvel.dev/) to template the YAML files.
|
This example deployment uses `ytt` and `kapp` from [Carvel](https://carvel.dev/) to template the YAML files
|
||||||
Either [install `ytt`](https://get-ytt.io/) or use the [container image from Dockerhub](https://hub.docker.com/r/k14s/image/tags).
|
and to deploy the app.
|
||||||
|
Either [install `ytt` and `kapp`](https://carvel.dev/) or use the [container image from Dockerhub](https://hub.docker.com/r/k14s/image/tags).
|
||||||
|
|
||||||
As well, this demo requires a tool capable of generating a `bcrypt` hash in order to interact with
|
As well, this demo requires a tool capable of generating a `bcrypt` hash in order to interact with
|
||||||
the webhook. The example below uses `htpasswd`, which is installed on most macOS systems, and can be
|
the webhook. The example below uses `htpasswd`, which is installed on most macOS systems, and can be
|
||||||
|
@ -9,8 +9,9 @@ for details.
|
|||||||
|
|
||||||
## Tools
|
## Tools
|
||||||
|
|
||||||
This example deployment uses `ytt` from [Carvel](https://carvel.dev/) to template the YAML files.
|
This example deployment uses `ytt` and `kapp` from [Carvel](https://carvel.dev/) to template the YAML files
|
||||||
Either [install `ytt`](https://get-ytt.io/) or use the [container image from Dockerhub](https://hub.docker.com/r/k14s/image/tags).
|
and to deploy the app.
|
||||||
|
Either [install `ytt` and `kapp`](https://carvel.dev/) or use the [container image from Dockerhub](https://hub.docker.com/r/k14s/image/tags).
|
||||||
|
|
||||||
## Procedure
|
## Procedure
|
||||||
|
|
||||||
|
115
doc/demo.md
Normal file
115
doc/demo.md
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
# Trying Pinniped
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
1. A Kubernetes cluster of a type supported by Pinniped.
|
||||||
|
Currently, Pinniped supports self-hosted clusters where the Kube Controller Manager pod
|
||||||
|
is accessible from Pinniped's pods.
|
||||||
|
Support for other types of Kubernetes distributions is coming soon.
|
||||||
|
|
||||||
|
Don't have a cluster handy? Consider using [kind](https://kind.sigs.k8s.io/) on your local machine.
|
||||||
|
See below for an example of using kind.
|
||||||
|
|
||||||
|
1. A kubeconfig where the current context points to that cluster and has admin-like
|
||||||
|
privileges on that cluster.
|
||||||
|
|
||||||
|
Don't have an identity provider of a type supported by Pinniped handy?
|
||||||
|
Start by installing `local-user-authenticator` on the same cluster where you would like to try Pinniped
|
||||||
|
by following the directions in [deploy-local-user-authenticator/README.md](../deploy-local-user-authenticator/README.md).
|
||||||
|
See below for an example of deploying this on kind.
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
### General Steps
|
||||||
|
|
||||||
|
1. Install Pinniped by following the directions in [deploy/README.md](../deploy/README.md).
|
||||||
|
1. Download the Pinniped CLI from [Pinniped's github Releases page](https://github.com/suzerain-io/pinniped/releases/latest).
|
||||||
|
1. Generate a kubeconfig using the Pinniped CLI. Run `pinniped get-kubeconfig --help` for more information.
|
||||||
|
1. Run `kubectl` commands using the generated kubeconfig to authenticate using Pinniped during those commands.
|
||||||
|
|
||||||
|
### Specific Example of Deploying on kind Using `local-user-authenticator` as the Identity Provider
|
||||||
|
|
||||||
|
1. Install the tools required for the following steps.
|
||||||
|
|
||||||
|
- This example deployment uses `ytt` and `kapp` from [Carvel](https://carvel.dev/) to template the YAML files
|
||||||
|
and to deploy the app.
|
||||||
|
Either [install `ytt` and `kapp`](https://carvel.dev/) or use the [container image from Dockerhub](https://hub.docker.com/r/k14s/image/tags).
|
||||||
|
E.g. `brew install k14s/tap/ytt k14s/tap/kapp` on a Mac.
|
||||||
|
|
||||||
|
- [Install kind](https://kind.sigs.k8s.io/docs/user/quick-start/), if not already installed. e.g. `brew install kind` on a Mac.
|
||||||
|
|
||||||
|
- kind depends on Docker. If not already installed, [install Docker](https://docs.docker.com/get-docker/), e.g. `brew cask install docker` on a Mac.
|
||||||
|
|
||||||
|
- This demo requires `kubectl`, which comes with Docker, or can be [installed separately](https://kubernetes.io/docs/tasks/tools/install-kubectl/).
|
||||||
|
|
||||||
|
- This demo requires a tool capable of generating a `bcrypt` hash in order to interact with
|
||||||
|
the webhook. The example below uses `htpasswd`, which is installed on most macOS systems, and can be
|
||||||
|
installed on some Linux systems via the `apache2-utils` package (e.g., `apt-get install
|
||||||
|
apache2-utils`).
|
||||||
|
|
||||||
|
1. Create a new Kubernetes cluster using `kind create cluster`. Optionally provide a cluster name using the `--name` flag.
|
||||||
|
kind will automatically update your kubeconfig to point to the new cluster.
|
||||||
|
|
||||||
|
1. Clone this repo.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/suzerain-io/pinniped.git /tmp/pinniped --depth 1
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Deploy the `local-user-authenticator` app:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /tmp/pinniped/deploy-local-user-authenticator
|
||||||
|
ytt --file . | kapp deploy --yes --app local-user-authenticator --diff-changes --file -
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Create a test user.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl create secret generic pinny-the-seal \
|
||||||
|
--namespace local-user-authenticator \
|
||||||
|
--from-literal=groups=group1,group2 \
|
||||||
|
--from-literal=passwordHash=$(htpasswd -nbBC 10 x password123 | sed -e "s/^x://")
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Fetch the auto-generated CA bundle for the `local-user-authenticator`'s HTTP TLS endpoint.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl get secret api-serving-cert --namespace local-user-authenticator \
|
||||||
|
-o jsonpath={.data.caCertificate} \
|
||||||
|
| base64 -d \
|
||||||
|
| tee /tmp/local-user-authenticator-ca
|
||||||
|
```
|
||||||
|
1. Deploy Pinniped.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /tmp/pinniped/deploy
|
||||||
|
ytt --file . | kapp deploy --yes --app pinniped --diff-changes --file - \
|
||||||
|
--data-value "webhook_url=https://local-user-authenticator.local-user-authenticator.svc/authenticate" \
|
||||||
|
--data-value "webhook_ca_bundle=$(cat /tmp/local-user-authenticator-ca)"
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Download the latest version of the Pinniped CLI binary for your platform
|
||||||
|
from [Pinniped's github Releases page](https://github.com/suzerain-io/pinniped/releases/latest).
|
||||||
|
|
||||||
|
1. Move the Pinniped CLI binary to your preferred directory and add the executable bit,
|
||||||
|
e.g. `chmod +x /usr/local/bin/pinniped`.
|
||||||
|
|
||||||
|
1. Generate a kubeconfig.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pinniped get-kubeconfig --token "pinny-the-seal:password123" > /tmp/pinniped-kubeconfig
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Create RBAC rules for the test user to give them permissions to perform actions on the cluster.
|
||||||
|
For example, grant the test user permission to view all cluster resources.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl create clusterrolebinding pinny-can-read --clusterrole view --user pinny-the-seal
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Use the generated kubeconfig to issue arbitrary `kubectl` commands as the `pinny-the-seal` user.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl --kubeconfig /tmp/pinniped-kubeconfig get pods -n pinniped
|
||||||
|
```
|
2
go.mod
2
go.mod
@ -3,7 +3,9 @@ module github.com/suzerain-io/pinniped
|
|||||||
go 1.14
|
go 1.14
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/MakeNowJust/heredoc/v2 v2.0.1
|
||||||
github.com/davecgh/go-spew v1.1.1
|
github.com/davecgh/go-spew v1.1.1
|
||||||
|
github.com/ghodss/yaml v1.0.0
|
||||||
github.com/go-logr/logr v0.2.1
|
github.com/go-logr/logr v0.2.1
|
||||||
github.com/go-logr/stdr v0.2.0
|
github.com/go-logr/stdr v0.2.0
|
||||||
github.com/golang/mock v1.4.4
|
github.com/golang/mock v1.4.4
|
||||||
|
3
go.sum
3
go.sum
@ -36,6 +36,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
|
|||||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||||
github.com/Djarvur/go-err113 v0.0.0-20200511133814-5174e21577d5 h1:XTrzB+F8+SpRmbhAH8HLxhiiG6nYNwaBZjrFps1oWEk=
|
github.com/Djarvur/go-err113 v0.0.0-20200511133814-5174e21577d5 h1:XTrzB+F8+SpRmbhAH8HLxhiiG6nYNwaBZjrFps1oWEk=
|
||||||
github.com/Djarvur/go-err113 v0.0.0-20200511133814-5174e21577d5/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs=
|
github.com/Djarvur/go-err113 v0.0.0-20200511133814-5174e21577d5/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs=
|
||||||
|
github.com/MakeNowJust/heredoc/v2 v2.0.1 h1:rlCHh70XXXv7toz95ajQWOWQnN4WNLt0TdpZYIR/J6A=
|
||||||
|
github.com/MakeNowJust/heredoc/v2 v2.0.1/go.mod h1:6/2Abh5s+hc3g9nbWLe9ObDIOhaRrqsyY9MWy+4JdRM=
|
||||||
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
|
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
|
||||||
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
|
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
|
||||||
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46 h1:lsxEuwrXEAokXB9qhlbKWPpo3KMLZQ5WB5WLQRW1uq0=
|
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46 h1:lsxEuwrXEAokXB9qhlbKWPpo3KMLZQ5WB5WLQRW1uq0=
|
||||||
@ -130,6 +132,7 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo
|
|||||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
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/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 v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||||
|
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||||
github.com/go-critic/go-critic v0.5.2 h1:3RJdgf6u4NZUumoP8nzbqiiNT8e1tC2Oc7jlgqre/IA=
|
github.com/go-critic/go-critic v0.5.2 h1:3RJdgf6u4NZUumoP8nzbqiiNT8e1tC2Oc7jlgqre/IA=
|
||||||
github.com/go-critic/go-critic v0.5.2/go.mod h1:cc0+HvdE3lFpqLecgqMaJcvWWH77sLdBp+wLGPM1Yyo=
|
github.com/go-critic/go-critic v0.5.2/go.mod h1:cc0+HvdE3lFpqLecgqMaJcvWWH77sLdBp+wLGPM1Yyo=
|
||||||
|
@ -88,6 +88,12 @@ check_dependency kapp "Please install kapp. e.g. 'brew tap k14s/tap && brew inst
|
|||||||
check_dependency kubectl "Please install kubectl. e.g. 'brew install kubectl' for MacOS"
|
check_dependency kubectl "Please install kubectl. e.g. 'brew install kubectl' for MacOS"
|
||||||
check_dependency htpasswd "Please install htpasswd. Should be pre-installed on MacOS. Usually found in 'apache2-utils' package for linux."
|
check_dependency htpasswd "Please install htpasswd. Should be pre-installed on MacOS. Usually found in 'apache2-utils' package for linux."
|
||||||
|
|
||||||
|
# Require kubectl >= 1.18.x
|
||||||
|
if [ "$(kubectl version --client=true --short | cut -d '.' -f 2)" -lt 18 ]; then
|
||||||
|
echo "kubectl >= 1.18.x is required, you have $(kubectl version --client=true --short | cut -d ':' -f2)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
#
|
#
|
||||||
# Setup kind and build the app
|
# Setup kind and build the app
|
||||||
#
|
#
|
||||||
|
@ -28,7 +28,7 @@ func CreateOrUpdateCredentialIssuerConfig(
|
|||||||
existingCredentialIssuerConfig, err := pinnipedClient.
|
existingCredentialIssuerConfig, err := pinnipedClient.
|
||||||
CrdV1alpha1().
|
CrdV1alpha1().
|
||||||
CredentialIssuerConfigs(credentialIssuerConfigNamespace).
|
CredentialIssuerConfigs(credentialIssuerConfigNamespace).
|
||||||
Get(ctx, configName, metav1.GetOptions{})
|
Get(ctx, ConfigName, metav1.GetOptions{})
|
||||||
|
|
||||||
notFound := k8serrors.IsNotFound(err)
|
notFound := k8serrors.IsNotFound(err)
|
||||||
if err != nil && !notFound {
|
if err != nil && !notFound {
|
||||||
@ -39,7 +39,7 @@ func CreateOrUpdateCredentialIssuerConfig(
|
|||||||
ctx,
|
ctx,
|
||||||
existingCredentialIssuerConfig,
|
existingCredentialIssuerConfig,
|
||||||
notFound,
|
notFound,
|
||||||
configName,
|
ConfigName,
|
||||||
credentialIssuerConfigNamespace,
|
credentialIssuerConfigNamespace,
|
||||||
pinnipedClient,
|
pinnipedClient,
|
||||||
applyUpdatesToCredentialIssuerConfigFunc)
|
applyUpdatesToCredentialIssuerConfigFunc)
|
||||||
|
@ -24,10 +24,10 @@ import (
|
|||||||
const (
|
const (
|
||||||
ClusterInfoNamespace = "kube-public"
|
ClusterInfoNamespace = "kube-public"
|
||||||
|
|
||||||
|
ConfigName = "pinniped-config"
|
||||||
|
|
||||||
clusterInfoName = "cluster-info"
|
clusterInfoName = "cluster-info"
|
||||||
clusterInfoConfigMapKey = "kubeconfig"
|
clusterInfoConfigMapKey = "kubeconfig"
|
||||||
|
|
||||||
configName = "pinniped-config"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type publisherController struct {
|
type publisherController struct {
|
||||||
@ -64,7 +64,7 @@ func NewPublisherController(
|
|||||||
),
|
),
|
||||||
withInformer(
|
withInformer(
|
||||||
credentialIssuerConfigInformer,
|
credentialIssuerConfigInformer,
|
||||||
pinnipedcontroller.NameAndNamespaceExactMatchFilterFactory(configName, namespace),
|
pinnipedcontroller.NameAndNamespaceExactMatchFilterFactory(ConfigName, namespace),
|
||||||
controllerlib.InformerOption{},
|
controllerlib.InformerOption{},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -114,7 +114,7 @@ func (c *publisherController) Sync(ctx controllerlib.Context) error {
|
|||||||
existingCredentialIssuerConfigFromInformerCache, err := c.credentialIssuerConfigInformer.
|
existingCredentialIssuerConfigFromInformerCache, err := c.credentialIssuerConfigInformer.
|
||||||
Lister().
|
Lister().
|
||||||
CredentialIssuerConfigs(c.namespace).
|
CredentialIssuerConfigs(c.namespace).
|
||||||
Get(configName)
|
Get(ConfigName)
|
||||||
notFound = k8serrors.IsNotFound(err)
|
notFound = k8serrors.IsNotFound(err)
|
||||||
if err != nil && !notFound {
|
if err != nil && !notFound {
|
||||||
return fmt.Errorf("could not get credentialissuerconfig: %w", err)
|
return fmt.Errorf("could not get credentialissuerconfig: %w", err)
|
||||||
@ -131,7 +131,7 @@ func (c *publisherController) Sync(ctx controllerlib.Context) error {
|
|||||||
ctx.Context,
|
ctx.Context,
|
||||||
existingCredentialIssuerConfigFromInformerCache,
|
existingCredentialIssuerConfigFromInformerCache,
|
||||||
notFound,
|
notFound,
|
||||||
configName,
|
ConfigName,
|
||||||
c.namespace,
|
c.namespace,
|
||||||
c.pinnipedClient,
|
c.pinnipedClient,
|
||||||
updateServerAndCAFunc)
|
updateServerAndCAFunc)
|
||||||
|
@ -8,7 +8,6 @@ package issuerconfig
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -27,6 +26,7 @@ import (
|
|||||||
pinnipedfake "github.com/suzerain-io/pinniped/generated/1.19/client/clientset/versioned/fake"
|
pinnipedfake "github.com/suzerain-io/pinniped/generated/1.19/client/clientset/versioned/fake"
|
||||||
pinnipedinformers "github.com/suzerain-io/pinniped/generated/1.19/client/informers/externalversions"
|
pinnipedinformers "github.com/suzerain-io/pinniped/generated/1.19/client/informers/externalversions"
|
||||||
"github.com/suzerain-io/pinniped/internal/controllerlib"
|
"github.com/suzerain-io/pinniped/internal/controllerlib"
|
||||||
|
"github.com/suzerain-io/pinniped/internal/here"
|
||||||
"github.com/suzerain-io/pinniped/internal/testutil"
|
"github.com/suzerain-io/pinniped/internal/testutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -256,14 +256,15 @@ func TestSync(t *testing.T) {
|
|||||||
ObjectMeta: metav1.ObjectMeta{Name: "cluster-info", Namespace: "kube-public"},
|
ObjectMeta: metav1.ObjectMeta{Name: "cluster-info", Namespace: "kube-public"},
|
||||||
// Note that go fmt puts tabs in our file, which we must remove from our configmap yaml below.
|
// Note that go fmt puts tabs in our file, which we must remove from our configmap yaml below.
|
||||||
Data: map[string]string{
|
Data: map[string]string{
|
||||||
"kubeconfig": strings.ReplaceAll(`
|
"kubeconfig": here.Docf(`
|
||||||
kind: Config
|
kind: Config
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
clusters:
|
clusters:
|
||||||
- name: ""
|
- name: ""
|
||||||
cluster:
|
cluster:
|
||||||
certificate-authority-data: "`+caData+`"
|
certificate-authority-data: "%s"
|
||||||
server: "`+kubeServerURL+`"`, "\t", " "),
|
server: "%s"`,
|
||||||
|
caData, kubeServerURL),
|
||||||
"uninteresting-key": "uninteresting-value",
|
"uninteresting-key": "uninteresting-value",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
25
internal/here/doc.go
Normal file
25
internal/here/doc.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2020 VMware, Inc.
|
||||||
|
SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package here
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/MakeNowJust/heredoc/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
tab = "\t"
|
||||||
|
fourSpaces = " "
|
||||||
|
)
|
||||||
|
|
||||||
|
func Doc(s string) string {
|
||||||
|
return strings.ReplaceAll(heredoc.Doc(s), tab, fourSpaces)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Docf(raw string, args ...interface{}) string {
|
||||||
|
return strings.ReplaceAll(heredoc.Docf(raw, args...), tab, fourSpaces)
|
||||||
|
}
|
104
internal/here/doc_test.go
Normal file
104
internal/here/doc_test.go
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2020 VMware, Inc.
|
||||||
|
SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package here
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/sclevine/spec"
|
||||||
|
"github.com/sclevine/spec/report"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDoc(t *testing.T) {
|
||||||
|
spec.Run(t, "here.Doc", func(t *testing.T, when spec.G, it spec.S) {
|
||||||
|
var r *require.Assertions
|
||||||
|
|
||||||
|
it.Before(func() {
|
||||||
|
r = require.New(t)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns single-line strings unchanged", func() {
|
||||||
|
r.Equal("the quick brown fox", Doc("the quick brown fox"))
|
||||||
|
r.Equal(" the quick brown fox", Doc(" the quick brown fox"))
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns multi-line strings with indentation removed", func() {
|
||||||
|
r.Equal(
|
||||||
|
"the quick brown fox\njumped over the\nlazy dog",
|
||||||
|
Doc(`the quick brown fox
|
||||||
|
jumped over the
|
||||||
|
lazy dog`),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("ignores the first empty line and the whitespace in the last line", func() {
|
||||||
|
r.Equal(
|
||||||
|
"the quick brown fox\njumped over the\nlazy dog\n",
|
||||||
|
Doc(`
|
||||||
|
the quick brown fox
|
||||||
|
jumped over the
|
||||||
|
lazy dog
|
||||||
|
`),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("turns all tabs into 4 spaces", func() {
|
||||||
|
r.Equal(
|
||||||
|
"the quick brown fox\n jumped over the\n lazy dog\n",
|
||||||
|
Doc(`
|
||||||
|
the quick brown fox
|
||||||
|
jumped over the
|
||||||
|
lazy dog
|
||||||
|
`),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}, spec.Parallel(), spec.Report(report.Terminal{}))
|
||||||
|
|
||||||
|
spec.Run(t, "here.Docf", func(t *testing.T, when spec.G, it spec.S) {
|
||||||
|
var r *require.Assertions
|
||||||
|
|
||||||
|
it.Before(func() {
|
||||||
|
r = require.New(t)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns single-line strings unchanged", func() {
|
||||||
|
r.Equal("the quick brown fox", Docf("the quick brown %s", "fox"))
|
||||||
|
r.Equal(" the quick brown fox", Docf(" the %s brown %s", "quick", "fox"))
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns multi-line strings with indentation removed", func() {
|
||||||
|
r.Equal(
|
||||||
|
"the quick brown fox\njumped over the\nlazy dog",
|
||||||
|
Docf(`the quick brown %s
|
||||||
|
jumped over the
|
||||||
|
lazy %s`, "fox", "dog"),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("ignores the first empty line and the whitespace in the last line", func() {
|
||||||
|
r.Equal(
|
||||||
|
"the quick brown fox\njumped over the\nlazy dog\n",
|
||||||
|
Docf(`
|
||||||
|
the quick brown %s
|
||||||
|
jumped over the
|
||||||
|
lazy %s
|
||||||
|
`, "fox", "dog"),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("turns all tabs into 4 spaces", func() {
|
||||||
|
r.Equal(
|
||||||
|
"the quick brown fox\n jumped over the\n lazy dog\n",
|
||||||
|
Docf(`
|
||||||
|
the quick brown %s
|
||||||
|
jumped over the
|
||||||
|
lazy %s
|
||||||
|
`, "fox", "dog"),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}, spec.Parallel(), spec.Report(report.Terminal{}))
|
||||||
|
}
|
@ -30,6 +30,7 @@ import (
|
|||||||
"github.com/suzerain-io/pinniped/internal/controller/issuerconfig"
|
"github.com/suzerain-io/pinniped/internal/controller/issuerconfig"
|
||||||
"github.com/suzerain-io/pinniped/internal/controllermanager"
|
"github.com/suzerain-io/pinniped/internal/controllermanager"
|
||||||
"github.com/suzerain-io/pinniped/internal/downward"
|
"github.com/suzerain-io/pinniped/internal/downward"
|
||||||
|
"github.com/suzerain-io/pinniped/internal/here"
|
||||||
"github.com/suzerain-io/pinniped/internal/provider"
|
"github.com/suzerain-io/pinniped/internal/provider"
|
||||||
"github.com/suzerain-io/pinniped/internal/registry/credentialrequest"
|
"github.com/suzerain-io/pinniped/internal/registry/credentialrequest"
|
||||||
"github.com/suzerain-io/pinniped/pkg/config"
|
"github.com/suzerain-io/pinniped/pkg/config"
|
||||||
@ -63,10 +64,11 @@ func (a *App) Run() error {
|
|||||||
// Create the server command and save it into the App.
|
// Create the server command and save it into the App.
|
||||||
func (a *App) addServerCommand(ctx context.Context, args []string, stdout, stderr io.Writer) {
|
func (a *App) addServerCommand(ctx context.Context, args []string, stdout, stderr io.Writer) {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: `pinniped-server`,
|
Use: "pinniped-server",
|
||||||
Long: "pinniped-server provides a generic API for mapping an external\n" +
|
Long: here.Doc(`
|
||||||
"credential from somewhere to an internal credential to be used for\n" +
|
pinniped-server provides a generic API for mapping an external
|
||||||
"authenticating to the Kubernetes API.",
|
credential from somewhere to an internal credential to be used for
|
||||||
|
authenticating to the Kubernetes API.`),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error { return a.runServer(ctx) },
|
RunE: func(cmd *cobra.Command, args []string) error { return a.runServer(ctx) },
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
}
|
}
|
||||||
|
91
test/integration/cli_test.go
Normal file
91
test/integration/cli_test.go
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2020 VMware, Inc.
|
||||||
|
SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/suzerain-io/pinniped/test/library"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCLI(t *testing.T) {
|
||||||
|
library.SkipUnlessIntegration(t)
|
||||||
|
library.SkipUnlessClusterHasCapability(t, library.ClusterSigningKeyIsAvailable)
|
||||||
|
token := library.GetEnv(t, "PINNIPED_TEST_USER_TOKEN")
|
||||||
|
namespaceName := library.GetEnv(t, "PINNIPED_NAMESPACE")
|
||||||
|
testUsername := library.GetEnv(t, "PINNIPED_TEST_USER_USERNAME")
|
||||||
|
expectedTestUserGroups := strings.Split(
|
||||||
|
strings.ReplaceAll(library.GetEnv(t, "PINNIPED_TEST_USER_GROUPS"), " ", ""), ",",
|
||||||
|
)
|
||||||
|
|
||||||
|
// Build pinniped CLI.
|
||||||
|
pinnipedExe, cleanupFunc := buildPinnipedCLI(t)
|
||||||
|
defer cleanupFunc()
|
||||||
|
|
||||||
|
// Run pinniped CLI to get kubeconfig.
|
||||||
|
kubeConfig := runPinnipedCLI(t, pinnipedExe, token, namespaceName)
|
||||||
|
|
||||||
|
// Create Kubernetes client with kubeconfig from pinniped CLI.
|
||||||
|
kubeClient := library.NewClientsetForKubeConfig(t, kubeConfig)
|
||||||
|
|
||||||
|
// Validate that we can auth to the API via our user.
|
||||||
|
ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*3)
|
||||||
|
defer cancelFunc()
|
||||||
|
|
||||||
|
adminClient := library.NewClientset(t)
|
||||||
|
|
||||||
|
t.Run("access as user", accessAsUserTest(ctx, adminClient, testUsername, kubeClient))
|
||||||
|
for _, group := range expectedTestUserGroups {
|
||||||
|
group := group
|
||||||
|
t.Run(
|
||||||
|
"access as group "+group,
|
||||||
|
accessAsGroupTest(ctx, adminClient, group, kubeClient),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildPinnipedCLI(t *testing.T) (string, func()) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
pinnipedExeDir, err := ioutil.TempDir("", "pinniped-cli-test-*")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
pinnipedExe := filepath.Join(pinnipedExeDir, "pinniped")
|
||||||
|
output, err := exec.Command(
|
||||||
|
"go",
|
||||||
|
"build",
|
||||||
|
"-o",
|
||||||
|
pinnipedExe,
|
||||||
|
"github.com/suzerain-io/pinniped/cmd/pinniped",
|
||||||
|
).CombinedOutput()
|
||||||
|
require.NoError(t, err, string(output))
|
||||||
|
|
||||||
|
return pinnipedExe, func() {
|
||||||
|
require.NoError(t, os.RemoveAll(pinnipedExeDir))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runPinnipedCLI(t *testing.T, pinnipedExe, token, namespaceName string) string {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
output, err := exec.Command(
|
||||||
|
pinnipedExe,
|
||||||
|
"get-kubeconfig",
|
||||||
|
"--token", token,
|
||||||
|
"--pinniped-namespace", namespaceName,
|
||||||
|
).CombinedOutput()
|
||||||
|
require.NoError(t, err, string(output))
|
||||||
|
|
||||||
|
return string(output)
|
||||||
|
}
|
@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/suzerain-io/pinniped/internal/client"
|
"github.com/suzerain-io/pinniped/internal/client"
|
||||||
|
"github.com/suzerain-io/pinniped/internal/here"
|
||||||
"github.com/suzerain-io/pinniped/test/library"
|
"github.com/suzerain-io/pinniped/test/library"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -28,7 +29,7 @@ Test certificate and private key that should get an authentication error. Genera
|
|||||||
[1]: https://github.com/cloudflare/cfssl
|
[1]: https://github.com/cloudflare/cfssl
|
||||||
*/
|
*/
|
||||||
var (
|
var (
|
||||||
testCert = strings.TrimSpace(`
|
testCert = here.Doc(`
|
||||||
-----BEGIN CERTIFICATE-----
|
-----BEGIN CERTIFICATE-----
|
||||||
MIICBDCCAaugAwIBAgIUeidKWlZQuoKfBGydObI1hMwzt9cwCgYIKoZIzj0EAwIw
|
MIICBDCCAaugAwIBAgIUeidKWlZQuoKfBGydObI1hMwzt9cwCgYIKoZIzj0EAwIw
|
||||||
SDELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1TYW4gRnJhbmNp
|
SDELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1TYW4gRnJhbmNp
|
||||||
@ -43,7 +44,8 @@ c2VyMAoGCCqGSM49BAMCA0cAMEQCIEwPZhPpYhYHndfTEsWOxnxzJkmhAcYIMCeJ
|
|||||||
d9kyq/fPAiBNCJw1MCLT8LjNlyUZCfwI2zuI3e0w6vuau89oj2zvVA==
|
d9kyq/fPAiBNCJw1MCLT8LjNlyUZCfwI2zuI3e0w6vuau89oj2zvVA==
|
||||||
-----END CERTIFICATE-----
|
-----END CERTIFICATE-----
|
||||||
`)
|
`)
|
||||||
testKey = maskKey(strings.TrimSpace(`
|
|
||||||
|
testKey = maskKey(here.Doc(`
|
||||||
-----BEGIN EC TESTING KEY-----
|
-----BEGIN EC TESTING KEY-----
|
||||||
MHcCAQEEIAqkBGGKTH5GzLx8XZLAHEFW2E8jT+jpy0p6w6MMR7DkoAoGCCqGSM49
|
MHcCAQEEIAqkBGGKTH5GzLx8XZLAHEFW2E8jT+jpy0p6w6MMR7DkoAoGCCqGSM49
|
||||||
AwEHoUQDQgAEZO1wQvjo2Jq1zoZu0WiYh1uEOVbBPojFfdAc6u2p7KgIW9OESOt2
|
AwEHoUQDQgAEZO1wQvjo2Jq1zoZu0WiYh1uEOVbBPojFfdAc6u2p7KgIW9OESOt2
|
||||||
|
102
test/integration/common_test.go
Normal file
102
test/integration/common_test.go
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2020 VMware, Inc.
|
||||||
|
SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
package integration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
v1 "k8s.io/api/core/v1"
|
||||||
|
rbacv1 "k8s.io/api/rbac/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/client-go/kubernetes"
|
||||||
|
)
|
||||||
|
|
||||||
|
// accessAsUserTest runs a generic test in which a clientUnderTest operating with username
|
||||||
|
// testUsername tries to auth to the kube API (i.e., list namespaces).
|
||||||
|
//
|
||||||
|
// Use this function if you want to simply validate that a user can auth to the kube API after
|
||||||
|
// performing a Pinniped credential exchange.
|
||||||
|
func accessAsUserTest(
|
||||||
|
ctx context.Context,
|
||||||
|
adminClient kubernetes.Interface,
|
||||||
|
testUsername string,
|
||||||
|
clientUnderTest kubernetes.Interface,
|
||||||
|
) func(t *testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
addTestClusterRoleBinding(ctx, t, adminClient, &rbacv1.ClusterRoleBinding{
|
||||||
|
TypeMeta: metav1.TypeMeta{},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "integration-test-user-readonly-role-binding",
|
||||||
|
},
|
||||||
|
Subjects: []rbacv1.Subject{{
|
||||||
|
Kind: rbacv1.UserKind,
|
||||||
|
APIGroup: rbacv1.GroupName,
|
||||||
|
Name: testUsername,
|
||||||
|
}},
|
||||||
|
RoleRef: rbacv1.RoleRef{
|
||||||
|
Kind: "ClusterRole",
|
||||||
|
APIGroup: rbacv1.GroupName,
|
||||||
|
Name: "view",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Use the client which is authenticated as the test user to list namespaces
|
||||||
|
var listNamespaceResponse *v1.NamespaceList
|
||||||
|
var err error
|
||||||
|
var canListNamespaces = func() bool {
|
||||||
|
listNamespaceResponse, err = clientUnderTest.CoreV1().Namespaces().List(ctx, metav1.ListOptions{})
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
assert.Eventually(t, canListNamespaces, 3*time.Second, 250*time.Millisecond)
|
||||||
|
require.NoError(t, err) // prints out the error and stops the test in case of failure
|
||||||
|
require.NotEmpty(t, listNamespaceResponse.Items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// accessAsGroupTest runs a generic test in which a clientUnderTest with membership in group
|
||||||
|
// testGroup tries to auth to the kube API (i.e., list namespaces).
|
||||||
|
//
|
||||||
|
// Use this function if you want to simply validate that a user can auth to the kube API (via
|
||||||
|
// a group membership) after performing a Pinniped credential exchange.
|
||||||
|
func accessAsGroupTest(
|
||||||
|
ctx context.Context,
|
||||||
|
adminClient kubernetes.Interface,
|
||||||
|
testGroup string,
|
||||||
|
clientUnderTest kubernetes.Interface,
|
||||||
|
) func(t *testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
addTestClusterRoleBinding(ctx, t, adminClient, &rbacv1.ClusterRoleBinding{
|
||||||
|
TypeMeta: metav1.TypeMeta{},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "integration-test-group-readonly-role-binding",
|
||||||
|
},
|
||||||
|
Subjects: []rbacv1.Subject{{
|
||||||
|
Kind: rbacv1.GroupKind,
|
||||||
|
APIGroup: rbacv1.GroupName,
|
||||||
|
Name: testGroup,
|
||||||
|
}},
|
||||||
|
RoleRef: rbacv1.RoleRef{
|
||||||
|
Kind: "ClusterRole",
|
||||||
|
APIGroup: rbacv1.GroupName,
|
||||||
|
Name: "view",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Use the client which is authenticated as the test user to list namespaces
|
||||||
|
var listNamespaceResponse *v1.NamespaceList
|
||||||
|
var err error
|
||||||
|
var canListNamespaces = func() bool {
|
||||||
|
listNamespaceResponse, err = clientUnderTest.CoreV1().Namespaces().List(ctx, metav1.ListOptions{})
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
assert.Eventually(t, canListNamespaces, 3*time.Second, 250*time.Millisecond)
|
||||||
|
require.NoError(t, err) // prints out the error and stops the test in case of failure
|
||||||
|
require.NotEmpty(t, listNamespaceResponse.Items)
|
||||||
|
}
|
||||||
|
}
|
@ -14,9 +14,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
v1 "k8s.io/api/core/v1"
|
|
||||||
rbacv1 "k8s.io/api/rbac/v1"
|
rbacv1 "k8s.io/api/rbac/v1"
|
||||||
"k8s.io/apimachinery/pkg/api/errors"
|
"k8s.io/apimachinery/pkg/api/errors"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
@ -61,65 +59,16 @@ func TestSuccessfulCredentialRequest(t *testing.T) {
|
|||||||
response.Status.Credential.ClientKeyData,
|
response.Status.Credential.ClientKeyData,
|
||||||
)
|
)
|
||||||
|
|
||||||
t.Run("access as user", func(t *testing.T) {
|
t.Run(
|
||||||
addTestClusterRoleBinding(ctx, t, adminClient, &rbacv1.ClusterRoleBinding{
|
"access as user",
|
||||||
TypeMeta: metav1.TypeMeta{},
|
accessAsUserTest(ctx, adminClient, testUsername, clientWithCertFromCredentialRequest),
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
)
|
||||||
Name: "integration-test-user-readonly-role-binding",
|
|
||||||
},
|
|
||||||
Subjects: []rbacv1.Subject{{
|
|
||||||
Kind: rbacv1.UserKind,
|
|
||||||
APIGroup: rbacv1.GroupName,
|
|
||||||
Name: testUsername,
|
|
||||||
}},
|
|
||||||
RoleRef: rbacv1.RoleRef{
|
|
||||||
Kind: "ClusterRole",
|
|
||||||
APIGroup: rbacv1.GroupName,
|
|
||||||
Name: "view",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// Use the client which is authenticated as the test user to list namespaces
|
|
||||||
var listNamespaceResponse *v1.NamespaceList
|
|
||||||
var canListNamespaces = func() bool {
|
|
||||||
listNamespaceResponse, err = clientWithCertFromCredentialRequest.CoreV1().Namespaces().List(ctx, metav1.ListOptions{})
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
assert.Eventually(t, canListNamespaces, 3*time.Second, 250*time.Millisecond)
|
|
||||||
require.NoError(t, err) // prints out the error and stops the test in case of failure
|
|
||||||
require.NotEmpty(t, listNamespaceResponse.Items)
|
|
||||||
})
|
|
||||||
|
|
||||||
for _, group := range expectedTestUserGroups {
|
for _, group := range expectedTestUserGroups {
|
||||||
group := group
|
group := group
|
||||||
t.Run("access as group "+group, func(t *testing.T) {
|
t.Run(
|
||||||
addTestClusterRoleBinding(ctx, t, adminClient, &rbacv1.ClusterRoleBinding{
|
"access as group "+group,
|
||||||
TypeMeta: metav1.TypeMeta{},
|
accessAsGroupTest(ctx, adminClient, group, clientWithCertFromCredentialRequest),
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
)
|
||||||
Name: "integration-test-group-readonly-role-binding",
|
|
||||||
},
|
|
||||||
Subjects: []rbacv1.Subject{{
|
|
||||||
Kind: rbacv1.GroupKind,
|
|
||||||
APIGroup: rbacv1.GroupName,
|
|
||||||
Name: group,
|
|
||||||
}},
|
|
||||||
RoleRef: rbacv1.RoleRef{
|
|
||||||
Kind: "ClusterRole",
|
|
||||||
APIGroup: rbacv1.GroupName,
|
|
||||||
Name: "view",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// Use the client which is authenticated as the test user to list namespaces
|
|
||||||
var listNamespaceResponse *v1.NamespaceList
|
|
||||||
var canListNamespaces = func() bool {
|
|
||||||
listNamespaceResponse, err = clientWithCertFromCredentialRequest.CoreV1().Namespaces().List(ctx, metav1.ListOptions{})
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
assert.Eventually(t, canListNamespaces, 3*time.Second, 250*time.Millisecond)
|
|
||||||
require.NoError(t, err) // prints out the error and stops the test in case of failure
|
|
||||||
require.NotEmpty(t, listNamespaceResponse.Items)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,6 +36,22 @@ func NewClientset(t *testing.T) kubernetes.Interface {
|
|||||||
return newClientsetWithConfig(t, NewClientConfig(t))
|
return newClientsetWithConfig(t, NewClientConfig(t))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewClientsetForKubeConfig(t *testing.T, kubeConfig string) kubernetes.Interface {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
kubeConfigFile, err := ioutil.TempFile("", "pinniped-cli-test-*")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer os.Remove(kubeConfigFile.Name())
|
||||||
|
|
||||||
|
_, err = kubeConfigFile.Write([]byte(kubeConfig))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
restConfig, err := clientcmd.BuildConfigFromFlags("", kubeConfigFile.Name())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return newClientsetWithConfig(t, restConfig)
|
||||||
|
}
|
||||||
|
|
||||||
func NewClientsetWithCertAndKey(t *testing.T, clientCertificateData, clientKeyData string) kubernetes.Interface {
|
func NewClientsetWithCertAndKey(t *testing.T, clientCertificateData, clientKeyData string) kubernetes.Interface {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user