package secrets
import (
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"path"
"strings"
kapi "k8s.io/kubernetes/pkg/api"
kcoreclient "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/core/unversioned"
kcmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
kvalidation "k8s.io/kubernetes/pkg/util/validation"
"github.com/openshift/origin/pkg/cmd/templates"
"github.com/openshift/origin/pkg/cmd/util/clientcmd"
"github.com/spf13/cobra"
)
const NewSecretRecommendedCommandName = "new"
var (
newLong = templates.LongDesc(`
Create a new secret based on a file or directory
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 create a secret
using with all valid keys in that directory.`)
newExample = templates.Examples(`
# Create a new secret named my-secret with a key named ssh-privatekey
%[1]s my-secret ~/.ssh/ssh-privatekey
# Create a new secret named my-secret with keys named ssh-privatekey and ssh-publickey instead of the names of the keys on disk
%[1]s my-secret ssh-privatekey=~/.ssh/id_rsa ssh-publickey=~/.ssh/id_rsa.pub
# Create a new secret named my-secret with keys for each file in the folder "bar"
%[1]s my-secret path/to/bar
# Create a new .dockercfg secret named my-secret
%[1]s my-secret path/to/.dockercfg
# Create a new .docker/config.json secret named my-secret
%[1]s my-secret .dockerconfigjson=path/to/.docker/config.json`)
)
type CreateSecretOptions struct {
// Name of the resulting secret
Name string
// SecretTypeName is the type to use when creating the secret. It is checked against known types.
SecretTypeName string
// Files/Directories to read from.
// Directory sources are listed and any direct file children included (but subfolders are not traversed)
Sources []string
SecretsInterface kcoreclient.SecretInterface
// Writer to write warnings to
Stderr io.Writer
Out io.Writer
// Controls whether to output warnings
Quiet bool
AllowUnknownTypes bool
}
func NewCmdCreateSecret(name, fullName string, f *clientcmd.Factory, out io.Writer) *cobra.Command {
options := NewCreateSecretOptions()
options.Out = out
cmd := &cobra.Command{
Use: fmt.Sprintf("%s NAME [KEY=]SOURCE ...", name),
Short: "Create a new secret based on a key file or on files within a directory",
Long: newLong,
Example: fmt.Sprintf(newExample, fullName),
Run: func(c *cobra.Command, args []string) {
if err := options.Complete(args, f); err != nil {
kcmdutil.CheckErr(kcmdutil.UsageError(c, err.Error()))
}
if err := options.Validate(); err != nil {
kcmdutil.CheckErr(kcmdutil.UsageError(c, err.Error()))
}
if len(kcmdutil.GetFlagString(c, "output")) != 0 {
secret, err := options.BundleSecret()
kcmdutil.CheckErr(err)
mapper, _ := f.Object(false)
kcmdutil.CheckErr(f.Factory.PrintObject(c, mapper, secret, out))
return
}
_, err := options.CreateSecret()
kcmdutil.CheckErr(err)
},
}
cmd.Flags().BoolVarP(&options.Quiet, "quiet", "q", options.Quiet, "If true, suppress warnings")
cmd.Flags().BoolVar(&options.AllowUnknownTypes, "confirm", options.AllowUnknownTypes, "If true, allow unknown secret types.")
cmd.Flags().StringVar(&options.SecretTypeName, "type", "", "The type of secret")
kcmdutil.AddPrinterFlags(cmd)
return cmd
}
func NewCreateSecretOptions() *CreateSecretOptions {
return &CreateSecretOptions{
Stderr: os.Stderr,
Sources: []string{},
}
}
func (o *CreateSecretOptions) Complete(args []string, f *clientcmd.Factory) error {
// Fill name from args[0]
if len(args) > 0 {
o.Name = args[0]
}
// Add sources from args[1:...] in addition to -f
if len(args) > 1 {
o.Sources = append(o.Sources, args[1:]...)
}
if f != nil {
_, _, kubeClient, err := f.Clients()
if err != nil {
return err
}
namespace, _, err := f.Factory.DefaultNamespace()
if err != nil {
return err
}
o.SecretsInterface = kubeClient.Core().Secrets(namespace)
}
return nil
}
func (o *CreateSecretOptions) Validate() error {
if len(o.Name) == 0 {
return errors.New("secret name is required")
}
if len(o.Sources) == 0 {
return errors.New("at least one source file or directory must be specified")
}
if !o.AllowUnknownTypes {
switch o.SecretTypeName {
case string(kapi.SecretTypeOpaque), "":
// this is ok
default:
found := false
for _, secretType := range KnownSecretTypes {
if o.SecretTypeName == string(secretType.Type) {
found = true
break
}
}
if !found {
return fmt.Errorf("unknown secret type %q; use --confirm to use it anyway", o.SecretTypeName)
}
}
}
return nil
}
func (o *CreateSecretOptions) CreateSecret() (*kapi.Secret, error) {
secret, err := o.BundleSecret()
if err != nil {
return nil, err
}
persistedSecret, err := o.SecretsInterface.Create(secret)
if err == nil {
fmt.Fprintf(o.Out, "secret/%s\n", persistedSecret.Name)
}
return persistedSecret, err
}
func (o *CreateSecretOptions) BundleSecret() (*kapi.Secret, error) {
secretData := make(map[string][]byte)
for _, source := range o.Sources {
keyName, filePath, err := parseSource(source)
if err != nil {
return nil, err
}
info, err := os.Stat(filePath)
if err != nil {
switch err := err.(type) {
case *os.PathError:
return nil, fmt.Errorf("error reading %s: %v", filePath, err.Err)
default:
return nil, fmt.Errorf("error reading %s: %v", filePath, err)
}
}
if info.IsDir() {
if strings.Contains(source, "=") {
return nil, errors.New("Cannot give a key name for a directory path.")
}
fileList, err := ioutil.ReadDir(filePath)
if err != nil {
return nil, fmt.Errorf("error listing files in %s: %v", filePath, err)
}
for _, item := range fileList {
itemPath := path.Join(filePath, item.Name())
if !item.Mode().IsRegular() {
if o.Stderr != nil && o.Quiet != true {
fmt.Fprintf(o.Stderr, "Skipping resource %s\n", itemPath)
}
} else {
keyName = item.Name()
err = addKeyToSecret(keyName, itemPath, secretData)
if err != nil {
return nil, err
}
}
}
} else {
err = addKeyToSecret(keyName, filePath, secretData)
if err != nil {
return nil, err
}
}
}
if len(secretData) == 0 {
return nil, errors.New("No files selected")
}
// if the secret type isn't specified, attempt to auto-detect likely hit
secretType := kapi.SecretType(o.SecretTypeName)
if len(o.SecretTypeName) == 0 {
secretType = kapi.SecretTypeOpaque
for _, knownSecretType := range KnownSecretTypes {
if knownSecretType.Matches(secretData) {
secretType = knownSecretType.Type
}
}
}
secret := &kapi.Secret{
ObjectMeta: kapi.ObjectMeta{Name: o.Name},
Type: secretType,
Data: secretData,
}
return secret, nil
}
func addKeyToSecret(keyName, filePath string, secretData map[string][]byte) error {
if errors := kvalidation.IsConfigMapKey(keyName); len(errors) > 0 {
return fmt.Errorf("%v is not a valid key name for a secret: %s", keyName, strings.Join(errors, ", "))
}
if _, entryExists := secretData[keyName]; entryExists {
return fmt.Errorf("cannot add key %s from path %s, another key by that name already exists: %v.", keyName, filePath, secretData)
}
data, err := ioutil.ReadFile(filePath)
if err != nil {
return err
}
secretData[keyName] = data
return nil
}
// parseSource parses the source given. Acceptable formats include:
// source-name=source-path, where source-name will become the key name and source-path is the path to the key file
// source-path, where source-path is a path to a file or directory, and key names will default to file names
// Key names cannot include '='.
func parseSource(source string) (keyName, filePath string, err error) {
numSeparators := strings.Count(source, "=")
switch {
case numSeparators == 0:
return path.Base(source), source, nil
case numSeparators == 1 && strings.HasPrefix(source, "="):
return "", "", fmt.Errorf("key name for file path %v missing.", strings.TrimPrefix(source, "="))
case numSeparators == 1 && strings.HasSuffix(source, "="):
return "", "", fmt.Errorf("file path for key name %v missing.", strings.TrimSuffix(source, "="))
case numSeparators > 1:
return "", "", errors.New("Key names or file paths cannot contain '='.")
default:
components := strings.Split(source, "=")
return components[0], components[1], nil
}
}