diff --git a/pkg/cmd/create/create_secret.go b/pkg/cmd/create/create_secret.go index f9499e54..608c823e 100644 --- a/pkg/cmd/create/create_secret.go +++ b/pkg/cmd/create/create_secret.go @@ -17,17 +17,32 @@ limitations under the License. package create import ( + "context" + "fmt" + "io/ioutil" + "os" + "path" + "strings" + "github.com/spf13/cobra" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation" "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/resource" + corev1client "k8s.io/client-go/kubernetes/typed/core/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" + "k8s.io/kubectl/pkg/util/hash" "k8s.io/kubectl/pkg/util/i18n" "k8s.io/kubectl/pkg/util/templates" ) -// NewCmdCreateSecret groups subcommands to create various types of secrets +// NewCmdCreateSecret groups subcommands to create various types of secrets. +// This is the entry point of create_secret.go which will be called by create.go func NewCmdCreateSecret(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command { cmd := &cobra.Command{ Use: "secret", @@ -73,16 +88,48 @@ var ( kubectl create secret generic my-secret --from-env-file=path/to/bar.env`)) ) -// SecretGenericOpts holds the options for 'create secret' sub command -type SecretGenericOpts struct { - CreateSubcommandOptions *CreateSubcommandOptions +// CreateSecretOptions holds the options for 'create secret' sub command +type CreateSecretOptions struct { + // PrintFlags holds options necessary for obtaining a printer + PrintFlags *genericclioptions.PrintFlags + PrintObj func(obj runtime.Object) error + + // Name of secret (required) + Name string + // Type of secret (optional) + Type string + // FileSources to derive the secret from (optional) + FileSources []string + // LiteralSources to derive the secret from (optional) + LiteralSources []string + // EnvFileSource to derive the secret from (optional) + EnvFileSource string + // AppendHash; if true, derive a hash from the Secret data and type and append it to the name + AppendHash bool + + FieldManager string + CreateAnnotation bool + Namespace string + EnforceNamespace bool + + Client corev1client.CoreV1Interface + DryRunStrategy cmdutil.DryRunStrategy + DryRunVerifier *resource.DryRunVerifier + + genericclioptions.IOStreams +} + +// NewSecretOptions creates a new *CreateSecretOptions with default value +func NewSecretOptions(ioStreams genericclioptions.IOStreams) *CreateSecretOptions { + return &CreateSecretOptions{ + PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme), + IOStreams: ioStreams, + } } // NewCmdCreateSecretGeneric is a command to create generic secrets from files, directories, or literal values func NewCmdCreateSecretGeneric(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command { - options := &SecretGenericOpts{ - CreateSubcommandOptions: NewCreateSubcommandOptions(ioStreams), - } + o := NewSecretOptions(ioStreams) cmd := &cobra.Command{ Use: "generic NAME [--type=string] [--from-file=[key=]source] [--from-literal=key1=value1] [--dry-run=server|client|none]", @@ -91,233 +138,279 @@ func NewCmdCreateSecretGeneric(f cmdutil.Factory, ioStreams genericclioptions.IO Long: secretLong, Example: secretExample, Run: func(cmd *cobra.Command, args []string) { - cmdutil.CheckErr(options.Complete(f, cmd, args)) - cmdutil.CheckErr(options.Run()) + cmdutil.CheckErr(o.Complete(f, cmd, args)) + cmdutil.CheckErr(o.Validate()) + cmdutil.CheckErr(o.Run()) }, } - - options.CreateSubcommandOptions.PrintFlags.AddFlags(cmd) + o.PrintFlags.AddFlags(cmd) cmdutil.AddApplyAnnotationFlags(cmd) cmdutil.AddValidateFlags(cmd) - cmdutil.AddGeneratorFlags(cmd, generateversioned.SecretV1GeneratorName) - cmd.Flags().StringSlice("from-file", []string{}, "Key files can be specified using their file path, in which case a default name will be given to them, or optionally with a name and file path, in which case the given name will be used. Specifying a directory will iterate each named file in the directory that is a valid secret key.") - cmd.Flags().StringArray("from-literal", []string{}, "Specify a key and literal value to insert in secret (i.e. mykey=somevalue)") - cmd.Flags().String("from-env-file", "", "Specify the path to a file to read lines of key=val pairs to create a secret (i.e. a Docker .env file).") - cmd.Flags().String("type", "", i18n.T("The type of secret to create")) - cmd.Flags().Bool("append-hash", false, "Append a hash of the secret to its name.") - cmdutil.AddFieldManagerFlagVar(cmd, &options.CreateSubcommandOptions.FieldManager, "kubectl-create") + cmdutil.AddDryRunFlag(cmd) + + cmd.Flags().StringSliceVar(&o.FileSources, "from-file", o.FileSources, "Key files can be specified using their file path, in which case a default name will be given to them, or optionally with a name and file path, in which case the given name will be used. Specifying a directory will iterate each named file in the directory that is a valid secret key.") + cmd.Flags().StringArrayVar(&o.LiteralSources, "from-literal", o.LiteralSources, "Specify a key and literal value to insert in secret (i.e. mykey=somevalue)") + cmd.Flags().StringVar(&o.EnvFileSource, "from-env-file", o.EnvFileSource, "Specify the path to a file to read lines of key=val pairs to create a secret (i.e. a Docker .env file).") + cmd.Flags().StringVar(&o.Type, "type", o.Type, i18n.T("The type of secret to create")) + cmd.Flags().BoolVar(&o.AppendHash, "append-hash", o.AppendHash, "Append a hash of the secret to its name.") + + cmdutil.AddFieldManagerFlagVar(cmd, &o.FieldManager, "kubectl-create") + return cmd } -// Complete completes all the required options -func (o *SecretGenericOpts) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { - name, err := NameFromCommandArgs(cmd, args) +// Complete loads data from the command line environment +func (o *CreateSecretOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { + var err error + o.Name, err = NameFromCommandArgs(cmd, args) if err != nil { return err } - var generator generate.StructuredGenerator - switch generatorName := cmdutil.GetFlagString(cmd, "generator"); generatorName { - case generateversioned.SecretV1GeneratorName: - generator = &generateversioned.SecretGeneratorV1{ - Name: name, - Type: cmdutil.GetFlagString(cmd, "type"), - FileSources: cmdutil.GetFlagStringSlice(cmd, "from-file"), - LiteralSources: cmdutil.GetFlagStringArray(cmd, "from-literal"), - EnvFileSource: cmdutil.GetFlagString(cmd, "from-env-file"), - AppendHash: cmdutil.GetFlagBool(cmd, "append-hash"), + restConfig, err := f.ToRESTConfig() + if err != nil { + return err + } + + o.Client, err = corev1client.NewForConfig(restConfig) + if err != nil { + return err + } + + o.CreateAnnotation = cmdutil.GetFlagBool(cmd, cmdutil.ApplyAnnotationsFlag) + + 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) + + o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace() + if err != nil { + return nil + } + + cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.DryRunStrategy) + printer, err := o.PrintFlags.ToPrinter() + if err != nil { + return nil + } + + o.PrintObj = func(obj runtime.Object) error { + return printer.PrintObj(obj, o.Out) + } + + return nil +} + +// Validate checks if CreateSecretOptions has sufficient value to run +func (o *CreateSecretOptions) Validate() error { + if len(o.Name) == 0 { + return fmt.Errorf("name must be specified") + } + if len(o.EnvFileSource) > 0 && (len(o.FileSources) > 0 || len(o.LiteralSources) > 0) { + return fmt.Errorf("from-env-file cannot be combined with from-file or from-literal") + } + return nil +} + +// Run calls createSecret which will create secret based on CreateSecretOptions +// and makes an API call to the server +func (o *CreateSecretOptions) Run() error { + secret, err := o.createSecret() + if err != nil { + return err + } + err = util.CreateOrUpdateAnnotation(o.CreateAnnotation, secret, scheme.DefaultJSONEncoder()) + if err != nil { + return err + } + if o.DryRunStrategy != cmdutil.DryRunClient { + createOptions := metav1.CreateOptions{} + if o.FieldManager != "" { + createOptions.FieldManager = o.FieldManager + } + if o.DryRunStrategy == cmdutil.DryRunServer { + err := o.DryRunVerifier.HasSupport(secret.GroupVersionKind()) + if err != nil { + return err + } + createOptions.DryRun = []string{metav1.DryRunAll} + } + secret, err = o.Client.Secrets(o.Namespace).Create(context.TODO(), secret, createOptions) + if err != nil { + return fmt.Errorf("failed to create secret %v", err) } - default: - return errUnsupportedGenerator(cmd, generatorName) } - return o.CreateSubcommandOptions.Complete(f, cmd, args, generator) + return o.PrintObj(secret) } -// Run calls the CreateSubcommandOptions.Run in SecretGenericOpts instance -func (o *SecretGenericOpts) Run() error { - return o.CreateSubcommandOptions.Run() -} - -var ( - secretForDockerRegistryLong = templates.LongDesc(i18n.T(` - Create a new secret for use with Docker registries. - - Dockercfg secrets are used to authenticate against Docker registries. - - When using the Docker command line to push images, you can authenticate to a given registry by running: - '$ docker login DOCKER_REGISTRY_SERVER --username=DOCKER_USER --password=DOCKER_PASSWORD --email=DOCKER_EMAIL'. - - That produces a ~/.dockercfg file that is used by subsequent 'docker push' and 'docker pull' commands to - authenticate to the registry. The email address is optional. - - When creating applications, you may have a Docker registry that requires authentication. In order for the - nodes to pull images on your behalf, they have to have the credentials. You can provide this information - by creating a dockercfg secret and attaching it to your service account.`)) - - secretForDockerRegistryExample = templates.Examples(i18n.T(` - # If you don't already have a .dockercfg file, you can create a dockercfg secret directly by using: - kubectl create secret docker-registry my-secret --docker-server=DOCKER_REGISTRY_SERVER --docker-username=DOCKER_USER --docker-password=DOCKER_PASSWORD --docker-email=DOCKER_EMAIL`)) -) - -// SecretDockerRegistryOpts holds the options for 'create secret docker-registry' sub command -type SecretDockerRegistryOpts struct { - CreateSubcommandOptions *CreateSubcommandOptions -} - -// NewCmdCreateSecretDockerRegistry is a macro command for creating secrets to work with Docker registries -func NewCmdCreateSecretDockerRegistry(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command { - options := &SecretDockerRegistryOpts{ - CreateSubcommandOptions: NewCreateSubcommandOptions(ioStreams), +// createSecret fills in key value pair from the information given in +// CreateSecretOptions into *corev1.Secret +func (o *CreateSecretOptions) createSecret() (*corev1.Secret, error) { + namespace := "" + if o.EnforceNamespace { + namespace = o.Namespace + } + secret := newSecretObj(o.Name, namespace, corev1.SecretType(o.Type)) + if len(o.LiteralSources) > 0 { + if err := handleSecretFromLiteralSources(secret, o.LiteralSources); err != nil { + return nil, err + } + } + if len(o.FileSources) > 0 { + if err := handleSecretFromFileSources(secret, o.FileSources); err != nil { + return nil, err + } + } + if len(o.EnvFileSource) > 0 { + if err := handleSecretFromEnvFileSource(secret, o.EnvFileSource); err != nil { + return nil, err + } + } + if o.AppendHash { + hash, err := hash.SecretHash(secret) + if err != nil { + return nil, err + } + secret.Name = fmt.Sprintf("%s-%s", secret.Name, hash) } - cmd := &cobra.Command{ - Use: "docker-registry NAME --docker-username=user --docker-password=password --docker-email=email [--docker-server=string] [--from-literal=key1=value1] [--dry-run=server|client|none]", - DisableFlagsInUseLine: true, - Short: i18n.T("Create a secret for use with a Docker registry"), - Long: secretForDockerRegistryLong, - Example: secretForDockerRegistryExample, - Run: func(cmd *cobra.Command, args []string) { - cmdutil.CheckErr(options.Complete(f, cmd, args)) - cmdutil.CheckErr(options.Run()) + return secret, nil +} + +// newSecretObj will create a new Secret Object given name, namespace and secretType +func newSecretObj(name, namespace string, secretType corev1.SecretType) *corev1.Secret { + return &corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "Secret", }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Type: secretType, + Data: map[string][]byte{}, } - - options.CreateSubcommandOptions.PrintFlags.AddFlags(cmd) - - cmdutil.AddApplyAnnotationFlags(cmd) - cmdutil.AddValidateFlags(cmd) - cmdutil.AddGeneratorFlags(cmd, generateversioned.SecretForDockerRegistryV1GeneratorName) - cmd.Flags().String("docker-username", "", i18n.T("Username for Docker registry authentication")) - cmd.Flags().String("docker-password", "", i18n.T("Password for Docker registry authentication")) - cmd.Flags().String("docker-email", "", i18n.T("Email for Docker registry")) - cmd.Flags().String("docker-server", "https://index.docker.io/v1/", i18n.T("Server location for Docker registry")) - cmd.Flags().Bool("append-hash", false, "Append a hash of the secret to its name.") - cmd.Flags().StringSlice("from-file", []string{}, "Key files can be specified using their file path, in which case a default name will be given to them, or optionally with a name and file path, in which case the given name will be used. Specifying a directory will iterate each named file in the directory that is a valid secret key.") - cmdutil.AddFieldManagerFlagVar(cmd, &options.CreateSubcommandOptions.FieldManager, "kubectl-create") - - return cmd } -// Complete completes all the required options -func (o *SecretDockerRegistryOpts) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { - name, err := NameFromCommandArgs(cmd, args) - if err != nil { - return err +// handleSecretFromLiteralSources adds the specified literal source +// information into the provided secret +func handleSecretFromLiteralSources(secret *corev1.Secret, literalSources []string) error { + for _, literalSource := range literalSources { + keyName, value, err := util.ParseLiteralSource(literalSource) + if err != nil { + return err + } + if err = addKeyFromLiteralToSecret(secret, keyName, []byte(value)); err != nil { + return err + } } - fromFileFlag := cmdutil.GetFlagStringSlice(cmd, "from-file") - if len(fromFileFlag) == 0 { - requiredFlags := []string{"docker-username", "docker-password", "docker-server"} - for _, requiredFlag := range requiredFlags { - if value := cmdutil.GetFlagString(cmd, requiredFlag); len(value) == 0 { - return cmdutil.UsageErrorf(cmd, "flag %s is required", requiredFlag) + return nil +} + +// handleSecretFromFileSources adds the specified file source information into the provided secret +func handleSecretFromFileSources(secret *corev1.Secret, fileSources []string) error { + for _, fileSource := range fileSources { + keyName, filePath, err := util.ParseFileSource(fileSource) + if err != nil { + return err + } + fileInfo, err := os.Stat(filePath) + if err != nil { + switch err := err.(type) { + case *os.PathError: + return fmt.Errorf("error reading %s: %v", filePath, err.Err) + default: + return fmt.Errorf("error reading %s: %v", filePath, err) } } - } - - var generator generate.StructuredGenerator - switch generatorName := cmdutil.GetFlagString(cmd, "generator"); generatorName { - case generateversioned.SecretForDockerRegistryV1GeneratorName: - generator = &generateversioned.SecretForDockerRegistryGeneratorV1{ - Name: name, - Username: cmdutil.GetFlagString(cmd, "docker-username"), - Email: cmdutil.GetFlagString(cmd, "docker-email"), - Password: cmdutil.GetFlagString(cmd, "docker-password"), - Server: cmdutil.GetFlagString(cmd, "docker-server"), - AppendHash: cmdutil.GetFlagBool(cmd, "append-hash"), - FileSources: cmdutil.GetFlagStringSlice(cmd, "from-file"), + // if the filePath is a directory + if fileInfo.IsDir() { + if strings.Contains(fileSource, "=") { + return fmt.Errorf("cannot give a key name for a directory path") + } + fileList, err := ioutil.ReadDir(filePath) + if err != nil { + return fmt.Errorf("error listing files in %s: %v", filePath, err) + } + for _, item := range fileList { + itemPath := path.Join(filePath, item.Name()) + if item.Mode().IsRegular() { + keyName = item.Name() + if err := addKeyFromFileToSecret(secret, keyName, itemPath); err != nil { + return err + } + } + } + // if the filepath is a file + } else { + if err := addKeyFromFileToSecret(secret, keyName, filePath); err != nil { + return err + } } - default: - return errUnsupportedGenerator(cmd, generatorName) + } - return o.CreateSubcommandOptions.Complete(f, cmd, args, generator) + return nil } -// Run calls CreateSubcommandOptions.Run in SecretDockerRegistryOpts instance -func (o *SecretDockerRegistryOpts) Run() error { - return o.CreateSubcommandOptions.Run() -} - -var ( - secretForTLSLong = templates.LongDesc(i18n.T(` - Create a TLS secret from the given public/private key pair. - - The public/private key pair must exist before hand. The public key certificate must be .PEM encoded and match - the given private key.`)) - - secretForTLSExample = templates.Examples(i18n.T(` - # Create a new TLS secret named tls-secret with the given key pair: - kubectl create secret tls tls-secret --cert=path/to/tls.cert --key=path/to/tls.key`)) -) - -// SecretTLSOpts holds the options for 'create secret tls' sub command -type SecretTLSOpts struct { - CreateSubcommandOptions *CreateSubcommandOptions -} - -// NewCmdCreateSecretTLS is a macro command for creating secrets to work with Docker registries -func NewCmdCreateSecretTLS(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command { - options := &SecretTLSOpts{ - CreateSubcommandOptions: NewCreateSubcommandOptions(ioStreams), +// handleSecretFromEnvFileSource adds the specified env file source information +// into the provided secret +func handleSecretFromEnvFileSource(secret *corev1.Secret, envFileSource string) error { + fileInfo, err := os.Stat(envFileSource) + if err != nil { + switch err := err.(type) { + case *os.PathError: + return fmt.Errorf("error reading %s: %v", envFileSource, err.Err) + default: + return fmt.Errorf("error reading %s: %v", envFileSource, err) + } + } + if fileInfo.IsDir() { + return fmt.Errorf("env secret file cannot be a directory") } - cmd := &cobra.Command{ - Use: "tls NAME --cert=path/to/cert/file --key=path/to/key/file [--dry-run=server|client|none]", - DisableFlagsInUseLine: true, - Short: i18n.T("Create a TLS secret"), - Long: secretForTLSLong, - Example: secretForTLSExample, - Run: func(cmd *cobra.Command, args []string) { - cmdutil.CheckErr(options.Complete(f, cmd, args)) - cmdutil.CheckErr(options.Run()) - }, - } - - options.CreateSubcommandOptions.PrintFlags.AddFlags(cmd) - - cmdutil.AddApplyAnnotationFlags(cmd) - cmdutil.AddValidateFlags(cmd) - cmdutil.AddGeneratorFlags(cmd, generateversioned.SecretForTLSV1GeneratorName) - cmd.Flags().String("cert", "", i18n.T("Path to PEM encoded public key certificate.")) - cmd.Flags().String("key", "", i18n.T("Path to private key associated with given certificate.")) - cmd.Flags().Bool("append-hash", false, "Append a hash of the secret to its name.") - cmdutil.AddFieldManagerFlagVar(cmd, &options.CreateSubcommandOptions.FieldManager, "kubectl-create") - return cmd + return cmdutil.AddFromEnvFile(envFileSource, func(key, value string) error { + return addKeyFromLiteralToSecret(secret, key, []byte(value)) + }) } -// Complete completes all the required options -func (o *SecretTLSOpts) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { - name, err := NameFromCommandArgs(cmd, args) +// addKeyFromFileToSecret adds a key with the given name to a Secret, populating +// the value with the content of the given file path, or returns an error. +func addKeyFromFileToSecret(secret *corev1.Secret, keyName, filePath string) error { + data, err := ioutil.ReadFile(filePath) if err != nil { return err } - - requiredFlags := []string{"cert", "key"} - for _, requiredFlag := range requiredFlags { - if value := cmdutil.GetFlagString(cmd, requiredFlag); len(value) == 0 { - return cmdutil.UsageErrorf(cmd, "flag %s is required", requiredFlag) - } - } - var generator generate.StructuredGenerator - switch generatorName := cmdutil.GetFlagString(cmd, "generator"); generatorName { - case generateversioned.SecretForTLSV1GeneratorName: - generator = &generateversioned.SecretForTLSGeneratorV1{ - Name: name, - Key: cmdutil.GetFlagString(cmd, "key"), - Cert: cmdutil.GetFlagString(cmd, "cert"), - AppendHash: cmdutil.GetFlagBool(cmd, "append-hash"), - } - default: - return errUnsupportedGenerator(cmd, generatorName) - } - - return o.CreateSubcommandOptions.Complete(f, cmd, args, generator) + return addKeyFromLiteralToSecret(secret, keyName, data) } -// Run calls CreateSubcommandOptions.Run in the SecretTLSOpts instance -func (o *SecretTLSOpts) Run() error { - return o.CreateSubcommandOptions.Run() +// addKeyFromLiteralToSecret adds the given key and data to the given secret, +// returning an error if the key is not valid or if the key already exists. +func addKeyFromLiteralToSecret(secret *corev1.Secret, keyName string, data []byte) error { + if errs := validation.IsConfigMapKey(keyName); len(errs) != 0 { + return fmt.Errorf("%q is not valid key name for a Secret %s", keyName, strings.Join(errs, ";")) + } + if _, entryExists := secret.Data[keyName]; entryExists { + return fmt.Errorf("cannot add key %s, another key by that name already exists", keyName) + } + secret.Data[keyName] = data + + return nil } diff --git a/pkg/cmd/create/create_secret_docker.go b/pkg/cmd/create/create_secret_docker.go new file mode 100644 index 00000000..40ac2ee3 --- /dev/null +++ b/pkg/cmd/create/create_secret_docker.go @@ -0,0 +1,308 @@ +/* +Copyright 2021 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" + "encoding/base64" + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + corev1 "k8s.io/api/core/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" + corev1client "k8s.io/client-go/kubernetes/typed/core/v1" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + "k8s.io/kubectl/pkg/scheme" + "k8s.io/kubectl/pkg/util" + "k8s.io/kubectl/pkg/util/hash" + "k8s.io/kubectl/pkg/util/i18n" + "k8s.io/kubectl/pkg/util/templates" +) + +var ( + secretForDockerRegistryLong = templates.LongDesc(i18n.T(` + Create a new secret for use with Docker registries. + + Dockercfg secrets are used to authenticate against Docker registries. + + When using the Docker command line to push images, you can authenticate to a given registry by running: + '$ docker login DOCKER_REGISTRY_SERVER --username=DOCKER_USER --password=DOCKER_PASSWORD --email=DOCKER_EMAIL'. + + That produces a ~/.dockercfg file that is used by subsequent 'docker push' and 'docker pull' commands to + authenticate to the registry. The email address is optional. + + When creating applications, you may have a Docker registry that requires authentication. In order for the + nodes to pull images on your behalf, they have to have the credentials. You can provide this information + by creating a dockercfg secret and attaching it to your service account.`)) + + secretForDockerRegistryExample = templates.Examples(i18n.T(` + # If you don't already have a .dockercfg file, you can create a dockercfg secret directly by using: + kubectl create secret docker-registry my-secret --docker-server=DOCKER_REGISTRY_SERVER --docker-username=DOCKER_USER --docker-password=DOCKER_PASSWORD --docker-email=DOCKER_EMAIL + + # Create a new secret named my-secret from ~/.docker/config.json + kubectl create secret docker-registry my-secret --from-file=.dockerconfigjson=path/to/.docker/config.json`)) +) + +// DockerConfigJSON represents a local docker auth config file +// for pulling images. +type DockerConfigJSON struct { + Auths DockerConfig `json:"auths" datapolicy:"token"` + // +optional + HttpHeaders map[string]string `json:"HttpHeaders,omitempty" datapolicy:"token"` +} + +// DockerConfig represents the config file used by the docker CLI. +// This config that represents the credentials that should be used +// when pulling images from specific image repositories. +type DockerConfig map[string]DockerConfigEntry + +// DockerConfigEntry holds the user information that grant the access to docker registry +type DockerConfigEntry struct { + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty" datapolicy:"password"` + Email string `json:"email,omitempty"` + Auth string `json:"auth,omitempty" datapolicy:"token"` +} + +// CreateSecretDockerRegistryOptions holds the options for 'create secret docker-registry' sub command +type CreateSecretDockerRegistryOptions struct { + // PrintFlags holds options necessary for obtaining a printer + PrintFlags *genericclioptions.PrintFlags + PrintObj func(obj runtime.Object) error + + // Name of secret (required) + Name string + // FileSources to derive the secret from (optional) + FileSources []string + // Username for registry (required) + Username string + // Email for registry (optional) + Email string + // Password for registry (required) + Password string `datapolicy:"password"` + // Server for registry (required) + Server string + // AppendHash; if true, derive a hash from the Secret and append it to the name + AppendHash bool + + FieldManager string + CreateAnnotation bool + Namespace string + EnforceNamespace bool + + Client corev1client.CoreV1Interface + DryRunStrategy cmdutil.DryRunStrategy + DryRunVerifier *resource.DryRunVerifier + + genericclioptions.IOStreams +} + +// NewSecretDockerRegistryOptions creates a new *CreateSecretDockerRegistryOptions with default value +func NewSecretDockerRegistryOptions(ioStreams genericclioptions.IOStreams) *CreateSecretDockerRegistryOptions { + return &CreateSecretDockerRegistryOptions{ + Server: "https://index.docker.io/v1/", + PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme), + IOStreams: ioStreams, + } +} + +// NewCmdCreateSecretDockerRegistry is a macro command for creating secrets to work with Docker registries +func NewCmdCreateSecretDockerRegistry(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command { + o := NewSecretDockerRegistryOptions(ioStreams) + + cmd := &cobra.Command{ + Use: "docker-registry NAME --docker-username=user --docker-password=password --docker-email=email [--docker-server=string] [--from-file=[key=]source] [--dry-run=server|client|none]", + DisableFlagsInUseLine: true, + Short: i18n.T("Create a secret for use with a Docker registry"), + Long: secretForDockerRegistryLong, + Example: secretForDockerRegistryExample, + 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.Username, "docker-username", o.Username, i18n.T("Username for Docker registry authentication")) + cmd.Flags().StringVar(&o.Password, "docker-password", o.Password, i18n.T("Password for Docker registry authentication")) + cmd.Flags().StringVar(&o.Email, "docker-email", o.Email, i18n.T("Email for Docker registry")) + cmd.Flags().StringVar(&o.Server, "docker-server", o.Server, i18n.T("Server location for Docker registry")) + cmd.Flags().BoolVar(&o.AppendHash, "append-hash", o.AppendHash, "Append a hash of the secret to its name.") + cmd.Flags().StringSliceVar(&o.FileSources, "from-file", o.FileSources, "Key files can be specified using their file path, in which case a default name will be given to them, or optionally with a name and file path, in which case the given name will be used. Specifying a directory will iterate each named file in the directory that is a valid secret key.") + + cmdutil.AddFieldManagerFlagVar(cmd, &o.FieldManager, "kubectl-create") + + return cmd +} + +// Complete loads data from the command line environment +func (o *CreateSecretDockerRegistryOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { + var err error + o.Name, err = NameFromCommandArgs(cmd, args) + if err != nil { + return err + } + + restConfig, err := f.ToRESTConfig() + if err != nil { + return err + } + + o.Client, err = corev1client.NewForConfig(restConfig) + if err != nil { + return err + } + + o.CreateAnnotation = cmdutil.GetFlagBool(cmd, cmdutil.ApplyAnnotationsFlag) + + 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) + + o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace() + if err != nil { + return nil + } + + cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.DryRunStrategy) + printer, err := o.PrintFlags.ToPrinter() + if err != nil { + return nil + } + + o.PrintObj = func(obj runtime.Object) error { + return printer.PrintObj(obj, o.Out) + } + + return nil +} + +// Validate checks if CreateSecretDockerRegistryOptions has sufficient value to run +func (o *CreateSecretDockerRegistryOptions) Validate() error { + if len(o.Name) == 0 { + return fmt.Errorf("name must be specified") + } + if len(o.FileSources) == 0 && (len(o.Username) == 0 || len(o.Password) == 0 || len(o.Server) == 0) { + return fmt.Errorf("either --from-file or the combination of --docker-username, --docker-password and --docker-server is required") + } + return nil +} + +// Run calls createSecretDockerRegistry which will create secretDockerRegistry based on CreateSecretDockerRegistryOptions +// and makes an API call to the server +func (o *CreateSecretDockerRegistryOptions) Run() error { + secretDockerRegistry, err := o.createSecretDockerRegistry() + if err != nil { + return err + } + err = util.CreateOrUpdateAnnotation(o.CreateAnnotation, secretDockerRegistry, scheme.DefaultJSONEncoder()) + if err != nil { + return err + } + if o.DryRunStrategy != cmdutil.DryRunClient { + createOptions := metav1.CreateOptions{} + if o.FieldManager != "" { + createOptions.FieldManager = o.FieldManager + } + if o.DryRunStrategy == cmdutil.DryRunServer { + err := o.DryRunVerifier.HasSupport(secretDockerRegistry.GroupVersionKind()) + if err != nil { + return err + } + createOptions.DryRun = []string{metav1.DryRunAll} + } + secretDockerRegistry, err = o.Client.Secrets(o.Namespace).Create(context.TODO(), secretDockerRegistry, createOptions) + if err != nil { + return fmt.Errorf("failed to create secret %v", err) + } + } + + return o.PrintObj(secretDockerRegistry) +} + +// createSecretDockerRegistry fills in key value pair from the information given in +// CreateSecretDockerRegistryOptions into *corev1.Secret +func (o *CreateSecretDockerRegistryOptions) createSecretDockerRegistry() (*corev1.Secret, error) { + namespace := "" + if o.EnforceNamespace { + namespace = o.Namespace + } + secretDockerRegistry := newSecretObj(o.Name, namespace, corev1.SecretTypeDockerConfigJson) + if len(o.FileSources) > 0 { + if err := handleSecretFromFileSources(secretDockerRegistry, o.FileSources); err != nil { + return nil, err + } + } else { + dockerConfigJSONContent, err := handleDockerCfgJSONContent(o.Username, o.Password, o.Email, o.Server) + if err != nil { + return nil, err + } + secretDockerRegistry.Data[corev1.DockerConfigJsonKey] = dockerConfigJSONContent + } + if o.AppendHash { + hash, err := hash.SecretHash(secretDockerRegistry) + if err != nil { + return nil, err + } + secretDockerRegistry.Name = fmt.Sprintf("%s-%s", secretDockerRegistry.Name, hash) + } + return secretDockerRegistry, nil +} + +// handleDockerCfgJSONContent serializes a ~/.docker/config.json file +func handleDockerCfgJSONContent(username, password, email, server string) ([]byte, error) { + dockerConfigAuth := DockerConfigEntry{ + Username: username, + Password: password, + Email: email, + Auth: encodeDockerConfigFieldAuth(username, password), + } + dockerConfigJSON := DockerConfigJSON{ + Auths: map[string]DockerConfigEntry{server: dockerConfigAuth}, + } + + return json.Marshal(dockerConfigJSON) +} + +// encodeDockerConfigFieldAuth returns base64 encoding of the username and password string +func encodeDockerConfigFieldAuth(username, password string) string { + fieldValue := username + ":" + password + return base64.StdEncoding.EncodeToString([]byte(fieldValue)) +} diff --git a/pkg/cmd/create/create_secret_docker_test.go b/pkg/cmd/create/create_secret_docker_test.go new file mode 100644 index 00000000..5bb1cff1 --- /dev/null +++ b/pkg/cmd/create/create_secret_docker_test.go @@ -0,0 +1,185 @@ +/* +Copyright 2021 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 ( + "testing" + + corev1 "k8s.io/api/core/v1" + apiequality "k8s.io/apimachinery/pkg/api/equality" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestCreateSecretDockerRegistry(t *testing.T) { + username, password, email, server := "test-user", "test-password", "test-user@example.org", "https://index.docker.io/v1/" + secretData, err := handleDockerCfgJSONContent(username, password, email, server) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + secretDataNoEmail, err := handleDockerCfgJSONContent(username, password, "", server) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + tests := map[string]struct { + dockerRegistrySecretName string + dockerUsername string + dockerEmail string + dockerPassword string + dockerServer string + appendHash bool + expected *corev1.Secret + expectErr bool + }{ + "create_secret_docker_registry_with_email": { + dockerRegistrySecretName: "foo", + dockerUsername: username, + dockerPassword: password, + dockerEmail: email, + dockerServer: server, + expected: &corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "Secret", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Type: corev1.SecretTypeDockerConfigJson, + Data: map[string][]byte{ + corev1.DockerConfigJsonKey: secretData, + }, + }, + expectErr: false, + }, + "create_secret_docker_registry_with_email_hash": { + dockerRegistrySecretName: "foo", + dockerUsername: username, + dockerPassword: password, + dockerEmail: email, + dockerServer: server, + appendHash: true, + expected: &corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "Secret", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-548cm7fgdh", + }, + Type: corev1.SecretTypeDockerConfigJson, + Data: map[string][]byte{ + corev1.DockerConfigJsonKey: secretData, + }, + }, + expectErr: false, + }, + "create_secret_docker_registry_without_email": { + dockerRegistrySecretName: "foo", + dockerUsername: username, + dockerPassword: password, + dockerEmail: "", + dockerServer: server, + expected: &corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "Secret", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Type: corev1.SecretTypeDockerConfigJson, + Data: map[string][]byte{ + corev1.DockerConfigJsonKey: secretDataNoEmail, + }, + }, + expectErr: false, + }, + "create_secret_docker_registry_without_email_hash": { + dockerRegistrySecretName: "foo", + dockerUsername: username, + dockerPassword: password, + dockerEmail: "", + dockerServer: server, + appendHash: true, + expected: &corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "Secret", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-bff5bt4f92", + }, + Type: corev1.SecretTypeDockerConfigJson, + Data: map[string][]byte{ + corev1.DockerConfigJsonKey: secretDataNoEmail, + }, + }, + expectErr: false, + }, + "create_invalid_secret_docker_registry_without_username": { + dockerRegistrySecretName: "foo", + dockerPassword: password, + dockerEmail: "", + dockerServer: server, + expectErr: true, + }, + "create_invalid_secret_docker_registry_without_password": { + dockerRegistrySecretName: "foo", + dockerUsername: username, + dockerEmail: "", + dockerServer: server, + expectErr: true, + }, + "create_invalid_secret_docker_registry_without_server": { + dockerRegistrySecretName: "foo", + dockerUsername: username, + dockerPassword: password, + dockerEmail: "", + expectErr: true, + }, + } + + // Run all the tests + for name, test := range tests { + t.Run(name, func(t *testing.T) { + var secretDockerRegistry *corev1.Secret = nil + secretDockerRegistryOptions := CreateSecretDockerRegistryOptions{ + Name: test.dockerRegistrySecretName, + Username: test.dockerUsername, + Email: test.dockerEmail, + Password: test.dockerPassword, + Server: test.dockerServer, + AppendHash: test.appendHash, + } + err := secretDockerRegistryOptions.Validate() + if err == nil { + secretDockerRegistry, err = secretDockerRegistryOptions.createSecretDockerRegistry() + } + + if !test.expectErr && err != nil { + t.Errorf("test %s, unexpected error: %v", name, err) + } + if test.expectErr && err == nil { + t.Errorf("test %s was expecting an error but no error occurred", name) + } + if !apiequality.Semantic.DeepEqual(secretDockerRegistry, test.expected) { + t.Errorf("test %s\n expected:\n%#v\ngot:\n%#v", name, test.expected, secretDockerRegistry) + } + }) + } +} diff --git a/pkg/cmd/create/create_secret_test.go b/pkg/cmd/create/create_secret_test.go index 9f1d4491..1b915f82 100644 --- a/pkg/cmd/create/create_secret_test.go +++ b/pkg/cmd/create/create_secret_test.go @@ -17,85 +17,515 @@ limitations under the License. package create import ( - "net/http" + "io/ioutil" + "os" "testing" - "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/cli-runtime/pkg/genericclioptions" - "k8s.io/client-go/rest/fake" - cmdtesting "k8s.io/kubectl/pkg/cmd/testing" - "k8s.io/kubectl/pkg/scheme" + corev1 "k8s.io/api/core/v1" + apiequality "k8s.io/apimachinery/pkg/api/equality" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +func TestCreateSecretObject(t *testing.T) { + secretObject := newSecretObj("foo", "foo-namespace", corev1.SecretTypeDockerConfigJson) + expectedSecretObject := &corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "Secret", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Namespace: "foo-namespace", + }, + Type: corev1.SecretTypeDockerConfigJson, + Data: map[string][]byte{}, + } + t.Run("Creating a Secret Object", func(t *testing.T) { + if !apiequality.Semantic.DeepEqual(secretObject, expectedSecretObject) { + t.Errorf("expected:\n%#v\ngot:\n%#v", secretObject, expectedSecretObject) + } + }) +} + func TestCreateSecretGeneric(t *testing.T) { - secretObject := &v1.Secret{ - Data: map[string][]byte{ - "password": []byte("includes,comma"), - "username": []byte("test_user"), + tests := map[string]struct { + secretName string + secretType string + fromLiteral []string + fromFile []string + fromEnvFile string + appendHash bool + setup func(t *testing.T, secretGenericOptions *CreateSecretOptions) func() + + expected *corev1.Secret + expectErr bool + }{ + "create_secret_foo": { + secretName: "foo", + expected: &corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "Secret", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Data: map[string][]byte{}, + }, + expectErr: false, + }, + "create_secret_foo_hash": { + secretName: "foo", + appendHash: true, + expected: &corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "Secret", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-949tdgdkgg", + }, + Data: map[string][]byte{}, + }, + expectErr: false, + }, + "create_secret_foo_type": { + secretName: "foo", + secretType: "my-type", + expected: &corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "Secret", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Data: map[string][]byte{}, + Type: "my-type", + }, + expectErr: false, + }, + "create_secret_foo_type_hash": { + secretName: "foo", + secretType: "my-type", + appendHash: true, + expected: &corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "Secret", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-dg474f9t76", + }, + Data: map[string][]byte{}, + Type: "my-type", + }, + expectErr: false, + }, + "create_secret_foo_two_literal": { + secretName: "foo", + fromLiteral: []string{"key1=value1", "key2=value2"}, + expected: &corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "Secret", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Data: map[string][]byte{ + "key1": []byte("value1"), + "key2": []byte("value2"), + }, + }, + expectErr: false, + }, + "create_secret_foo_two_literal_hash": { + secretName: "foo", + fromLiteral: []string{"key1=value1", "key2=value2"}, + appendHash: true, + expected: &corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "Secret", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-tf72c228m4", + }, + Data: map[string][]byte{ + "key1": []byte("value1"), + "key2": []byte("value2"), + }, + }, + expectErr: false, + }, + "create_secret_foo_key1_=value1": { + secretName: "foo", + fromLiteral: []string{"key1==value1"}, + expected: &corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "Secret", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Data: map[string][]byte{ + "key1": []byte("=value1"), + }, + }, + expectErr: false, + }, + "create_secret_foo_key1_=value1_hash": { + secretName: "foo", + fromLiteral: []string{"key1==value1"}, + appendHash: true, + expected: &corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "Secret", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-fdcc8tkhh5", + }, + Data: map[string][]byte{ + "key1": []byte("=value1"), + }, + }, + expectErr: false, + }, + "create_secret_foo_from_file_foo1_foo2_secret": { + secretName: "foo", + setup: setupSecretBinaryFile([]byte{0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64}, "foo1", "foo2"), + fromFile: []string{"foo1", "foo2"}, + expected: &corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "Secret", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Data: map[string][]byte{ + "foo1": []byte("hello world"), + "foo2": []byte("hello world"), + }, + }, + expectErr: false, + }, + "create_secret_foo_from_file_foo1_foo2_hash": { + secretName: "foo", + setup: setupSecretBinaryFile([]byte{0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64}, "foo1", "foo2"), + fromFile: []string{"foo1", "foo2"}, + appendHash: true, + expected: &corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "Secret", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-hbkh2cdb57", + }, + Data: map[string][]byte{ + "foo1": []byte("hello world"), + "foo2": []byte("hello world"), + }, + }, + expectErr: false, + }, + "create_secret_foo_from_file_foo1_foo2_and": { + secretName: "foo", + setup: setupSecretBinaryFile([]byte{0xff, 0xfd}, "foo1", "foo2"), + fromFile: []string{"foo1", "foo2"}, + expected: &corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "Secret", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Data: map[string][]byte{ + "foo1": {0xff, 0xfd}, + "foo2": {0xff, 0xfd}, + }, + }, + expectErr: false, + }, + "create_secret_foo_from_file_foo1_foo2_and_hash": { + secretName: "foo", + setup: setupSecretBinaryFile([]byte{0xff, 0xfd}, "foo1", "foo2"), + fromFile: []string{"foo1", "foo2"}, + appendHash: true, + expected: &corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "Secret", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-mkhg4ktk4d", + }, + Data: map[string][]byte{ + "foo1": {0xff, 0xfd}, + "foo2": {0xff, 0xfd}, + }, + }, + expectErr: false, + }, + "create_secret_valid_env_from_env_file": { + secretName: "valid_env", + setup: setupSecretEnvFile("key1=value1", "#", "", "key2=value2"), + fromEnvFile: "file.env", + expected: &corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "Secret", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "valid_env", + }, + Data: map[string][]byte{ + "key1": []byte("value1"), + "key2": []byte("value2"), + }, + }, + expectErr: false, + }, + "create_secret_valid_env_from_env_file_hash": { + secretName: "valid_env", + setup: setupSecretEnvFile("key1=value1", "#", "", "key2=value2"), + fromEnvFile: "file.env", + appendHash: true, + expected: &corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "Secret", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "valid_env-bkb2m2965h", + }, + Data: map[string][]byte{ + "key1": []byte("value1"), + "key2": []byte("value2"), + }, + }, + expectErr: false, + }, + "create_secret_get_env_from_env_file": { + secretName: "get_env", + setup: func() func(t *testing.T, secretGenericOptions *CreateSecretOptions) func() { + os.Setenv("g_key1", "1") + os.Setenv("g_key2", "2") + return setupSecretEnvFile("g_key1", "g_key2=") + }(), + fromEnvFile: "file.env", + expected: &corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "Secret", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "get_env", + }, + Data: map[string][]byte{ + "g_key1": []byte("1"), + "g_key2": []byte(""), + }, + }, + expectErr: false, + }, + "create_secret_get_env_from_env_file_hash": { + secretName: "get_env", + setup: func() func(t *testing.T, secretGenericOptions *CreateSecretOptions) func() { + os.Setenv("g_key1", "1") + os.Setenv("g_key2", "2") + return setupSecretEnvFile("g_key1", "g_key2=") + }(), + fromEnvFile: "file.env", + appendHash: true, + expected: &corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "Secret", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "get_env-68mt8f2kkt", + }, + Data: map[string][]byte{ + "g_key1": []byte("1"), + "g_key2": []byte(""), + }, + }, + expectErr: false, + }, + "create_secret_value_with_space_from_env_file": { + secretName: "value_with_space", + setup: setupSecretEnvFile(" key1= value1"), + fromEnvFile: "file.env", + expected: &corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "Secret", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "value_with_space", + }, + Data: map[string][]byte{ + "key1": []byte(" value1"), + }, + }, + expectErr: false, + }, + "create_secret_value_with_space_from_env_file_hash": { + secretName: "valid_with_space", + setup: setupSecretEnvFile(" key1= value1"), + fromEnvFile: "file.env", + appendHash: true, + expected: &corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "Secret", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "valid_with_space-bhkb4gfck6", + }, + Data: map[string][]byte{ + "key1": []byte(" value1"), + }, + }, + expectErr: false, + }, + "create_invalid_secret_filepath_contains_=": { + secretName: "foo", + fromFile: []string{"key1=/file=2"}, + expectErr: true, + }, + "create_invalid_secret_filepath_key_contains_=": { + secretName: "foo", + fromFile: []string{"=key=/file1"}, + expectErr: true, + }, + "create_invalid_secret_literal_key_contains_=": { + secretName: "foo", + fromFile: []string{"=key=value1"}, + expectErr: true, + }, + "create_invalid_secret_env_key_contains_#": { + secretName: "invalid_key", + setup: setupSecretEnvFile("key#1=value1"), + fromEnvFile: "file.env", + expectErr: true, + }, + "create_invalid_secret_duplicate_key1": { + secretName: "foo", + fromLiteral: []string{"key1=value1", "key1=value2"}, + expectErr: true, + }, + "create_invalid_secret_no_file": { + secretName: "foo", + fromFile: []string{"key1=/file1"}, + expectErr: true, + }, + "create_invalid_secret_invalid_literal": { + secretName: "foo", + fromLiteral: []string{"key1value1"}, + expectErr: true, + }, + "create_invalid_secret_invalid_filepath": { + secretName: "foo", + fromFile: []string{"key1==file1"}, + expectErr: true, + }, + "create_invalid_secret_no_name": { + expectErr: true, + }, + "create_invalid_secret_too_many_args": { + secretName: "too_many_args", + fromFile: []string{"key1=/file1"}, + fromEnvFile: "foo", + expectErr: true, + }, + "create_invalid_secret_too_many_args_1": { + secretName: "too_many_args_1", + fromLiteral: []string{"key1=value1"}, + fromEnvFile: "foo", + expectErr: true, + }, + "create_invalid_secret_too_many_args_2": { + secretName: "too_many_args_2", + fromFile: []string{"key1=/file1"}, + fromLiteral: []string{"key1=value1"}, + fromEnvFile: "foo", + expectErr: true, }, } - secretObject.Name = "my-secret" - tf := cmdtesting.NewTestFactory().WithNamespace("test") - defer tf.Cleanup() - codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) - ns := scheme.Codecs.WithoutConversion() - - tf.Client = &fake.RESTClient{ - GroupVersion: schema.GroupVersion{Version: "v1"}, - NegotiatedSerializer: ns, - Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { - switch p, m := req.URL.Path, req.Method; { - case p == "/namespaces/test/secrets" && m == "POST": - return &http.Response{StatusCode: http.StatusCreated, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, secretObject)}, nil - default: - t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) - return nil, nil + // run all the tests + for name, test := range tests { + t.Run(name, func(t *testing.T) { + var secret *corev1.Secret = nil + secretOptions := CreateSecretOptions{ + Name: test.secretName, + Type: test.secretType, + AppendHash: test.appendHash, + FileSources: test.fromFile, + LiteralSources: test.fromLiteral, + EnvFileSource: test.fromEnvFile, } - }), - } - ioStreams, _, buf, _ := genericclioptions.NewTestIOStreams() - cmd := NewCmdCreateSecretGeneric(tf, ioStreams) - cmd.Flags().Set("output", "name") - cmd.Flags().Set("from-literal", "password=includes,comma") - cmd.Flags().Set("from-literal", "username=test_user") - cmd.Run(cmd, []string{secretObject.Name}) - expectedOutput := "secret/" + secretObject.Name + "\n" - if buf.String() != expectedOutput { - t.Errorf("expected output: %s, but got: %s", expectedOutput, buf.String()) + if test.setup != nil { + if teardown := test.setup(t, &secretOptions); teardown != nil { + defer teardown() + } + } + err := secretOptions.Validate() + if err == nil { + secret, err = secretOptions.createSecret() + } + + if !test.expectErr && err != nil { + t.Errorf("test %s, unexpected error: %v", name, err) + } + if test.expectErr && err == nil { + t.Errorf("test %s was expecting an error but no error occurred", name) + } + if !apiequality.Semantic.DeepEqual(secret, test.expected) { + t.Errorf("test %s\n expected:\n%#v\ngot:\n%#v", name, test.expected, secret) + } + }) } } -func TestCreateSecretDockerRegistry(t *testing.T) { - secretObject := &v1.Secret{} - secretObject.Name = "my-secret" - tf := cmdtesting.NewTestFactory().WithNamespace("test") - codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) - ns := scheme.Codecs.WithoutConversion() - - tf.Client = &fake.RESTClient{ - GroupVersion: schema.GroupVersion{Version: "v1"}, - NegotiatedSerializer: ns, - Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { - switch p, m := req.URL.Path, req.Method; { - case p == "/namespaces/test/secrets" && m == "POST": - return &http.Response{StatusCode: http.StatusCreated, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, secretObject)}, nil - default: - t.Fatalf("unexpected request: %#v\n%#v", req.URL, req) - return nil, nil - } - }), - } - ioStreams, _, buf, _ := genericclioptions.NewTestIOStreams() - cmd := NewCmdCreateSecretDockerRegistry(tf, ioStreams) - cmd.Flags().Set("docker-username", "test-user") - cmd.Flags().Set("docker-password", "test-pass") - cmd.Flags().Set("docker-email", "test-email") - cmd.Flags().Set("output", "name") - cmd.Run(cmd, []string{secretObject.Name}) - expectedOutput := "secret/" + secretObject.Name + "\n" - if buf.String() != expectedOutput { - t.Errorf("expected output: %s, but got: %s", buf.String(), expectedOutput) +func setupSecretEnvFile(lines ...string) func(*testing.T, *CreateSecretOptions) func() { + return func(t *testing.T, secretOptions *CreateSecretOptions) func() { + f, err := ioutil.TempFile("", "cme") + if err != nil { + t.Errorf("unexpected error: %v", err) + } + for _, l := range lines { + f.WriteString(l) + f.WriteString("\r\n") + } + f.Close() + secretOptions.EnvFileSource = f.Name() + return func() { + os.Remove(f.Name()) + } + } +} + +func setupSecretBinaryFile(data []byte, files ...string) func(*testing.T, *CreateSecretOptions) func() { + return func(t *testing.T, secretOptions *CreateSecretOptions) func() { + tmp, _ := ioutil.TempDir("", "") + for i, file := range files { + f := tmp + "/" + file + ioutil.WriteFile(f, data, 0644) + secretOptions.FileSources[i] = f + } + return func() { + for _, file := range files { + f := tmp + "/" + file + os.RemoveAll(f) + } + } } } diff --git a/pkg/cmd/create/create_secret_tls.go b/pkg/cmd/create/create_secret_tls.go new file mode 100644 index 00000000..ca27af7a --- /dev/null +++ b/pkg/cmd/create/create_secret_tls.go @@ -0,0 +1,259 @@ +/* +Copyright 2021 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" + "crypto/tls" + "fmt" + "io/ioutil" + + "github.com/spf13/cobra" + corev1 "k8s.io/api/core/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" + corev1client "k8s.io/client-go/kubernetes/typed/core/v1" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + "k8s.io/kubectl/pkg/scheme" + "k8s.io/kubectl/pkg/util" + "k8s.io/kubectl/pkg/util/hash" + "k8s.io/kubectl/pkg/util/i18n" + "k8s.io/kubectl/pkg/util/templates" +) + +var ( + secretForTLSLong = templates.LongDesc(i18n.T(` + Create a TLS secret from the given public/private key pair. + + The public/private key pair must exist before hand. The public key certificate must be .PEM encoded and match + the given private key.`)) + + secretForTLSExample = templates.Examples(i18n.T(` + # Create a new TLS secret named tls-secret with the given key pair: + kubectl create secret tls tls-secret --cert=path/to/tls.cert --key=path/to/tls.key`)) +) + +// CreateSecretTLSOptions holds the options for 'create secret tls' sub command +type CreateSecretTLSOptions struct { + // PrintFlags holds options necessary for obtaining a printer + PrintFlags *genericclioptions.PrintFlags + PrintObj func(obj runtime.Object) error + + // Name is the name of this TLS secret. + Name string + // Key is the path to the user's private key. + Key string + // Cert is the path to the user's public key certificate. + Cert string + // AppendHash; if true, derive a hash from the Secret and append it to the name + AppendHash bool + + FieldManager string + CreateAnnotation bool + Namespace string + EnforceNamespace bool + + Client corev1client.CoreV1Interface + DryRunStrategy cmdutil.DryRunStrategy + DryRunVerifier *resource.DryRunVerifier + + genericclioptions.IOStreams +} + +// NewSecretTLSOptions creates a new *CreateSecretTLSOptions with default value +func NewSecretTLSOptions(ioStrems genericclioptions.IOStreams) *CreateSecretTLSOptions { + return &CreateSecretTLSOptions{ + PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme), + IOStreams: ioStrems, + } +} + +// NewCmdCreateSecretTLS is a macro command for creating secrets to work with TLS client or server +func NewCmdCreateSecretTLS(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command { + o := NewSecretTLSOptions(ioStreams) + + cmd := &cobra.Command{ + Use: "tls NAME --cert=path/to/cert/file --key=path/to/key/file [--dry-run=server|client|none]", + DisableFlagsInUseLine: true, + Short: i18n.T("Create a TLS secret"), + Long: secretForTLSLong, + Example: secretForTLSExample, + 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.Cert, "cert", o.Cert, i18n.T("Path to PEM encoded public key certificate.")) + cmd.Flags().StringVar(&o.Key, "key", o.Key, i18n.T("Path to private key associated with given certificate.")) + cmd.Flags().BoolVar(&o.AppendHash, "append-hash", o.AppendHash, "Append a hash of the secret to its name.") + + cmdutil.AddFieldManagerFlagVar(cmd, &o.FieldManager, "kubectl-create") + + return cmd +} + +// Complete loads data from the command line environment +func (o *CreateSecretTLSOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { + var err error + o.Name, err = NameFromCommandArgs(cmd, args) + if err != nil { + return err + } + + restConfig, err := f.ToRESTConfig() + if err != nil { + return err + } + + o.Client, err = corev1client.NewForConfig(restConfig) + if err != nil { + return err + } + + o.CreateAnnotation = cmdutil.GetFlagBool(cmd, cmdutil.ApplyAnnotationsFlag) + + 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) + + o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace() + if err != nil { + return nil + } + + cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.DryRunStrategy) + printer, err := o.PrintFlags.ToPrinter() + if err != nil { + return nil + } + + o.PrintObj = func(obj runtime.Object) error { + return printer.PrintObj(obj, o.Out) + } + + return nil +} + +// Validate checks if CreateSecretTLSOptions hass sufficient value to run +func (o *CreateSecretTLSOptions) Validate() error { + // TODO: This is not strictly necessary. We can generate a self signed cert + // if no key/cert is given. The only requirement is that we either get both + // or none. See test/e2e/ingress_utils for self signed cert generation. + if len(o.Key) == 0 || len(o.Cert) == 0 { + return fmt.Errorf("key and cert must be specified") + } + return nil +} + +// Run calls createSecretTLS which will create secretTLS based on CreateSecretTLSOptions +// and makes an API call to the server +func (o *CreateSecretTLSOptions) Run() error { + secretTLS, err := o.createSecretTLS() + if err != nil { + return err + } + err = util.CreateOrUpdateAnnotation(o.CreateAnnotation, secretTLS, scheme.DefaultJSONEncoder()) + if err != nil { + return err + } + if o.DryRunStrategy != cmdutil.DryRunClient { + createOptions := metav1.CreateOptions{} + if o.FieldManager != "" { + createOptions.FieldManager = o.FieldManager + } + if o.DryRunStrategy == cmdutil.DryRunServer { + err := o.DryRunVerifier.HasSupport(secretTLS.GroupVersionKind()) + if err != nil { + return err + } + createOptions.DryRun = []string{metav1.DryRunAll} + } + secretTLS, err = o.Client.Secrets(o.Namespace).Create(context.TODO(), secretTLS, createOptions) + if err != nil { + return fmt.Errorf("failed to create secret %v", err) + } + } + return o.PrintObj(secretTLS) +} + +// createSecretTLS fills in key value pair from the information given in +// CreateSecretTLSOptions into *corev1.Secret +func (o *CreateSecretTLSOptions) createSecretTLS() (*corev1.Secret, error) { + namespace := "" + if o.EnforceNamespace { + namespace = o.Namespace + } + tlsCert, err := readFile(o.Cert) + if err != nil { + return nil, err + } + tlsKey, err := readFile(o.Key) + if err != nil { + return nil, err + } + if _, err := tls.X509KeyPair(tlsCert, tlsKey); err != nil { + return nil, err + } + // TODO: Add more validation. + // 1. If the certificate contains intermediates, it is a valid chain. + // 2. Format etc. + + secretTLS := newSecretObj(o.Name, namespace, corev1.SecretTypeTLS) + secretTLS.Data[corev1.TLSCertKey] = []byte(tlsCert) + secretTLS.Data[corev1.TLSPrivateKeyKey] = []byte(tlsKey) + if o.AppendHash { + hash, err := hash.SecretHash(secretTLS) + if err != nil { + return nil, err + } + secretTLS.Name = fmt.Sprintf("%s-%s", secretTLS.Name, hash) + } + + return secretTLS, nil +} + +// readFile just reads a file into a byte array. +func readFile(file string) ([]byte, error) { + b, err := ioutil.ReadFile(file) + if err != nil { + return []byte{}, fmt.Errorf("Cannot read file %v, %v", file, err) + } + return b, nil +} diff --git a/pkg/cmd/create/create_secret_tls_test.go b/pkg/cmd/create/create_secret_tls_test.go new file mode 100644 index 00000000..0aea2f2b --- /dev/null +++ b/pkg/cmd/create/create_secret_tls_test.go @@ -0,0 +1,217 @@ +/* +Copyright 2021 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 ( + "fmt" + "os" + "path" + "testing" + + corev1 "k8s.io/api/core/v1" + apiequality "k8s.io/apimachinery/pkg/api/equality" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + utiltesting "k8s.io/client-go/util/testing" +) + +var rsaCertPEM = `-----BEGIN CERTIFICATE----- +MIIB0zCCAX2gAwIBAgIJAI/M7BYjwB+uMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwHhcNMTIwOTEyMjE1MjAyWhcNMTUwOTEyMjE1MjAyWjBF +MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50 +ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANLJ +hPHhITqQbPklG3ibCVxwGMRfp/v4XqhfdQHdcVfHap6NQ5Wok/4xIA+ui35/MmNa +rtNuC+BdZ1tMuVCPFZcCAwEAAaNQME4wHQYDVR0OBBYEFJvKs8RfJaXTH08W+SGv +zQyKn0H8MB8GA1UdIwQYMBaAFJvKs8RfJaXTH08W+SGvzQyKn0H8MAwGA1UdEwQF +MAMBAf8wDQYJKoZIhvcNAQEFBQADQQBJlffJHybjDGxRMqaRmDhX0+6v02TUKZsW +r5QuVbpQhH6u+0UgcW0jp9QwpxoPTLTWGXEWBBBurxFwiCBhkQ+V +-----END CERTIFICATE----- +` + +var rsaKeyPEM = `-----BEGIN RSA PRIVATE KEY----- +MIIBOwIBAAJBANLJhPHhITqQbPklG3ibCVxwGMRfp/v4XqhfdQHdcVfHap6NQ5Wo +k/4xIA+ui35/MmNartNuC+BdZ1tMuVCPFZcCAwEAAQJAEJ2N+zsR0Xn8/Q6twa4G +6OB1M1WO+k+ztnX/1SvNeWu8D6GImtupLTYgjZcHufykj09jiHmjHx8u8ZZB/o1N +MQIhAPW+eyZo7ay3lMz1V01WVjNKK9QSn1MJlb06h/LuYv9FAiEA25WPedKgVyCW +SmUwbPw8fnTcpqDWE3yTO3vKcebqMSsCIBF3UmVue8YU3jybC3NxuXq3wNm34R8T +xVLHwDXh/6NJAiEAl2oHGGLz64BuAfjKrqwz7qMYr9HCLIe/YsoWq/olzScCIQDi +D2lWusoe2/nEqfDVVWGWlyJ7yOmqaVm/iNUN9B2N2g== +-----END RSA PRIVATE KEY----- +` + +const mismatchRSAKeyPEM = `-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC/665h55hWD4V2 +kiQ+B/G9NNfBw69eBibEhI9vWkPUyn36GO2r3HPtRE63wBfFpV486ns9DoZnnAYE +JaGjVNCCqS5tQyMBWp843o66KBrEgBpuddChigvyul33FhD1ImFnN+Vy0ajOJ+1/ +Zai28zBXWbxCWEbqz7s8e2UsPlBd0Caj4gcd32yD2BwiHqzB8odToWRUT7l+pS8R +qA1BruQvtjEIrcoWVlE170ZYe7+Apm96A+WvtVRkozPynxHF8SuEiw4hAh0lXR6b +4zZz4tZVV8ev2HpffveV/68GiCyeFDbglqd4sZ/Iga/rwu7bVY/BzFApHwu2hmmV +XLnaa3uVAgMBAAECggEAG+kvnCdtPR7Wvw6z3J2VJ3oW4qQNzfPBEZVhssUC1mB4 +f7W+Yt8VsOzdMdXq3yCUmvFS6OdC3rCPI21Bm5pLFKV8DgHUhm7idwfO4/3PHsKu +lV/m7odAA5Xc8oEwCCZu2e8EHHWnQgwGex+SsMCfSCTRvyhNb/qz9TDQ3uVVFL9e +9a4OKqZl/GlRspJSuXhy+RSVulw9NjeX1VRjIbhqpdXAmQNXgShA+gZSQh8T/tgv +XQYsMtg+FUDvcunJQf4OW5BY7IenYBV/GvsnJU8L7oD0wjNSAwe/iLKqV/NpYhre +QR4DsGnmoRYlUlHdHFTTJpReDjWm+vH3T756yDdFAQKBgQD2/sP5dM/aEW7Z1TgS +TG4ts1t8Rhe9escHxKZQR81dfOxBeCJMBDm6ySfR8rvyUM4VsogxBL/RhRQXsjJM +7wN08MhdiXG0J5yy/oNo8W6euD8m8Mk1UmqcZjSgV4vA7zQkvkr6DRJdybKsT9mE +jouEwev8sceS6iBpPw/+Ws8z1QKBgQDG6uYHMfMcS844xKQQWhargdN2XBzeG6TV +YXfNFstNpD84d9zIbpG/AKJF8fKrseUhXkJhkDjFGJTriD3QQsntOFaDOrHMnveV +zGzvC4OTFUUFHe0SVJ0HuLf8YCHoZ+DXEeCKCN6zBXnUue+bt3NvLOf2yN5o9kYx +SIa8O1vIwQKBgEdONXWG65qg/ceVbqKZvhUjen3eHmxtTZhIhVsX34nlzq73567a +aXArMnvB/9Bs05IgAIFmRZpPOQW+RBdByVWxTabzTwgbh3mFUJqzWKQpvNGZIf1q +1axhNUA1BfulEwCojyyxKWQ6HoLwanOCU3T4JxDEokEfpku8EPn1bWwhAoGAAN8A +eOGYHfSbB5ac3VF3rfKYmXkXy0U1uJV/r888vq9Mc5PazKnnS33WOBYyKNxTk4zV +H5ZBGWPdKxbipmnUdox7nIGCS9IaZXaKt5VGUzuRnM8fvafPNDxz2dAV9e2Wh3qV +kCUvzHrmqK7TxMvN3pvEvEju6GjDr+2QYXylD0ECgYAGK5r+y+EhtKkYFLeYReUt +znvSsWq+JCQH/cmtZLaVOldCaMRL625hSl3XPPcMIHE14xi3d4njoXWzvzPcg8L6 +vNXk3GiNldACS+vwk4CwEqe5YlZRm5doD07wIdsg2zRlnKsnXNM152OwgmcchDul +rLTt0TTazzwBCgCD0Jkoqg== +-----END PRIVATE KEY-----` + +func TestCreateSecretTLS(t *testing.T) { + + validCertTmpDir := utiltesting.MkTmpdirOrDie("tls-valid-cert-test") + validKeyPath, validCertPath := writeKeyPair(validCertTmpDir, rsaKeyPEM, rsaCertPEM, t) + defer tearDown(validCertTmpDir) + + invalidCertTmpDir := utiltesting.MkTmpdirOrDie("tls-invalid-cert-test") + invalidKeyPath, invalidCertPath := writeKeyPair(invalidCertTmpDir, "test", "test", t) + defer tearDown(invalidCertTmpDir) + + mismatchCertTmpDir := utiltesting.MkTmpdirOrDie("tls-mismatch-test") + mismatchKeyPath, mismatchCertPath := writeKeyPair(mismatchCertTmpDir, rsaKeyPEM, mismatchRSAKeyPEM, t) + defer tearDown(mismatchCertTmpDir) + + tests := map[string]struct { + tlsSecretName string + tlsKey string + tlsCert string + appendHash bool + expected *corev1.Secret + expectErr bool + }{ + "create_secret_tls": { + tlsSecretName: "foo", + tlsKey: validKeyPath, + tlsCert: validCertPath, + expected: &corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "Secret", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + }, + Type: corev1.SecretTypeTLS, + Data: map[string][]byte{ + corev1.TLSPrivateKeyKey: []byte(rsaKeyPEM), + corev1.TLSCertKey: []byte(rsaCertPEM), + }, + }, + expectErr: false, + }, + "create_secret_tls_hash": { + tlsSecretName: "foo", + tlsKey: validKeyPath, + tlsCert: validCertPath, + appendHash: true, + expected: &corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "Secret", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-272h6tt825", + }, + Type: corev1.SecretTypeTLS, + Data: map[string][]byte{ + corev1.TLSPrivateKeyKey: []byte(rsaKeyPEM), + corev1.TLSCertKey: []byte(rsaCertPEM), + }, + }, + expectErr: false, + }, + "create_secret_invalid_tls": { + tlsSecretName: "foo", + tlsKey: invalidKeyPath, + tlsCert: invalidCertPath, + expectErr: true, + }, + "create_secret_mismatch_tls": { + tlsSecretName: "foo", + tlsKey: mismatchKeyPath, + tlsCert: mismatchCertPath, + expectErr: true, + }, + "create_invalid_filepath_and_certpath_secret_tls": { + tlsSecretName: "foo", + tlsKey: "testKeyPath", + tlsCert: "testCertPath", + expectErr: true, + }, + } + + // Run all the tests + for name, test := range tests { + t.Run(name, func(t *testing.T) { + secretTLSOptions := CreateSecretTLSOptions{ + Name: test.tlsSecretName, + Key: test.tlsKey, + Cert: test.tlsCert, + AppendHash: test.appendHash, + } + secretTLS, err := secretTLSOptions.createSecretTLS() + + if !test.expectErr && err != nil { + t.Errorf("test %s, unexpected error: %v", name, err) + } + if test.expectErr && err == nil { + t.Errorf("test %s was expecting an error but no error occurred", name) + } + if !apiequality.Semantic.DeepEqual(secretTLS, test.expected) { + t.Errorf("test %s\n expected:\n%#v\ngot:\n%#v", name, test.expected, secretTLS) + } + }) + } +} + +func tearDown(tmpDir string) { + err := os.RemoveAll(tmpDir) + if err != nil { + fmt.Printf("Error in cleaning up test: %v", err) + } +} + +func write(path, contents string, t *testing.T) { + f, err := os.Create(path) + if err != nil { + t.Fatalf("Failed to create %v.", path) + } + defer f.Close() + _, err = f.WriteString(contents) + if err != nil { + t.Fatalf("Failed to write to %v.", path) + } +} + +func writeKeyPair(tmpDirPath, key, cert string, t *testing.T) (keyPath, certPath string) { + keyPath = path.Join(tmpDirPath, "tls.key") + certPath = path.Join(tmpDirPath, "tls.cert") + write(keyPath, key, t) + write(certPath, cert, t) + return +} diff --git a/pkg/cmd/create/create_serviceaccount.go b/pkg/cmd/create/create_serviceaccount.go index 1317d906..0634f41d 100644 --- a/pkg/cmd/create/create_serviceaccount.go +++ b/pkg/cmd/create/create_serviceaccount.go @@ -206,7 +206,3 @@ func (o *ServiceAccountOpts) createServiceAccount() (*corev1.ServiceAccount, err serviceAccount.Name = o.Name return serviceAccount, nil } - -func errUnsupportedGenerator(cmd *cobra.Command, generatorName string) error { - return cmdutil.UsageErrorf(cmd, "Generator %s not supported. ", generatorName) -}