Merge pull request #30 from cfryanr/new_cli
Create a client CLI command
This commit is contained in:
commit
271eb9b837
@ -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"]
|
||||
|
31
cmd/placeholder-name-server/main.go
Normal file
31
cmd/placeholder-name-server/main.go
Normal 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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
149
cmd/placeholder-name/main_test.go
Normal file
149
cmd/placeholder-name/main_test.go
Normal 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{}))
|
||||
}
|
@ -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
1
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
|
||||
|
2
go.sum
2
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=
|
||||
|
14
internal/constable/error.go
Normal file
14
internal/constable/error.go
Normal 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)
|
||||
}
|
@ -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 {
|
@ -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
82
pkg/client/client.go
Normal 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
126
pkg/client/client_test.go
Normal 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)
|
||||
})
|
||||
}
|
77
test/integration/client_test.go
Normal file
77
test/integration/client_test.go
Normal 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
17
test/library/ioutil.go
Normal 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 }
|
Loading…
Reference in New Issue
Block a user