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
This commit is contained in:
Amir Mofasser 2020-10-13 16:54:17 +02:00 committed by Kubernetes Publisher
parent 45d8f16e6b
commit 77e9a6f47f
3 changed files with 353 additions and 0 deletions

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}
})
}
}