From fe32c3454c989060d957808bbd16b1660e8eaa1a Mon Sep 17 00:00:00 2001 From: Jordan Liggitt Date: Thu, 4 Jun 2020 11:39:02 -0400 Subject: [PATCH] Add support for v1 CSRs to kubectl certificate commands Kubernetes-commit: ca234db60151ccffdd4dc8ceb2ec3c69fd83af69 --- pkg/cmd/certificates/certificates.go | 159 ++++++---- pkg/cmd/certificates/certificates_test.go | 338 ++++++++++++++++++++++ pkg/cmd/testing/fake.go | 1 + 3 files changed, 447 insertions(+), 51 deletions(-) create mode 100644 pkg/cmd/certificates/certificates_test.go diff --git a/pkg/cmd/certificates/certificates.go b/pkg/cmd/certificates/certificates.go index 399790c6..570f1427 100644 --- a/pkg/cmd/certificates/certificates.go +++ b/pkg/cmd/certificates/certificates.go @@ -23,14 +23,18 @@ import ( "github.com/spf13/cobra" + certificatesv1 "k8s.io/api/certificates/v1" certificatesv1beta1 "k8s.io/api/certificates/v1beta1" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/printers" "k8s.io/cli-runtime/pkg/resource" - certificatesv1beta1client "k8s.io/client-go/kubernetes/typed/certificates/v1beta1" + clientset "k8s.io/client-go/kubernetes" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/scheme" "k8s.io/kubectl/pkg/util/i18n" @@ -63,7 +67,7 @@ type CertificateOptions struct { csrNames []string outputStyle string - clientSet certificatesv1beta1client.CertificatesV1beta1Interface + clientSet clientset.Interface builder *resource.Builder genericclioptions.IOStreams @@ -92,11 +96,7 @@ func (o *CertificateOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, arg o.builder = f.NewBuilder() - clientConfig, err := f.ToRESTConfig() - if err != nil { - return err - } - o.clientSet, err = certificatesv1beta1client.NewForConfig(clientConfig) + o.clientSet, err = f.KubernetesClientSet() if err != nil { return err } @@ -146,24 +146,12 @@ func NewCmdCertificateApprove(f cmdutil.Factory, ioStreams genericclioptions.IOS } func (o *CertificateOptions) RunCertificateApprove(force bool) error { - return o.modifyCertificateCondition(o.builder, o.clientSet, force, func(csr *certificatesv1beta1.CertificateSigningRequest) (*certificatesv1beta1.CertificateSigningRequest, bool) { - var alreadyApproved bool - for _, c := range csr.Status.Conditions { - if c.Type == certificatesv1beta1.CertificateApproved { - alreadyApproved = true - } - } - if alreadyApproved { - return csr, true - } - csr.Status.Conditions = append(csr.Status.Conditions, certificatesv1beta1.CertificateSigningRequestCondition{ - Type: certificatesv1beta1.CertificateApproved, - Reason: "KubectlApprove", - Message: "This CSR was approved by kubectl certificate approve.", - LastUpdateTime: metav1.Now(), - }) - return csr, false - }) + return o.modifyCertificateCondition( + o.builder, + o.clientSet, + force, + addConditionIfNeeded(string(certificatesv1.CertificateDenied), string(certificatesv1.CertificateApproved), "KubectlApprove", "This CSR was approved by kubectl certificate approve."), + ) } func NewCmdCertificateDeny(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command { @@ -196,33 +184,21 @@ func NewCmdCertificateDeny(f cmdutil.Factory, ioStreams genericclioptions.IOStre } func (o *CertificateOptions) RunCertificateDeny(force bool) error { - return o.modifyCertificateCondition(o.builder, o.clientSet, force, func(csr *certificatesv1beta1.CertificateSigningRequest) (*certificatesv1beta1.CertificateSigningRequest, bool) { - var alreadyDenied bool - for _, c := range csr.Status.Conditions { - if c.Type == certificatesv1beta1.CertificateDenied { - alreadyDenied = true - } - } - if alreadyDenied { - return csr, true - } - csr.Status.Conditions = append(csr.Status.Conditions, certificatesv1beta1.CertificateSigningRequestCondition{ - Type: certificatesv1beta1.CertificateDenied, - Reason: "KubectlDeny", - Message: "This CSR was denied by kubectl certificate deny.", - LastUpdateTime: metav1.Now(), - }) - return csr, false - }) + return o.modifyCertificateCondition( + o.builder, + o.clientSet, + force, + addConditionIfNeeded(string(certificatesv1.CertificateApproved), string(certificatesv1.CertificateDenied), "KubectlDeny", "This CSR was denied by kubectl certificate deny."), + ) } -func (o *CertificateOptions) modifyCertificateCondition(builder *resource.Builder, clientSet certificatesv1beta1client.CertificatesV1beta1Interface, force bool, modify func(csr *certificatesv1beta1.CertificateSigningRequest) (*certificatesv1beta1.CertificateSigningRequest, bool)) error { +func (o *CertificateOptions) modifyCertificateCondition(builder *resource.Builder, clientSet clientset.Interface, force bool, modify func(csr runtime.Object) (runtime.Object, bool, error)) error { var found int r := builder. - WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...). + Unstructured(). ContinueOnError(). FilenameParam(false, &o.FilenameOptions). - ResourceNames("certificatesigningrequests.v1beta1.certificates.k8s.io", o.csrNames...). + ResourceNames("certificatesigningrequests", o.csrNames...). RequireObject(true). Flatten(). Latest(). @@ -232,13 +208,41 @@ func (o *CertificateOptions) modifyCertificateCondition(builder *resource.Builde return err } for i := 0; ; i++ { - csr, ok := info.Object.(*certificatesv1beta1.CertificateSigningRequest) + obj, ok := info.Object.(*unstructured.Unstructured) if !ok { - return fmt.Errorf("can only handle certificates.k8s.io/v1beta1 certificate signing requests") + return fmt.Errorf("expected *unstructured.Unstructured, got %T", obj) + } + if want, got := certificatesv1.Kind("CertificateSigningRequest"), obj.GetObjectKind().GroupVersionKind().GroupKind(); want != got { + return fmt.Errorf("can only handle %s objects, got %s", want.String(), got.String()) + } + var csr runtime.Object + // get a typed object + // first try v1 + csr, err = clientSet.CertificatesV1().CertificateSigningRequests().Get(context.TODO(), obj.GetName(), metav1.GetOptions{}) + if apierrors.IsNotFound(err) { + // fall back to v1beta1 + csr, err = clientSet.CertificatesV1beta1().CertificateSigningRequests().Get(context.TODO(), obj.GetName(), metav1.GetOptions{}) + } + if apierrors.IsNotFound(err) { + return fmt.Errorf("could not find v1 or v1beta1 version of %s: %v", obj.GetName(), err) + } + if err != nil { + return err + } + + modifiedCSR, hasCondition, err := modify(csr) + if err != nil { + return err } - csr, hasCondition := modify(csr) if !hasCondition || force { - _, err = clientSet.CertificateSigningRequests().UpdateApproval(context.TODO(), csr, metav1.UpdateOptions{}) + switch modifiedCSR := modifiedCSR.(type) { + case *certificatesv1.CertificateSigningRequest: + _, err = clientSet.CertificatesV1().CertificateSigningRequests().UpdateApproval(context.TODO(), modifiedCSR.Name, modifiedCSR, metav1.UpdateOptions{}) + case *certificatesv1beta1.CertificateSigningRequest: + _, err = clientSet.CertificatesV1beta1().CertificateSigningRequests().UpdateApproval(context.TODO(), modifiedCSR, metav1.UpdateOptions{}) + default: + return fmt.Errorf("can only handle certificates.k8s.io CertificateSigningRequest objects, got %T", modifiedCSR) + } if errors.IsConflict(err) && i < 10 { if err := info.Get(); err != nil { return err @@ -255,8 +259,61 @@ func (o *CertificateOptions) modifyCertificateCondition(builder *resource.Builde return o.PrintObj(info.Object, o.Out) }) - if found == 0 { + if found == 0 && err == nil { fmt.Fprintf(o.Out, "No resources found\n") } return err } + +func addConditionIfNeeded(mustNotHaveConditionType, conditionType, reason, message string) func(runtime.Object) (runtime.Object, bool, error) { + return func(csr runtime.Object) (runtime.Object, bool, error) { + switch csr := csr.(type) { + case *certificatesv1.CertificateSigningRequest: + var alreadyHasCondition bool + for _, c := range csr.Status.Conditions { + if string(c.Type) == mustNotHaveConditionType { + return nil, false, fmt.Errorf("certificate signing request %q is already %s", csr.Name, c.Type) + } + if string(c.Type) == conditionType { + alreadyHasCondition = true + } + } + if alreadyHasCondition { + return csr, true, nil + } + csr.Status.Conditions = append(csr.Status.Conditions, certificatesv1.CertificateSigningRequestCondition{ + Type: certificatesv1.RequestConditionType(conditionType), + Status: corev1.ConditionTrue, + Reason: reason, + Message: message, + LastUpdateTime: metav1.Now(), + }) + return csr, false, nil + + case *certificatesv1beta1.CertificateSigningRequest: + var alreadyHasCondition bool + for _, c := range csr.Status.Conditions { + if string(c.Type) == mustNotHaveConditionType { + return nil, false, fmt.Errorf("certificate signing request %q is already %s", csr.Name, c.Type) + } + if string(c.Type) == conditionType { + alreadyHasCondition = true + } + } + if alreadyHasCondition { + return csr, true, nil + } + csr.Status.Conditions = append(csr.Status.Conditions, certificatesv1beta1.CertificateSigningRequestCondition{ + Type: certificatesv1beta1.RequestConditionType(conditionType), + Status: corev1.ConditionTrue, + Reason: reason, + Message: message, + LastUpdateTime: metav1.Now(), + }) + return csr, false, nil + + default: + return csr, false, nil + } + } +} diff --git a/pkg/cmd/certificates/certificates_test.go b/pkg/cmd/certificates/certificates_test.go new file mode 100644 index 00000000..847e3c6b --- /dev/null +++ b/pkg/cmd/certificates/certificates_test.go @@ -0,0 +1,338 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package certificates + +import ( + "bytes" + "io/ioutil" + "net/http" + "reflect" + "strings" + "testing" + + "github.com/spf13/cobra" + + certificatesv1 "k8s.io/api/certificates/v1" + certificatesv1beta1 "k8s.io/api/certificates/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/resource" + "k8s.io/client-go/rest/fake" + cmdtesting "k8s.io/kubectl/pkg/cmd/testing" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + "k8s.io/kubectl/pkg/scheme" +) + +func TestCertificates(t *testing.T) { + testcases := []struct { + name string + nov1 bool + nov1beta1 bool + command string + force bool + args []string + expectFailure bool + expectActions []string + expectOutput string + expectErrOutput string + }{ + { + name: "approve existing", + command: "approve", + args: []string{"existing"}, + expectActions: []string{ + `GET /apis/certificates.k8s.io/v1/certificatesigningrequests/existing`, // unstructured get + `GET /apis/certificates.k8s.io/v1/certificatesigningrequests/existing`, // typed get + `PUT /apis/certificates.k8s.io/v1/certificatesigningrequests/existing/approval`, + }, + expectOutput: `approved`, + }, + { + name: "approve existing, no v1", + nov1: true, + command: "approve", + args: []string{"certificatesigningrequests.v1beta1.certificates.k8s.io/existing"}, + expectActions: []string{ + `GET /apis/certificates.k8s.io/v1beta1/certificatesigningrequests/existing`, // unstructured get + `GET /apis/certificates.k8s.io/v1/certificatesigningrequests/existing`, // typed get, 404 + `GET /apis/certificates.k8s.io/v1beta1/certificatesigningrequests/existing`, // typed get fallback + `PUT /apis/certificates.k8s.io/v1beta1/certificatesigningrequests/existing/approval`, + }, + expectOutput: `approved`, + }, + { + name: "approve existing, no v1 or v1beta1", + nov1: true, + nov1beta1: true, + command: "approve", + args: []string{"existing"}, + expectActions: []string{ + `GET /apis/certificates.k8s.io/v1/certificatesigningrequests/existing`, // unstructured get, 404 + }, + expectFailure: true, + expectErrOutput: `could not find the requested resource`, + }, + { + name: "approve already approved", + command: "approve", + args: []string{"approved"}, + expectActions: []string{ + `GET /apis/certificates.k8s.io/v1/certificatesigningrequests/approved`, // unstructured get + `GET /apis/certificates.k8s.io/v1/certificatesigningrequests/approved`, // typed get + }, + expectOutput: `approved`, + }, + { + name: "approve already approved, force", + command: "approve", + args: []string{"approved"}, + force: true, + expectActions: []string{ + `GET /apis/certificates.k8s.io/v1/certificatesigningrequests/approved`, // unstructured get + `GET /apis/certificates.k8s.io/v1/certificatesigningrequests/approved`, // typed get + `PUT /apis/certificates.k8s.io/v1/certificatesigningrequests/approved/approval`, + }, + expectOutput: `approved`, + }, + { + name: "approve already denied", + command: "approve", + args: []string{"denied"}, + expectActions: []string{ + `GET /apis/certificates.k8s.io/v1/certificatesigningrequests/denied`, // unstructured get + `GET /apis/certificates.k8s.io/v1/certificatesigningrequests/denied`, // typed get + }, + expectFailure: true, + expectErrOutput: `is already Denied`, + }, + + { + name: "deny existing", + command: "deny", + args: []string{"existing"}, + expectActions: []string{ + `GET /apis/certificates.k8s.io/v1/certificatesigningrequests/existing`, // unstructured get + `GET /apis/certificates.k8s.io/v1/certificatesigningrequests/existing`, // typed get + `PUT /apis/certificates.k8s.io/v1/certificatesigningrequests/existing/approval`, + }, + expectOutput: `denied`, + }, + { + name: "deny existing, no v1", + nov1: true, + command: "deny", + args: []string{"certificatesigningrequests.v1beta1.certificates.k8s.io/existing"}, + expectActions: []string{ + `GET /apis/certificates.k8s.io/v1beta1/certificatesigningrequests/existing`, // unstructured get + `GET /apis/certificates.k8s.io/v1/certificatesigningrequests/existing`, // typed get, 404 + `GET /apis/certificates.k8s.io/v1beta1/certificatesigningrequests/existing`, // typed get fallback + `PUT /apis/certificates.k8s.io/v1beta1/certificatesigningrequests/existing/approval`, + }, + expectOutput: `denied`, + }, + { + name: "deny existing, no v1 or v1beta1", + nov1: true, + nov1beta1: true, + command: "deny", + args: []string{"existing"}, + expectActions: []string{ + `GET /apis/certificates.k8s.io/v1/certificatesigningrequests/existing`, // unstructured get, 404 + }, + expectFailure: true, + expectErrOutput: `could not find the requested resource`, + }, + { + name: "deny already denied", + command: "deny", + args: []string{"denied"}, + expectActions: []string{ + `GET /apis/certificates.k8s.io/v1/certificatesigningrequests/denied`, // unstructured get + `GET /apis/certificates.k8s.io/v1/certificatesigningrequests/denied`, // typed get + }, + expectOutput: `denied`, + }, + { + name: "deny already denied, force", + command: "deny", + args: []string{"denied"}, + force: true, + expectActions: []string{ + `GET /apis/certificates.k8s.io/v1/certificatesigningrequests/denied`, // unstructured get + `GET /apis/certificates.k8s.io/v1/certificatesigningrequests/denied`, // typed get + `PUT /apis/certificates.k8s.io/v1/certificatesigningrequests/denied/approval`, + }, + expectOutput: `denied`, + }, + { + name: "deny already approved", + command: "deny", + args: []string{"approved"}, + expectActions: []string{ + `GET /apis/certificates.k8s.io/v1/certificatesigningrequests/approved`, // unstructured get + `GET /apis/certificates.k8s.io/v1/certificatesigningrequests/approved`, // typed get + }, + expectFailure: true, + expectErrOutput: `is already Approved`, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + tf := cmdtesting.NewTestFactory().WithNamespace("test") + defer tf.Cleanup() + codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) + + existingV1 := &certificatesv1.CertificateSigningRequest{ + TypeMeta: metav1.TypeMeta{APIVersion: "certificates.k8s.io/v1", Kind: "CertificateSigningRequest"}, + ObjectMeta: metav1.ObjectMeta{Name: "existing"}, + } + existingV1beta1 := &certificatesv1beta1.CertificateSigningRequest{ + TypeMeta: metav1.TypeMeta{APIVersion: "certificates.k8s.io/v1beta1", Kind: "CertificateSigningRequest"}, + ObjectMeta: metav1.ObjectMeta{Name: "existing"}, + } + + approvedV1 := &certificatesv1.CertificateSigningRequest{ + TypeMeta: metav1.TypeMeta{APIVersion: "certificates.k8s.io/v1", Kind: "CertificateSigningRequest"}, + ObjectMeta: metav1.ObjectMeta{Name: "approved"}, + Status: certificatesv1.CertificateSigningRequestStatus{Conditions: []certificatesv1.CertificateSigningRequestCondition{{Type: certificatesv1.CertificateApproved}}}, + } + approvedV1beta1 := &certificatesv1beta1.CertificateSigningRequest{ + TypeMeta: metav1.TypeMeta{APIVersion: "certificates.k8s.io/v1beta1", Kind: "CertificateSigningRequest"}, + ObjectMeta: metav1.ObjectMeta{Name: "existing"}, + Status: certificatesv1beta1.CertificateSigningRequestStatus{Conditions: []certificatesv1beta1.CertificateSigningRequestCondition{{Type: certificatesv1beta1.CertificateApproved}}}, + } + + deniedV1 := &certificatesv1.CertificateSigningRequest{ + TypeMeta: metav1.TypeMeta{APIVersion: "certificates.k8s.io/v1", Kind: "CertificateSigningRequest"}, + ObjectMeta: metav1.ObjectMeta{Name: "denied"}, + Status: certificatesv1.CertificateSigningRequestStatus{Conditions: []certificatesv1.CertificateSigningRequestCondition{{Type: certificatesv1.CertificateDenied}}}, + } + deniedV1beta1 := &certificatesv1beta1.CertificateSigningRequest{ + TypeMeta: metav1.TypeMeta{APIVersion: "certificates.k8s.io/v1beta1", Kind: "CertificateSigningRequest"}, + ObjectMeta: metav1.ObjectMeta{Name: "denied"}, + Status: certificatesv1beta1.CertificateSigningRequestStatus{Conditions: []certificatesv1beta1.CertificateSigningRequestCondition{{Type: certificatesv1beta1.CertificateDenied}}}, + } + + actions := []string{} + fakeClient := fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { + actions = append(actions, req.Method+" "+req.URL.Path) + switch p, m := req.URL.Path, req.Method; { + case tc.nov1 && strings.HasPrefix(p, "/apis/certificates.k8s.io/v1/"): + return &http.Response{StatusCode: http.StatusNotFound, Body: ioutil.NopCloser(bytes.NewBuffer([]byte{}))}, nil + case tc.nov1beta1 && strings.HasPrefix(p, "/apis/certificates.k8s.io/v1beta1/"): + return &http.Response{StatusCode: http.StatusNotFound, Body: ioutil.NopCloser(bytes.NewBuffer([]byte{}))}, nil + + case p == "/apis/certificates.k8s.io/v1/certificatesigningrequests/missing" && m == http.MethodGet: + return &http.Response{StatusCode: http.StatusNotFound}, nil + case p == "/apis/certificates.k8s.io/v1beta1/certificatesigningrequests/missing" && m == http.MethodGet: + return &http.Response{StatusCode: http.StatusNotFound}, nil + + case p == "/apis/certificates.k8s.io/v1/certificatesigningrequests/existing" && m == http.MethodGet: + return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, existingV1)}, nil + case p == "/apis/certificates.k8s.io/v1/certificatesigningrequests/existing/approval" && m == http.MethodPut: + return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, existingV1)}, nil + case p == "/apis/certificates.k8s.io/v1beta1/certificatesigningrequests/existing" && m == http.MethodGet: + return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, existingV1beta1)}, nil + case p == "/apis/certificates.k8s.io/v1beta1/certificatesigningrequests/existing/approval" && m == http.MethodPut: + return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, existingV1beta1)}, nil + + case p == "/apis/certificates.k8s.io/v1/certificatesigningrequests/approved" && m == http.MethodGet: + return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, approvedV1)}, nil + case p == "/apis/certificates.k8s.io/v1/certificatesigningrequests/approved/approval" && m == http.MethodPut: + return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, approvedV1)}, nil + case p == "/apis/certificates.k8s.io/v1beta1/certificatesigningrequests/approved" && m == http.MethodGet: + return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, approvedV1beta1)}, nil + case p == "/apis/certificates.k8s.io/v1beta1/certificatesigningrequests/approved/approval" && m == http.MethodPut: + return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, approvedV1beta1)}, nil + + case p == "/apis/certificates.k8s.io/v1/certificatesigningrequests/denied" && m == http.MethodGet: + return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, deniedV1)}, nil + case p == "/apis/certificates.k8s.io/v1/certificatesigningrequests/denied/approval" && m == http.MethodPut: + return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, deniedV1)}, nil + case p == "/apis/certificates.k8s.io/v1beta1/certificatesigningrequests/denied" && m == http.MethodGet: + return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, deniedV1beta1)}, nil + case p == "/apis/certificates.k8s.io/v1beta1/certificatesigningrequests/denied/approval" && m == http.MethodPut: + return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, deniedV1beta1)}, nil + + default: + t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) + return nil, nil + } + }) + tf.UnstructuredClientForMappingFunc = func(gv schema.GroupVersion) (resource.RESTClient, error) { + versionedAPIPath := "" + if gv.Group == "" { + versionedAPIPath = "/api/" + gv.Version + } else { + versionedAPIPath = "/apis/" + gv.Group + "/" + gv.Version + } + return &fake.RESTClient{ + VersionedAPIPath: versionedAPIPath, + NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, + Client: fakeClient, + }, nil + } + tf.Client = &fake.RESTClient{ + NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, + Client: fakeClient, + } + streams, _, buf, errbuf := genericclioptions.NewTestIOStreams() + + defer func() { + // Restore cmdutil behavior. + cmdutil.DefaultBehaviorOnFatal() + }() + // Check exit code. + cmdutil.BehaviorOnFatal(func(e string, code int) { + if !tc.expectFailure { + t.Log(e) + t.Errorf("unexpected failure exit code %d", code) + } + errbuf.Write([]byte(e)) + }) + + var cmd *cobra.Command + switch tc.command { + case "approve": + cmd = NewCmdCertificateApprove(tf, streams) + case "deny": + cmd = NewCmdCertificateDeny(tf, streams) + default: + t.Errorf("unknown command: %s", tc.command) + } + + if tc.force { + cmd.Flags().Set("force", "true") + } + cmd.Run(cmd, tc.args) + + if !strings.Contains(buf.String(), tc.expectOutput) { + t.Errorf("expected output to contain %q:\n%s", tc.expectOutput, buf.String()) + } + if !strings.Contains(errbuf.String(), tc.expectErrOutput) { + t.Errorf("expected error output to contain %q:\n%s", tc.expectErrOutput, errbuf.String()) + } + if !reflect.DeepEqual(tc.expectActions, actions) { + t.Logf("stdout:\n%s", buf.String()) + t.Logf("stderr:\n%s", errbuf.String()) + t.Errorf("expected\n%s\ngot\n%s", strings.Join(tc.expectActions, "\n"), strings.Join(actions, "\n")) + } + }) + } +} diff --git a/pkg/cmd/testing/fake.go b/pkg/cmd/testing/fake.go index 7adb42ed..d6b3007a 100644 --- a/pkg/cmd/testing/fake.go +++ b/pkg/cmd/testing/fake.go @@ -519,6 +519,7 @@ func (f *TestFactory) KubernetesClientSet() (*kubernetes.Clientset, error) { clientset.AutoscalingV2beta1().RESTClient().(*restclient.RESTClient).Client = fakeClient.Client clientset.BatchV1().RESTClient().(*restclient.RESTClient).Client = fakeClient.Client clientset.BatchV2alpha1().RESTClient().(*restclient.RESTClient).Client = fakeClient.Client + clientset.CertificatesV1().RESTClient().(*restclient.RESTClient).Client = fakeClient.Client clientset.CertificatesV1beta1().RESTClient().(*restclient.RESTClient).Client = fakeClient.Client clientset.ExtensionsV1beta1().RESTClient().(*restclient.RESTClient).Client = fakeClient.Client clientset.RbacV1alpha1().RESTClient().(*restclient.RESTClient).Client = fakeClient.Client