diff --git a/Dockerfile b/Dockerfile index 6649b5dd..474deb58 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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"] diff --git a/cmd/placeholder-name/app/app.go b/cmd/placeholder-name/app/app.go index cdc45d35..82a420c9 100644 --- a/cmd/placeholder-name/app/app.go +++ b/cmd/placeholder-name/app/app.go @@ -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) + } + + return server.GenericAPIServer.PrepareRun().Run(ctx.Done()) } -// 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() +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 - 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) + serverConfig := genericapiserver.NewRecommendedConfig(apiserver.Codecs) + if err := a.recommendedOptions.ApplyTo(serverConfig); err != nil { + return nil, err + } - _, _ = w.Write([]byte("hello world")) -} - -// 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) - - // If/when the context is canceled or times out, initiate shutting down the serve. - <-ctx.Done() - - 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) +} diff --git a/cmd/placeholder-name/app/app_test.go b/cmd/placeholder-name/app/app_test.go index d7d8abd1..2cacf6d3 100644 --- a/cmd/placeholder-name/app/app_test.go +++ b/cmd/placeholder-name/app/app_test.go @@ -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 = ` @@ -29,9 +25,10 @@ Usage: placeholder-name [flags] 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 + -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") - }) -} diff --git a/cmd/placeholder-name/main.go b/cmd/placeholder-name/main.go index 69219924..b816ad36 100644 --- a/cmd/placeholder-name/main.go +++ b/cmd/placeholder-name/main.go @@ -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) } } diff --git a/deploy/deployment.yaml b/deploy/deployment.yaml index 4523c996..764ec5f8 100644 --- a/deploy/deployment.yaml +++ b/deploy/deployment.yaml @@ -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 diff --git a/deploy/rbac.yaml b/deploy/rbac.yaml index dda81a1b..e3d0858a 100644 --- a/deploy/rbac.yaml +++ b/deploy/rbac.yaml @@ -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 diff --git a/go.mod b/go.mod index cc14f8f0..f738f5b4 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index c2f5027d..d0a134d0 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/hack/lib/version.sh b/hack/lib/version.sh index 15180fb3..712c2b2a 100644 --- a/hack/lib/version.sh +++ b/hack/lib/version.sh @@ -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... diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go new file mode 100644 index 00000000..315389ff --- /dev/null +++ b/pkg/apiserver/apiserver.go @@ -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 +} diff --git a/pkg/registry/loginrequest/rest.go b/pkg/registry/loginrequest/rest.go new file mode 100644 index 00000000..ca366263 --- /dev/null +++ b/pkg/registry/loginrequest/rest.go @@ -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 +} diff --git a/pkg/registry/loginrequest/rest_test.go b/pkg/registry/loginrequest/rest_test.go new file mode 100644 index 00000000..624a3834 --- /dev/null +++ b/pkg/registry/loginrequest/rest_test.go @@ -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(), + ¬ALoginRequest, + 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) +} diff --git a/test/integration/loginrequest_test.go b/test/integration/loginrequest_test.go new file mode 100644 index 00000000..9799aa14 --- /dev/null +++ b/test/integration/loginrequest_test.go @@ -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 +} diff --git a/test/library/client.go b/test/library/client.go index 2c7cf020..1df00147 100644 --- a/test/library/client.go +++ b/test/library/client.go @@ -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)) +}