Initial aggregated API server (#15)

Add initial aggregated API server (squashed from a bunch of commits).

Signed-off-by: Andrew Keesler <akeesler@vmware.com>
Signed-off-by: Aram Price <pricear@vmware.com>
Signed-off-by: Ryan Richard <richardry@vmware.com>
This commit is contained in:
Mo Khan 2020-07-23 11:05:21 -04:00 committed by GitHub
parent 23c1b32a02
commit 5fdc20886d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 906 additions and 152 deletions

View File

@ -25,8 +25,12 @@ WORKDIR /work
COPY go.mod .
COPY go.sum .
RUN go mod download
# Copy the source code
COPY . .
# Copy only the production source code to avoid cache misses when editing other files
COPY cmd ./cmd
COPY internal ./internal
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 ./...
@ -37,6 +41,6 @@ WORKDIR /root/
# Copy the binary from the build-env stage
COPY --from=build-env /work/out/placeholder-name placeholder-name
# Document the port
EXPOSE 8080
EXPOSE 443
# Set the command
CMD ["./placeholder-name"]

View File

@ -9,38 +9,40 @@ package app
import (
"context"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"errors"
"encoding/pem"
"fmt"
"io"
"log"
"net"
"net/http"
"time"
"github.com/spf13/cobra"
"golang.org/x/sync/errgroup"
"k8s.io/apiserver/pkg/server/dynamiccertificates"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/client-go/kubernetes"
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
restclient "k8s.io/client-go/rest"
apiregistrationv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1"
aggregationv1client "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset"
placeholderv1alpha1 "github.com/suzerain-io/placeholder-name-api/pkg/apis/placeholder/v1alpha1"
"github.com/suzerain-io/placeholder-name/internal/autoregistration"
"github.com/suzerain-io/placeholder-name/internal/certauthority"
"github.com/suzerain-io/placeholder-name/internal/downward"
"github.com/suzerain-io/placeholder-name/pkg/config"
"github.com/suzerain-io/placeholder-name/pkg/handlers"
)
// shutdownGracePeriod controls how long active connections are allowed to continue at shutdown.
const shutdownGracePeriod = 5 * time.Second
"k8s.io/apimachinery/pkg/runtime"
"github.com/spf13/cobra"
genericapiserver "k8s.io/apiserver/pkg/server"
genericoptions "k8s.io/apiserver/pkg/server/options"
"k8s.io/apiserver/plugin/pkg/authenticator/token/webhook"
"k8s.io/client-go/kubernetes"
corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
restclient "k8s.io/client-go/rest"
aggregationv1client "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset"
placeholderv1alpha1 "github.com/suzerain-io/placeholder-name-api/pkg/apis/placeholder/v1alpha1"
"github.com/suzerain-io/placeholder-name/pkg/apiserver"
"github.com/suzerain-io/placeholder-name/pkg/config"
)
// App is an object that represents the placeholder-name application.
type App struct {
@ -50,22 +52,23 @@ type App struct {
configPath string
downwardAPIPath string
// listen address for healthz serve
healthAddr string
// listen address for main serve
mainAddr string
// webhook authenticates tokens
webhook authenticator.Token
recommendedOptions *genericoptions.RecommendedOptions
}
// This is ignored for now because we turn off etcd storage below, but this is
// the right prefix in case we turn it back on.
const defaultEtcdPathPrefix = "/registry/" + placeholderv1alpha1.GroupName
// New constructs a new App with command line args, stdout and stderr.
func New(args []string, stdout, stderr io.Writer) *App {
func New(ctx context.Context, args []string, stdout, stderr io.Writer) *App {
a := &App{
healthAddr: ":8080",
mainAddr: ":443",
recommendedOptions: genericoptions.NewRecommendedOptions(
defaultEtcdPathPrefix,
apiserver.Codecs.LegacyCodec(placeholderv1alpha1.SchemeGroupVersion),
// TODO we should check to see if all the other default settings are acceptable for us
),
}
a.recommendedOptions.Etcd = nil // turn off etcd storage because we don't need it yet
cmd := &cobra.Command{
Use: `placeholder-name`,
@ -93,7 +96,8 @@ authenticating to the Kubernetes API.`,
if err != nil {
return fmt.Errorf("could not initialize Kubernetes client: %w", err)
}
return a.serve(context.Background(), k8s.CoreV1(), aggregation)
return a.run(ctx, k8s.CoreV1(), aggregation)
},
Args: cobra.NoArgs,
}
@ -126,23 +130,28 @@ func (a *App) Run() error {
return a.cmd.Execute()
}
func (a *App) serve(ctx context.Context, k8s corev1client.CoreV1Interface, aggregation aggregationv1client.Interface) error {
func (a *App) run(
ctx context.Context,
k8s corev1client.CoreV1Interface,
aggregation aggregationv1client.Interface,
) error {
cfg, err := config.FromPath(a.configPath)
if err != nil {
return fmt.Errorf("could not load config: %w", err)
}
webhook, err := config.NewWebhook(cfg.WebhookConfig)
webhookTokenAuthenticator, err := config.NewWebhook(cfg.WebhookConfig)
if err != nil {
return fmt.Errorf("could create webhook client: %w", err)
}
a.webhook = webhook
podinfo, err := downward.Load(a.downwardAPIPath)
if err != nil {
return fmt.Errorf("could not read pod metadata: %w", err)
}
// TODO use the postStart hook to generate certs?
ca, err := certauthority.New(pkix.Name{CommonName: "Placeholder CA"})
if err != nil {
return fmt.Errorf("could not initialize CA: %w", err)
@ -153,27 +162,26 @@ func (a *App) serve(ctx context.Context, k8s corev1client.CoreV1Interface, aggre
}
log.Printf("initialized CA bundle:\n%s", string(caBundle))
const serviceName = "placeholder-name-api"
cert, err := ca.Issue(
pkix.Name{CommonName: "Placeholder Server"},
[]string{"placeholder-serve"},
pkix.Name{CommonName: serviceName + "." + podinfo.Namespace + ".svc"},
[]string{},
24*365*time.Hour,
)
if err != nil {
return fmt.Errorf("could not issue serving certificate: %w", err)
}
// Start an errgroup to manage the lifetimes of the various listener goroutines.
eg, ctx := errgroup.WithContext(ctx)
// Dynamically register our v1alpha1 API service.
service := corev1.Service{
ObjectMeta: metav1.ObjectMeta{Name: "placeholder-name-api"},
ObjectMeta: metav1.ObjectMeta{Name: serviceName},
Spec: corev1.ServiceSpec{
Ports: []corev1.ServicePort{
{
Protocol: corev1.ProtocolTCP,
Port: 443,
TargetPort: intstr.IntOrString{IntVal: 443}, //TODO: parse this out of mainAddr
TargetPort: intstr.IntOrString{IntVal: 443},
},
},
Selector: podinfo.Labels,
@ -188,8 +196,8 @@ func (a *App) serve(ctx context.Context, k8s corev1client.CoreV1Interface, aggre
Group: placeholderv1alpha1.GroupName,
Version: placeholderv1alpha1.SchemeGroupVersion.Version,
CABundle: caBundle,
GroupPriorityMinimum: 2500,
VersionPriority: 10,
GroupPriorityMinimum: 2500, // TODO what is the right value? https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#apiservicespec-v1beta1-apiregistration-k8s-io
VersionPriority: 10, // TODO what is the right value? https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#apiservicespec-v1beta1-apiregistration-k8s-io
},
}
if err := autoregistration.Setup(ctx, autoregistration.SetupOptions{
@ -202,66 +210,38 @@ func (a *App) serve(ctx context.Context, k8s corev1client.CoreV1Interface, aggre
return fmt.Errorf("could not register API service: %w", err)
}
// Start healthz listener
eg.Go(func() error {
log.Printf("Starting healthz serve on %v", a.healthAddr)
server := http.Server{
BaseContext: func(_ net.Listener) context.Context { return ctx },
Addr: a.healthAddr,
Handler: handlers.New(),
}
return runGracefully(ctx, &server, eg, server.ListenAndServe)
})
// Start main service listener
eg.Go(func() error {
log.Printf("Starting main serve on %v", a.mainAddr)
server := http.Server{
BaseContext: func(_ net.Listener) context.Context { return ctx },
Addr: a.mainAddr,
TLSConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
Certificates: []tls.Certificate{*cert},
},
Handler: http.HandlerFunc(a.exampleHandler),
}
return runGracefully(ctx, &server, eg, func() error {
// Doc for ListenAndServeTLS says we can pass empty strings if we configured
// keypair for TLS in http.Server.TLSConfig.
return server.ListenAndServeTLS("", "")
})
})
if err := eg.Wait(); !errors.Is(err, http.ErrServerClosed) {
apiServerConfig, err := a.ConfigServer(cert, webhookTokenAuthenticator)
if err != nil {
return err
}
return nil
server, err := apiServerConfig.Complete().New()
if err != nil {
return fmt.Errorf("could not issue serving certificate: %w", err)
}
// exampleHandler is a stub to be replaced with our real server logic.
func (a *App) exampleHandler(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
defer cancel()
rsp, authenticated, err := a.webhook.AuthenticateToken(ctx, "")
log.Printf("token response: %+v", rsp)
log.Printf("token authenticated: %+v", authenticated)
log.Printf("token err: %+v", err)
_, _ = w.Write([]byte("hello world"))
return server.GenericAPIServer.PrepareRun().Run(ctx.Done())
}
// runGracefully runs an http.Server with graceful shutdown.
func runGracefully(ctx context.Context, srv *http.Server, eg *errgroup.Group, f func() error) error {
// Start the listener in a child goroutine.
eg.Go(f)
func (a *App) ConfigServer(cert *tls.Certificate, webhookTokenAuthenticator *webhook.WebhookTokenAuthenticator) (*apiserver.Config, error) {
provider, err := createStaticCertKeyProvider(cert)
if err != nil {
return nil, fmt.Errorf("could not create static cert key provider: %w", err)
}
a.recommendedOptions.SecureServing.ServerCert.GeneratedCert = provider
// If/when the context is canceled or times out, initiate shutting down the serve.
<-ctx.Done()
serverConfig := genericapiserver.NewRecommendedConfig(apiserver.Codecs)
if err := a.recommendedOptions.ApplyTo(serverConfig); err != nil {
return nil, err
}
shutdownCtx, cancel := context.WithTimeout(context.Background(), shutdownGracePeriod)
defer cancel()
return srv.Shutdown(shutdownCtx)
apiServerConfig := &apiserver.Config{
GenericConfig: serverConfig,
ExtraConfig: apiserver.ExtraConfig{
Webhook: webhookTokenAuthenticator,
},
}
return apiServerConfig, nil
}
// createProtoKubeConfig returns a copy of the input config with the ContentConfig set to use protobuf.
@ -273,3 +253,27 @@ func createProtoKubeConfig(kubeConfig *restclient.Config) *restclient.Config {
protoKubeConfig.ContentType = runtime.ContentTypeProtobuf
return protoKubeConfig
}
func createStaticCertKeyProvider(cert *tls.Certificate) (dynamiccertificates.CertKeyContentProvider, error) {
privateKeyDER, err := x509.MarshalPKCS8PrivateKey(cert.PrivateKey)
if err != nil {
return nil, fmt.Errorf("error marshalling private key: %w", err)
}
privateKeyPEM := pem.EncodeToMemory(&pem.Block{
Type: "PRIVATE KEY",
Headers: nil,
Bytes: privateKeyDER,
})
certChainPEM := make([]byte, 0)
for _, certFromChain := range cert.Certificate {
certPEMBytes := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Headers: nil,
Bytes: certFromChain,
})
certChainPEM = append(certChainPEM, certPEMBytes...)
}
return dynamiccertificates.NewStaticCertKeyContent("some-name???", certChainPEM, privateKeyPEM)
}

View File

@ -10,14 +10,10 @@ import (
"context"
"strings"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/spf13/cobra"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
corev1fake "k8s.io/client-go/kubernetes/fake"
aggregationv1fake "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/fake"
)
const knownGoodUsage = `
@ -32,6 +28,7 @@ Flags:
-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
--log-flush-frequency duration Maximum number of seconds between log flushes (default 5s)
`
func TestCommand(t *testing.T) {
@ -78,7 +75,7 @@ func TestCommand(t *testing.T) {
stdout := bytes.NewBuffer([]byte{})
stderr := bytes.NewBuffer([]byte{})
a := New(test.args, stdout, stderr)
a := New(context.Background(), test.args, stdout, stderr)
a.cmd.RunE = func(cmd *cobra.Command, args []string) error {
return nil
}
@ -89,45 +86,8 @@ func TestCommand(t *testing.T) {
require.NoError(t, err)
}
if test.wantStdout != "" {
require.Equal(t, strings.TrimSpace(test.wantStdout), strings.TrimSpace(stdout.String()))
require.Equal(t, strings.TrimSpace(test.wantStdout), strings.TrimSpace(stdout.String()), cmp.Diff(test.wantStdout, stdout.String()))
}
})
}
}
func TestServeApp(t *testing.T) {
t.Parallel()
fakev1 := corev1fake.NewSimpleClientset(&corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "test-namespace"}})
fakeaggregationv1 := aggregationv1fake.NewSimpleClientset()
t.Run("success", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
cancel()
a := App{
healthAddr: "127.0.0.1:0",
mainAddr: "127.0.0.1:8443",
configPath: "testdata/valid-config.yaml",
downwardAPIPath: "testdata/podinfo",
}
err := a.serve(ctx, fakev1.CoreV1(), fakeaggregationv1)
require.NoError(t, err)
})
t.Run("failure", func(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
a := App{
healthAddr: "127.0.0.1:8081",
mainAddr: "127.0.0.1:8081",
configPath: "testdata/valid-config.yaml",
downwardAPIPath: "testdata/podinfo",
}
err := a.serve(ctx, fakev1.CoreV1(), fakeaggregationv1)
require.EqualError(t, err, "listen tcp 127.0.0.1:8081: bind: address already in use")
})
}

View File

@ -8,16 +8,24 @@ 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/cmd/placeholder-name/app"
)
func main() {
logs.InitLogs()
defer logs.FlushLogs()
klog.Infof("Running %s at %#v", rest.DefaultKubernetesUserAgent(), version.Get())
if err := app.New(os.Args[1:], os.Stdout, os.Stderr).Run(); err != nil {
os.Exit(1)
ctx := genericapiserver.SetupSignalContext()
if err := app.New(ctx, os.Args[1:], os.Stdout, os.Stderr).Run(); err != nil {
klog.Fatal(err)
}
}

View File

@ -23,11 +23,14 @@ metadata:
app: #@ data.values.app_name
data:
#@yaml/text-templated-strings
placeholder-config.yaml: |
placeholder-name.yaml: |
webhook:
url: (@= data.values.webhook_url @)
caBundle: (@= data.values.webhook_ca_bundle @)
---
#! TODO set up healthy, ready, etc. probes correctly for our deployment
#! TODO set the priority-critical-urgent on our deployment to ask kube to never let it die
#! TODO set resource minimums (e.g. 512MB RAM) on the deployment to make sure we get scheduled onto a reasonable node
apiVersion: apps/v1
kind: Deployment
metadata:
@ -36,7 +39,7 @@ metadata:
labels:
app: #@ data.values.app_name
spec:
replicas: 1
replicas: 1 #! TODO more than one replica for high availability, and share the same serving certificate among them (maybe using client-go leader election)
selector:
matchLabels:
app: #@ data.values.app_name
@ -57,7 +60,7 @@ spec:
command:
- ./placeholder-name
args:
- --config=/etc/config/placeholder-config.yaml
- --config=/etc/config/placeholder-name.yaml
- --downward-api-path=/etc/podinfo
volumeMounts:
- name: config-volume

View File

@ -12,6 +12,9 @@ rules:
- apiGroups: [apiregistration.k8s.io]
resources: [apiservices]
verbs: [create, get, list, patch, update, watch]
- apiGroups: [admissionregistration.k8s.io]
resources: [validatingwebhookconfigurations, mutatingwebhookconfigurations]
verbs: [get, list, watch]
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
@ -49,3 +52,59 @@ roleRef:
kind: Role
name: #@ data.values.app_name + "-aggregated-api-server-role"
apiGroup: rbac.authorization.k8s.io
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: #@ data.values.app_name + "-loginrequests-cluster-role"
rules:
- apiGroups: [placeholder.suzerain-io.github.io]
resources: [loginrequests]
verbs: [create]
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: #@ data.values.app_name + "-loginrequests-cluster-role-binding"
subjects:
#! both authenticated and unauthenticated requests (i.e. all requests) should be allowed
- kind: Group
name: system:authenticated
apiGroup: rbac.authorization.k8s.io
- kind: Group
name: system:unauthenticated
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: #@ data.values.app_name + "-loginrequests-cluster-role"
apiGroup: rbac.authorization.k8s.io
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: #@ data.values.app_name + "-service-account-cluster-role-binding"
namespace: #@ data.values.namespace
subjects:
- kind: ServiceAccount
name: #@ data.values.app_name + "-service-account"
namespace: #@ data.values.namespace
roleRef:
kind: ClusterRole
#! give permissions for subjectaccessreviews, tokenreview that is needed by aggregated api servers
name: system:auth-delegator
apiGroup: rbac.authorization.k8s.io
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: #@ data.values.app_name + "-extension-apiserver-authentication-reader-role-binding"
namespace: kube-system
subjects:
- kind: ServiceAccount
name: #@ data.values.app_name + "-service-account"
namespace: #@ data.values.namespace
roleRef:
kind: Role
#! give permissions for a special configmap of CA bundles that is needed by aggregated api servers
name: extension-apiserver-authentication-reader
apiGroup: rbac.authorization.k8s.io

4
go.mod
View File

@ -5,15 +5,17 @@ go 1.14
require (
github.com/davecgh/go-spew v1.1.1
github.com/golangci/golangci-lint v1.28.1
github.com/google/go-cmp v0.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-20200714184318-8ad91581433a
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208
github.com/suzerain-io/placeholder-name-client-go v0.0.0-20200714203950-a414963b4f95
golang.org/x/tools v0.0.0-20200707134715-9e0a013e855f // indirect
k8s.io/api v0.19.0-rc.0
k8s.io/apimachinery v0.19.0-rc.0
k8s.io/apiserver v0.19.0-rc.0
k8s.io/client-go v0.19.0-rc.0
k8s.io/component-base v0.19.0-rc.0
k8s.io/klog/v2 v2.2.0
k8s.io/kube-aggregator v0.19.0-rc.0
k8s.io/utils v0.0.0-20200619165400-6e3d28b6ed19

36
go.sum
View File

@ -31,13 +31,16 @@ github.com/Djarvur/go-err113 v0.0.0-20200511133814-5174e21577d5 h1:XTrzB+F8+SpRm
github.com/Djarvur/go-err113 v0.0.0-20200511133814-5174e21577d5/go.mod h1:4UJr5HIiMZrwgkSPdsjy2uOQExX/WEILpIrO9UPGuXs=
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46 h1:lsxEuwrXEAokXB9qhlbKWPpo3KMLZQ5WB5WLQRW1uq0=
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/OpenPeeDeeP/depguard v1.0.1 h1:VlW4R6jmBIv3/u1JNlawEvJMM4J+dPORPaZasQee8Us=
github.com/OpenPeeDeeP/depguard v1.0.1/go.mod h1:xsIw86fROiiwelg+jB2uM9PiKihMMmUx/1V+TNhjQvM=
github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
@ -67,16 +70,20 @@ github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWR
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa h1:OaNxuTZr7kxeODyLWsRMC+OD03aFUH+mW6r2d+MWa5Y=
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e h1:Wf6HqHfScWJN9/ZjdUKyjop4mf3Qdd+1TvvltAvM3m8=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f h1:lBNOc5arjvs8E5mO2tbpBpLoyyu8B6e44T7hJy6potg=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
@ -86,14 +93,17 @@ 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/denis-tingajkin/go-header v0.3.1 h1:ymEpSiFjeItCy1FOP+x0M2KdCELdEAHUsNa8F+hHc6w=
github.com/denis-tingajkin/go-header v0.3.1/go.mod h1:sq/2IxMhaZX+RRcgHfCRx/m0M5na0fBt4/CRe7Lrji0=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
github.com/emicklei/go-restful v2.9.5+incompatible h1:spTtZBk5DYEvbxMVutUuTyh1Ao2r4iyvLdACqsl/Ljk=
github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
@ -122,14 +132,18 @@ github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTg
github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0=
github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg=
github.com/go-openapi/jsonpointer v0.19.3 h1:gihV7YNZK1iK6Tgwwsxo2rJbD1GTbdm72325Bq8FI3w=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg=
github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc=
github.com/go-openapi/jsonreference v0.19.3 h1:5cxNfTy0UVC3X8JL5ymxzyoUZmo8iZb+jeTWn7tUa8o=
github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8=
github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc=
github.com/go-openapi/spec v0.19.3 h1:0XRyw8kguri6Yw4SxhsQA/atC88yqrk0+G4YhI2wabc=
github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo=
github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=
github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
@ -218,6 +232,7 @@ github.com/golangci/revgrep v0.0.0-20180526074752-d9c87f5ffaf0/go.mod h1:qOQCunE
github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 h1:zwtduBRr5SSWhqsYNgcuWO2kFlpdOZbP0+yRjmvPGys=
github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4/go.mod h1:Izgrg8RkN3rCIMLGE9CyYmU9pY2Jer6DgANEnZ/L/cQ=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@ -244,15 +259,19 @@ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGa
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE=
github.com/gostaticanalysis/analysisutil v0.0.3 h1:iwp+5/UAyzQSFgQ4uR2sni99sJ8Eo9DEacKWM5pekIg=
github.com/gostaticanalysis/analysisutil v0.0.3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4 h1:z53tR0945TRRQO/fLEVPI6SMv7ZflF0TEaTAoU7tOzg=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/grpc-ecosystem/grpc-gateway v1.9.5 h1:UImYN5qQ8tuGpGE16ZmjvcTtTw24zw1QAp/SlnNrZhI=
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
@ -289,6 +308,7 @@ github.com/jirfag/go-printf-func-name v0.0.0-20191110105641-45db9963cdd3 h1:jNYP
github.com/jirfag/go-printf-func-name v0.0.0-20191110105641-45db9963cdd3/go.mod h1:HEWGJkRDzjJY2sqdDwxccsGicWEf9BQOZsq2tV+xzM0=
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
github.com/jmoiron/sqlx v1.2.1-0.20190826204134-d7d95172beb5/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
@ -327,6 +347,7 @@ github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czP
github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.0 h1:aizVhC/NAAcKWb+5QsU1iNOZb4Yws5UO2I+aIprQITM=
github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
github.com/maratori/testpackage v1.0.1 h1:QtJ5ZjqapShm0w5DosRjg0PRlSdAdlx+W6cCKoALdbQ=
github.com/maratori/testpackage v1.0.1/go.mod h1:ddKdw+XG0Phzhx8BFDTKgpWP4i7MpApTE5fXSKAqwDU=
@ -369,6 +390,7 @@ github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mozilla/tls-observatory v0.0.0-20200317151703-4fa42e1c2dee/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk=
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
@ -460,6 +482,7 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykE
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4 h1:0HKaf1o97UwFjHH9o5XsHUOF+tqmdA7KEzXLpiyaw0E=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/sonatard/noctx v0.0.1 h1:VC1Qhl6Oxx9vvWo3UDgrGXYCeKCe3Wbw7qAWL6FrmTY=
github.com/sonatard/noctx v0.0.1/go.mod h1:9D2D/EoULe8Yy2joDHJj7bv3sZoq9AaSb8B4lqBjiZI=
@ -498,6 +521,8 @@ github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/suzerain-io/placeholder-name-api v0.0.0-20200714184318-8ad91581433a h1:ycG6TufZM7ZDrgBklkXEMauvSyh44JQDvSUdJawAjGA=
github.com/suzerain-io/placeholder-name-api v0.0.0-20200714184318-8ad91581433a/go.mod h1:bNHheAnmAISdW/ZYTnhCmg8QQKwA5WD64ZvPdsTrWjw=
github.com/suzerain-io/placeholder-name-client-go v0.0.0-20200714203950-a414963b4f95 h1:O1aoszdMJEekLXD7pNWP23vq0g8eXLp40AQgY8Hj+Sw=
github.com/suzerain-io/placeholder-name-client-go v0.0.0-20200714203950-a414963b4f95/go.mod h1:NwirXEgd1VaA+6R0W7PYL/ae6wfPT3vA+tu7POAArjU=
github.com/tdakkota/asciicheck v0.0.0-20200416190851-d7f85be797a2 h1:Xr9gkxfOP0KQWXKNqmwe8vEeSUiUj4Rlee9CMVX2ZUQ=
github.com/tdakkota/asciicheck v0.0.0-20200416190851-d7f85be797a2/go.mod h1:yHp0ai0Z9gUljN3o0xMhYJnH/IcvkdTBOX2fmJ93JEM=
github.com/tetafro/godot v0.4.2 h1:Dib7un+rYJFUi8vN0Bk6EHheKy6fv6ZzFURHw75g6m8=
@ -505,6 +530,7 @@ github.com/tetafro/godot v0.4.2/go.mod h1:/7NLHhv08H1+8DNj0MElpAACw1ajsCuf3TKNQx
github.com/timakin/bodyclose v0.0.0-20190930140734-f7f2e9bca95e h1:RumXZ56IrCj4CL+g1b9OL/oH0QnsF976bC8xQFYUD5Q=
github.com/timakin/bodyclose v0.0.0-20190930140734-f7f2e9bca95e/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk=
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5 h1:LnC5Kc/wtumK+WB441p7ynQJzVuNRJiqddSIE3IlSEQ=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/tommy-muehle/go-mnd v1.3.1-0.20200224220436-e6f9a994e8fa h1:RC4maTWLKKwb7p1cnoygsbKIgNlJqSYBeAFON3Ar8As=
github.com/tommy-muehle/go-mnd v1.3.1-0.20200224220436-e6f9a994e8fa/go.mod h1:dSUh0FtTP8VhvkL1S+gUR1OKd9ZnSaozuI6r3m6wOig=
@ -520,6 +546,7 @@ github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyC
github.com/valyala/fasthttp v1.12.0/go.mod h1:229t1eWu9UXTPmoUkbpN/fctKPBY4IJoFXQnxHGXy6E=
github.com/valyala/quicktemplate v1.5.0/go.mod h1:v7yYWpBEiutDyNfVaph6oC/yKwejzVyTX/2cwwHxyok=
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@ -527,14 +554,19 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0=
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
go.etcd.io/etcd v0.5.0-alpha.5.0.20200520232829-54ba9589114f h1:pBCD+Z7cy5WPTq+R6MmJJvDRpn88cp7bmTypBsn91g4=
go.etcd.io/etcd v0.5.0-alpha.5.0.20200520232829-54ba9589114f/go.mod h1:skWido08r9w6Lq/w70DO5XYIKMu4QFu1+4VsqLQuJy8=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@ -650,6 +682,8 @@ golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxb
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e h1:EHBhcS0mlXEAVwNyO2dLfjToGsyY4j24pTs2ScHnX7s=
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -759,6 +793,7 @@ gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
@ -801,6 +836,7 @@ k8s.io/klog/v2 v2.2.0 h1:XRvcwJozkgZ1UQJmfMGpvRthQHOvihEhYtDfAaxMz/A=
k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
k8s.io/kube-aggregator v0.19.0-rc.0 h1:+u9y1c0R2GF8fuaEnlJrdUtxoEmQOON98oatycSquOA=
k8s.io/kube-aggregator v0.19.0-rc.0/go.mod h1:DCq8Korz9XUEZVsq0wAGIAyJW79xdcYhIBtvWNTsTkc=
k8s.io/kube-openapi v0.0.0-20200427153329-656914f816f9 h1:5NC2ITmvg8RoxoH0wgmL4zn4VZqXGsKbxrikjaQx6s4=
k8s.io/kube-openapi v0.0.0-20200427153329-656914f816f9/go.mod h1:bfCVj+qXcEaE5SCvzBaqpOySr6tuCcpPKqF6HD8nyCw=
k8s.io/kube-openapi v0.0.0-20200615155156-dffdd1682719 h1:n/ElZyI1dzFPXKS8nZMw8wozBUz7vEfL0Ja7jN2rSvA=
k8s.io/kube-openapi v0.0.0-20200615155156-dffdd1682719/go.mod h1:bfCVj+qXcEaE5SCvzBaqpOySr6tuCcpPKqF6HD8nyCw=

View File

@ -71,7 +71,7 @@ kube::version::get_version_vars() {
# compatible semantic version that looks something like this:
# v1.1.0-alpha.0.6+84c76d1142ea4d
#
# TODO: We continue calling this "git version" because so many
# xTODO: We continue calling this "git version" because so many
# downstream consumers are expecting it there.
#
# These regexes are painful enough in sed...

132
pkg/apiserver/apiserver.go Normal file
View File

@ -0,0 +1,132 @@
/*
Copyright 2020 VMware, Inc.
SPDX-License-Identifier: Apache-2.0
*/
package apiserver
import (
"fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/registry/rest"
genericapiserver "k8s.io/apiserver/pkg/server"
"k8s.io/client-go/pkg/version"
"k8s.io/klog/v2"
placeholderapi "github.com/suzerain-io/placeholder-name-api/pkg/apis/placeholder"
placeholderv1alpha1 "github.com/suzerain-io/placeholder-name-api/pkg/apis/placeholder/v1alpha1"
"github.com/suzerain-io/placeholder-name/pkg/registry/loginrequest"
)
var (
//nolint: gochecknoglobals
scheme = runtime.NewScheme()
//nolint: gochecknoglobals
//nolint: golint
Codecs = serializer.NewCodecFactory(scheme)
)
//nolint: gochecknoinits
func init() {
utilruntime.Must(placeholderv1alpha1.AddToScheme(scheme))
utilruntime.Must(placeholderapi.AddToScheme(scheme))
// add the options to empty v1
metav1.AddToGroupVersion(scheme, schema.GroupVersion{Version: "v1"})
unversioned := schema.GroupVersion{Group: "", Version: "v1"}
scheme.AddUnversionedTypes(unversioned,
&metav1.Status{},
&metav1.APIVersions{},
&metav1.APIGroupList{},
&metav1.APIGroup{},
&metav1.APIResourceList{},
)
}
type Config struct {
GenericConfig *genericapiserver.RecommendedConfig
ExtraConfig ExtraConfig
}
type ExtraConfig struct {
Webhook authenticator.Token
}
type PlaceHolderServer struct {
GenericAPIServer *genericapiserver.GenericAPIServer
}
type completedConfig struct {
GenericConfig genericapiserver.CompletedConfig
ExtraConfig *ExtraConfig
}
type CompletedConfig struct {
// Embed a private pointer that cannot be instantiated outside of this package.
*completedConfig
}
// Complete fills in any fields not set that are required to have valid data. It's mutating the receiver.
func (c *Config) Complete() CompletedConfig {
completedCfg := completedConfig{
c.GenericConfig.Complete(),
&c.ExtraConfig,
}
versionInfo := version.Get()
completedCfg.GenericConfig.Version = &versionInfo
return CompletedConfig{completedConfig: &completedCfg}
}
// New returns a new instance of AdmissionServer from the given config.
func (c completedConfig) New() (*PlaceHolderServer, error) {
genericServer, err := c.GenericConfig.New("place-holder-server", genericapiserver.NewEmptyDelegate()) // completion is done in Complete, no need for a second time
if err != nil {
return nil, fmt.Errorf("completion error: %w", err)
}
s := &PlaceHolderServer{
GenericAPIServer: genericServer,
}
gvr := placeholderv1alpha1.SchemeGroupVersion.WithResource("loginrequests")
apiGroupInfo := genericapiserver.APIGroupInfo{
PrioritizedVersions: []schema.GroupVersion{gvr.GroupVersion()},
VersionedResourcesStorageMap: map[string]map[string]rest.Storage{},
OptionsExternalVersion: &schema.GroupVersion{Version: "v1"},
Scheme: scheme,
ParameterCodec: metav1.ParameterCodec,
NegotiatedSerializer: Codecs,
}
loginRequestStorage := loginrequest.NewREST(c.ExtraConfig.Webhook)
v1alpha1Storage, ok := apiGroupInfo.VersionedResourcesStorageMap[gvr.Version]
if !ok {
v1alpha1Storage = map[string]rest.Storage{}
}
v1alpha1Storage[gvr.Resource] = loginRequestStorage
apiGroupInfo.VersionedResourcesStorageMap[gvr.Version] = v1alpha1Storage
if err := s.GenericAPIServer.InstallAPIGroup(&apiGroupInfo); err != nil {
return nil, fmt.Errorf("install API group error: %w", err)
}
s.GenericAPIServer.AddPostStartHookOrDie("place-holder-post-start-hook",
func(context genericapiserver.PostStartHookContext) error {
klog.InfoS("post start hook", "foo", "bar")
return nil
},
)
return s, nil
}

View File

@ -0,0 +1,114 @@
/*
Copyright 2020 VMware, Inc.
SPDX-License-Identifier: Apache-2.0
*/
// Package loginrequest provides REST functionality for the LoginRequest resource.
package loginrequest
import (
"context"
"fmt"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/registry/rest"
placeholderapi "github.com/suzerain-io/placeholder-name-api/pkg/apis/placeholder"
)
var (
_ rest.Creater = &REST{}
_ rest.NamespaceScopedStrategy = &REST{}
_ rest.Scoper = &REST{}
_ rest.Storage = &REST{}
)
func NewREST(webhook authenticator.Token) *REST {
return &REST{
webhook: webhook,
}
}
type REST struct {
webhook authenticator.Token
}
func (r *REST) New() runtime.Object {
return &placeholderapi.LoginRequest{}
}
func (r *REST) NamespaceScoped() bool {
return false
}
func (r *REST) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
loginRequest, ok := obj.(*placeholderapi.LoginRequest)
if !ok {
return nil, apierrors.NewBadRequest(fmt.Sprintf("not a LoginRequest: %#v", obj))
}
// TODO refactor all validation checks into a validation function in another package (e.g. see subjectaccessreqview api in k8s)
// TODO also validate .spec.type
token := loginRequest.Spec.Token
if token == nil || len(token.Value) == 0 {
errs := field.ErrorList{field.Required(field.NewPath("spec", "token", "value"), "token must be supplied")}
return nil, apierrors.NewInvalid(placeholderapi.Kind(loginRequest.Kind), loginRequest.Name, errs)
}
// let dynamic admission webhooks have a chance to validate (but not mutate) as well
// TODO Are we okay with admission webhooks being able to see tokens? Maybe strip token out before passing obj to createValidation.
// Since we are an aggregated API, we should investigate to see if the kube API server is already invoking admission hooks for us.
// Even if it is, its okay to call it again here. If the kube API server is already calling the webhooks and passing
// the token, then there is probably no reason for us to avoid passing the token when we call the webhooks here, since
// they already got the token.
if createValidation != nil {
if err := createValidation(ctx, obj.DeepCopyObject()); err != nil {
return nil, err
}
}
// just a sanity check, not sure how to honor a dry run on a virtual API
if options != nil {
if len(options.DryRun) != 0 {
errs := field.ErrorList{field.NotSupported(field.NewPath("dryRun"), options.DryRun, nil)}
return nil, apierrors.NewInvalid(placeholderapi.Kind(loginRequest.Kind), loginRequest.Name, errs)
}
}
// the incoming context could have an audience attached to it technically
// sine we do not want to handle audiences right now, do not pass it through directly
// instead we just propagate cancellation of the parent context
cancelCtx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
select {
case <-ctx.Done():
cancel()
case <-cancelCtx.Done():
}
}()
// TODO do the actual business logic of this endpoint here
_, _, err := r.webhook.AuthenticateToken(cancelCtx, token.Value)
if err != nil {
return nil, fmt.Errorf("authenticate token failed: %w", err)
}
// make a new object so that we do not return the original token in the response
out := &placeholderapi.LoginRequest{
Status: placeholderapi.LoginRequestStatus{
ExpirationTimestamp: nil,
Token: "snorlax",
ClientCertificateData: "",
ClientKeyData: "",
},
}
return out, nil
}

View File

@ -0,0 +1,240 @@
/*
Copyright 2020 VMware, Inc.
SPDX-License-Identifier: Apache-2.0
*/
package loginrequest
import (
"context"
"errors"
"testing"
"time"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/authentication/authenticator"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/rest"
placeholderapi "github.com/suzerain-io/placeholder-name-api/pkg/apis/placeholder"
)
type contextKey struct{}
type FakeToken struct {
calledWithToken string
calledWithContext context.Context
timeout time.Duration
reachedTimeout bool
cancelled bool
webhookStartedRunningNotificationChan chan bool
returnErr error
}
func (f *FakeToken) AuthenticateToken(ctx context.Context, token string) (*authenticator.Response, bool, error) {
f.calledWithToken = token
f.calledWithContext = ctx
if f.webhookStartedRunningNotificationChan != nil {
f.webhookStartedRunningNotificationChan <- true
}
afterCh := time.After(f.timeout)
select {
case <-afterCh:
f.reachedTimeout = true
case <-ctx.Done():
f.cancelled = true
}
return &authenticator.Response{}, true, f.returnErr
}
func callCreate(ctx context.Context, storage *REST, loginRequest *placeholderapi.LoginRequest) (runtime.Object, error) {
return storage.Create(
ctx,
loginRequest,
rest.ValidateAllObjectFunc,
&metav1.CreateOptions{
DryRun: []string{},
})
}
func validLoginRequest() *placeholderapi.LoginRequest {
return loginRequest(placeholderapi.LoginRequestSpec{
Type: placeholderapi.TokenLoginCredentialType,
Token: &placeholderapi.LoginRequestTokenCredential{Value: "a token"},
})
}
func loginRequest(spec placeholderapi.LoginRequestSpec) *placeholderapi.LoginRequest {
return &placeholderapi.LoginRequest{
TypeMeta: metav1.TypeMeta{},
ObjectMeta: metav1.ObjectMeta{
Name: "request name",
},
Spec: spec,
}
}
func TestCreateSucceedsWhenGivenAToken(t *testing.T) {
webhook := FakeToken{}
storage := NewREST(&webhook)
requestToken := "a token"
response, err := callCreate(context.Background(), storage, loginRequest(placeholderapi.LoginRequestSpec{
Type: placeholderapi.TokenLoginCredentialType,
Token: &placeholderapi.LoginRequestTokenCredential{Value: requestToken},
}))
require.NoError(t, err)
require.Equal(t, response, &placeholderapi.LoginRequest{
Status: placeholderapi.LoginRequestStatus{
ExpirationTimestamp: nil,
Token: "snorlax",
ClientCertificateData: "",
ClientKeyData: "",
},
})
require.Equal(t, requestToken, webhook.calledWithToken)
}
func TestCreateDoesNotPassAdditionalContextInfoToTheWebhook(t *testing.T) {
webhook := FakeToken{}
storage := NewREST(&webhook)
ctx := context.WithValue(context.Background(), contextKey{}, "context-value")
_, err := callCreate(ctx, storage, validLoginRequest())
require.NoError(t, err)
require.Nil(t, webhook.calledWithContext.Value("context-key"))
}
func TestCreateCancelsTheWebhookInvocationWhenTheCallToCreateIsCancelledItself(t *testing.T) {
webhookStartedRunningNotificationChan := make(chan bool)
webhook := FakeToken{
timeout: time.Second * 2,
webhookStartedRunningNotificationChan: webhookStartedRunningNotificationChan,
}
storage := NewREST(&webhook)
ctx, cancel := context.WithCancel(context.Background())
c := make(chan bool)
go func() {
_, err := callCreate(ctx, storage, validLoginRequest())
c <- true
require.NoError(t, err)
}()
require.False(t, webhook.cancelled)
require.False(t, webhook.reachedTimeout)
<-webhookStartedRunningNotificationChan // wait long enough to make sure that the webhook has started
cancel() // cancel the context that was passed to storage.Create() above
<-c // wait for the above call to storage.Create() to be finished
require.True(t, webhook.cancelled)
require.False(t, webhook.reachedTimeout)
require.Equal(t, context.Canceled, webhook.calledWithContext.Err()) // the inner context is cancelled
}
func TestCreateAllowsTheWebhookInvocationToFinishWhenTheCallToCreateIsNotCancelledItself(t *testing.T) {
webhookStartedRunningNotificationChan := make(chan bool)
webhook := FakeToken{
timeout: 0,
webhookStartedRunningNotificationChan: webhookStartedRunningNotificationChan,
}
storage := NewREST(&webhook)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
c := make(chan bool)
go func() {
_, err := callCreate(ctx, storage, validLoginRequest())
c <- true
require.NoError(t, err)
}()
require.False(t, webhook.cancelled)
require.False(t, webhook.reachedTimeout)
<-webhookStartedRunningNotificationChan // wait long enough to make sure that the webhook has started
<-c // wait for the above call to storage.Create() to be finished
require.False(t, webhook.cancelled)
require.True(t, webhook.reachedTimeout)
require.Equal(t, context.Canceled, webhook.calledWithContext.Err()) // the inner context is cancelled (in this case by the "defer")
}
func TestCreateFailsWhenWebhookFails(t *testing.T) {
webhook := FakeToken{
returnErr: errors.New("some webhook error"),
}
storage := NewREST(&webhook)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
_, err := callCreate(ctx, storage, validLoginRequest())
require.EqualError(t, err, "authenticate token failed: some webhook error")
}
func TestCreateFailsWhenGivenTheWrongInputType(t *testing.T) {
notALoginRequest := runtime.Unknown{}
response, err := NewREST(&FakeToken{}).Create(
genericapirequest.NewContext(),
&notALoginRequest,
rest.ValidateAllObjectFunc,
&metav1.CreateOptions{})
require.Nil(t, response)
require.True(t, apierrors.IsBadRequest(err))
var status apierrors.APIStatus
errors.As(err, &status)
require.Contains(t, status.Status().Message, "not a LoginRequest")
}
func TestCreateFailsWhenTokenIsNilInRequest(t *testing.T) {
storage := NewREST(&FakeToken{})
response, err := callCreate(context.Background(), storage, loginRequest(placeholderapi.LoginRequestSpec{
Type: placeholderapi.TokenLoginCredentialType,
Token: nil,
}))
require.Nil(t, response)
require.True(t, apierrors.IsInvalid(err))
var status apierrors.APIStatus
errors.As(err, &status)
require.Equal(t,
`.placeholder.suzerain-io.github.io "request name" is invalid: spec.token.value: Required value: token must be supplied`,
status.Status().Message)
}
func TestCreateFailsWhenTokenValueIsEmptyInRequest(t *testing.T) {
storage := NewREST(&FakeToken{})
response, err := callCreate(context.Background(), storage, loginRequest(placeholderapi.LoginRequestSpec{
Type: placeholderapi.TokenLoginCredentialType,
Token: &placeholderapi.LoginRequestTokenCredential{Value: ""},
}))
require.Nil(t, response)
require.True(t, apierrors.IsInvalid(err))
var status apierrors.APIStatus
errors.As(err, &status)
require.Equal(t,
`.placeholder.suzerain-io.github.io "request name" is invalid: spec.token.value: Required value: token must be supplied`,
status.Status().Message)
}
func TestCreateFailsWhenRequestOptionsDryRunIsNotEmpty(t *testing.T) {
response, err := NewREST(&FakeToken{}).Create(
genericapirequest.NewContext(),
validLoginRequest(),
rest.ValidateAllObjectFunc,
&metav1.CreateOptions{
DryRun: []string{"some dry run flag"},
})
require.Nil(t, response)
require.True(t, apierrors.IsInvalid(err))
var status apierrors.APIStatus
errors.As(err, &status)
require.Equal(t,
`.placeholder.suzerain-io.github.io "request name" is invalid: dryRun: Unsupported value: []string{"some dry run flag"}`,
status.Status().Message)
}

View File

@ -0,0 +1,184 @@
/*
Copyright 2020 VMware, Inc.
SPDX-License-Identifier: Apache-2.0
*/
package integration
import (
"context"
"encoding/json"
"os"
"testing"
"time"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/suzerain-io/placeholder-name-api/pkg/apis/placeholder/v1alpha1"
"github.com/suzerain-io/placeholder-name/test/library"
)
func makeRequest(t *testing.T, spec v1alpha1.LoginRequestSpec) (*v1alpha1.LoginRequest, error) {
t.Helper()
client := library.NewPlaceholderNameClientset(t)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
return client.PlaceholderV1alpha1().LoginRequests().Create(ctx, &v1alpha1.LoginRequest{
TypeMeta: metav1.TypeMeta{},
ObjectMeta: metav1.ObjectMeta{},
Spec: spec,
}, metav1.CreateOptions{})
}
func TestSuccessfulLoginRequest(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")
response, err := makeRequest(t, v1alpha1.LoginRequestSpec{
Token: &v1alpha1.LoginRequestTokenCredential{Value: tmcClusterToken},
})
require.NoError(t, err)
require.Empty(t, response.Spec)
require.NotEmpty(t, response.Status.Token)
require.Empty(t, response.Status.ClientCertificateData)
require.Empty(t, response.Status.ClientKeyData)
require.Nil(t, response.Status.ExpirationTimestamp)
}
func TestLoginRequest_ShouldFailWhenRequestDoesNotIncludeToken(t *testing.T) {
_, err := makeRequest(t, v1alpha1.LoginRequestSpec{})
require.Error(t, err)
statusError, isStatus := err.(*errors.StatusError)
require.True(t, isStatus)
require.Equal(t, 1, len(statusError.ErrStatus.Details.Causes))
cause := statusError.ErrStatus.Details.Causes[0]
require.Equal(t, metav1.CauseType("FieldValueRequired"), cause.Type)
require.Equal(t, "Required value: token must be supplied", cause.Message)
require.Equal(t, "spec.token.value", cause.Field)
}
func TestGetDiscovery(t *testing.T) {
client := library.NewPlaceholderNameClientset(t)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
result, err := client.Discovery().RESTClient().Get().Do(ctx).Raw()
require.NoError(t, err)
var parsedResult map[string]interface{}
err = json.Unmarshal(result, &parsedResult)
require.NoError(t, err)
require.Contains(t, parsedResult["paths"], "/apis/placeholder.suzerain-io.github.io")
require.Contains(t, parsedResult["paths"], "/apis/placeholder.suzerain-io.github.io/v1alpha1")
}
func TestGetAPIResourceList(t *testing.T) {
var expectedAPIResourceList = `{
"kind": "APIResourceList",
"apiVersion": "v1",
"groupVersion": "placeholder.suzerain-io.github.io/v1alpha1",
"resources": [
{
"name": "loginrequests",
"singularName": "",
"namespaced": false,
"kind": "LoginRequest",
"verbs": [
"create"
]
}
]
}`
client := library.NewPlaceholderNameClientset(t)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
result, err := client.PlaceholderV1alpha1().RESTClient().Get().Do(ctx).Raw()
require.NoError(t, err)
require.JSONEq(t, expectedAPIResourceList, string(result))
// proposed:
groups, resources, err := client.Discovery().ServerGroupsAndResources()
require.NoError(t, err)
groupName := "placeholder.suzerain-io.github.io"
actualGroup := findGroup(groupName, groups)
require.NotNil(t, actualGroup)
expectedGroup := &metav1.APIGroup{
Name: "placeholder.suzerain-io.github.io",
Versions: []metav1.GroupVersionForDiscovery{
metav1.GroupVersionForDiscovery{
GroupVersion: "placeholder.suzerain-io.github.io/v1alpha1",
Version: "v1alpha1",
},
},
PreferredVersion: metav1.GroupVersionForDiscovery{
GroupVersion: "placeholder.suzerain-io.github.io/v1alpha1",
Version: "v1alpha1",
},
}
require.Equal(t, expectedGroup, actualGroup)
resourceGroupVersion := "placeholder.suzerain-io.github.io/v1alpha1"
actualResources := findResources(resourceGroupVersion, resources)
require.NotNil(t, actualResources)
expectedResources := &metav1.APIResourceList{
TypeMeta: metav1.TypeMeta{
Kind: "APIResourceList",
APIVersion: "v1",
},
GroupVersion: "placeholder.suzerain-io.github.io/v1alpha1",
APIResources: []metav1.APIResource{
metav1.APIResource{
Name: "loginrequests",
Kind: "LoginRequest",
SingularName: "", // TODO(akeesler): what should this be?
Verbs: metav1.Verbs([]string{
"create",
}),
},
},
}
require.Equal(t, expectedResources, actualResources)
}
func TestGetAPIVersion(t *testing.T) {
client := library.NewPlaceholderNameClientset(t)
version, err := client.Discovery().ServerVersion()
require.NoError(t, err)
require.NotNil(t, version) // TODO(akeesler: what can we assert here?
}
func findGroup(name string, groups []*metav1.APIGroup) *metav1.APIGroup {
for _, group := range groups {
if group.Name == name {
return group
}
}
return nil
}
func findResources(groupVersion string, resources []*metav1.APIResourceList) *metav1.APIResourceList {
for _, resource := range resources {
if resource.GroupVersion == groupVersion {
return resource
}
}
return nil
}

View File

@ -12,6 +12,8 @@ import (
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
placeholdernameclientset "github.com/suzerain-io/placeholder-name-client-go/pkg/generated/clientset/versioned"
)
func NewClientConfig(t *testing.T) *rest.Config {
@ -30,3 +32,9 @@ func NewClientset(t *testing.T) kubernetes.Interface {
return kubernetes.NewForConfigOrDie(NewClientConfig(t))
}
func NewPlaceholderNameClientset(t *testing.T) placeholdernameclientset.Interface {
t.Helper()
return placeholdernameclientset.NewForConfigOrDie(NewClientConfig(t))
}