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 tools ./tools
COPY hack ./hack COPY hack ./hack
# Build the executable binary # 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 FROM alpine:latest
# Install CA certs and some tools for debugging # Install CA certs and some tools for debugging
RUN apk --update --no-cache add ca-certificates bash curl RUN apk --update --no-cache add ca-certificates bash curl
WORKDIR /root/ WORKDIR /root/
# Copy the binary from the build-env stage # 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 # Document the port
EXPOSE 443 EXPOSE 443
# Set the command # 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 package main
import ( import (
"context"
"encoding/json"
"fmt"
"io"
"os" "os"
"time"
genericapiserver "k8s.io/apiserver/pkg/server" "k8s.io/client-go/pkg/apis/clientauthentication"
"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/cmd/placeholder-name/app" "github.com/suzerain-io/placeholder-name/internal/constable"
"github.com/suzerain-io/placeholder-name/pkg/client"
) )
func main() { func main() {
logs.InitLogs() err := run(os.LookupEnv, client.ExchangeToken, os.Stdout, 30*time.Second)
defer logs.FlushLogs() if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "%s\n", err.Error())
klog.Infof("Running %s at %#v", rest.DefaultKubernetesUserAgent(), version.Get()) os.Exit(1)
ctx := genericapiserver.SetupSignalContext()
if err := app.New(ctx, os.Args[1:], os.Stdout, os.Stderr).Run(); err != nil {
klog.Fatal(err)
} }
} }
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 #@ end
imagePullPolicy: IfNotPresent imagePullPolicy: IfNotPresent
command: command:
- ./placeholder-name - ./placeholder-name-server
args: args:
- --config=/etc/config/placeholder-name.yaml - --config=/etc/config/placeholder-name.yaml
- --downward-api-path=/etc/podinfo - --downward-api-path=/etc/podinfo

1
go.mod
View File

@ -7,6 +7,7 @@ require (
github.com/golang/mock v1.4.3 github.com/golang/mock v1.4.3
github.com/golangci/golangci-lint v1.28.1 github.com/golangci/golangci-lint v1.28.1
github.com/google/go-cmp v0.4.0 github.com/google/go-cmp v0.4.0
github.com/sclevine/spec v1.4.0
github.com/spf13/cobra v1.0.0 github.com/spf13/cobra v1.0.0
github.com/stretchr/testify v1.6.1 github.com/stretchr/testify v1.6.1
github.com/suzerain-io/placeholder-name-api v0.0.0-20200724000517-dc602fd8d75e 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 h1:AZx+Bixh8zdUBxUA1NxbxVAS78vTPq4rCb8OUZI9xFw=
github.com/ryanrolds/sqlclosecheck v0.3.0/go.mod h1:1gREqxyTGR3lVtpngyFo3hZAgk0KCtEdgEkHwDbigdA= 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/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/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 h1:y/9mCF2WPDbSDpL3QDWZD3HHGrSYw0QSHnCqTfs4JPE=
github.com/securego/gosec/v2 v2.3.0/go.mod h1:UzeVyUXbxukhLeHKV3VVqo7HdoQR9MrRfFmZYotn8ME= 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 SPDX-License-Identifier: Apache-2.0
*/ */
// Package app is the command line entry point for placeholder-name. // Package server is the command line entry point for placeholder-name-server.
package app package server
import ( import (
"context" "context"
@ -39,7 +39,7 @@ import (
"github.com/suzerain-io/placeholder-name/pkg/config" "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 { type App struct {
cmd *cobra.Command 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 a.recommendedOptions.Etcd = nil // turn off etcd storage because we don't need it yet
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: `placeholder-name`, Use: `placeholder-name-server`,
Long: `placeholder-name provides a generic API for mapping an external Long: `placeholder-name-server provides a generic API for mapping an external
credential from somewhere to an internal credential to be used for credential from somewhere to an internal credential to be used for
authenticating to the Kubernetes API.`, authenticating to the Kubernetes API.`,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {

View File

@ -3,7 +3,7 @@ Copyright 2020 VMware, Inc.
SPDX-License-Identifier: Apache-2.0 SPDX-License-Identifier: Apache-2.0
*/ */
package app package server
import ( import (
"bytes" "bytes"
@ -17,19 +17,19 @@ import (
) )
const knownGoodUsage = ` 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 credential from somewhere to an internal credential to be used for
authenticating to the Kubernetes API. authenticating to the Kubernetes API.
Usage: Usage:
placeholder-name [flags] placeholder-name-server [flags]
Flags: Flags:
--cluster-signing-cert-file string path to cluster signing certificate --cluster-signing-cert-file string path to cluster signing certificate
--cluster-signing-key-file string path to cluster signing private key --cluster-signing-key-file string path to cluster signing private key
-c, --config string path to configuration file (default "placeholder-name.yaml") -c, --config string path to configuration file (default "placeholder-name.yaml")
--downward-api-path string path to Downward API volume mount (default "/etc/podinfo") --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) --log-flush-frequency duration Maximum number of seconds between log flushes (default 5s)
` `
@ -52,7 +52,7 @@ func TestCommand(t *testing.T) {
{ {
name: "OneArgFails", name: "OneArgFails",
args: []string{"tuna"}, args: []string{"tuna"},
wantErr: `unknown command "tuna" for "placeholder-name"`, wantErr: `unknown command "tuna" for "placeholder-name-server"`,
}, },
{ {
name: "ShortConfigFlagSucceeds", name: "ShortConfigFlagSucceeds",
@ -68,7 +68,7 @@ func TestCommand(t *testing.T) {
"--config", "some/path/to/config.yaml", "--config", "some/path/to/config.yaml",
"tuna", "tuna",
}, },
wantErr: `unknown command "tuna" for "placeholder-name"`, wantErr: `unknown command "tuna" for "placeholder-name-server"`,
}, },
} }
for _, test := range tests { 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 }