Merge pull request #30 from cfryanr/new_cli

Create a client CLI command
This commit is contained in:
Matt Moyer 2020-07-28 15:29:13 -05:00 committed by GitHub
commit 271eb9b837
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 567 additions and 30 deletions

View File

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

View File

@ -0,0 +1,31 @@
/*
Copyright 2020 VMware, Inc.
SPDX-License-Identifier: Apache-2.0
*/
package main
import (
"os"
genericapiserver "k8s.io/apiserver/pkg/server"
"k8s.io/client-go/pkg/version"
"k8s.io/client-go/rest"
"k8s.io/component-base/logs"
"k8s.io/klog/v2"
"github.com/suzerain-io/placeholder-name/internal/server"
)
func main() {
logs.InitLogs()
defer logs.FlushLogs()
klog.Infof("Running %s at %#v", rest.DefaultKubernetesUserAgent(), version.Get())
ctx := genericapiserver.SetupSignalContext()
if err := server.New(ctx, os.Args[1:], os.Stdout, os.Stderr).Run(); err != nil {
klog.Fatal(err)
}
}

View File

@ -6,26 +6,64 @@ SPDX-License-Identifier: Apache-2.0
package main
import (
"context"
"encoding/json"
"fmt"
"io"
"os"
"time"
genericapiserver "k8s.io/apiserver/pkg/server"
"k8s.io/client-go/pkg/version"
"k8s.io/client-go/rest"
"k8s.io/component-base/logs"
"k8s.io/klog/v2"
"k8s.io/client-go/pkg/apis/clientauthentication"
"github.com/suzerain-io/placeholder-name/cmd/placeholder-name/app"
"github.com/suzerain-io/placeholder-name/internal/constable"
"github.com/suzerain-io/placeholder-name/pkg/client"
)
func main() {
logs.InitLogs()
defer logs.FlushLogs()
klog.Infof("Running %s at %#v", rest.DefaultKubernetesUserAgent(), version.Get())
ctx := genericapiserver.SetupSignalContext()
if err := app.New(ctx, os.Args[1:], os.Stdout, os.Stderr).Run(); err != nil {
klog.Fatal(err)
err := run(os.LookupEnv, client.ExchangeToken, os.Stdout, 30*time.Second)
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) (*clientauthentication.ExecCredential, error)
const ErrMissingEnvVar = constable.Error("failed to login: 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("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(ctx, 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 marshal response to stdout: %w", err)
}
return nil
}
func envVarNotSetError(varName string) error {
return fmt.Errorf("%w: %s", ErrMissingEnvVar, varName)
}

View File

@ -0,0 +1,149 @@
/*
Copyright 2020 VMware, Inc.
SPDX-License-Identifier: Apache-2.0
*/
package main
import (
"bytes"
"context"
"fmt"
"testing"
"time"
"github.com/sclevine/spec"
"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"
)
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, 30*time.Second)
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() {
fakeEnv = map[string]string{
"PLACEHOLDER_NAME_K8S_API_ENDPOINT": "a",
"PLACEHOLDER_NAME_TOKEN": "b",
}
err := run(envGetter, tokenExchanger, buffer, 30*time.Second)
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() {
fakeEnv = map[string]string{
"PLACEHOLDER_NAME_TOKEN": "a",
"PLACEHOLDER_NAME_CA_BUNDLE": "b",
}
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(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, 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(ctx context.Context, 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, &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())
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(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"},
}, nil
}
})
it("writes the execCredential to the given writer", func() {
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)
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{}))
}

View File

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

1
go.mod
View File

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

2
go.sum
View File

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

View File

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

View File

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

View File

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

82
pkg/client/client.go Normal file
View File

@ -0,0 +1,82 @@
/*
Copyright 2020 VMware, Inc.
SPDX-License-Identifier: Apache-2.0
*/
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) {
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)
}

126
pkg/client/client_test.go Normal file
View File

@ -0,0 +1,126 @@
/*
Copyright 2020 VMware, Inc.
SPDX-License-Identifier: Apache-2.0
*/
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)
})
}

View File

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

17
test/library/ioutil.go Normal file
View File

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