Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove the dependency between create deploy command and generators #90676

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 3 additions & 1 deletion staging/src/k8s.io/kubectl/pkg/cmd/create/BUILD
Expand Up @@ -24,6 +24,7 @@ go_library(
importpath = "k8s.io/kubectl/pkg/cmd/create",
visibility = ["//build/visible_to:pkg_kubectl_cmd_create_CONSUMERS"],
deps = [
"//staging/src/k8s.io/api/apps/v1:go_default_library",
"//staging/src/k8s.io/api/batch/v1:go_default_library",
"//staging/src/k8s.io/api/batch/v1beta1:go_default_library",
"//staging/src/k8s.io/api/core/v1:go_default_library",
Expand All @@ -33,11 +34,13 @@ go_library(
"//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/rand:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library",
"//staging/src/k8s.io/cli-runtime/pkg/genericclioptions:go_default_library",
"//staging/src/k8s.io/cli-runtime/pkg/printers:go_default_library",
"//staging/src/k8s.io/cli-runtime/pkg/resource:go_default_library",
"//staging/src/k8s.io/client-go/dynamic:go_default_library",
"//staging/src/k8s.io/client-go/kubernetes/typed/apps/v1:go_default_library",
"//staging/src/k8s.io/client-go/kubernetes/typed/batch/v1:go_default_library",
"//staging/src/k8s.io/client-go/kubernetes/typed/batch/v1beta1:go_default_library",
"//staging/src/k8s.io/client-go/kubernetes/typed/rbac/v1:go_default_library",
Expand Down Expand Up @@ -95,7 +98,6 @@ go_test(
"//staging/src/k8s.io/client-go/rest/fake:go_default_library",
"//staging/src/k8s.io/kubectl/pkg/cmd/testing:go_default_library",
"//staging/src/k8s.io/kubectl/pkg/cmd/util:go_default_library",
"//staging/src/k8s.io/kubectl/pkg/generate/versioned:go_default_library",
"//staging/src/k8s.io/kubectl/pkg/scheme:go_default_library",
"//vendor/github.com/stretchr/testify/assert:go_default_library",
],
Expand Down
213 changes: 142 additions & 71 deletions staging/src/k8s.io/kubectl/pkg/cmd/create/create_deployment.go
Expand Up @@ -17,12 +17,21 @@ limitations under the License.
package create

import (
"github.com/spf13/cobra"
"context"
"fmt"
"strings"

"github.com/spf13/cobra"
appsv1 "k8s.io/api/apps/v1"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
utilrand "k8s.io/apimachinery/pkg/util/rand"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/cli-runtime/pkg/resource"
appsv1client "k8s.io/client-go/kubernetes/typed/apps/v1"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
"k8s.io/kubectl/pkg/generate"
generateversioned "k8s.io/kubectl/pkg/generate/versioned"
"k8s.io/kubectl/pkg/scheme"
"k8s.io/kubectl/pkg/util/i18n"
"k8s.io/kubectl/pkg/util/templates"
)
Expand All @@ -38,22 +47,35 @@ var (

// DeploymentOpts is returned by NewCmdCreateDeployment
type DeploymentOpts struct {
CreateSubcommandOptions *CreateSubcommandOptions
PrintFlags *genericclioptions.PrintFlags
PrintObj func(obj runtime.Object) error

Name string
Images []string
Namespace string
EnforceNamespace bool
FieldManager string

Client appsv1client.AppsV1Interface
DryRunStrategy cmdutil.DryRunStrategy
DryRunVerifier *resource.DryRunVerifier

genericclioptions.IOStreams
}

// NewCmdCreateDeployment is a macro command to create a new deployment.
// This command is better known to users as `kubectl create deployment`.
// Note that this command overlaps significantly with the `kubectl run` command.
func NewCmdCreateDeployment(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
options := &DeploymentOpts{
CreateSubcommandOptions: NewCreateSubcommandOptions(ioStreams),
PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme),
IOStreams: ioStreams,
}

cmd := &cobra.Command{
Use: "deployment NAME --image=image [--dry-run=server|client|none]",
DisableFlagsInUseLine: true,
Aliases: []string{"deploy"},
Short: i18n.T("Create a deployment with the specified name."),
Short: deploymentLong,
Long: deploymentLong,
Example: deploymentExample,
Run: func(cmd *cobra.Command, args []string) {
Expand All @@ -62,56 +84,16 @@ func NewCmdCreateDeployment(f cmdutil.Factory, ioStreams genericclioptions.IOStr
},
}

options.CreateSubcommandOptions.PrintFlags.AddFlags(cmd)
options.PrintFlags.AddFlags(cmd)

cmdutil.AddApplyAnnotationFlags(cmd)
cmdutil.AddValidateFlags(cmd)
cmdutil.AddGeneratorFlags(cmd, "")
cmd.Flags().StringSlice("image", []string{}, "Image name to run.")
cmd.MarkFlagRequired("image")
cmdutil.AddFieldManagerFlagVar(cmd, &options.CreateSubcommandOptions.FieldManager, "kubectl-create")
return cmd
}

// generatorFromName returns the appropriate StructuredGenerator based on the
// generatorName. If the generatorName is unrecognized, then return (nil,
// false).
func generatorFromName(
generatorName string,
imageNames []string,
deploymentName string,
) (generate.StructuredGenerator, bool) {

switch generatorName {
case generateversioned.DeploymentBasicAppsV1GeneratorName:
generator := &generateversioned.DeploymentBasicAppsGeneratorV1{
BaseDeploymentGenerator: generateversioned.BaseDeploymentGenerator{
Name: deploymentName,
Images: imageNames,
},
}
return generator, true

case generateversioned.DeploymentBasicAppsV1Beta1GeneratorName:
generator := &generateversioned.DeploymentBasicAppsGeneratorV1Beta1{
BaseDeploymentGenerator: generateversioned.BaseDeploymentGenerator{
Name: deploymentName,
Images: imageNames,
},
}
return generator, true

case generateversioned.DeploymentBasicV1Beta1GeneratorName:
generator := &generateversioned.DeploymentBasicGeneratorV1{
BaseDeploymentGenerator: generateversioned.BaseDeploymentGenerator{
Name: deploymentName,
Images: imageNames,
},
}
return generator, true
}
cmd.Flags().StringSliceVar(&options.Images, "image", []string{}, "Image name to run.")
_ = cmd.MarkFlagRequired("image")
cmdutil.AddFieldManagerFlagVar(cmd, &options.FieldManager, "kubectl-create")

return nil, false
return cmd
}

// Complete completes all the options
Expand All @@ -120,37 +102,126 @@ func (o *DeploymentOpts) Complete(f cmdutil.Factory, cmd *cobra.Command, args []
if err != nil {
return err
}
o.Name = name

clientset, err := f.KubernetesClientSet()
clientConfig, err := f.ToRESTConfig()
if err != nil {
return err
}
o.Client, err = appsv1client.NewForConfig(clientConfig)
if err != nil {
return err
}

generatorName := cmdutil.GetFlagString(cmd, "generator")
o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace()
if err != nil {
return err
}

if len(generatorName) == 0 {
generatorName = generateversioned.DeploymentBasicAppsV1GeneratorName
generatorNameTemp, err := generateversioned.FallbackGeneratorNameIfNecessary(generatorName, clientset.Discovery(), o.CreateSubcommandOptions.ErrOut)
if err != nil {
return err
}
if generatorNameTemp != generatorName {
cmdutil.Warning(o.CreateSubcommandOptions.ErrOut, generatorName, generatorNameTemp)
} else {
generatorName = generatorNameTemp
}
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)

imageNames := cmdutil.GetFlagStringSlice(cmd, "image")
generator, ok := generatorFromName(generatorName, imageNames, name)
if !ok {
return errUnsupportedGenerator(cmd, generatorName)
printer, err := o.PrintFlags.ToPrinter()
if err != nil {
return err
}
o.PrintObj = func(obj runtime.Object) error {
return printer.PrintObj(obj, o.Out)
}

return o.CreateSubcommandOptions.Complete(f, cmd, args, generator)
return nil
}

// Run performs the execution of 'create deployment' sub command
func (o *DeploymentOpts) Run() error {
return o.CreateSubcommandOptions.Run()
one := int32(1)
labels := map[string]string{"app": o.Name}
selector := metav1.LabelSelector{MatchLabels: labels}
namespace := ""
if o.EnforceNamespace {
namespace = o.Namespace
}

deploy := &appsv1.Deployment{
TypeMeta: metav1.TypeMeta{APIVersion: appsv1.SchemeGroupVersion.String(), Kind: "Deployment"},
ObjectMeta: metav1.ObjectMeta{
Name: o.Name,
Labels: labels,
Namespace: namespace,
},
Spec: appsv1.DeploymentSpec{
Replicas: &one,
Selector: &selector,
Template: v1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: labels,
},
Spec: o.buildPodSpec(),
},
},
}

if o.DryRunStrategy != cmdutil.DryRunClient {
createOptions := metav1.CreateOptions{}
if o.FieldManager != "" {
createOptions.FieldManager = o.FieldManager
}
if o.DryRunStrategy == cmdutil.DryRunServer {
if err := o.DryRunVerifier.HasSupport(deploy.GroupVersionKind()); err != nil {
return err
}
createOptions.DryRun = []string{metav1.DryRunAll}
}
var err error
deploy, err = o.Client.Deployments(o.Namespace).Create(context.TODO(), deploy, createOptions)
if err != nil {
return fmt.Errorf("failed to create deployment: %v", err)
}
}

return o.PrintObj(deploy)
}

// buildPodSpec parses the image strings and assemble them into the Containers
// of a PodSpec. This is all you need to create the PodSpec for a deployment.
func (o *DeploymentOpts) buildPodSpec() v1.PodSpec {
podSpec := v1.PodSpec{Containers: []v1.Container{}}
for _, imageString := range o.Images {
// Retain just the image name
imageSplit := strings.Split(imageString, "/")
name := imageSplit[len(imageSplit)-1]
// Remove any tag or hash
if strings.Contains(name, ":") {
name = strings.Split(name, ":")[0]
}
if strings.Contains(name, "@") {
name = strings.Split(name, "@")[0]
}
name = sanitizeAndUniquify(name)
podSpec.Containers = append(podSpec.Containers, v1.Container{Name: name, Image: imageString})
}
return podSpec
}

// sanitizeAndUniquify replaces characters like "." or "_" into "-" to follow DNS1123 rules.
// Then add random suffix to make it uniquified.
func sanitizeAndUniquify(name string) string {
if strings.ContainsAny(name, "_.") {
name = strings.Replace(name, "_", "-", -1)
name = strings.Replace(name, ".", "-", -1)
name = fmt.Sprintf("%s-%s", name, utilrand.String(5))
}
return name
}
Expand Up @@ -29,64 +29,9 @@ import (
"k8s.io/client-go/rest/fake"
cmdtesting "k8s.io/kubectl/pkg/cmd/testing"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
generateversioned "k8s.io/kubectl/pkg/generate/versioned"
"k8s.io/kubectl/pkg/scheme"
)

func Test_generatorFromName(t *testing.T) {
const (
nonsenseName = "not-a-real-generator-name"
basicName = generateversioned.DeploymentBasicV1Beta1GeneratorName
basicAppsV1Beta1Name = generateversioned.DeploymentBasicAppsV1Beta1GeneratorName
basicAppsV1Name = generateversioned.DeploymentBasicAppsV1GeneratorName
deploymentName = "deployment-name"
)
imageNames := []string{"image-1", "image-2"}

generator, ok := generatorFromName(nonsenseName, imageNames, deploymentName)
assert.Nil(t, generator)
assert.False(t, ok)

generator, ok = generatorFromName(basicName, imageNames, deploymentName)
assert.True(t, ok)

{
expectedGenerator := &generateversioned.DeploymentBasicGeneratorV1{
BaseDeploymentGenerator: generateversioned.BaseDeploymentGenerator{
Name: deploymentName,
Images: imageNames,
},
}
assert.Equal(t, expectedGenerator, generator)
}

generator, ok = generatorFromName(basicAppsV1Beta1Name, imageNames, deploymentName)
assert.True(t, ok)

{
expectedGenerator := &generateversioned.DeploymentBasicAppsGeneratorV1Beta1{
BaseDeploymentGenerator: generateversioned.BaseDeploymentGenerator{
Name: deploymentName,
Images: imageNames,
},
}
assert.Equal(t, expectedGenerator, generator)
}

generator, ok = generatorFromName(basicAppsV1Name, imageNames, deploymentName)
assert.True(t, ok)

{
expectedGenerator := &generateversioned.DeploymentBasicAppsGeneratorV1{
BaseDeploymentGenerator: generateversioned.BaseDeploymentGenerator{
Name: deploymentName,
Images: imageNames,
},
}
assert.Equal(t, expectedGenerator, generator)
}
}

func TestCreateDeployment(t *testing.T) {
depName := "jonny-dep"
tf := cmdtesting.NewTestFactory().WithNamespace("test")
Expand Down Expand Up @@ -139,11 +84,9 @@ func TestCreateDeploymentNoImage(t *testing.T) {
cmd := NewCmdCreateDeployment(tf, ioStreams)
cmd.Flags().Set("output", "name")
options := &DeploymentOpts{
CreateSubcommandOptions: &CreateSubcommandOptions{
PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme),
DryRunStrategy: cmdutil.DryRunClient,
IOStreams: ioStreams,
},
PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme),
DryRunStrategy: cmdutil.DryRunClient,
IOStreams: ioStreams,
}

err := options.Complete(tf, cmd, []string{depName})
Expand Down