From 77e9a6f47fb190b9b967c9edd11e1ca24c52ed56 Mon Sep 17 00:00:00 2001 From: Amir Mofasser Date: Tue, 13 Oct 2020 16:54:17 +0200 Subject: [PATCH] Add `create ingress` command to `cmd/kubectl` Add `create ingress` unit tests Move src code to staging dir Update create command to reflect new API Replaced deprecated `extensions` api with `networking` Fix `missing strict dependencies` Update BUILD Update BUILD Fix commit conflict with upstream Update after review * Removed obsolete files * Moved v1beta to v1 api Fixed gofmt Fixed deps imports Merge with PR #94327 Revert changes Revert go.mod Revert BUILD No need to update generated BUILD Add required deps to BUILD Update BUILD Kubernetes-commit: be45584a03aa9f3a3fd73e3d9cc69545da92616e --- pkg/cmd/create/create.go | 1 + pkg/cmd/create/create_ingress.go | 223 ++++++++++++++++++++++++++ pkg/cmd/create/create_ingress_test.go | 129 +++++++++++++++ 3 files changed, 353 insertions(+) create mode 100644 pkg/cmd/create/create_ingress.go create mode 100644 pkg/cmd/create/create_ingress_test.go diff --git a/pkg/cmd/create/create.go b/pkg/cmd/create/create.go index 44c4ede4..d5a9b95c 100644 --- a/pkg/cmd/create/create.go +++ b/pkg/cmd/create/create.go @@ -152,6 +152,7 @@ func NewCmdCreate(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cob cmd.AddCommand(NewCmdCreatePriorityClass(f, ioStreams)) cmd.AddCommand(NewCmdCreateJob(f, ioStreams)) cmd.AddCommand(NewCmdCreateCronJob(f, ioStreams)) + cmd.AddCommand(NewCmdCreateIngress(f, ioStreams)) return cmd } diff --git a/pkg/cmd/create/create_ingress.go b/pkg/cmd/create/create_ingress.go new file mode 100644 index 00000000..311875aa --- /dev/null +++ b/pkg/cmd/create/create_ingress.go @@ -0,0 +1,223 @@ +/* +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 create + +import ( + "context" + "fmt" + "strconv" + + "github.com/spf13/cobra" + + "k8s.io/api/networking/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/resource" + networkingv1client "k8s.io/client-go/kubernetes/typed/networking/v1" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + "k8s.io/kubectl/pkg/scheme" + "k8s.io/kubectl/pkg/util/i18n" + "k8s.io/kubectl/pkg/util/templates" +) + +var ( + ingressLong = templates.LongDesc(i18n.T(` + Create an ingress with the specified name.`)) + + ingressExample = templates.Examples(i18n.T(` + # Create a new ingress named my-app. + kubectl create ingress my-app --host=foo.bar.com --service-name=my-svc`)) +) + +// CreateIngressOptions is returned by NewCmdCreateIngress +type CreateIngressOptions struct { + PrintFlags *genericclioptions.PrintFlags + + PrintObj func(obj runtime.Object) error + + Name string + Host string + ServiceName string + ServicePort string + Path string + + Namespace string + Client *networkingv1client.NetworkingV1Client + DryRunStrategy cmdutil.DryRunStrategy + DryRunVerifier *resource.DryRunVerifier + Builder *resource.Builder + Cmd *cobra.Command + + genericclioptions.IOStreams +} + +// NewCreateCreateIngressOptions creates and returns an instance of CreateIngressOptions +func NewCreateCreateIngressOptions(ioStreams genericclioptions.IOStreams) *CreateIngressOptions { + return &CreateIngressOptions{ + PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme), + IOStreams: ioStreams, + } +} + +// NewCmdCreateIngress is a macro command to create a new ingress. +func NewCmdCreateIngress(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command { + o := NewCreateCreateIngressOptions(ioStreams) + + cmd := &cobra.Command{ + Use: "ingress NAME --host=hostname| --service-name=servicename [--service-port=serviceport] [--path=path] [--dry-run]", + Aliases: []string{"ing"}, + Short: i18n.T("Create an ingress with the specified name."), + Long: ingressLong, + Example: ingressExample, + Run: func(cmd *cobra.Command, args []string) { + cmdutil.CheckErr(o.Complete(f, cmd, args)) + cmdutil.CheckErr(o.Validate()) + cmdutil.CheckErr(o.Run()) + }, + } + + o.PrintFlags.AddFlags(cmd) + + cmdutil.AddApplyAnnotationFlags(cmd) + cmdutil.AddValidateFlags(cmd) + cmdutil.AddDryRunFlag(cmd) + cmd.Flags().StringVar(&o.Host, "host", o.Host, i18n.T("Host name this Ingress should route traffic on")) + cmd.Flags().StringVar(&o.ServiceName, "service-name", o.ServiceName, i18n.T("Service this Ingress should route traffic to")) + cmd.Flags().StringVar(&o.ServicePort, "service-port", o.ServicePort, "Port name or number of the Service to route traffic to") + cmd.Flags().StringVar(&o.Path, "path", o.Path, "Path on which to route traffic to") + cmd.MarkFlagRequired("host") + cmd.MarkFlagRequired("service-name") + return cmd +} + +// Complete completes all the options +func (o *CreateIngressOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { + name, err := NameFromCommandArgs(cmd, args) + if err != nil { + return err + } + o.Name = name + + clientConfig, err := f.ToRESTConfig() + if err != nil { + return err + } + o.Client, err = networkingv1client.NewForConfig(clientConfig) + if err != nil { + return err + } + + o.Namespace, _, err = f.ToRawKubeConfigLoader().Namespace() + if err != nil { + return err + } + o.Builder = f.NewBuilder() + o.Cmd = cmd + + o.DryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd) + if err != nil { + return err + } + dynamicClient, err := f.DynamicClient() + if err != nil { + return err + } + discoveryClient, err := f.ToDiscoveryClient() + if err != nil { + return err + } + o.DryRunVerifier = resource.NewDryRunVerifier(dynamicClient, discoveryClient) + cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.DryRunStrategy) + printer, err := o.PrintFlags.ToPrinter() + if err != nil { + return err + } + o.PrintObj = func(obj runtime.Object) error { + return printer.PrintObj(obj, o.Out) + } + + return nil +} + +func (o *CreateIngressOptions) Validate() error { + return nil +} + +// Run performs the execution of 'create ingress' sub command +func (o *CreateIngressOptions) Run() error { + var ingress *v1.Ingress + ingress = o.createIngress() + + if o.DryRunStrategy != cmdutil.DryRunClient { + createOptions := metav1.CreateOptions{} + if o.DryRunStrategy == cmdutil.DryRunServer { + if err := o.DryRunVerifier.HasSupport(ingress.GroupVersionKind()); err != nil { + return err + } + createOptions.DryRun = []string{metav1.DryRunAll} + } + var err error + ingress, err = o.Client.Ingresses(o.Namespace).Create(context.TODO(), ingress, createOptions) + if err != nil { + return fmt.Errorf("failed to create ingress: %v", err) + } + } + + return o.PrintObj(ingress) +} + +func (o *CreateIngressOptions) createIngress() *v1.Ingress { + i := &v1.Ingress{ + TypeMeta: metav1.TypeMeta{APIVersion: v1.SchemeGroupVersion.String(), Kind: "Ingress"}, + ObjectMeta: metav1.ObjectMeta{ + Name: o.Name, + }, + Spec: v1.IngressSpec{ + Rules: []v1.IngressRule{ + { + Host: o.Host, + IngressRuleValue: v1.IngressRuleValue{ + HTTP: &v1.HTTPIngressRuleValue{ + Paths: []v1.HTTPIngressPath{ + { + Path: o.Path, + Backend: v1.IngressBackend{ + Service: &v1.IngressServiceBackend{ + Name: o.ServiceName, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + + var port v1.ServiceBackendPort + if n, err := strconv.Atoi(o.ServicePort); err != nil { + port.Name = o.ServicePort + } else { + port.Number = int32(n) + } + + i.Spec.Rules[0].IngressRuleValue.HTTP.Paths[0].Backend.Service.Port = port + + return i +} diff --git a/pkg/cmd/create/create_ingress_test.go b/pkg/cmd/create/create_ingress_test.go new file mode 100644 index 00000000..c37b54df --- /dev/null +++ b/pkg/cmd/create/create_ingress_test.go @@ -0,0 +1,129 @@ +/* +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 create + +import ( + "strings" + "testing" + + "k8s.io/api/networking/v1" + apiequality "k8s.io/apimachinery/pkg/api/equality" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestCreateIngress(t *testing.T) { + ingressName := "fake-ingress" + tests := map[string]struct { + name string + host string + serviceName string + servicePort string + path string + expectErrMsg string + expect *v1.Ingress + }{ + "test-valid-case": { + name: "fake-ingress", + host: "foo.bar.com", + serviceName: "fake-service", + servicePort: "https", + path: "/api", + expect: &v1.Ingress{ + TypeMeta: metav1.TypeMeta{APIVersion: v1.SchemeGroupVersion.String(), Kind: "Ingress"}, + ObjectMeta: metav1.ObjectMeta{ + Name: "fake-ingress", + }, + Spec: v1.IngressSpec{ + Rules: []v1.IngressRule{ + { + Host: "foo.bar.com", + IngressRuleValue: v1.IngressRuleValue{ + HTTP: &v1.HTTPIngressRuleValue{ + Paths: []v1.HTTPIngressPath{ + { + Path: "/api", + Backend: v1.IngressBackend{ + Service: &v1.IngressServiceBackend{ + Name: "fake-service", + Port: v1.ServiceBackendPort{ + Name: "https", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + o := &CreateIngressOptions{ + Name: ingressName, + Host: tc.host, + ServiceName: tc.serviceName, + ServicePort: tc.servicePort, + Path: tc.path, + } + ingress := o.createIngress() + if !apiequality.Semantic.DeepEqual(ingress, tc.expect) { + t.Errorf("expected:\n%+v\ngot:\n%+v", tc.expect, ingress) + } + }) + } + +} + +func TestCreateIngressValidation(t *testing.T) { + tests := map[string]struct { + name string + host string + serviceName string + servicePort string + path string + expect string + }{ + "test-missing-host": { + serviceName: "fake-ingress", + expect: "--host must be specified", + }, + "test-missing-service": { + host: "foo.bar.com", + expect: "--service-name must be specified", + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + o := &CreateIngressOptions{ + Host: tc.host, + ServiceName: tc.serviceName, + ServicePort: tc.servicePort, + Path: tc.path, + } + + err := o.Validate() + if err != nil && !strings.Contains(err.Error(), tc.expect) { + t.Errorf("unexpected error: %v", err) + } + }) + } +}