From 9e44bc28d9b9b20cad1f268204d064b7c12ba42b Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Mon, 27 Jul 2020 13:32:14 -0700 Subject: [PATCH 01/12] Change the name of the placeholder-name CLI to placeholder-name-server Signed-off-by: Matt Moyer --- Dockerfile | 6 +++--- .../main.go | 4 ++-- deploy/deployment.yaml | 2 +- .../app/app.go => internal/server/server.go | 10 +++++----- .../app_test.go => internal/server/server_test.go | 12 ++++++------ .../app => internal/server}/testdata/podinfo/labels | 0 .../server}/testdata/podinfo/namespace | 0 .../server}/testdata/valid-config.yaml | 0 8 files changed, 17 insertions(+), 17 deletions(-) rename cmd/{placeholder-name => placeholder-name-server}/main.go (76%) rename cmd/placeholder-name/app/app.go => internal/server/server.go (96%) rename cmd/placeholder-name/app/app_test.go => internal/server/server_test.go (89%) rename {cmd/placeholder-name/app => internal/server}/testdata/podinfo/labels (100%) rename {cmd/placeholder-name/app => internal/server}/testdata/podinfo/namespace (100%) rename {cmd/placeholder-name/app => internal/server}/testdata/valid-config.yaml (100%) diff --git a/Dockerfile b/Dockerfile index 474deb58..0017c62a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,15 +32,15 @@ COPY pkg ./pkg COPY tools ./tools COPY hack ./hack # Build the executable binary -RUN GOOS=linux GOARCH=amd64 go build -ldflags "$(hack/get-ldflags.sh)" -o out ./... +RUN GOOS=linux GOARCH=amd64 go build -ldflags "$(hack/get-ldflags.sh)" -o out ./cmd/placeholder-name-server/... FROM alpine:latest # Install CA certs and some tools for debugging RUN apk --update --no-cache add ca-certificates bash curl WORKDIR /root/ # Copy the binary from the build-env stage -COPY --from=build-env /work/out/placeholder-name placeholder-name +COPY --from=build-env /work/out/placeholder-name-server placeholder-name-server # Document the port EXPOSE 443 # Set the command -CMD ["./placeholder-name"] +CMD ["./placeholder-name-server"] diff --git a/cmd/placeholder-name/main.go b/cmd/placeholder-name-server/main.go similarity index 76% rename from cmd/placeholder-name/main.go rename to cmd/placeholder-name-server/main.go index b816ad36..35562733 100644 --- a/cmd/placeholder-name/main.go +++ b/cmd/placeholder-name-server/main.go @@ -14,7 +14,7 @@ import ( "k8s.io/component-base/logs" "k8s.io/klog/v2" - "github.com/suzerain-io/placeholder-name/cmd/placeholder-name/app" + "github.com/suzerain-io/placeholder-name/internal/server" ) func main() { @@ -25,7 +25,7 @@ func main() { ctx := genericapiserver.SetupSignalContext() - if err := app.New(ctx, os.Args[1:], os.Stdout, os.Stderr).Run(); err != nil { + if err := server.New(ctx, os.Args[1:], os.Stdout, os.Stderr).Run(); err != nil { klog.Fatal(err) } } diff --git a/deploy/deployment.yaml b/deploy/deployment.yaml index 1b93a647..61d584e8 100644 --- a/deploy/deployment.yaml +++ b/deploy/deployment.yaml @@ -60,7 +60,7 @@ spec: #@ end imagePullPolicy: IfNotPresent command: - - ./placeholder-name + - ./placeholder-name-server args: - --config=/etc/config/placeholder-name.yaml - --downward-api-path=/etc/podinfo diff --git a/cmd/placeholder-name/app/app.go b/internal/server/server.go similarity index 96% rename from cmd/placeholder-name/app/app.go rename to internal/server/server.go index d6d6c6b9..442e4fd5 100644 --- a/cmd/placeholder-name/app/app.go +++ b/internal/server/server.go @@ -3,8 +3,8 @@ Copyright 2020 VMware, Inc. SPDX-License-Identifier: Apache-2.0 */ -// Package app is the command line entry point for placeholder-name. -package app +// Package server is the command line entry point for placeholder-name-server. +package server import ( "context" @@ -39,7 +39,7 @@ import ( "github.com/suzerain-io/placeholder-name/pkg/config" ) -// App is an object that represents the placeholder-name application. +// App is an object that represents the placeholder-name-server application. type App struct { cmd *cobra.Command @@ -68,8 +68,8 @@ func New(ctx context.Context, args []string, stdout, stderr io.Writer) *App { a.recommendedOptions.Etcd = nil // turn off etcd storage because we don't need it yet cmd := &cobra.Command{ - Use: `placeholder-name`, - Long: `placeholder-name provides a generic API for mapping an external + Use: `placeholder-name-server`, + Long: `placeholder-name-server provides a generic API for mapping an external credential from somewhere to an internal credential to be used for authenticating to the Kubernetes API.`, RunE: func(cmd *cobra.Command, args []string) error { diff --git a/cmd/placeholder-name/app/app_test.go b/internal/server/server_test.go similarity index 89% rename from cmd/placeholder-name/app/app_test.go rename to internal/server/server_test.go index 849925da..5ae9abe8 100644 --- a/cmd/placeholder-name/app/app_test.go +++ b/internal/server/server_test.go @@ -3,7 +3,7 @@ Copyright 2020 VMware, Inc. SPDX-License-Identifier: Apache-2.0 */ -package app +package server import ( "bytes" @@ -17,19 +17,19 @@ import ( ) const knownGoodUsage = ` -placeholder-name provides a generic API for mapping an external +placeholder-name-server provides a generic API for mapping an external credential from somewhere to an internal credential to be used for authenticating to the Kubernetes API. Usage: - placeholder-name [flags] + placeholder-name-server [flags] Flags: --cluster-signing-cert-file string path to cluster signing certificate --cluster-signing-key-file string path to cluster signing private key -c, --config string path to configuration file (default "placeholder-name.yaml") --downward-api-path string path to Downward API volume mount (default "/etc/podinfo") - -h, --help help for placeholder-name + -h, --help help for placeholder-name-server --log-flush-frequency duration Maximum number of seconds between log flushes (default 5s) ` @@ -52,7 +52,7 @@ func TestCommand(t *testing.T) { { name: "OneArgFails", args: []string{"tuna"}, - wantErr: `unknown command "tuna" for "placeholder-name"`, + wantErr: `unknown command "tuna" for "placeholder-name-server"`, }, { name: "ShortConfigFlagSucceeds", @@ -68,7 +68,7 @@ func TestCommand(t *testing.T) { "--config", "some/path/to/config.yaml", "tuna", }, - wantErr: `unknown command "tuna" for "placeholder-name"`, + wantErr: `unknown command "tuna" for "placeholder-name-server"`, }, } for _, test := range tests { diff --git a/cmd/placeholder-name/app/testdata/podinfo/labels b/internal/server/testdata/podinfo/labels similarity index 100% rename from cmd/placeholder-name/app/testdata/podinfo/labels rename to internal/server/testdata/podinfo/labels diff --git a/cmd/placeholder-name/app/testdata/podinfo/namespace b/internal/server/testdata/podinfo/namespace similarity index 100% rename from cmd/placeholder-name/app/testdata/podinfo/namespace rename to internal/server/testdata/podinfo/namespace diff --git a/cmd/placeholder-name/app/testdata/valid-config.yaml b/internal/server/testdata/valid-config.yaml similarity index 100% rename from cmd/placeholder-name/app/testdata/valid-config.yaml rename to internal/server/testdata/valid-config.yaml From 27cd82065bdfc0243972db24df1315a6b09797c5 Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Mon, 27 Jul 2020 16:49:43 -0700 Subject: [PATCH 02/12] Add placeholder-name CLI - main and unit tests for main - client package to be done in a future commit Signed-off-by: Aram Price --- cmd/placeholder-name/main.go | 64 +++++++++++++++++ cmd/placeholder-name/main_test.go | 112 ++++++++++++++++++++++++++++++ go.mod | 1 + go.sum | 2 + internal/constable/error.go | 14 ++++ pkg/client/client.go | 15 ++++ 6 files changed, 208 insertions(+) create mode 100644 cmd/placeholder-name/main.go create mode 100644 cmd/placeholder-name/main_test.go create mode 100644 internal/constable/error.go create mode 100644 pkg/client/client.go diff --git a/cmd/placeholder-name/main.go b/cmd/placeholder-name/main.go new file mode 100644 index 00000000..acd829fe --- /dev/null +++ b/cmd/placeholder-name/main.go @@ -0,0 +1,64 @@ +/* +Copyright 2020 VMware, Inc. +SPDX-License-Identifier: Apache-2.0 +*/ + +package main + +import ( + "encoding/json" + "fmt" + "io" + "os" + + "k8s.io/client-go/pkg/apis/clientauthentication" + + "github.com/suzerain-io/placeholder-name/internal/constable" + "github.com/suzerain-io/placeholder-name/pkg/client" +) + +func main() { + err := run(os.LookupEnv, client.ExchangeToken, os.Stdout) + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "%s", err.Error()) + os.Exit(1) + } +} + +type envGetter func(string) (string, bool) +type tokenExchanger func(token, caBundle, apiEndpoint string) (*clientauthentication.ExecCredential, error) + +const EnvVarNotSetError = constable.Error("failed to login: environment variable not set") + +func run(envGetter envGetter, tokenExchanger tokenExchanger, outputWriter io.Writer) error { + token, varExists := envGetter("PLACEHOLDER_NAME_TOKEN") + if !varExists { + return envVarNotSetError("PLACEHOLDER_NAME_TOKEN") + } + + caBundle, varExists := envGetter("PLACEHOLDER_NAME_CA_BUNDLE") + if !varExists { + return envVarNotSetError("PLACEHOLDER_NAME_CA_BUNDLE") + } + + apiEndpoint, varExists := envGetter("PLACEHOLDER_NAME_K8S_API_ENDPOINT") + if !varExists { + return envVarNotSetError("PLACEHOLDER_NAME_K8S_API_ENDPOINT") + } + + execCredential, err := tokenExchanger(token, caBundle, apiEndpoint) + if err != nil { + return fmt.Errorf("failed to login: %w", err) + } + + err = json.NewEncoder(outputWriter).Encode(execCredential) + if err != nil { + return fmt.Errorf("failed to marshall response to stdout: %w", err) + } + + return nil +} + +func envVarNotSetError(varName string) error { + return fmt.Errorf("%w: %s", EnvVarNotSetError, varName) +} diff --git a/cmd/placeholder-name/main_test.go b/cmd/placeholder-name/main_test.go new file mode 100644 index 00000000..55320064 --- /dev/null +++ b/cmd/placeholder-name/main_test.go @@ -0,0 +1,112 @@ +/* +Copyright 2020 VMware, Inc. +SPDX-License-Identifier: Apache-2.0 +*/ + +package main + +import ( + "bytes" + "fmt" + "testing" + + "k8s.io/client-go/pkg/apis/clientauthentication" + + "github.com/stretchr/testify/require" + + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" +) + +func TestRun(t *testing.T) { + spec.Run(t, "Run", func(t *testing.T, when spec.G, it spec.S) { + var buffer *bytes.Buffer + var tokenExchanger tokenExchanger + var fakeEnv map[string]string + + var envGetter envGetter = func(envVarName string) (string, bool) { + value, present := fakeEnv[envVarName] + if !present { + return "", false + } + return value, true + } + + it.Before(func() { + buffer = new(bytes.Buffer) + fakeEnv = map[string]string{ + "PLACEHOLDER_NAME_TOKEN": "token from env", + "PLACEHOLDER_NAME_CA_BUNDLE": "ca bundle from env", + "PLACEHOLDER_NAME_K8S_API_ENDPOINT": "k8s api from env", + } + }) + + when("env vars are missing", func() { + it("returns an error when PLACEHOLDER_NAME_TOKEN is missing", func() { + fakeEnv = map[string]string{ + "PLACEHOLDER_NAME_K8S_API_ENDPOINT": "a", + "PLACEHOLDER_NAME_CA_BUNDLE": "b", + } + err := run(envGetter, tokenExchanger, buffer) + require.Error(t, err, "failed to login: environment variable not set: PLACEHOLDER_NAME_TOKEN") + }) + + it("returns an error when PLACEHOLDER_NAME_CA_BUNDLE is missing", func() { + fakeEnv = map[string]string{ + "PLACEHOLDER_NAME_K8S_API_ENDPOINT": "a", + "PLACEHOLDER_NAME_TOKEN": "b", + } + err := run(envGetter, tokenExchanger, buffer) + require.Error(t, err, "failed to login: environment variable not set: PLACEHOLDER_NAME_CA_BUNDLE") + }) + + it("returns an error when PLACEHOLDER_NAME_K8S_API_ENDPOINT is missing", func() { + fakeEnv = map[string]string{ + "PLACEHOLDER_NAME_TOKEN": "a", + "PLACEHOLDER_NAME_CA_BUNDLE": "b", + } + err := run(envGetter, tokenExchanger, buffer) + require.Error(t, err, "failed to login: environment variable not set: PLACEHOLDER_NAME_K8S_API_ENDPOINT") + }) + }, spec.Parallel()) + + when("the token exchange fails", func() { + it.Before(func() { + tokenExchanger = func(token, caBundle, apiEndpoint string) (*clientauthentication.ExecCredential, error) { + return nil, fmt.Errorf("some error") + } + }) + + it("returns an error", func() { + err := run(envGetter, tokenExchanger, buffer) + require.Error(t, err, "failed to login: some error") + }) + }, spec.Parallel()) + + when("the token exchange succeeds", func() { + var actualToken, actualCaBundle, actualAPIEndpoint string + + it.Before(func() { + tokenExchanger = func(token, caBundle, apiEndpoint string) (*clientauthentication.ExecCredential, error) { + actualToken, actualCaBundle, actualAPIEndpoint = token, caBundle, apiEndpoint + return &clientauthentication.ExecCredential{ + Status: &clientauthentication.ExecCredentialStatus{Token: "some token"}, + }, nil + } + }) + + it("writes the execCredential to the given writer", func() { + err := run(envGetter, tokenExchanger, buffer) + require.NoError(t, err) + require.Equal(t, fakeEnv["PLACEHOLDER_NAME_TOKEN"], actualToken) + require.Equal(t, fakeEnv["PLACEHOLDER_NAME_CA_BUNDLE"], actualCaBundle) + require.Equal(t, fakeEnv["PLACEHOLDER_NAME_K8S_API_ENDPOINT"], actualAPIEndpoint) + expected := `{ + "Spec": {"Interactive": false, "Response": null}, + "Status": {"ClientCertificateData": "", "ClientKeyData": "", "ExpirationTimestamp": null, "Token": "some token"} + }` + require.JSONEq(t, expected, buffer.String()) + }) + }, spec.Parallel()) + }, spec.Report(report.Terminal{})) +} diff --git a/go.mod b/go.mod index 0091acad..7764b378 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/golang/mock v1.4.3 github.com/golangci/golangci-lint v1.28.1 github.com/google/go-cmp v0.4.0 + github.com/sclevine/spec v1.4.0 github.com/spf13/cobra v1.0.0 github.com/stretchr/testify v1.6.1 github.com/suzerain-io/placeholder-name-api v0.0.0-20200724000517-dc602fd8d75e diff --git a/go.sum b/go.sum index 38f799c9..503ef603 100644 --- a/go.sum +++ b/go.sum @@ -467,6 +467,8 @@ github.com/ryancurrah/gomodguard v1.1.0/go.mod h1:4O8tr7hBODaGE6VIhfJDHcwzh5GUcc github.com/ryanrolds/sqlclosecheck v0.3.0 h1:AZx+Bixh8zdUBxUA1NxbxVAS78vTPq4rCb8OUZI9xFw= github.com/ryanrolds/sqlclosecheck v0.3.0/go.mod h1:1gREqxyTGR3lVtpngyFo3hZAgk0KCtEdgEkHwDbigdA= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sclevine/spec v1.4.0 h1:z/Q9idDcay5m5irkZ28M7PtQM4aOISzOpj4bUPkDee8= +github.com/sclevine/spec v1.4.0/go.mod h1:LvpgJaFyvQzRvc1kaDs0bulYwzC70PbiYjC4QnFHkOM= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/securego/gosec/v2 v2.3.0 h1:y/9mCF2WPDbSDpL3QDWZD3HHGrSYw0QSHnCqTfs4JPE= github.com/securego/gosec/v2 v2.3.0/go.mod h1:UzeVyUXbxukhLeHKV3VVqo7HdoQR9MrRfFmZYotn8ME= diff --git a/internal/constable/error.go b/internal/constable/error.go new file mode 100644 index 00000000..462d26a4 --- /dev/null +++ b/internal/constable/error.go @@ -0,0 +1,14 @@ +/* +Copyright 2020 VMware, Inc. +SPDX-License-Identifier: Apache-2.0 +*/ + +package constable + +var _ error = Error("") + +type Error string + +func (e Error) Error() string { + return string(e) +} diff --git a/pkg/client/client.go b/pkg/client/client.go new file mode 100644 index 00000000..c878771a --- /dev/null +++ b/pkg/client/client.go @@ -0,0 +1,15 @@ +/* +Copyright 2020 VMware, Inc. +SPDX-License-Identifier: Apache-2.0 +*/ + +package client + +import "k8s.io/client-go/pkg/apis/clientauthentication" + +func ExchangeToken(token, caBundle, apiEndpoint string) (*clientauthentication.ExecCredential, error) { + _ = token + _ = caBundle + _ = apiEndpoint + return nil, nil +} From a5dbc324f6d525b6dc29653b87ed2edaf496d194 Mon Sep 17 00:00:00 2001 From: Matt Moyer Date: Tue, 28 Jul 2020 08:37:27 -0500 Subject: [PATCH 03/12] Use the "Err*" idiomatic naming for error variables/consts. Signed-off-by: Matt Moyer --- cmd/placeholder-name/main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/placeholder-name/main.go b/cmd/placeholder-name/main.go index acd829fe..7d6afdb3 100644 --- a/cmd/placeholder-name/main.go +++ b/cmd/placeholder-name/main.go @@ -28,7 +28,7 @@ func main() { type envGetter func(string) (string, bool) type tokenExchanger func(token, caBundle, apiEndpoint string) (*clientauthentication.ExecCredential, error) -const EnvVarNotSetError = constable.Error("failed to login: environment variable not set") +const ErrMissingEnvVar = constable.Error("failed to login: environment variable not set") func run(envGetter envGetter, tokenExchanger tokenExchanger, outputWriter io.Writer) error { token, varExists := envGetter("PLACEHOLDER_NAME_TOKEN") @@ -60,5 +60,5 @@ func run(envGetter envGetter, tokenExchanger tokenExchanger, outputWriter io.Wri } func envVarNotSetError(varName string) error { - return fmt.Errorf("%w: %s", EnvVarNotSetError, varName) + return fmt.Errorf("%w: %s", ErrMissingEnvVar, varName) } From 1e8463ac2dc2efda037792155b0e6ce5fc39674b Mon Sep 17 00:00:00 2001 From: Matt Moyer Date: Tue, 28 Jul 2020 08:41:26 -0500 Subject: [PATCH 04/12] Use Go's favorite version of the word "marshal". Again, no idea why but this word has two commonly accepted spelling and Go code seems to very consistently use the one with one "l". Signed-off-by: Matt Moyer --- cmd/placeholder-name/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/placeholder-name/main.go b/cmd/placeholder-name/main.go index 7d6afdb3..07c13d02 100644 --- a/cmd/placeholder-name/main.go +++ b/cmd/placeholder-name/main.go @@ -53,7 +53,7 @@ func run(envGetter envGetter, tokenExchanger tokenExchanger, outputWriter io.Wri err = json.NewEncoder(outputWriter).Encode(execCredential) if err != nil { - return fmt.Errorf("failed to marshall response to stdout: %w", err) + return fmt.Errorf("failed to marshal response to stdout: %w", err) } return nil From ebe39c8663509e0058cc02fb3cff12f427ec23db Mon Sep 17 00:00:00 2001 From: Matt Moyer Date: Tue, 28 Jul 2020 08:42:25 -0500 Subject: [PATCH 05/12] Add a test for "failed to marshal response to stdout" error case. Signed-off-by: Matt Moyer --- cmd/placeholder-name/main_test.go | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/cmd/placeholder-name/main_test.go b/cmd/placeholder-name/main_test.go index 55320064..30adbe51 100644 --- a/cmd/placeholder-name/main_test.go +++ b/cmd/placeholder-name/main_test.go @@ -10,14 +10,16 @@ import ( "fmt" "testing" - "k8s.io/client-go/pkg/apis/clientauthentication" - - "github.com/stretchr/testify/require" - "github.com/sclevine/spec" "github.com/sclevine/spec/report" + "github.com/stretchr/testify/require" + "k8s.io/client-go/pkg/apis/clientauthentication" ) +type errWriter struct{ returnErr error } + +func (e *errWriter) Write([]byte) (int, error) { return 0, e.returnErr } + func TestRun(t *testing.T) { spec.Run(t, "Run", func(t *testing.T, when spec.G, it spec.S) { var buffer *bytes.Buffer @@ -83,6 +85,21 @@ func TestRun(t *testing.T) { }) }, spec.Parallel()) + when("the JSON encoder fails", func() { + it.Before(func() { + tokenExchanger = func(token, caBundle, apiEndpoint string) (*clientauthentication.ExecCredential, error) { + return &clientauthentication.ExecCredential{ + Status: &clientauthentication.ExecCredentialStatus{Token: "some token"}, + }, nil + } + }) + + it("returns an error", func() { + err := run(envGetter, tokenExchanger, &errWriter{returnErr: fmt.Errorf("some IO error")}) + require.EqualError(t, err, "failed to marshal response to stdout: some IO error") + }) + }, spec.Parallel()) + when("the token exchange succeeds", func() { var actualToken, actualCaBundle, actualAPIEndpoint string From 0ee4f0417dd26deb898970be43ade7e22d20fceb Mon Sep 17 00:00:00 2001 From: Matt Moyer Date: Tue, 28 Jul 2020 08:47:11 -0500 Subject: [PATCH 06/12] Use require.EqualError instead of require.Error. The type signatures of these methods make them easy to mix up. `require.Error()` asserts that there is any non-nil error -- the last parameter is an optional human-readable message to log when the assertion fails. `require.EqualError()` asserts that there is a non-nil error _and_ that when you call `err.Error()`, the string matches the expected value. It also takes an additional optional parameter to specify the log message. Signed-off-by: Matt Moyer --- cmd/placeholder-name/main_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/placeholder-name/main_test.go b/cmd/placeholder-name/main_test.go index 30adbe51..5a5eae33 100644 --- a/cmd/placeholder-name/main_test.go +++ b/cmd/placeholder-name/main_test.go @@ -50,7 +50,7 @@ func TestRun(t *testing.T) { "PLACEHOLDER_NAME_CA_BUNDLE": "b", } err := run(envGetter, tokenExchanger, buffer) - require.Error(t, err, "failed to login: environment variable not set: PLACEHOLDER_NAME_TOKEN") + require.EqualError(t, err, "failed to login: environment variable not set: PLACEHOLDER_NAME_TOKEN") }) it("returns an error when PLACEHOLDER_NAME_CA_BUNDLE is missing", func() { @@ -59,7 +59,7 @@ func TestRun(t *testing.T) { "PLACEHOLDER_NAME_TOKEN": "b", } err := run(envGetter, tokenExchanger, buffer) - require.Error(t, err, "failed to login: environment variable not set: PLACEHOLDER_NAME_CA_BUNDLE") + require.EqualError(t, err, "failed to login: environment variable not set: PLACEHOLDER_NAME_CA_BUNDLE") }) it("returns an error when PLACEHOLDER_NAME_K8S_API_ENDPOINT is missing", func() { @@ -68,7 +68,7 @@ func TestRun(t *testing.T) { "PLACEHOLDER_NAME_CA_BUNDLE": "b", } err := run(envGetter, tokenExchanger, buffer) - require.Error(t, err, "failed to login: environment variable not set: PLACEHOLDER_NAME_K8S_API_ENDPOINT") + require.EqualError(t, err, "failed to login: environment variable not set: PLACEHOLDER_NAME_K8S_API_ENDPOINT") }) }, spec.Parallel()) @@ -81,7 +81,7 @@ func TestRun(t *testing.T) { it("returns an error", func() { err := run(envGetter, tokenExchanger, buffer) - require.Error(t, err, "failed to login: some error") + require.EqualError(t, err, "failed to login: some error") }) }, spec.Parallel()) From 1a349bb609d173da7ae285ec13d0988234ac199b Mon Sep 17 00:00:00 2001 From: Matt Moyer Date: Tue, 28 Jul 2020 09:10:40 -0500 Subject: [PATCH 07/12] Add a context parameter so we can enforce a timeout for the token exchange. Signed-off-by: Matt Moyer --- cmd/placeholder-name/main.go | 13 ++++++---- cmd/placeholder-name/main_test.go | 40 ++++++++++++++++++++++++------- pkg/client/client.go | 12 ++++++---- 3 files changed, 47 insertions(+), 18 deletions(-) diff --git a/cmd/placeholder-name/main.go b/cmd/placeholder-name/main.go index 07c13d02..93466944 100644 --- a/cmd/placeholder-name/main.go +++ b/cmd/placeholder-name/main.go @@ -6,10 +6,12 @@ SPDX-License-Identifier: Apache-2.0 package main import ( + "context" "encoding/json" "fmt" "io" "os" + "time" "k8s.io/client-go/pkg/apis/clientauthentication" @@ -18,7 +20,7 @@ import ( ) func main() { - err := run(os.LookupEnv, client.ExchangeToken, os.Stdout) + err := run(os.LookupEnv, client.ExchangeToken, os.Stdout, 30*time.Second) if err != nil { _, _ = fmt.Fprintf(os.Stderr, "%s", err.Error()) os.Exit(1) @@ -26,11 +28,14 @@ func main() { } type envGetter func(string) (string, bool) -type tokenExchanger func(token, caBundle, apiEndpoint string) (*clientauthentication.ExecCredential, error) +type tokenExchanger func(ctx context.Context, token, caBundle, apiEndpoint string) (*clientauthentication.ExecCredential, error) const ErrMissingEnvVar = constable.Error("failed to login: environment variable not set") -func run(envGetter envGetter, tokenExchanger tokenExchanger, outputWriter io.Writer) error { +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("PLACEHOLDER_NAME_TOKEN") if !varExists { return envVarNotSetError("PLACEHOLDER_NAME_TOKEN") @@ -46,7 +51,7 @@ func run(envGetter envGetter, tokenExchanger tokenExchanger, outputWriter io.Wri return envVarNotSetError("PLACEHOLDER_NAME_K8S_API_ENDPOINT") } - execCredential, err := tokenExchanger(token, caBundle, apiEndpoint) + execCredential, err := tokenExchanger(ctx, token, caBundle, apiEndpoint) if err != nil { return fmt.Errorf("failed to login: %w", err) } diff --git a/cmd/placeholder-name/main_test.go b/cmd/placeholder-name/main_test.go index 5a5eae33..871119b6 100644 --- a/cmd/placeholder-name/main_test.go +++ b/cmd/placeholder-name/main_test.go @@ -7,8 +7,10 @@ package main import ( "bytes" + "context" "fmt" "testing" + "time" "github.com/sclevine/spec" "github.com/sclevine/spec/report" @@ -49,7 +51,7 @@ func TestRun(t *testing.T) { "PLACEHOLDER_NAME_K8S_API_ENDPOINT": "a", "PLACEHOLDER_NAME_CA_BUNDLE": "b", } - err := run(envGetter, tokenExchanger, buffer) + err := run(envGetter, tokenExchanger, buffer, 30*time.Second) require.EqualError(t, err, "failed to login: environment variable not set: PLACEHOLDER_NAME_TOKEN") }) @@ -58,7 +60,7 @@ func TestRun(t *testing.T) { "PLACEHOLDER_NAME_K8S_API_ENDPOINT": "a", "PLACEHOLDER_NAME_TOKEN": "b", } - err := run(envGetter, tokenExchanger, buffer) + err := run(envGetter, tokenExchanger, buffer, 30*time.Second) require.EqualError(t, err, "failed to login: environment variable not set: PLACEHOLDER_NAME_CA_BUNDLE") }) @@ -67,27 +69,27 @@ func TestRun(t *testing.T) { "PLACEHOLDER_NAME_TOKEN": "a", "PLACEHOLDER_NAME_CA_BUNDLE": "b", } - err := run(envGetter, tokenExchanger, buffer) + err := run(envGetter, tokenExchanger, buffer, 30*time.Second) require.EqualError(t, err, "failed to login: environment variable not set: PLACEHOLDER_NAME_K8S_API_ENDPOINT") }) }, spec.Parallel()) when("the token exchange fails", func() { it.Before(func() { - tokenExchanger = func(token, caBundle, apiEndpoint string) (*clientauthentication.ExecCredential, error) { + tokenExchanger = func(ctx context.Context, token, caBundle, apiEndpoint string) (*clientauthentication.ExecCredential, error) { return nil, fmt.Errorf("some error") } }) it("returns an error", func() { - err := run(envGetter, tokenExchanger, buffer) + err := run(envGetter, tokenExchanger, buffer, 30*time.Second) require.EqualError(t, err, "failed to login: some error") }) }, spec.Parallel()) when("the JSON encoder fails", func() { it.Before(func() { - tokenExchanger = func(token, caBundle, apiEndpoint string) (*clientauthentication.ExecCredential, error) { + tokenExchanger = func(ctx context.Context, token, caBundle, apiEndpoint string) (*clientauthentication.ExecCredential, error) { return &clientauthentication.ExecCredential{ Status: &clientauthentication.ExecCredentialStatus{Token: "some token"}, }, nil @@ -95,16 +97,36 @@ func TestRun(t *testing.T) { }) it("returns an error", func() { - err := run(envGetter, tokenExchanger, &errWriter{returnErr: fmt.Errorf("some IO error")}) + err := run(envGetter, tokenExchanger, &errWriter{returnErr: fmt.Errorf("some IO error")}, 30*time.Second) require.EqualError(t, err, "failed to marshal response to stdout: some IO error") }) }, spec.Parallel()) + when("the token exchange times out", func() { + it.Before(func() { + tokenExchanger = func(ctx context.Context, token, caBundle, apiEndpoint string) (*clientauthentication.ExecCredential, error) { + select { + case <-time.After(100 * time.Millisecond): + return &clientauthentication.ExecCredential{ + Status: &clientauthentication.ExecCredentialStatus{Token: "some token"}, + }, nil + case <-ctx.Done(): + return nil, ctx.Err() + } + } + }) + + it("returns an error", func() { + err := run(envGetter, tokenExchanger, buffer, 1*time.Millisecond) + require.EqualError(t, err, "failed to login: context deadline exceeded") + }) + }, spec.Parallel()) + when("the token exchange succeeds", func() { var actualToken, actualCaBundle, actualAPIEndpoint string it.Before(func() { - tokenExchanger = func(token, caBundle, apiEndpoint string) (*clientauthentication.ExecCredential, error) { + tokenExchanger = func(ctx context.Context, token, caBundle, apiEndpoint string) (*clientauthentication.ExecCredential, error) { actualToken, actualCaBundle, actualAPIEndpoint = token, caBundle, apiEndpoint return &clientauthentication.ExecCredential{ Status: &clientauthentication.ExecCredentialStatus{Token: "some token"}, @@ -113,7 +135,7 @@ func TestRun(t *testing.T) { }) it("writes the execCredential to the given writer", func() { - err := run(envGetter, tokenExchanger, buffer) + err := run(envGetter, tokenExchanger, buffer, 30*time.Second) require.NoError(t, err) require.Equal(t, fakeEnv["PLACEHOLDER_NAME_TOKEN"], actualToken) require.Equal(t, fakeEnv["PLACEHOLDER_NAME_CA_BUNDLE"], actualCaBundle) diff --git a/pkg/client/client.go b/pkg/client/client.go index c878771a..4f00eaea 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -5,11 +5,13 @@ SPDX-License-Identifier: Apache-2.0 package client -import "k8s.io/client-go/pkg/apis/clientauthentication" +import ( + "context" -func ExchangeToken(token, caBundle, apiEndpoint string) (*clientauthentication.ExecCredential, error) { - _ = token - _ = caBundle - _ = apiEndpoint + "k8s.io/client-go/pkg/apis/clientauthentication" +) + +func ExchangeToken(ctx context.Context, token, caBundle, apiEndpoint string) (*clientauthentication.ExecCredential, error) { + _, _, _, _ = ctx, token, caBundle, apiEndpoint return nil, nil } From b0d9db1bccbe5433f6610b6180e90924799733e0 Mon Sep 17 00:00:00 2001 From: Matt Moyer Date: Tue, 28 Jul 2020 10:27:07 -0500 Subject: [PATCH 08/12] Implement client.ExchangeToken. Signed-off-by: Matt Moyer --- pkg/client/client.go | 69 +++++++++++++++++++++- pkg/client/client_test.go | 121 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 188 insertions(+), 2 deletions(-) create mode 100644 pkg/client/client_test.go diff --git a/pkg/client/client.go b/pkg/client/client.go index 4f00eaea..b9f372f6 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -7,11 +7,76 @@ package client import ( "context" + "fmt" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/pkg/apis/clientauthentication" + "k8s.io/client-go/tools/clientcmd" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" + + placeholderv1alpha1 "github.com/suzerain-io/placeholder-name-api/pkg/apis/placeholder/v1alpha1" + placeholderclientset "github.com/suzerain-io/placeholder-name-client-go/pkg/generated/clientset/versioned" + "github.com/suzerain-io/placeholder-name/internal/constable" ) +// ErrLoginFailed is returned by ExchangeToken when the server rejects the login request. +const ErrLoginFailed = constable.Error("login failed") + func ExchangeToken(ctx context.Context, token, caBundle, apiEndpoint string) (*clientauthentication.ExecCredential, error) { - _, _, _, _ = ctx, token, caBundle, apiEndpoint - return nil, nil + clientset, err := getClient(apiEndpoint, caBundle) + if err != nil { + return nil, fmt.Errorf("could not get API client: %w", err) + } + + resp, err := clientset.PlaceholderV1alpha1().LoginRequests().Create(ctx, &placeholderv1alpha1.LoginRequest{ + Spec: placeholderv1alpha1.LoginRequestSpec{ + Type: placeholderv1alpha1.TokenLoginCredentialType, + Token: &placeholderv1alpha1.LoginRequestTokenCredential{ + Value: token, + }, + }, + }, metav1.CreateOptions{}) + if err != nil { + return nil, fmt.Errorf("could not login: %w", err) + } + if resp.Status.Credential == nil || resp.Status.Message != "" { + return nil, fmt.Errorf("%w: %s", ErrLoginFailed, resp.Status.Message) + } + + return &clientauthentication.ExecCredential{ + TypeMeta: metav1.TypeMeta{ + Kind: "ExecCredential", + APIVersion: "client.authentication.k8s.io/v1beta1", + }, + Status: &clientauthentication.ExecCredentialStatus{ + ExpirationTimestamp: resp.Status.Credential.ExpirationTimestamp, + ClientCertificateData: resp.Status.Credential.ClientCertificateData, + ClientKeyData: resp.Status.Credential.ClientKeyData, + }, + }, nil +} + +// getClient returns an anonymous clientset for the placeholder-name API at the provided endpoint/CA bundle. +func getClient(apiEndpoint string, caBundle string) (placeholderclientset.Interface, error) { + cfg, err := clientcmd.NewNonInteractiveClientConfig(clientcmdapi.Config{ + Clusters: map[string]*clientcmdapi.Cluster{ + "cluster": { + Server: apiEndpoint, + CertificateAuthorityData: []byte(caBundle), + }, + }, + Contexts: map[string]*clientcmdapi.Context{ + "current": { + Cluster: "cluster", + AuthInfo: "client", + }, + }, + AuthInfos: map[string]*clientcmdapi.AuthInfo{ + "client": {}, + }, + }, "current", nil, nil).ClientConfig() + if err != nil { + return nil, err + } + return placeholderclientset.NewForConfig(cfg) } diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go new file mode 100644 index 00000000..120025df --- /dev/null +++ b/pkg/client/client_test.go @@ -0,0 +1,121 @@ +package client + +import ( + "context" + "encoding/json" + "encoding/pem" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/pkg/apis/clientauthentication" + + placeholderv1alpha1 "github.com/suzerain-io/placeholder-name-api/pkg/apis/placeholder/v1alpha1" +) + +func startTestServer(t *testing.T, handler http.HandlerFunc) (string, string) { + t.Helper() + server := httptest.NewTLSServer(handler) + t.Cleanup(server.Close) + + caBundle := string(pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: server.TLS.Certificates[0].Certificate[0], + })) + return caBundle, server.URL +} + +func TestExchangeToken(t *testing.T) { + t.Parallel() + ctx := context.Background() + + t.Run("invalid configuration", func(t *testing.T) { + t.Parallel() + got, err := ExchangeToken(ctx, "", "", "") + require.EqualError(t, err, "could not get API client: invalid configuration: no configuration has been provided, try setting KUBERNETES_MASTER environment variable") + require.Nil(t, got) + }) + + t.Run("server error", func(t *testing.T) { + t.Parallel() + // Start a test server that returns only 500 errors. + caBundle, endpoint := startTestServer(t, func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte("some server error")) + }) + + got, err := ExchangeToken(ctx, "", caBundle, endpoint) + require.EqualError(t, err, `could not login: an error on the server ("some server error") has prevented the request from succeeding (post loginrequests.placeholder.suzerain-io.github.io)`) + require.Nil(t, got) + }) + + t.Run("login failure", func(t *testing.T) { + t.Parallel() + // Start a test server that returns success but with an error message + caBundle, endpoint := startTestServer(t, func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("content-type", "application/json") + _ = json.NewEncoder(w).Encode(&placeholderv1alpha1.LoginRequest{ + TypeMeta: metav1.TypeMeta{APIVersion: "placeholder.suzerain-io.github.io/v1alpha1", Kind: "LoginRequest"}, + Status: placeholderv1alpha1.LoginRequestStatus{Message: "some login failure"}, + }) + }) + + got, err := ExchangeToken(ctx, "", caBundle, endpoint) + require.EqualError(t, err, `login failed: some login failure`) + require.Nil(t, got) + }) + + t.Run("success", func(t *testing.T) { + t.Parallel() + + // Start a test server that returns successfully and asserts various properties of the request. + caBundle, endpoint := startTestServer(t, func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, "application/json", r.Header.Get("content-type")) + + body, err := ioutil.ReadAll(r.Body) + require.NoError(t, err) + require.JSONEq(t, + `{ + "kind": "LoginRequest", + "apiVersion": "placeholder.suzerain-io.github.io/v1alpha1", + "metadata": { + "creationTimestamp": null + }, + "spec": { + "type": "token", + "token": {} + }, + "status": {} + }`, + string(body), + ) + + w.Header().Set("content-type", "application/json") + _ = json.NewEncoder(w).Encode(&placeholderv1alpha1.LoginRequest{ + TypeMeta: metav1.TypeMeta{APIVersion: "placeholder.suzerain-io.github.io/v1alpha1", Kind: "LoginRequest"}, + Status: placeholderv1alpha1.LoginRequestStatus{ + Credential: &placeholderv1alpha1.LoginRequestCredential{ + ClientCertificateData: "test-certificate", + ClientKeyData: "test-key", + }, + }, + }) + }) + + got, err := ExchangeToken(ctx, "", caBundle, endpoint) + require.NoError(t, err) + require.Equal(t, &clientauthentication.ExecCredential{ + TypeMeta: metav1.TypeMeta{ + Kind: "ExecCredential", + APIVersion: "client.authentication.k8s.io/v1beta1", + }, + Status: &clientauthentication.ExecCredentialStatus{ + ClientCertificateData: "test-certificate", + ClientKeyData: "test-key", + }, + }, got) + }) +} From a15a106fd3d7c44a658b74a0b3ea16e86a54814e Mon Sep 17 00:00:00 2001 From: Matt Moyer Date: Tue, 28 Jul 2020 10:39:29 -0500 Subject: [PATCH 09/12] Add a trailing newline to our CLI error output. Signed-off-by: Matt Moyer --- cmd/placeholder-name/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/placeholder-name/main.go b/cmd/placeholder-name/main.go index 93466944..e6f5d881 100644 --- a/cmd/placeholder-name/main.go +++ b/cmd/placeholder-name/main.go @@ -22,7 +22,7 @@ import ( func main() { err := run(os.LookupEnv, client.ExchangeToken, os.Stdout, 30*time.Second) if err != nil { - _, _ = fmt.Fprintf(os.Stderr, "%s", err.Error()) + _, _ = fmt.Fprintf(os.Stderr, "%s\n", err.Error()) os.Exit(1) } } From 531954511b61f83f8a435d99d2a809839d09b9ca Mon Sep 17 00:00:00 2001 From: Matt Moyer Date: Tue, 28 Jul 2020 10:44:43 -0500 Subject: [PATCH 10/12] Extract a test library helper for ErrorWriter{}. Signed-off-by: Matt Moyer --- cmd/placeholder-name/main_test.go | 8 +++----- test/library/ioutil.go | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 5 deletions(-) create mode 100644 test/library/ioutil.go diff --git a/cmd/placeholder-name/main_test.go b/cmd/placeholder-name/main_test.go index 871119b6..1276d6fd 100644 --- a/cmd/placeholder-name/main_test.go +++ b/cmd/placeholder-name/main_test.go @@ -16,12 +16,10 @@ import ( "github.com/sclevine/spec/report" "github.com/stretchr/testify/require" "k8s.io/client-go/pkg/apis/clientauthentication" + + "github.com/suzerain-io/placeholder-name/test/library" ) -type errWriter struct{ returnErr error } - -func (e *errWriter) Write([]byte) (int, error) { return 0, e.returnErr } - func TestRun(t *testing.T) { spec.Run(t, "Run", func(t *testing.T, when spec.G, it spec.S) { var buffer *bytes.Buffer @@ -97,7 +95,7 @@ func TestRun(t *testing.T) { }) it("returns an error", func() { - err := run(envGetter, tokenExchanger, &errWriter{returnErr: fmt.Errorf("some IO error")}, 30*time.Second) + err := run(envGetter, tokenExchanger, &library.ErrorWriter{ReturnError: fmt.Errorf("some IO error")}, 30*time.Second) require.EqualError(t, err, "failed to marshal response to stdout: some IO error") }) }, spec.Parallel()) diff --git a/test/library/ioutil.go b/test/library/ioutil.go new file mode 100644 index 00000000..44bf24ca --- /dev/null +++ b/test/library/ioutil.go @@ -0,0 +1,17 @@ +/* +Copyright 2020 VMware, Inc. +SPDX-License-Identifier: Apache-2.0 +*/ + +package library + +import "io" + +// ErrorWriter implements io.Writer by returning a fixed error. +type ErrorWriter struct { + ReturnError error +} + +var _ io.Writer = &ErrorWriter{} + +func (e *ErrorWriter) Write([]byte) (int, error) { return 0, e.ReturnError } From bc4351f51af4ddc9cc2b821f208211bb54ee59c2 Mon Sep 17 00:00:00 2001 From: aram price Date: Tue, 28 Jul 2020 09:14:01 -0700 Subject: [PATCH 11/12] Add copyright, appease the linter --- pkg/client/client_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go index 120025df..251f56e9 100644 --- a/pkg/client/client_test.go +++ b/pkg/client/client_test.go @@ -1,3 +1,8 @@ +/* +Copyright 2020 VMware, Inc. +SPDX-License-Identifier: Apache-2.0 +*/ + package client import ( From 48433eb36b50e294cac024b85f74414bbc26d085 Mon Sep 17 00:00:00 2001 From: Matt Moyer Date: Tue, 28 Jul 2020 14:50:49 -0500 Subject: [PATCH 12/12] Add integration tests for the client package. Signed-off-by: Matt Moyer --- test/integration/client_test.go | 77 +++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 test/integration/client_test.go diff --git a/test/integration/client_test.go b/test/integration/client_test.go new file mode 100644 index 00000000..95f0fb7e --- /dev/null +++ b/test/integration/client_test.go @@ -0,0 +1,77 @@ +/* +Copyright 2020 VMware, Inc. +SPDX-License-Identifier: Apache-2.0 +*/ + +package integration + +import ( + "context" + "os" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/suzerain-io/placeholder-name/pkg/client" + "github.com/suzerain-io/placeholder-name/test/library" +) + +var ( + // Test certificate and private key that should get an authentication error. Generated with + // https://github.com/cloudflare/cfssl, like this: + // $ brew install cfssl + // $ cfssl print-defaults csr | cfssl genkey -initca - | cfssljson -bare ca + // $ cfssl print-defaults csr | cfssl gencert -ca ca.pem -ca-key ca-key.pem -hostname=testuser - | cfssljson -bare client + // $ cat client.pem client-key.pem + + testCert = strings.TrimSpace(` +-----BEGIN CERTIFICATE----- +MIICBDCCAaugAwIBAgIUeidKWlZQuoKfBGydObI1hMwzt9cwCgYIKoZIzj0EAwIw +SDELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1TYW4gRnJhbmNp +c2NvMRQwEgYDVQQDEwtleGFtcGxlLm5ldDAeFw0yMDA3MjgxOTI3MDBaFw0yMTA3 +MjgxOTI3MDBaMEgxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTEWMBQGA1UEBxMN +U2FuIEZyYW5jaXNjbzEUMBIGA1UEAxMLZXhhbXBsZS5uZXQwWTATBgcqhkjOPQIB +BggqhkjOPQMBBwNCAARk7XBC+OjYmrXOhm7RaJiHW4Q5VsE+iMV90Bzq7ansqAhb +04RI63Y7YPwu1aExutjLvnkWCrgf2ze8KB+8djUBo3MwcTAOBgNVHQ8BAf8EBAMC +BaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAw +HQYDVR0OBBYEFG0oZxV+LHUKfE4gQ67xfHJuGQ/4MBMGA1UdEQQMMAqCCHRlc3R1 +c2VyMAoGCCqGSM49BAMCA0cAMEQCIEwPZhPpYhYHndfTEsWOxnxzJkmhAcYIMCeJ +d9kyq/fPAiBNCJw1MCLT8LjNlyUZCfwI2zuI3e0w6vuau89oj2zvVA== +-----END CERTIFICATE----- + `) + + testKey = strings.TrimSpace(` +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIAqkBGGKTH5GzLx8XZLAHEFW2E8jT+jpy0p6w6MMR7DkoAoGCCqGSM49 +AwEHoUQDQgAEZO1wQvjo2Jq1zoZu0WiYh1uEOVbBPojFfdAc6u2p7KgIW9OESOt2 +O2D8LtWhMbrYy755Fgq4H9s3vCgfvHY1AQ== +-----END EC PRIVATE KEY----- + `) +) + +func TestClient(t *testing.T) { + tmcClusterToken := os.Getenv("PLACEHOLDER_NAME_TMC_CLUSTER_TOKEN") + require.NotEmptyf(t, tmcClusterToken, "must specify PLACEHOLDER_NAME_TMC_CLUSTER_TOKEN env var for integration tests") + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + // Use an invalid certificate/key to validate that the ServerVersion API fails like we assume. + invalidClient := library.NewClientsetWithConfig(t, library.NewClientConfigWithCertAndKey(t, testCert, testKey)) + _, err := invalidClient.Discovery().ServerVersion() + require.EqualError(t, err, "the server has asked for the client to provide credentials") + + // Using the CA bundle and host from the current (admin) kubeconfig, do the token exchange. + clientConfig := library.NewClientConfig(t) + resp, err := client.ExchangeToken(ctx, tmcClusterToken, string(clientConfig.CAData), clientConfig.Host) + require.NoError(t, err) + + // Create a client using the certificate and key returned by the token exchange. + validClient := library.NewClientsetWithConfig(t, library.NewClientConfigWithCertAndKey(t, resp.Status.ClientCertificateData, resp.Status.ClientKeyData)) + + // Make a version request, which should succeed even without any authorization. + _, err = validClient.Discovery().ServerVersion() + require.NoError(t, err) +}