From a01970602a9cfd778e048dfb92207b6ea0db7824 Mon Sep 17 00:00:00 2001 From: Matt Moyer Date: Wed, 15 Jul 2020 15:38:39 -0500 Subject: [PATCH 1/3] Add a package for loading Downward API metadata. Signed-off-by: Matt Moyer --- internal/downward/downward.go | 70 +++++++++++ internal/downward/downward_test.go | 111 ++++++++++++++++++ .../downward/testdata/invalidlabels/labels | 1 + .../downward/testdata/invalidlabels/namespace | 1 + .../downward/testdata/missinglabels/namespace | 1 + internal/downward/testdata/valid/labels | 2 + internal/downward/testdata/valid/namespace | 1 + 7 files changed, 187 insertions(+) create mode 100644 internal/downward/downward.go create mode 100644 internal/downward/downward_test.go create mode 100644 internal/downward/testdata/invalidlabels/labels create mode 100644 internal/downward/testdata/invalidlabels/namespace create mode 100644 internal/downward/testdata/missinglabels/namespace create mode 100644 internal/downward/testdata/valid/labels create mode 100644 internal/downward/testdata/valid/namespace diff --git a/internal/downward/downward.go b/internal/downward/downward.go new file mode 100644 index 00000000..47ee73fb --- /dev/null +++ b/internal/downward/downward.go @@ -0,0 +1,70 @@ +/* +Copyright 2020 VMware, Inc. +SPDX-License-Identifier: Apache-2.0 +*/ + +// Package downward implements a client interface for interacting with Kubernetes "downwardAPI" volumes. +// See https://kubernetes.io/docs/tasks/inject-data-application/downward-api-volume-expose-pod-information/. +package downward + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "path/filepath" + "strconv" + "strings" +) + +// PodInfo contains pod metadata about the current pod. +type PodInfo struct { + // Namespace where the current pod is running. + Namespace string + + // Labels of the current pod. + Labels map[string]string +} + +// Load pod metadata from a downwardAPI volume directory. +func Load(directory string) (*PodInfo, error) { + var result PodInfo + ns, err := ioutil.ReadFile(filepath.Join(directory, "namespace")) + if err != nil { + return nil, fmt.Errorf("could not load namespace: %w", err) + } + result.Namespace = strings.TrimSpace(string(ns)) + + labels, err := ioutil.ReadFile(filepath.Join(directory, "labels")) + if err != nil { + return nil, fmt.Errorf("could not load labels: %w", err) + } + result.Labels, err = parseMap(labels) + if err != nil { + return nil, fmt.Errorf("could not parse labels: %w", err) + } + return &result, nil +} + +// parseMap parses the key/value format emitted by the Kubernetes Downward API for pod labels and annotations. +// See https://kubernetes.io/docs/tasks/inject-data-application/downward-api-volume-expose-pod-information/. +// See https://github.com/kubernetes/kubernetes/blob/4b2cb072dba10227083b16731f019f096c581787/pkg/fieldpath/fieldpath.go#L28. +func parseMap(input []byte) (map[string]string, error) { + result := map[string]string{} + for _, line := range bytes.Split(input, []byte("\n")) { + line = bytes.TrimSpace(line) + if len(line) == 0 { + continue + } + parts := bytes.SplitN(line, []byte("="), 2) + if len(parts) != 2 { + return nil, fmt.Errorf("expected 2 parts, found %d: %w", len(parts), io.ErrShortBuffer) + } + value, err := strconv.Unquote(string(parts[1])) + if err != nil { + return nil, fmt.Errorf("invalid quoted value: %w", err) + } + result[string(parts[0])] = value + } + return result, nil +} diff --git a/internal/downward/downward_test.go b/internal/downward/downward_test.go new file mode 100644 index 00000000..048d5657 --- /dev/null +++ b/internal/downward/downward_test.go @@ -0,0 +1,111 @@ +/* +Copyright 2020 VMware, Inc. +SPDX-License-Identifier: Apache-2.0 +*/ + +package downward + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestLoad(t *testing.T) { + t.Parallel() + tests := []struct { + name string + inputDir string + wantErr string + want *PodInfo + }{ + { + name: "missing directory", + inputDir: "./testdata/no-such-directory", + wantErr: "could not load namespace: open testdata/no-such-directory/namespace: no such file or directory", + }, + { + name: "missing labels file", + inputDir: "./testdata/missinglabels", + wantErr: "could not load labels: open testdata/missinglabels/labels: no such file or directory", + }, + { + name: "invalid labels file", + inputDir: "./testdata/invalidlabels", + wantErr: "could not parse labels: expected 2 parts, found 1: short buffer", + }, + { + name: "valid", + inputDir: "./testdata/valid", + want: &PodInfo{ + Namespace: "test-namespace", + Labels: map[string]string{"foo": "bar", "bat": "baz"}, + }, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + got, err := Load(tt.inputDir) + if tt.wantErr != "" { + require.EqualError(t, err, tt.wantErr) + require.Empty(t, got) + return + } + require.NoError(t, err) + require.Equal(t, tt.want, got) + }) + } +} + +func TestParseMap(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input []byte + wantErr string + want map[string]string + }{ + { + name: "empty", + want: map[string]string{}, + }, + { + name: "missing equal", + input: []byte(`akjhlakjh`), + wantErr: "expected 2 parts, found 1: short buffer", + }, + { + name: "missing invalid value", + input: []byte(`akjhlakjh="foo\qbar"`), + wantErr: "invalid quoted value: invalid syntax", + }, + { + name: "success", + input: []byte(` +fooTime="2020-07-15T19:35:12.027636555Z" +example.com/config.source="api" +example.com/bar="baz\x01" +`), + want: map[string]string{ + "fooTime": "2020-07-15T19:35:12.027636555Z", + "example.com/config.source": "api", + "example.com/bar": "baz\x01", + }, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + got, err := parseMap(tt.input) + if tt.wantErr != "" { + require.EqualError(t, err, tt.wantErr) + require.Empty(t, got) + return + } + require.NoError(t, err) + require.Equal(t, tt.want, got) + }) + } +} diff --git a/internal/downward/testdata/invalidlabels/labels b/internal/downward/testdata/invalidlabels/labels new file mode 100644 index 00000000..e466dcbd --- /dev/null +++ b/internal/downward/testdata/invalidlabels/labels @@ -0,0 +1 @@ +invalid \ No newline at end of file diff --git a/internal/downward/testdata/invalidlabels/namespace b/internal/downward/testdata/invalidlabels/namespace new file mode 100644 index 00000000..d2826d00 --- /dev/null +++ b/internal/downward/testdata/invalidlabels/namespace @@ -0,0 +1 @@ +test-namespace \ No newline at end of file diff --git a/internal/downward/testdata/missinglabels/namespace b/internal/downward/testdata/missinglabels/namespace new file mode 100644 index 00000000..d2826d00 --- /dev/null +++ b/internal/downward/testdata/missinglabels/namespace @@ -0,0 +1 @@ +test-namespace \ No newline at end of file diff --git a/internal/downward/testdata/valid/labels b/internal/downward/testdata/valid/labels new file mode 100644 index 00000000..433404c1 --- /dev/null +++ b/internal/downward/testdata/valid/labels @@ -0,0 +1,2 @@ +foo="bar" +bat="baz" \ No newline at end of file diff --git a/internal/downward/testdata/valid/namespace b/internal/downward/testdata/valid/namespace new file mode 100644 index 00000000..d2826d00 --- /dev/null +++ b/internal/downward/testdata/valid/namespace @@ -0,0 +1 @@ +test-namespace \ No newline at end of file From a3bce5f42eec5d7fb3a4b3b6a58f0692d8c31402 Mon Sep 17 00:00:00 2001 From: Matt Moyer Date: Thu, 16 Jul 2020 11:40:44 -0500 Subject: [PATCH 2/3] Add autoregistration package to manage APIService. Signed-off-by: Matt Moyer --- go.mod | 2 + go.sum | 9 + internal/autoregistration/autoregistration.go | 138 +++++ .../autoregistration/autoregistration_test.go | 540 ++++++++++++++++++ 4 files changed, 689 insertions(+) create mode 100644 internal/autoregistration/autoregistration.go create mode 100644 internal/autoregistration/autoregistration_test.go diff --git a/go.mod b/go.mod index fa6ebfff..b83ff495 100644 --- a/go.mod +++ b/go.mod @@ -13,5 +13,7 @@ require ( 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/kube-aggregator v0.19.0-rc.0 + k8s.io/utils v0.0.0-20200619165400-6e3d28b6ed19 sigs.k8s.io/yaml v1.2.0 ) diff --git a/go.sum b/go.sum index 33be8f48..55d5c981 100644 --- a/go.sum +++ b/go.sum @@ -101,6 +101,7 @@ github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb 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= +github.com/evanphx/json-patch v0.0.0-20190815234213-e83c0a1c26c8 h1:DM7gHzQfHwIj+St8zaPOI6iQEPAxOwIkskvw6s9rDaM= github.com/evanphx/json-patch v0.0.0-20190815234213-e83c0a1c26c8/go.mod h1:pmLOTb3x90VhIKxsA9yeQG5yfOkkKnkk1h+Ql8NDYDw= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= @@ -302,6 +303,8 @@ github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCV github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= @@ -715,6 +718,7 @@ golang.org/x/tools v0.0.0-20200414032229-332987a829c3/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200422022333-3d57cf2e726e/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200428185508-e9a00ec82136/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200519015757-0d0afa43d58a/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200602230032-c00d67ef29d0/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200625211823-6506e20df31f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200702044944-0cc1aa72b347 h1:/e4fNMHdLn7SQSxTrRZTma2xjQW6ELdxcnpqMhpo9X4= golang.org/x/tools v0.0.0-20200702044944-0cc1aa72b347/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= @@ -817,12 +821,17 @@ k8s.io/apiserver v0.19.0-rc.0 h1:SaF/gMgUeDPbQDKHTMvB2yynBUZpp6s4HYQIOx/LdDQ= k8s.io/apiserver v0.19.0-rc.0/go.mod h1:yEjU524zw/pxiG6nOsgY5Hu/akAg7tH/J/tKrLUp/mo= k8s.io/client-go v0.19.0-rc.0 h1:6WW8MElhoLeYcLiN4ky1159XG5E39KYdmLCrV/6lNiE= k8s.io/client-go v0.19.0-rc.0/go.mod h1:3kWGD05F7c58atlk7ep9ob1hg2Yu9NSz8gJxCNNTHhc= +k8s.io/code-generator v0.19.0-rc.0/go.mod h1:2jgaU9hVSqti1GiO69UFSoTZcL5XAvZSrXaNnK5RVA0= k8s.io/component-base v0.19.0-rc.0 h1:S/jt6xey1Wg5i5A9/BCkPYekpjJ5zlfuSCCVlNSJ/Yc= k8s.io/component-base v0.19.0-rc.0/go.mod h1:8cHxNUQdeDIIcORXOrMABUPbuEmbbHRtEweSSk8Il4g= k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/gengo v0.0.0-20200428234225-8167cfdcfc14/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= 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/utils v0.0.0-20200619165400-6e3d28b6ed19 h1:7Nu2dTj82c6IaWvL7hImJzcXoTPz1MsSCH7r+0m6rfo= k8s.io/utils v0.0.0-20200619165400-6e3d28b6ed19/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= diff --git a/internal/autoregistration/autoregistration.go b/internal/autoregistration/autoregistration.go new file mode 100644 index 00000000..e5098e50 --- /dev/null +++ b/internal/autoregistration/autoregistration.go @@ -0,0 +1,138 @@ +/* +Copyright 2020 VMware, Inc. +SPDX-License-Identifier: Apache-2.0 +*/ + +// Package autoregistration registers a Kubernetes APIService pointing at the current pod. +package autoregistration + +import ( + "context" + "errors" + "fmt" + + corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + corev1client "k8s.io/client-go/kubernetes/typed/core/v1" + "k8s.io/client-go/util/retry" + apiregistrationv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1" + aggregatationv1client "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset" +) + +// ErrInvalidServiceTemplate is returned by Setup when the provided ServiceTemplate is not valid. +var ErrInvalidServiceTemplate = errors.New("invalid service template") + +// SetupOptions specifies the inputs for Setup(). +type SetupOptions struct { + CoreV1 corev1client.CoreV1Interface + AggregationV1 aggregatationv1client.Interface + Namespace string + ServiceTemplate corev1.Service + APIServiceTemplate apiregistrationv1.APIService +} + +// Setup registers a Kubernetes Service, and an aggregation APIService which points to it. +func Setup(ctx context.Context, options SetupOptions) error { + // Get the namespace so we can use its UID set owner references on other objects. + ns, err := options.CoreV1.Namespaces().Get(ctx, options.Namespace, metav1.GetOptions{}) + if err != nil { + return fmt.Errorf("could not get namespace: %w", err) + } + + // Make a copy of the Service template. + svc := options.ServiceTemplate.DeepCopy() + svc.Namespace = ns.Name + + // Validate that the Service meets our expectations. + if len(svc.Spec.Ports) != 1 { + return fmt.Errorf("%w: must have 1 port (found %d)", ErrInvalidServiceTemplate, len(svc.Spec.Ports)) + } + if port := svc.Spec.Ports[0]; port.Protocol != corev1.ProtocolTCP || port.Port != 443 { + return fmt.Errorf("%w: must expose TCP/443 (found %s/%d)", ErrInvalidServiceTemplate, port.Protocol, port.Port) + } + + // Create or update the Service. + if err := createOrUpdateService(ctx, options.CoreV1, svc); err != nil { + return err + } + + apiSvc := options.APIServiceTemplate.DeepCopy() + apiSvc.Spec.Service = &apiregistrationv1.ServiceReference{ + Namespace: ns.Name, + Name: svc.Name, + Port: &svc.Spec.Ports[0].Port, + } + apiSvc.ObjectMeta.OwnerReferences = []metav1.OwnerReference{{ + APIVersion: ns.APIVersion, + Kind: ns.Kind, + UID: ns.UID, + Name: ns.Name, + }} + if err := createOrUpdateAPIService(ctx, options.AggregationV1, apiSvc); err != nil { + return err + } + return nil +} + +func createOrUpdateService(ctx context.Context, client corev1client.CoreV1Interface, svc *corev1.Service) error { + services := client.Services(svc.Namespace) + + _, err := services.Create(ctx, svc, metav1.CreateOptions{}) + if err == nil { + return nil + } + if !k8serrors.IsAlreadyExists(err) { + return fmt.Errorf("could not create service: %w", err) + } + + if err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + // Retrieve the latest version of the Service before attempting update + // RetryOnConflict uses exponential backoff to avoid exhausting the apiserver + result, err := services.Get(ctx, svc.Name, metav1.GetOptions{}) + if err != nil { + return fmt.Errorf("could not get existing version of service: %w", err) + } + + // Update just the fields we care about. + result.Spec.Ports = svc.Spec.Ports + result.Spec.Selector = svc.Spec.Selector + + _, updateErr := services.Update(ctx, result, metav1.UpdateOptions{}) + return updateErr + }); err != nil { + return fmt.Errorf("could not update service: %w", err) + } + return nil +} + +func createOrUpdateAPIService(ctx context.Context, client aggregatationv1client.Interface, apiSvc *apiregistrationv1.APIService) error { + apiServices := client.ApiregistrationV1().APIServices() + + _, err := apiServices.Create(ctx, apiSvc, metav1.CreateOptions{}) + if err == nil { + return nil + } + if !k8serrors.IsAlreadyExists(err) { + return fmt.Errorf("could not create API service: %w", err) + } + + if err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + // Retrieve the latest version of the Service before attempting update + // RetryOnConflict uses exponential backoff to avoid exhausting the apiserver + result, err := apiServices.Get(ctx, apiSvc.Name, metav1.GetOptions{}) + if err != nil { + return fmt.Errorf("could not get existing version of API service: %w", err) + } + + // Update just the fields we care about. + apiSvc.Spec.DeepCopyInto(&result.Spec) + apiSvc.OwnerReferences = result.OwnerReferences + + _, updateErr := apiServices.Update(ctx, result, metav1.UpdateOptions{}) + return updateErr + }); err != nil { + return fmt.Errorf("could not update API service: %w", err) + } + return nil +} diff --git a/internal/autoregistration/autoregistration_test.go b/internal/autoregistration/autoregistration_test.go new file mode 100644 index 00000000..013cf974 --- /dev/null +++ b/internal/autoregistration/autoregistration_test.go @@ -0,0 +1,540 @@ +/* +Copyright 2020 VMware, Inc. +SPDX-License-Identifier: Apache-2.0 +*/ + +package autoregistration + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + 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" + kubefake "k8s.io/client-go/kubernetes/fake" + kubetesting "k8s.io/client-go/testing" + apiregistrationv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1" + aggregationv1fake "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/fake" + "k8s.io/utils/pointer" +) + +func TestSetup(t *testing.T) { + tests := []struct { + name string + input SetupOptions + mocks func(*kubefake.Clientset, *aggregationv1fake.Clientset) + wantErr string + wantServices []corev1.Service + wantAPIServices []apiregistrationv1.APIService + }{ + { + name: "no such namespace", + input: SetupOptions{ + Namespace: "foo", + }, + wantErr: `could not get namespace: namespaces "foo" not found`, + }, + { + name: "service template missing port", + input: SetupOptions{ + Namespace: "test-namespace", + }, + mocks: func(kube *kubefake.Clientset, agg *aggregationv1fake.Clientset) { + _ = kube.Tracker().Add(&corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{Name: "test-namespace", UID: "test-namespace-uid"}, + }) + }, + wantErr: `invalid service template: must have 1 port (found 0)`, + }, + { + name: "service template missing port", + input: SetupOptions{ + Namespace: "test-namespace", + ServiceTemplate: corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-service", + Namespace: "replaceme", + }, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + { + Protocol: "UDP", + Port: 1234, + TargetPort: intstr.IntOrString{IntVal: 1234}, + }, + }, + }, + }, + }, + mocks: func(kube *kubefake.Clientset, agg *aggregationv1fake.Clientset) { + _ = kube.Tracker().Add(&corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{Name: "test-namespace", UID: "test-namespace-uid"}, + }) + }, + wantErr: `invalid service template: must expose TCP/443 (found UDP/1234)`, + }, + { + name: "fail to create service", + input: SetupOptions{ + Namespace: "test-namespace", + ServiceTemplate: corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-service", + Namespace: "replaceme", + }, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + { + Protocol: "TCP", + Port: 443, + TargetPort: intstr.IntOrString{IntVal: 1234}, + }, + }, + }, + }, + }, + mocks: func(kube *kubefake.Clientset, agg *aggregationv1fake.Clientset) { + _ = kube.Tracker().Add(&corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{Name: "test-namespace", UID: "test-namespace-uid"}, + }) + kube.PrependReactor("create", "services", func(_ kubetesting.Action) (bool, runtime.Object, error) { + return true, nil, fmt.Errorf("some Service creation failure") + }) + }, + wantErr: `could not create service: some Service creation failure`, + }, + { + name: "fail to create API service", + input: SetupOptions{ + Namespace: "test-namespace", + ServiceTemplate: corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-service", + Namespace: "replaceme", + }, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + { + Protocol: "TCP", + Port: 443, + TargetPort: intstr.IntOrString{IntVal: 1234}, + }, + }, + }, + }, + }, + mocks: func(kube *kubefake.Clientset, agg *aggregationv1fake.Clientset) { + _ = kube.Tracker().Add(&corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{Name: "test-namespace", UID: "test-namespace-uid"}, + }) + agg.PrependReactor("create", "apiservices", func(_ kubetesting.Action) (bool, runtime.Object, error) { + return true, nil, fmt.Errorf("some APIService creation failure") + }) + }, + wantErr: `could not create API service: some APIService creation failure`, + }, + { + name: "success", + input: SetupOptions{ + Namespace: "test-namespace", + ServiceTemplate: corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-service", + Namespace: "replaceme", + }, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + { + Protocol: "TCP", + Port: 443, + TargetPort: intstr.IntOrString{IntVal: 1234}, + }, + }, + }, + }, + APIServiceTemplate: apiregistrationv1.APIService{ + ObjectMeta: metav1.ObjectMeta{Name: "test-api-service"}, + Spec: apiregistrationv1.APIServiceSpec{ + Group: "test-api-group", + Version: "test-version", + CABundle: []byte("test-ca-bundle"), + GroupPriorityMinimum: 1234, + VersionPriority: 4321, + }, + }, + }, + mocks: func(kube *kubefake.Clientset, agg *aggregationv1fake.Clientset) { + _ = kube.Tracker().Add(&corev1.Namespace{ + TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Namespace"}, + ObjectMeta: metav1.ObjectMeta{Name: "test-namespace", UID: "test-namespace-uid"}, + }) + }, + wantServices: []corev1.Service{{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-service", + Namespace: "test-namespace", + }, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + { + Protocol: "TCP", + Port: 443, + TargetPort: intstr.IntOrString{IntVal: 1234}, + }, + }, + }, + }}, + wantAPIServices: []apiregistrationv1.APIService{{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-api-service", + OwnerReferences: []metav1.OwnerReference{{ + APIVersion: "v1", + Kind: "Namespace", + Name: "test-namespace", + UID: "test-namespace-uid", + }}, + }, + Spec: apiregistrationv1.APIServiceSpec{ + Service: &apiregistrationv1.ServiceReference{ + Namespace: "test-namespace", + Name: "test-service", + Port: pointer.Int32Ptr(443), + }, + Group: "test-api-group", + Version: "test-version", + CABundle: []byte("test-ca-bundle"), + GroupPriorityMinimum: 1234, + VersionPriority: 4321, + }, + }}, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + + kubeClient := kubefake.NewSimpleClientset() + aggregationClient := aggregationv1fake.NewSimpleClientset() + if tt.mocks != nil { + tt.mocks(kubeClient, aggregationClient) + } + + tt.input.CoreV1 = kubeClient.CoreV1() + tt.input.AggregationV1 = aggregationClient + err := Setup(context.Background(), tt.input) + if tt.wantErr != "" { + require.EqualError(t, err, tt.wantErr) + return + } + require.NoError(t, err) + if tt.wantServices != nil { + objects, err := kubeClient.CoreV1().Services(tt.input.Namespace).List(ctx, metav1.ListOptions{}) + require.NoError(t, err) + require.Equal(t, tt.wantServices, objects.Items) + } + if tt.wantAPIServices != nil { + objects, err := aggregationClient.ApiregistrationV1().APIServices().List(ctx, metav1.ListOptions{}) + require.NoError(t, err) + require.Equal(t, tt.wantAPIServices, objects.Items) + } + }) + } +} + +func TestCreateOrUpdateService(t *testing.T) { + tests := []struct { + name string + input *corev1.Service + mocks func(*kubefake.Clientset) + wantObjects []corev1.Service + wantErr string + }{ + { + name: "error on create", + input: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "ns"}, + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeClusterIP, + ClusterIP: "1.2.3.4", + }, + }, + mocks: func(c *kubefake.Clientset) { + c.PrependReactor("create", "services", func(_ kubetesting.Action) (bool, runtime.Object, error) { + return true, nil, fmt.Errorf("error on create") + }) + }, + wantErr: "could not create service: error on create", + }, + { + name: "new", + input: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "ns"}, + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeClusterIP, + ClusterIP: "1.2.3.4", + }, + }, + wantObjects: []corev1.Service{{ + ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "ns"}, + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeClusterIP, + ClusterIP: "1.2.3.4", + }, + }}, + }, + { + name: "update", + mocks: func(c *kubefake.Clientset) { + _ = c.Tracker().Add(&corev1.Service{ + ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "ns"}, + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeClusterIP, + ClusterIP: "1.2.3.4", + }, + }) + }, + input: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "ns"}, + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeClusterIP, + ClusterIP: "1.2.3.4", + }, + }, + wantObjects: []corev1.Service{{ + ObjectMeta: metav1.ObjectMeta{Name: "foo", Namespace: "ns"}, + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeClusterIP, + ClusterIP: "1.2.3.4", + }, + }}, + }, + { + name: "error on get", + mocks: func(c *kubefake.Clientset) { + _ = c.Tracker().Add(&corev1.Service{ + ObjectMeta: metav1.ObjectMeta{Name: "foo"}, + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeClusterIP, + ClusterIP: "1.2.3.4", + }, + }) + c.PrependReactor("get", "services", func(_ kubetesting.Action) (bool, runtime.Object, error) { + return true, nil, fmt.Errorf("error on get") + }) + }, + input: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{Name: "foo"}, + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeClusterIP, + ClusterIP: "1.2.3.4", + }, + }, + wantErr: "could not update service: could not get existing version of service: error on get", + }, + { + name: "error on get, successful retry", + mocks: func(c *kubefake.Clientset) { + _ = c.Tracker().Add(&corev1.Service{ + ObjectMeta: metav1.ObjectMeta{Name: "foo"}, + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeClusterIP, + ClusterIP: "1.2.3.4", + }, + }) + + hit := false + c.PrependReactor("get", "services", func(_ kubetesting.Action) (bool, runtime.Object, error) { + // Return an error on the first call, then fall through to the default (successful) response. + if !hit { + hit = true + return true, nil, fmt.Errorf("error on get") + } + return false, nil, nil + }) + }, + input: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{Name: "foo"}, + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeClusterIP, + ClusterIP: "1.2.3.4", + }, + }, + wantErr: "could not update service: could not get existing version of service: error on get", + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + + client := kubefake.NewSimpleClientset() + if tt.mocks != nil { + tt.mocks(client) + } + + err := createOrUpdateService(ctx, client.CoreV1(), tt.input) + + if tt.wantErr != "" { + require.EqualError(t, err, tt.wantErr) + return + } + require.NoError(t, err) + if tt.wantObjects != nil { + objects, err := client.CoreV1().Services(tt.input.ObjectMeta.Namespace).List(ctx, metav1.ListOptions{}) + require.NoError(t, err) + require.Equal(t, tt.wantObjects, objects.Items) + } + }) + } +} + +func TestCreateOrUpdateAPIService(t *testing.T) { + tests := []struct { + name string + input *apiregistrationv1.APIService + mocks func(*aggregationv1fake.Clientset) + wantObjects []apiregistrationv1.APIService + wantErr string + }{ + { + name: "error on create", + input: &apiregistrationv1.APIService{ + ObjectMeta: metav1.ObjectMeta{Name: "foo"}, + Spec: apiregistrationv1.APIServiceSpec{ + GroupPriorityMinimum: 123, + VersionPriority: 456, + }, + }, + mocks: func(c *aggregationv1fake.Clientset) { + c.PrependReactor("create", "apiservices", func(_ kubetesting.Action) (bool, runtime.Object, error) { + return true, nil, fmt.Errorf("error on create") + }) + }, + wantErr: "could not create API service: error on create", + }, + { + name: "new", + input: &apiregistrationv1.APIService{ + ObjectMeta: metav1.ObjectMeta{Name: "foo"}, + Spec: apiregistrationv1.APIServiceSpec{ + GroupPriorityMinimum: 123, + VersionPriority: 456, + }, + }, + wantObjects: []apiregistrationv1.APIService{{ + ObjectMeta: metav1.ObjectMeta{Name: "foo"}, + Spec: apiregistrationv1.APIServiceSpec{ + GroupPriorityMinimum: 123, + VersionPriority: 456, + }, + }}, + }, + { + name: "update", + mocks: func(c *aggregationv1fake.Clientset) { + _ = c.Tracker().Add(&apiregistrationv1.APIService{ + ObjectMeta: metav1.ObjectMeta{Name: "foo"}, + Spec: apiregistrationv1.APIServiceSpec{ + GroupPriorityMinimum: 999, + VersionPriority: 999, + }, + }) + }, + input: &apiregistrationv1.APIService{ + ObjectMeta: metav1.ObjectMeta{Name: "foo"}, + Spec: apiregistrationv1.APIServiceSpec{ + GroupPriorityMinimum: 123, + VersionPriority: 456, + }, + }, + wantObjects: []apiregistrationv1.APIService{{ + ObjectMeta: metav1.ObjectMeta{Name: "foo"}, + Spec: apiregistrationv1.APIServiceSpec{ + GroupPriorityMinimum: 123, + VersionPriority: 456, + }, + }}, + }, + { + name: "error on get", + mocks: func(c *aggregationv1fake.Clientset) { + _ = c.Tracker().Add(&apiregistrationv1.APIService{ + ObjectMeta: metav1.ObjectMeta{Name: "foo"}, + Spec: apiregistrationv1.APIServiceSpec{ + GroupPriorityMinimum: 999, + VersionPriority: 999, + }, + }) + c.PrependReactor("get", "apiservices", func(_ kubetesting.Action) (bool, runtime.Object, error) { + return true, nil, fmt.Errorf("error on get") + }) + }, + input: &apiregistrationv1.APIService{ + ObjectMeta: metav1.ObjectMeta{Name: "foo"}, + Spec: apiregistrationv1.APIServiceSpec{ + GroupPriorityMinimum: 123, + VersionPriority: 456, + }, + }, + wantErr: "could not update API service: could not get existing version of API service: error on get", + }, + { + name: "error on get, successful retry", + mocks: func(c *aggregationv1fake.Clientset) { + _ = c.Tracker().Add(&apiregistrationv1.APIService{ + ObjectMeta: metav1.ObjectMeta{Name: "foo"}, + Spec: apiregistrationv1.APIServiceSpec{ + GroupPriorityMinimum: 999, + VersionPriority: 999, + }, + }) + + hit := false + c.PrependReactor("get", "apiservices", func(_ kubetesting.Action) (bool, runtime.Object, error) { + // Return an error on the first call, then fall through to the default (successful) response. + if !hit { + hit = true + return true, nil, fmt.Errorf("error on get") + } + return false, nil, nil + }) + }, + input: &apiregistrationv1.APIService{ + ObjectMeta: metav1.ObjectMeta{Name: "foo"}, + Spec: apiregistrationv1.APIServiceSpec{ + GroupPriorityMinimum: 123, + VersionPriority: 456, + }, + }, + wantErr: "could not update API service: could not get existing version of API service: error on get", + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + + client := aggregationv1fake.NewSimpleClientset() + if tt.mocks != nil { + tt.mocks(client) + } + + err := createOrUpdateAPIService(ctx, client, tt.input) + if tt.wantErr != "" { + require.EqualError(t, err, tt.wantErr) + return + } + require.NoError(t, err) + if tt.wantObjects != nil { + objects, err := client.ApiregistrationV1().APIServices().List(ctx, metav1.ListOptions{}) + require.NoError(t, err) + require.Equal(t, tt.wantObjects, objects.Items) + } + }) + } +} From 092cc267899a62f3f18971c244235e47b2e485ab Mon Sep 17 00:00:00 2001 From: Matt Moyer Date: Thu, 16 Jul 2020 14:24:30 -0500 Subject: [PATCH 3/3] Refactor app.go and wire in autoregistration. Signed-off-by: Matt Moyer --- cmd/placeholder-name/app/app.go | 99 ++++++++++++++++--- cmd/placeholder-name/app/app_test.go | 93 ++++++++++------- .../app/testdata/podinfo/labels | 2 + .../app/testdata/podinfo/namespace | 1 + deploy/deployment.yaml | 16 ++- go.mod | 1 + go.sum | 37 +------ 7 files changed, 163 insertions(+), 86 deletions(-) create mode 100644 cmd/placeholder-name/app/testdata/podinfo/labels create mode 100644 cmd/placeholder-name/app/testdata/podinfo/namespace diff --git a/cmd/placeholder-name/app/app.go b/cmd/placeholder-name/app/app.go index 52e8df50..5b7a5c41 100644 --- a/cmd/placeholder-name/app/app.go +++ b/cmd/placeholder-name/app/app.go @@ -20,10 +20,20 @@ import ( "github.com/spf13/cobra" "golang.org/x/sync/errgroup" - + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "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" + "github.com/suzerain-io/placeholder-name-api/pkg/apis/placeholder" + "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" ) @@ -35,6 +45,10 @@ const shutdownGracePeriod = 5 * time.Second type App struct { cmd *cobra.Command + // CLI flags + configPath string + downwardAPIPath string + // listen address for healthz serve healthAddr string @@ -43,29 +57,39 @@ type App struct { // webhook authenticates tokens webhook authenticator.Token - - // runFunc runs the actual program, after the parsing of flags has been done. - // - // It is mostly a field for the sake of testing. - runFunc func(ctx context.Context, configPath string) error } // New constructs a new App with command line args, stdout and stderr. func New(args []string, stdout, stderr io.Writer) *App { a := &App{ healthAddr: ":8080", - mainAddr: ":8443", + mainAddr: ":443", } - a.runFunc = a.serve - var configPath string cmd := &cobra.Command{ Use: `placeholder-name`, Long: `placeholder-name provides a generic API for mapping an external credential from somewhere to an internal credential to be used for authenticating to the Kubernetes API.`, RunE: func(cmd *cobra.Command, args []string) error { - return a.runFunc(context.Background(), configPath) + // Load the Kubernetes client configuration (kubeconfig), + kubeconfig, err := restclient.InClusterConfig() + if err != nil { + return fmt.Errorf("could not load in-cluster configuration: %w", err) + } + + // Connect to the core Kubernetes API. + k8s, err := kubernetes.NewForConfig(kubeconfig) + if err != nil { + return fmt.Errorf("could not initialize Kubernetes client: %w", err) + } + + // Connect to the Kubernetes aggregation API. + aggregation, err := aggregationv1client.NewForConfig(kubeconfig) + if err != nil { + return fmt.Errorf("could not initialize Kubernetes client: %w", err) + } + return a.serve(context.Background(), k8s.CoreV1(), aggregation) }, Args: cobra.NoArgs, } @@ -75,13 +99,20 @@ authenticating to the Kubernetes API.`, cmd.SetErr(stderr) cmd.Flags().StringVarP( - &configPath, + &a.configPath, "config", "c", "placeholder-name.yaml", "path to configuration file", ) + cmd.Flags().StringVar( + &a.downwardAPIPath, + "downward-api-path", + "/etc/podinfo", + "path to Downward API volume mount", + ) + a.cmd = cmd return a @@ -91,8 +122,8 @@ func (a *App) Run() error { return a.cmd.Execute() } -func (a *App) serve(ctx context.Context, configPath string) error { - cfg, err := config.FromPath(configPath) +func (a *App) serve(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) } @@ -103,6 +134,11 @@ func (a *App) serve(ctx context.Context, configPath string) error { } a.webhook = webhook + podinfo, err := downward.Load(a.downwardAPIPath) + if err != nil { + return fmt.Errorf("could not read pod metadata: %w", err) + } + ca, err := certauthority.New(pkix.Name{CommonName: "Placeholder CA"}) if err != nil { return fmt.Errorf("could not initialize CA: %w", err) @@ -125,6 +161,43 @@ func (a *App) serve(ctx context.Context, configPath string) error { // 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"}, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + { + Protocol: corev1.ProtocolTCP, + Port: 443, + TargetPort: intstr.IntOrString{IntVal: 443}, //TODO: parse this out of mainAddr + }, + }, + Selector: podinfo.Labels, + Type: corev1.ServiceTypeClusterIP, + }, + } + apiService := apiregistrationv1.APIService{ + ObjectMeta: metav1.ObjectMeta{ + Name: "v1alpha1." + placeholder.GroupName, + }, + Spec: apiregistrationv1.APIServiceSpec{ + Group: placeholder.GroupName, + Version: "v1alpha1", + CABundle: caBundle, + GroupPriorityMinimum: 2500, + VersionPriority: 10, + }, + } + if err := autoregistration.Setup(ctx, autoregistration.SetupOptions{ + CoreV1: k8s, + AggregationV1: aggregation, + Namespace: podinfo.Namespace, + ServiceTemplate: service, + APIServiceTemplate: apiService, + }); err != nil { + 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) diff --git a/cmd/placeholder-name/app/app_test.go b/cmd/placeholder-name/app/app_test.go index e8ef3d73..d7d8abd1 100644 --- a/cmd/placeholder-name/app/app_test.go +++ b/cmd/placeholder-name/app/app_test.go @@ -8,45 +8,60 @@ package app import ( "bytes" "context" + "strings" "testing" "time" + "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 = `Usage: +const knownGoodUsage = ` +placeholder-name provides a generic API for mapping an external +credential from somewhere to an internal credential to be used for +authenticating to the Kubernetes API. + +Usage: placeholder-name [flags] Flags: - -c, --config string path to configuration file (default "placeholder-name.yaml") - -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 ` func TestCommand(t *testing.T) { tests := []struct { - name string - args []string - - wantConfigPath string + name string + args []string + wantErr string + wantStdout string }{ { - name: "NoArgsSucceeds", - args: []string{}, - wantConfigPath: "placeholder-name.yaml", + name: "NoArgsSucceeds", + args: []string{}, }, { - name: "OneArgFails", - args: []string{"tuna"}, + name: "Usage", + args: []string{"-h"}, + wantStdout: knownGoodUsage, }, { - name: "ShortConfigFlagSucceeds", - args: []string{"-c", "some/path/to/config.yaml"}, - wantConfigPath: "some/path/to/config.yaml", + name: "OneArgFails", + args: []string{"tuna"}, + wantErr: `unknown command "tuna" for "placeholder-name"`, }, { - name: "LongConfigFlagSucceeds", - args: []string{"--config", "some/path/to/config.yaml"}, - wantConfigPath: "some/path/to/config.yaml", + name: "ShortConfigFlagSucceeds", + args: []string{"-c", "some/path/to/config.yaml"}, + }, + { + name: "LongConfigFlagSucceeds", + args: []string{"--config", "some/path/to/config.yaml"}, }, { name: "OneArgWithConfigFlagFails", @@ -54,32 +69,27 @@ func TestCommand(t *testing.T) { "--config", "some/path/to/config.yaml", "tuna", }, + wantErr: `unknown command "tuna" for "placeholder-name"`, }, } for _, test := range tests { test := test t.Run(test.name, func(t *testing.T) { - expect := require.New(t) - stdout := bytes.NewBuffer([]byte{}) stderr := bytes.NewBuffer([]byte{}) - configPaths := make([]string, 0, 1) - runFunc := func(ctx context.Context, configPath string) error { - configPaths = append(configPaths, configPath) + a := New(test.args, stdout, stderr) + a.cmd.RunE = func(cmd *cobra.Command, args []string) error { return nil } - - a := New(test.args, stdout, stderr) - a.runFunc = runFunc err := a.Run() - - if test.wantConfigPath != "" { - expect.Equal(1, len(configPaths)) - expect.Equal(test.wantConfigPath, configPaths[0]) + if test.wantErr != "" { + require.EqualError(t, err, test.wantErr) } else { - expect.Error(err) - expect.Contains(stdout.String(), knownGoodUsage) + require.NoError(t, err) + } + if test.wantStdout != "" { + require.Equal(t, strings.TrimSpace(test.wantStdout), strings.TrimSpace(stdout.String())) } }) } @@ -88,16 +98,21 @@ func TestCommand(t *testing.T) { 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", + healthAddr: "127.0.0.1:0", + mainAddr: "127.0.0.1:8443", + configPath: "testdata/valid-config.yaml", + downwardAPIPath: "testdata/podinfo", } - err := a.serve(ctx, "testdata/valid-config.yaml") + err := a.serve(ctx, fakev1.CoreV1(), fakeaggregationv1) require.NoError(t, err) }) @@ -107,10 +122,12 @@ func TestServeApp(t *testing.T) { defer cancel() a := App{ - healthAddr: "127.0.0.1:8081", - mainAddr: "127.0.0.1:8081", + healthAddr: "127.0.0.1:8081", + mainAddr: "127.0.0.1:8081", + configPath: "testdata/valid-config.yaml", + downwardAPIPath: "testdata/podinfo", } - err := a.serve(ctx, "testdata/valid-config.yaml") + 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/app/testdata/podinfo/labels b/cmd/placeholder-name/app/testdata/podinfo/labels new file mode 100644 index 00000000..433404c1 --- /dev/null +++ b/cmd/placeholder-name/app/testdata/podinfo/labels @@ -0,0 +1,2 @@ +foo="bar" +bat="baz" \ No newline at end of file diff --git a/cmd/placeholder-name/app/testdata/podinfo/namespace b/cmd/placeholder-name/app/testdata/podinfo/namespace new file mode 100644 index 00000000..d2826d00 --- /dev/null +++ b/cmd/placeholder-name/app/testdata/podinfo/namespace @@ -0,0 +1 @@ +test-namespace \ No newline at end of file diff --git a/deploy/deployment.yaml b/deploy/deployment.yaml index ea2697cc..4a0e087f 100644 --- a/deploy/deployment.yaml +++ b/deploy/deployment.yaml @@ -49,8 +49,8 @@ spec: imagePullPolicy: IfNotPresent command: - ./app - - --config - - /etc/config/placeholder-config.yaml + - --config=/etc/config/placeholder-config.yaml + - --downward-api-path=/etc/podinfo volumeMounts: - name: config-volume mountPath: /etc/config @@ -58,3 +58,15 @@ spec: - name: config-volume configMap: name: #@ data.values.app_name + "-config" + - name: podinfo + mountPath: /etc/podinfo + volumes: + - name: podinfo + downwardAPI: + items: + - path: "labels" + fieldRef: + fieldPath: metadata.labels + - path: "namespace" + fieldRef: + fieldPath: metadata.namespace diff --git a/go.mod b/go.mod index b83ff495..b5dfabd0 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/golangci/golangci-lint v1.28.1 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 golang.org/x/tools v0.0.0-20200707134715-9e0a013e855f // indirect k8s.io/api v0.19.0-rc.0 diff --git a/go.sum b/go.sum index 55d5c981..c2f5027d 100644 --- a/go.sum +++ b/go.sum @@ -49,7 +49,6 @@ github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5 github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= @@ -76,15 +75,12 @@ github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHo github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 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/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= 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= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -106,7 +102,6 @@ github.com/evanphx/json-patch v0.0.0-20190815234213-e83c0a1c26c8/go.mod h1:pmLOT github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= -github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= @@ -167,25 +162,21 @@ github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJA github.com/gofrs/flock v0.7.1 h1:DP+LD/t0njgoPBvT5MJLeliUIVQR03hiKR6vezdwHlc= github.com/gofrs/flock v0.7.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef h1:veQD95Isof8w9/WXiA+pa3tz3fJXkt5B7QaRBrM62gk= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7 h1:5ZkaAPbicIKTF2I64qf5Fh8Aa83Q/dnOafMYV0OMwjA= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1 h1:qGJ6qTW+x6xX/my+8YUVl4WNpX9B7+/l2tRsHGZ7f2s= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= @@ -301,7 +292,6 @@ github.com/jmoiron/sqlx v1.2.1-0.20190826204134-d7d95172beb5/go.mod h1:1FEQNm3xl 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= -github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -355,7 +345,6 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= -github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= @@ -420,7 +409,6 @@ github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndr github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= -github.com/prometheus/client_golang v1.0.0 h1:vrDKnkGzuGvhNAL56c7DBz29ZL+KxnoR0x7enabFceM= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.6.0 h1:YVPodQOcK15POxhgARIvnDRVpLcuK8mglnMrWfyrw6A= github.com/prometheus/client_golang v1.6.0/go.mod h1:ZLOG9ck3JLRdB5MgO8f+lLTe83AXG6ro35rLTxvnIl4= @@ -431,13 +419,11 @@ github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2 github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.4.1 h1:K0MGApIoQvMw27RTdJkPbr3JZ7DNbtxQNyi5STVM6Kw= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.9.1 h1:KOMtN28tlbam3/7ZKEYKHhKoJZYYj3gMH4uc62x7X7U= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.2 h1:6LJUbpNm42llc4HRCuvApCSWB/WfhuNo9K98Q9sNGfs= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.11 h1:DhHlBtkHWPYi8O2y31JkK0TF+DGM+51OopZjH/Ia5qI= github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= @@ -450,7 +436,6 @@ github.com/quasilyte/regex/syntax v0.0.0-20200407221936-30656e2c4a95/go.mod h1:r github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.5.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryancurrah/gomodguard v1.1.0 h1:DWbye9KyMgytn8uYpuHkwf0RHqAYO6Ay/D0TbCpPtVU= github.com/ryancurrah/gomodguard v1.1.0/go.mod h1:4O8tr7hBODaGE6VIhfJDHcwzh5GUccKSJBU0UMXJFVM= @@ -466,7 +451,6 @@ github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e h1:MZM7FHLqUHYI0Y/mQAt github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041 h1:llrF3Fs4018ePo4+G/HV/uQUqEI1HMDjCeOf2V6puPc= github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= -github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= @@ -482,7 +466,6 @@ github.com/sonatard/noctx v0.0.1/go.mod h1:9D2D/EoULe8Yy2joDHJj7bv3sZoq9AaSb8B4l github.com/sourcegraph/go-diff v0.5.3 h1:lhIKJ2nXLZZ+AfbHpYxTn0pXpNTTui0DX7DO3xeb1Zs= github.com/sourcegraph/go-diff v0.5.3/go.mod h1:v9JDtjCE4HHHCZGId75rg8gkKKa98RVjBcBGsVmMmak= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= @@ -502,7 +485,6 @@ github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/y github.com/spf13/viper v1.7.0 h1:xVKxvI7ouOI5I+U9s2eeiUfMaWBVoXA3AWskkrqK0VM= github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= @@ -514,6 +496,8 @@ github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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/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= @@ -541,13 +525,11 @@ github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1: github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.etcd.io/bbolt v1.3.2 h1:Z/90sZLPOeCy2PwprqkFa25PdkusRzaj9P8zm/KNyvk= 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/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= 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 h1:C9hSCOW830chIVkdja34wa6Ky+IzWllkUinR+BtRZd4= 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= @@ -610,13 +592,11 @@ golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6 h1:pE8b58s1HRDMi8RDc79m0HISf9D4TzseP40cEA6IGfs= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -625,7 +605,6 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -657,20 +636,17 @@ golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4 h1:5/PjkGUjvEU5Gl6BxmvKRPpqo2uNMv4rcHBMwzk/st8= golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= 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= @@ -720,7 +696,6 @@ golang.org/x/tools v0.0.0-20200428185508-e9a00ec82136/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200519015757-0d0afa43d58a/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200602230032-c00d67ef29d0/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200625211823-6506e20df31f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200702044944-0cc1aa72b347 h1:/e4fNMHdLn7SQSxTrRZTma2xjQW6ELdxcnpqMhpo9X4= golang.org/x/tools v0.0.0-20200702044944-0cc1aa72b347/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200707134715-9e0a013e855f h1:y1MEz+/UGBJqz2A4K9QOu4TeXQ9Vs5MlmvhETgaR0Kg= golang.org/x/tools v0.0.0-20200707134715-9e0a013e855f/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= @@ -737,7 +712,6 @@ google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsb google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= @@ -749,7 +723,6 @@ google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRn google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a h1:Ob5/580gVHBJZgXnff1cZDbG+xLtMVE5mDRTe+nIsX4= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY= @@ -757,10 +730,8 @@ google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEY google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.21.1 h1:j6XxA85m/6txkUCHvzlV5f+HBNl/1r5cZ2A/3IEFOO8= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.26.0 h1:2dTRdpdFEEhJYQD8EMLB61nnrzSCTbG38PhqdhvOltg= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0 h1:rRYRFMVgRv6E0D70Skyfsr28tDXIuuPZyWGMPdMcnXg= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= @@ -775,7 +746,6 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.24.0 h1:UhZDfRO8JRQru4/+LlLE0BRKGF8L+PICnvYZmx/fEGA= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -831,8 +801,9 @@ 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= k8s.io/utils v0.0.0-20200619165400-6e3d28b6ed19 h1:7Nu2dTj82c6IaWvL7hImJzcXoTPz1MsSCH7r+0m6rfo= k8s.io/utils v0.0.0-20200619165400-6e3d28b6ed19/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= mvdan.cc/gofumpt v0.0.0-20200513141252-abc0db2c416a h1:TTEzidAa7rn93JGy1ACigx6o9VcsRLKG7qICdErmvUs=