Make ./pkg/client
into an internal package using the native k8s client.
This should simplify our build/test setup quite a bit, since it means we have only a single module (at the top level) with all hand-written code. I'll leave `module.sh` alone for now but we may be able to simplify that a bit more. Signed-off-by: Matt Moyer <moyerm@vmware.com>
This commit is contained in:
parent
68893a1e15
commit
8f93fbb87b
@ -19,7 +19,6 @@ RUN printf "machine github.com\n\
|
|||||||
WORKDIR /work
|
WORKDIR /work
|
||||||
# Get dependencies first so they can be cached as a layer
|
# Get dependencies first so they can be cached as a layer
|
||||||
COPY go.* ./
|
COPY go.* ./
|
||||||
COPY pkg/client/go.* ./pkg/client/
|
|
||||||
COPY generated/1.19/apis/go.* ./generated/1.19/apis/
|
COPY generated/1.19/apis/go.* ./generated/1.19/apis/
|
||||||
COPY generated/1.19/client/go.* ./generated/1.19/client/
|
COPY generated/1.19/client/go.* ./generated/1.19/client/
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
@ -35,7 +34,6 @@ COPY hack ./hack
|
|||||||
# Build the executable binary (CGO_ENABLED=0 means static linking)
|
# Build the executable binary (CGO_ENABLED=0 means static linking)
|
||||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "$(hack/get-ldflags.sh)" -o out ./cmd/pinniped-server/...
|
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "$(hack/get-ldflags.sh)" -o out ./cmd/pinniped-server/...
|
||||||
|
|
||||||
|
|
||||||
# Use a runtime image based on Debian slim
|
# Use a runtime image based on Debian slim
|
||||||
FROM debian:10.5-slim
|
FROM debian:10.5-slim
|
||||||
|
|
||||||
|
@ -13,11 +13,10 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
||||||
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
||||||
|
|
||||||
|
"github.com/suzerain-io/pinniped/internal/client"
|
||||||
"github.com/suzerain-io/pinniped/internal/constable"
|
"github.com/suzerain-io/pinniped/internal/constable"
|
||||||
"github.com/suzerain-io/pinniped/pkg/client"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@ -29,7 +28,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type envGetter func(string) (string, bool)
|
type envGetter func(string) (string, bool)
|
||||||
type tokenExchanger func(ctx context.Context, token, caBundle, apiEndpoint string) (*client.Credential, error)
|
type tokenExchanger func(ctx context.Context, token, caBundle, apiEndpoint string) (*clientauthenticationv1beta1.ExecCredential, error)
|
||||||
|
|
||||||
const ErrMissingEnvVar = constable.Error("failed to get credential: environment variable not set")
|
const ErrMissingEnvVar = constable.Error("failed to get credential: environment variable not set")
|
||||||
|
|
||||||
@ -57,24 +56,7 @@ func run(envGetter envGetter, tokenExchanger tokenExchanger, outputWriter io.Wri
|
|||||||
return fmt.Errorf("failed to get credential: %w", err)
|
return fmt.Errorf("failed to get credential: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var expiration *metav1.Time
|
err = json.NewEncoder(outputWriter).Encode(cred)
|
||||||
if cred.ExpirationTimestamp != nil {
|
|
||||||
t := metav1.NewTime(*cred.ExpirationTimestamp)
|
|
||||||
expiration = &t
|
|
||||||
}
|
|
||||||
execCredential := clientauthenticationv1beta1.ExecCredential{
|
|
||||||
TypeMeta: metav1.TypeMeta{
|
|
||||||
Kind: "ExecCredential",
|
|
||||||
APIVersion: "client.authentication.k8s.io/v1beta1",
|
|
||||||
},
|
|
||||||
Status: &clientauthenticationv1beta1.ExecCredentialStatus{
|
|
||||||
ExpirationTimestamp: expiration,
|
|
||||||
Token: cred.Token,
|
|
||||||
ClientCertificateData: cred.ClientCertificateData,
|
|
||||||
ClientKeyData: cred.ClientKeyData,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
err = json.NewEncoder(outputWriter).Encode(execCredential)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to marshal response to stdout: %w", err)
|
return fmt.Errorf("failed to marshal response to stdout: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -12,13 +12,13 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/suzerain-io/pinniped/internal/testutil"
|
|
||||||
|
|
||||||
"github.com/sclevine/spec"
|
"github.com/sclevine/spec"
|
||||||
"github.com/sclevine/spec/report"
|
"github.com/sclevine/spec/report"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
||||||
|
|
||||||
"github.com/suzerain-io/pinniped/pkg/client"
|
"github.com/suzerain-io/pinniped/internal/testutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRun(t *testing.T) {
|
func TestRun(t *testing.T) {
|
||||||
@ -68,7 +68,7 @@ func TestRun(t *testing.T) {
|
|||||||
|
|
||||||
when("the token exchange fails", func() {
|
when("the token exchange fails", func() {
|
||||||
it.Before(func() {
|
it.Before(func() {
|
||||||
tokenExchanger = func(ctx context.Context, token, caBundle, apiEndpoint string) (*client.Credential, error) {
|
tokenExchanger = func(ctx context.Context, token, caBundle, apiEndpoint string) (*clientauthenticationv1beta1.ExecCredential, error) {
|
||||||
return nil, fmt.Errorf("some error")
|
return nil, fmt.Errorf("some error")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -81,8 +81,12 @@ func TestRun(t *testing.T) {
|
|||||||
|
|
||||||
when("the JSON encoder fails", func() {
|
when("the JSON encoder fails", func() {
|
||||||
it.Before(func() {
|
it.Before(func() {
|
||||||
tokenExchanger = func(ctx context.Context, token, caBundle, apiEndpoint string) (*client.Credential, error) {
|
tokenExchanger = func(ctx context.Context, token, caBundle, apiEndpoint string) (*clientauthenticationv1beta1.ExecCredential, error) {
|
||||||
return &client.Credential{Token: "some token"}, nil
|
return &clientauthenticationv1beta1.ExecCredential{
|
||||||
|
Status: &clientauthenticationv1beta1.ExecCredentialStatus{
|
||||||
|
Token: "some token",
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -94,10 +98,14 @@ func TestRun(t *testing.T) {
|
|||||||
|
|
||||||
when("the token exchange times out", func() {
|
when("the token exchange times out", func() {
|
||||||
it.Before(func() {
|
it.Before(func() {
|
||||||
tokenExchanger = func(ctx context.Context, token, caBundle, apiEndpoint string) (*client.Credential, error) {
|
tokenExchanger = func(ctx context.Context, token, caBundle, apiEndpoint string) (*clientauthenticationv1beta1.ExecCredential, error) {
|
||||||
select {
|
select {
|
||||||
case <-time.After(100 * time.Millisecond):
|
case <-time.After(100 * time.Millisecond):
|
||||||
return &client.Credential{Token: "some token"}, nil
|
return &clientauthenticationv1beta1.ExecCredential{
|
||||||
|
Status: &clientauthenticationv1beta1.ExecCredentialStatus{
|
||||||
|
Token: "some token",
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return nil, ctx.Err()
|
return nil, ctx.Err()
|
||||||
}
|
}
|
||||||
@ -114,14 +122,20 @@ func TestRun(t *testing.T) {
|
|||||||
var actualToken, actualCaBundle, actualAPIEndpoint string
|
var actualToken, actualCaBundle, actualAPIEndpoint string
|
||||||
|
|
||||||
it.Before(func() {
|
it.Before(func() {
|
||||||
tokenExchanger = func(ctx context.Context, token, caBundle, apiEndpoint string) (*client.Credential, error) {
|
tokenExchanger = func(ctx context.Context, token, caBundle, apiEndpoint string) (*clientauthenticationv1beta1.ExecCredential, error) {
|
||||||
actualToken, actualCaBundle, actualAPIEndpoint = token, caBundle, apiEndpoint
|
actualToken, actualCaBundle, actualAPIEndpoint = token, caBundle, apiEndpoint
|
||||||
now := time.Date(2020, 7, 29, 1, 2, 3, 0, time.UTC)
|
now := metav1.NewTime(time.Date(2020, 7, 29, 1, 2, 3, 0, time.UTC))
|
||||||
return &client.Credential{
|
return &clientauthenticationv1beta1.ExecCredential{
|
||||||
ExpirationTimestamp: &now,
|
TypeMeta: metav1.TypeMeta{
|
||||||
ClientCertificateData: "some certificate",
|
Kind: "ExecCredential",
|
||||||
ClientKeyData: "some key",
|
APIVersion: "client.authentication.k8s.io/v1beta1",
|
||||||
Token: "some token",
|
},
|
||||||
|
Status: &clientauthenticationv1beta1.ExecCredentialStatus{
|
||||||
|
ExpirationTimestamp: &now,
|
||||||
|
ClientCertificateData: "some certificate",
|
||||||
|
ClientKeyData: "some key",
|
||||||
|
Token: "some token",
|
||||||
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
2
go.mod
2
go.mod
@ -15,7 +15,6 @@ require (
|
|||||||
github.com/suzerain-io/controller-go v0.0.0-20200730212956-7f99b569ca9f
|
github.com/suzerain-io/controller-go v0.0.0-20200730212956-7f99b569ca9f
|
||||||
github.com/suzerain-io/pinniped/generated/1.19/apis v0.0.0-00010101000000-000000000000
|
github.com/suzerain-io/pinniped/generated/1.19/apis v0.0.0-00010101000000-000000000000
|
||||||
github.com/suzerain-io/pinniped/generated/1.19/client v0.0.0-00010101000000-000000000000
|
github.com/suzerain-io/pinniped/generated/1.19/client v0.0.0-00010101000000-000000000000
|
||||||
github.com/suzerain-io/pinniped/pkg/client v0.0.0-00010101000000-000000000000
|
|
||||||
k8s.io/api v0.19.0-rc.0
|
k8s.io/api v0.19.0-rc.0
|
||||||
k8s.io/apimachinery v0.19.0-rc.0
|
k8s.io/apimachinery v0.19.0-rc.0
|
||||||
k8s.io/apiserver v0.19.0-rc.0
|
k8s.io/apiserver v0.19.0-rc.0
|
||||||
@ -30,5 +29,4 @@ require (
|
|||||||
replace (
|
replace (
|
||||||
github.com/suzerain-io/pinniped/generated/1.19/apis => ./generated/1.19/apis
|
github.com/suzerain-io/pinniped/generated/1.19/apis => ./generated/1.19/apis
|
||||||
github.com/suzerain-io/pinniped/generated/1.19/client => ./generated/1.19/client
|
github.com/suzerain-io/pinniped/generated/1.19/client => ./generated/1.19/client
|
||||||
github.com/suzerain-io/pinniped/pkg/client => ./pkg/client
|
|
||||||
)
|
)
|
||||||
|
7
go.sum
7
go.sum
@ -5,6 +5,7 @@ cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6A
|
|||||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||||
|
cloud.google.com/go v0.51.0 h1:PvKAVQWCtlGUSlZkGW3QLelKaWq7KYv/MW1EboG8bfM=
|
||||||
cloud.google.com/go v0.51.0/go.mod h1:hWtGJ6gnXH+KgDv+V0zFGDvpi07n3z8ZNj3T1RW0Gcw=
|
cloud.google.com/go v0.51.0/go.mod h1:hWtGJ6gnXH+KgDv+V0zFGDvpi07n3z8ZNj3T1RW0Gcw=
|
||||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||||
@ -14,15 +15,21 @@ cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiy
|
|||||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
|
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
|
||||||
github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=
|
github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=
|
||||||
|
github.com/Azure/go-autorest/autorest v0.9.6 h1:5YWtOnckcudzIw8lPPBcWOnmIFWMtHci1ZWAZulMSx0=
|
||||||
github.com/Azure/go-autorest/autorest v0.9.6/go.mod h1:/FALq9T/kS7b5J5qsQ+RSTUdAmGFqi0vUdVNNx8q630=
|
github.com/Azure/go-autorest/autorest v0.9.6/go.mod h1:/FALq9T/kS7b5J5qsQ+RSTUdAmGFqi0vUdVNNx8q630=
|
||||||
github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0=
|
github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0=
|
||||||
|
github.com/Azure/go-autorest/autorest/adal v0.8.2 h1:O1X4oexUxnZCaEUGsvMnr8ZGj8HI37tNezwY4npRqA0=
|
||||||
github.com/Azure/go-autorest/autorest/adal v0.8.2/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q=
|
github.com/Azure/go-autorest/autorest/adal v0.8.2/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q=
|
||||||
github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA=
|
github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA=
|
||||||
|
github.com/Azure/go-autorest/autorest/date v0.2.0 h1:yW+Zlqf26583pE43KhfnhFcdmSWlm5Ew6bxipnr/tbM=
|
||||||
github.com/Azure/go-autorest/autorest/date v0.2.0/go.mod h1:vcORJHLJEh643/Ioh9+vPmf1Ij9AEBM5FuBIXLmIy0g=
|
github.com/Azure/go-autorest/autorest/date v0.2.0/go.mod h1:vcORJHLJEh643/Ioh9+vPmf1Ij9AEBM5FuBIXLmIy0g=
|
||||||
github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
|
github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
|
||||||
github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
|
github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
|
||||||
|
github.com/Azure/go-autorest/autorest/mocks v0.3.0 h1:qJumjCaCudz+OcqE9/XtEPfvtOjOmKaui4EOpFI6zZc=
|
||||||
github.com/Azure/go-autorest/autorest/mocks v0.3.0/go.mod h1:a8FDP3DYzQ4RYfVAxAN3SVSiiO77gL2j2ronKKP0syM=
|
github.com/Azure/go-autorest/autorest/mocks v0.3.0/go.mod h1:a8FDP3DYzQ4RYfVAxAN3SVSiiO77gL2j2ronKKP0syM=
|
||||||
|
github.com/Azure/go-autorest/logger v0.1.0 h1:ruG4BSDXONFRrZZJ2GUXDiUyVpayPmb1GnWeHDdaNKY=
|
||||||
github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc=
|
github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc=
|
||||||
|
github.com/Azure/go-autorest/tracing v0.5.0 h1:TRn4WjSnkcSy5AEG3pnbtFSwNtwzjr4VYyQflFE619k=
|
||||||
github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
|
github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
|
||||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
85
internal/client/client.go
Normal file
85
internal/client/client.go
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2020 VMware, Inc.
|
||||||
|
SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Package client is a wrapper for interacting with Pinniped's CredentialRequest API.
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
||||||
|
"k8s.io/client-go/tools/clientcmd"
|
||||||
|
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||||
|
|
||||||
|
"github.com/suzerain-io/pinniped/generated/1.19/apis/pinniped/v1alpha1"
|
||||||
|
"github.com/suzerain-io/pinniped/generated/1.19/client/clientset/versioned"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrLoginFailed is returned by ExchangeToken when the server rejects the login request.
|
||||||
|
var ErrLoginFailed = errors.New("login failed")
|
||||||
|
|
||||||
|
// ExchangeToken exchanges an opaque token using the Pinniped CredentialRequest API, returning a client-go ExecCredential valid on the target cluster.
|
||||||
|
func ExchangeToken(ctx context.Context, token string, caBundle string, apiEndpoint string) (*clientauthenticationv1beta1.ExecCredential, error) {
|
||||||
|
client, err := getClient(apiEndpoint, caBundle)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not get API client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.PinnipedV1alpha1().CredentialRequests().Create(ctx, &v1alpha1.CredentialRequest{
|
||||||
|
Spec: v1alpha1.CredentialRequestSpec{
|
||||||
|
Type: v1alpha1.TokenCredentialType,
|
||||||
|
Token: &v1alpha1.CredentialRequestTokenCredential{
|
||||||
|
Value: token,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, metav1.CreateOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not login: %w", err)
|
||||||
|
}
|
||||||
|
if resp.Status.Credential == nil || resp.Status.Message != nil {
|
||||||
|
return nil, fmt.Errorf("%w: %s", ErrLoginFailed, *resp.Status.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &clientauthenticationv1beta1.ExecCredential{
|
||||||
|
TypeMeta: metav1.TypeMeta{
|
||||||
|
Kind: "ExecCredential",
|
||||||
|
APIVersion: "client.authentication.k8s.io/v1beta1",
|
||||||
|
},
|
||||||
|
Status: &clientauthenticationv1beta1.ExecCredentialStatus{
|
||||||
|
ExpirationTimestamp: &resp.Status.Credential.ExpirationTimestamp,
|
||||||
|
ClientCertificateData: resp.Status.Credential.ClientCertificateData,
|
||||||
|
ClientKeyData: resp.Status.Credential.ClientKeyData,
|
||||||
|
Token: resp.Status.Credential.Token,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getClient returns an anonymous client for the Pinniped API at the provided endpoint/CA bundle.
|
||||||
|
func getClient(apiEndpoint string, caBundle string) (versioned.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", &clientcmd.ConfigOverrides{}, nil).ClientConfig()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return versioned.NewForConfig(cfg)
|
||||||
|
}
|
133
internal/client/client_test.go
Normal file
133
internal/client/client_test.go
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
/*
|
||||||
|
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"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
||||||
|
|
||||||
|
"github.com/suzerain-io/pinniped/generated/1.19/apis/pinniped/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 credentialrequests.pinniped.dev)`)
|
||||||
|
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
|
||||||
|
errorMessage := "some login failure"
|
||||||
|
caBundle, endpoint := startTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("content-type", "application/json")
|
||||||
|
_ = json.NewEncoder(w).Encode(&v1alpha1.CredentialRequest{
|
||||||
|
TypeMeta: metav1.TypeMeta{APIVersion: "pinniped.dev/v1alpha1", Kind: "CredentialRequest"},
|
||||||
|
Status: v1alpha1.CredentialRequestStatus{Message: &errorMessage},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
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()
|
||||||
|
expires := metav1.NewTime(time.Now().Truncate(time.Second))
|
||||||
|
|
||||||
|
// 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, http.MethodPost, r.Method)
|
||||||
|
require.Equal(t, "/apis/pinniped.dev/v1alpha1/credentialrequests", r.URL.Path)
|
||||||
|
require.Equal(t, "application/json", r.Header.Get("content-type"))
|
||||||
|
|
||||||
|
body, err := ioutil.ReadAll(r.Body)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.JSONEq(t,
|
||||||
|
`{
|
||||||
|
"kind": "CredentialRequest",
|
||||||
|
"apiVersion": "pinniped.dev/v1alpha1",
|
||||||
|
"metadata": {
|
||||||
|
"creationTimestamp": null
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"type": "token",
|
||||||
|
"token": {}
|
||||||
|
},
|
||||||
|
"status": {}
|
||||||
|
}`,
|
||||||
|
string(body),
|
||||||
|
)
|
||||||
|
|
||||||
|
w.Header().Set("content-type", "application/json")
|
||||||
|
_ = json.NewEncoder(w).Encode(&v1alpha1.CredentialRequest{
|
||||||
|
TypeMeta: metav1.TypeMeta{APIVersion: "pinniped.dev/v1alpha1", Kind: "CredentialRequest"},
|
||||||
|
Status: v1alpha1.CredentialRequestStatus{
|
||||||
|
Credential: &v1alpha1.CredentialRequestCredential{
|
||||||
|
ExpirationTimestamp: expires,
|
||||||
|
ClientCertificateData: "test-certificate",
|
||||||
|
ClientKeyData: "test-key",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
got, err := ExchangeToken(ctx, "", caBundle, endpoint)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, &clientauthenticationv1beta1.ExecCredential{
|
||||||
|
TypeMeta: metav1.TypeMeta{
|
||||||
|
Kind: "ExecCredential",
|
||||||
|
APIVersion: "client.authentication.k8s.io/v1beta1",
|
||||||
|
},
|
||||||
|
Status: &clientauthenticationv1beta1.ExecCredentialStatus{
|
||||||
|
ClientCertificateData: "test-certificate",
|
||||||
|
ClientKeyData: "test-key",
|
||||||
|
ExpirationTimestamp: &expires,
|
||||||
|
},
|
||||||
|
}, got)
|
||||||
|
})
|
||||||
|
}
|
@ -1,165 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2020 VMware, Inc.
|
|
||||||
SPDX-License-Identifier: Apache-2.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"crypto/x509"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"path/filepath"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// ErrCredentialRequestFailed is returned by ExchangeToken when the server rejects the credential request.
|
|
||||||
ErrCredentialRequestFailed = fmt.Errorf("credential request failed")
|
|
||||||
|
|
||||||
// ErrInvalidAPIEndpoint is returned by ExchangeToken when the provided API endpoint is invalid.
|
|
||||||
ErrInvalidAPIEndpoint = fmt.Errorf("invalid API endpoint")
|
|
||||||
|
|
||||||
// ErrInvalidCABundle is returned by ExchangeToken when the provided CA bundle is invalid.
|
|
||||||
ErrInvalidCABundle = fmt.Errorf("invalid CA bundle")
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// credentialRequestsAPIPath is the API path for the v1alpha1 CredentialRequest API.
|
|
||||||
credentialRequestsAPIPath = "/apis/pinniped.dev/v1alpha1/credentialrequests"
|
|
||||||
|
|
||||||
// userAgent is the user agent header value sent with requests.
|
|
||||||
userAgent = "pinniped"
|
|
||||||
)
|
|
||||||
|
|
||||||
func credentialRequest(ctx context.Context, apiEndpoint *url.URL, token string) (*http.Request, error) {
|
|
||||||
type CredentialRequestTokenCredential struct {
|
|
||||||
Value string `json:"value"`
|
|
||||||
}
|
|
||||||
type CredentialRequestSpec struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Token *CredentialRequestTokenCredential `json:"token"`
|
|
||||||
}
|
|
||||||
body := struct {
|
|
||||||
APIVersion string `json:"apiVersion"`
|
|
||||||
Kind string `json:"kind"`
|
|
||||||
Metadata struct {
|
|
||||||
CreationTimestamp *string `json:"creationTimestamp"`
|
|
||||||
} `json:"metadata"`
|
|
||||||
Spec CredentialRequestSpec `json:"spec"`
|
|
||||||
Status struct{} `json:"status"`
|
|
||||||
}{
|
|
||||||
APIVersion: "pinniped.dev/v1alpha1",
|
|
||||||
Kind: "CredentialRequest",
|
|
||||||
Spec: CredentialRequestSpec{Type: "token", Token: &CredentialRequestTokenCredential{Value: token}},
|
|
||||||
}
|
|
||||||
bodyJSON, err := json.Marshal(&body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, apiEndpoint.String(), bytes.NewReader(bodyJSON))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
req.Header.Set("User-Agent", userAgent)
|
|
||||||
return req, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Credential is the output of an ExchangeToken operation. It is equivalent to the data
|
|
||||||
// in the Kubernetes client.authentication.k8s.io/v1beta1 ExecCredentialStatus type.
|
|
||||||
type Credential struct {
|
|
||||||
// ExpirationTimestamp indicates a time when the provided credentials expire.
|
|
||||||
ExpirationTimestamp *time.Time
|
|
||||||
|
|
||||||
// Token is a bearer token used by the client for request authentication.
|
|
||||||
Token string
|
|
||||||
|
|
||||||
// PEM-encoded client TLS certificates (including intermediates, if any).
|
|
||||||
ClientCertificateData string
|
|
||||||
|
|
||||||
// PEM-encoded private key for the above certificate.
|
|
||||||
ClientKeyData string
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExchangeToken(ctx context.Context, token, caBundle, apiEndpoint string) (*Credential, error) {
|
|
||||||
// Parse and validate the provided API endpoint.
|
|
||||||
endpointURL, err := url.Parse(apiEndpoint)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%w: %s", ErrInvalidAPIEndpoint, err.Error())
|
|
||||||
}
|
|
||||||
if endpointURL.Scheme != "https" {
|
|
||||||
return nil, fmt.Errorf(`%w: protocol must be "https", not %q`, ErrInvalidAPIEndpoint, endpointURL.Scheme)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Form the CredentialRequest API URL by appending the API path to the main API endpoint.
|
|
||||||
pinnipedEndpointURL := *endpointURL
|
|
||||||
pinnipedEndpointURL.Path = filepath.Join(pinnipedEndpointURL.Path, credentialRequestsAPIPath)
|
|
||||||
|
|
||||||
// Initialize a TLS client configuration from the provided CA bundle.
|
|
||||||
tlsConfig := tls.Config{
|
|
||||||
MinVersion: tls.VersionTLS12,
|
|
||||||
RootCAs: x509.NewCertPool(),
|
|
||||||
}
|
|
||||||
if !tlsConfig.RootCAs.AppendCertsFromPEM([]byte(caBundle)) {
|
|
||||||
return nil, fmt.Errorf("%w: no certificates found", ErrInvalidCABundle)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a request object for the "POST /apis/pinniped.dev/v1alpha1/credentialrequests" request.
|
|
||||||
req, err := credentialRequest(ctx, &pinnipedEndpointURL, token)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("could not build request: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
client := http.Client{Transport: &http.Transport{TLSClientConfig: &tlsConfig}}
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("could not get credential: %w", err)
|
|
||||||
}
|
|
||||||
defer func() { _ = resp.Body.Close() }()
|
|
||||||
if resp.StatusCode != http.StatusCreated {
|
|
||||||
return nil, fmt.Errorf("%w: server returned status %d", ErrCredentialRequestFailed, resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
var respBody struct {
|
|
||||||
APIVersion string `json:"apiVersion"`
|
|
||||||
Kind string `json:"kind"`
|
|
||||||
Status struct {
|
|
||||||
Credential *struct {
|
|
||||||
ExpirationTimestamp string `json:"expirationTimestamp"`
|
|
||||||
Token string `json:"token"`
|
|
||||||
ClientCertificateData string `json:"clientCertificateData"`
|
|
||||||
ClientKeyData string `json:"clientKeyData"`
|
|
||||||
}
|
|
||||||
Message string `json:"message"`
|
|
||||||
} `json:"status"`
|
|
||||||
}
|
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&respBody); err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid credential response: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if respBody.Status.Credential == nil || respBody.Status.Message != "" {
|
|
||||||
return nil, fmt.Errorf("%w: %s", ErrCredentialRequestFailed, respBody.Status.Message)
|
|
||||||
}
|
|
||||||
|
|
||||||
result := Credential{
|
|
||||||
Token: respBody.Status.Credential.Token,
|
|
||||||
ClientCertificateData: respBody.Status.Credential.ClientCertificateData,
|
|
||||||
ClientKeyData: respBody.Status.Credential.ClientKeyData,
|
|
||||||
}
|
|
||||||
if str := respBody.Status.Credential.ExpirationTimestamp; str != "" {
|
|
||||||
expiration, err := time.Parse(time.RFC3339, str)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("invalid credential response: %w", err)
|
|
||||||
}
|
|
||||||
result.ExpirationTimestamp = &expiration
|
|
||||||
}
|
|
||||||
|
|
||||||
return &result, nil
|
|
||||||
}
|
|
@ -1,242 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2020 VMware, Inc.
|
|
||||||
SPDX-License-Identifier: Apache-2.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/pem"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
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()
|
|
||||||
for _, tt := range []struct {
|
|
||||||
name string
|
|
||||||
caBundle string
|
|
||||||
apiEndpoint string
|
|
||||||
wantErr string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "bad URL",
|
|
||||||
apiEndpoint: "%@Q$!",
|
|
||||||
wantErr: `invalid API endpoint: parse "%@Q$!": invalid URL escape "%@Q"`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "plain HTTP URL",
|
|
||||||
apiEndpoint: "http://example.com",
|
|
||||||
wantErr: `invalid API endpoint: protocol must be "https", not "http"`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "no CA certs",
|
|
||||||
apiEndpoint: "https://example.com",
|
|
||||||
caBundle: "",
|
|
||||||
wantErr: `invalid CA bundle: no certificates found`,
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
tt := tt
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
got, err := ExchangeToken(ctx, "", tt.caBundle, tt.apiEndpoint)
|
|
||||||
require.EqualError(t, err, tt.wantErr)
|
|
||||||
require.Nil(t, got)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("request creation failure", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
// Start a test server that doesn't do anything.
|
|
||||||
caBundle, endpoint := startTestServer(t, func(w http.ResponseWriter, r *http.Request) {})
|
|
||||||
|
|
||||||
//nolint:staticcheck // ignore "do not pass a nil Context" linter error since that's what we're testing here.
|
|
||||||
got, err := ExchangeToken(nil, "", caBundle, endpoint)
|
|
||||||
require.EqualError(t, err, `could not build request: net/http: nil Context`)
|
|
||||||
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, `credential request failed: server returned status 500`)
|
|
||||||
require.Nil(t, got)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("request failure", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
clientTimeout := 500 * time.Millisecond
|
|
||||||
|
|
||||||
// Start a test server that is slow to respond.
|
|
||||||
caBundle, endpoint := startTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.WriteHeader(http.StatusCreated)
|
|
||||||
time.Sleep(2 * clientTimeout)
|
|
||||||
_, _ = w.Write([]byte("slow response"))
|
|
||||||
})
|
|
||||||
|
|
||||||
// Make a request using short timeout.
|
|
||||||
ctx, cancel := context.WithTimeout(ctx, clientTimeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
got, err := ExchangeToken(ctx, "", caBundle, endpoint)
|
|
||||||
require.Error(t, err)
|
|
||||||
require.Contains(t, err.Error(), "context deadline exceeded")
|
|
||||||
require.Contains(t, err.Error(), "could not get credential:")
|
|
||||||
require.Nil(t, got)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("server invalid JSON", 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.StatusCreated)
|
|
||||||
_, _ = w.Write([]byte("not valid json"))
|
|
||||||
})
|
|
||||||
|
|
||||||
got, err := ExchangeToken(ctx, "", caBundle, endpoint)
|
|
||||||
require.EqualError(t, err, `invalid credential response: invalid character 'o' in literal null (expecting 'u')`)
|
|
||||||
require.Nil(t, got)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("credential request 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")
|
|
||||||
w.WriteHeader(http.StatusCreated)
|
|
||||||
_, _ = w.Write([]byte(`
|
|
||||||
{
|
|
||||||
"kind": "CredentialRequest",
|
|
||||||
"apiVersion": "pinniped.dev/v1alpha1",
|
|
||||||
"metadata": {
|
|
||||||
"creationTimestamp": null
|
|
||||||
},
|
|
||||||
"spec": {},
|
|
||||||
"status": {
|
|
||||||
"message": "some credential request failure"
|
|
||||||
}
|
|
||||||
}`))
|
|
||||||
})
|
|
||||||
|
|
||||||
got, err := ExchangeToken(ctx, "", caBundle, endpoint)
|
|
||||||
require.EqualError(t, err, `credential request failed: some credential request failure`)
|
|
||||||
require.Nil(t, got)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("invalid timestamp 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")
|
|
||||||
w.WriteHeader(http.StatusCreated)
|
|
||||||
_, _ = w.Write([]byte(`
|
|
||||||
{
|
|
||||||
"kind": "CredentialRequest",
|
|
||||||
"apiVersion": "pinniped.dev/v1alpha1",
|
|
||||||
"metadata": {
|
|
||||||
"creationTimestamp": null
|
|
||||||
},
|
|
||||||
"spec": {},
|
|
||||||
"status": {
|
|
||||||
"credential": {
|
|
||||||
"expirationTimestamp": "invalid"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`))
|
|
||||||
})
|
|
||||||
|
|
||||||
got, err := ExchangeToken(ctx, "", caBundle, endpoint)
|
|
||||||
require.EqualError(t, err, `invalid credential response: parsing time "invalid" as "2006-01-02T15:04:05Z07:00": cannot parse "invalid" as "2006"`)
|
|
||||||
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, http.MethodPost, r.Method)
|
|
||||||
require.Equal(t, "/apis/pinniped.dev/v1alpha1/credentialrequests", r.URL.Path)
|
|
||||||
require.Equal(t, "application/json", r.Header.Get("content-type"))
|
|
||||||
|
|
||||||
body, err := ioutil.ReadAll(r.Body)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.JSONEq(t,
|
|
||||||
`{
|
|
||||||
"kind": "CredentialRequest",
|
|
||||||
"apiVersion": "pinniped.dev/v1alpha1",
|
|
||||||
"metadata": {
|
|
||||||
"creationTimestamp": null
|
|
||||||
},
|
|
||||||
"spec": {
|
|
||||||
"type": "token",
|
|
||||||
"token": {
|
|
||||||
"value": "test-token"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"status": {}
|
|
||||||
}`,
|
|
||||||
string(body),
|
|
||||||
)
|
|
||||||
|
|
||||||
w.Header().Set("content-type", "application/json")
|
|
||||||
w.WriteHeader(http.StatusCreated)
|
|
||||||
_, _ = w.Write([]byte(`
|
|
||||||
{
|
|
||||||
"kind": "CredentialRequest",
|
|
||||||
"apiVersion": "pinniped.dev/v1alpha1",
|
|
||||||
"metadata": {
|
|
||||||
"creationTimestamp": null
|
|
||||||
},
|
|
||||||
"spec": {},
|
|
||||||
"status": {
|
|
||||||
"credential": {
|
|
||||||
"expirationTimestamp": "2020-07-30T15:52:01Z",
|
|
||||||
"token": "test-token",
|
|
||||||
"clientCertificateData": "test-certificate",
|
|
||||||
"clientKeyData": "test-key"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`))
|
|
||||||
})
|
|
||||||
|
|
||||||
got, err := ExchangeToken(ctx, "test-token", caBundle, endpoint)
|
|
||||||
require.NoError(t, err)
|
|
||||||
expires := time.Date(2020, 07, 30, 15, 52, 1, 0, time.UTC)
|
|
||||||
require.Equal(t, &Credential{
|
|
||||||
ExpirationTimestamp: &expires,
|
|
||||||
Token: "test-token",
|
|
||||||
ClientCertificateData: "test-certificate",
|
|
||||||
ClientKeyData: "test-key",
|
|
||||||
}, got)
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
module github.com/suzerain-io/pinniped/pkg/client
|
|
||||||
|
|
||||||
go 1.14
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
|
||||||
github.com/kr/text v0.2.0 // indirect
|
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
|
||||||
github.com/stretchr/testify v1.6.1
|
|
||||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
|
|
||||||
)
|
|
@ -1,22 +0,0 @@
|
|||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
|
||||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
||||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
@ -13,7 +13,7 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/suzerain-io/pinniped/pkg/client"
|
"github.com/suzerain-io/pinniped/internal/client"
|
||||||
"github.com/suzerain-io/pinniped/test/library"
|
"github.com/suzerain-io/pinniped/test/library"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -71,11 +71,11 @@ func TestClient(t *testing.T) {
|
|||||||
clientConfig := library.NewClientConfig(t)
|
clientConfig := library.NewClientConfig(t)
|
||||||
resp, err := client.ExchangeToken(ctx, tmcClusterToken, string(clientConfig.CAData), clientConfig.Host)
|
resp, err := client.ExchangeToken(ctx, tmcClusterToken, string(clientConfig.CAData), clientConfig.Host)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, resp.ExpirationTimestamp)
|
require.NotNil(t, resp.Status.ExpirationTimestamp)
|
||||||
require.InDelta(t, time.Until(*resp.ExpirationTimestamp), 1*time.Hour, float64(3*time.Minute))
|
require.InDelta(t, time.Until(resp.Status.ExpirationTimestamp.Time), 1*time.Hour, float64(3*time.Minute))
|
||||||
|
|
||||||
// Create a client using the certificate and key returned by the token exchange.
|
// Create a client using the certificate and key returned by the token exchange.
|
||||||
validClient := library.NewClientsetWithCertAndKey(t, resp.ClientCertificateData, resp.ClientKeyData)
|
validClient := library.NewClientsetWithCertAndKey(t, resp.Status.ClientCertificateData, resp.Status.ClientKeyData)
|
||||||
|
|
||||||
// Make a version request, which should succeed even without any authorization.
|
// Make a version request, which should succeed even without any authorization.
|
||||||
_, err = validClient.Discovery().ServerVersion()
|
_, err = validClient.Discovery().ServerVersion()
|
||||||
|
Loading…
Reference in New Issue
Block a user