github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/worker/uniter/runner/jujuc/secret-add.go (about)

     1  // Copyright 2021 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package jujuc
     5  
     6  import (
     7  	"fmt"
     8  	"time"
     9  
    10  	"github.com/juju/cmd/v3"
    11  	"github.com/juju/errors"
    12  	"github.com/juju/gnuflag"
    13  	"github.com/juju/names/v5"
    14  
    15  	jujucmd "github.com/juju/juju/cmd"
    16  	"github.com/juju/juju/core/secrets"
    17  )
    18  
    19  type secretUpsertCommand struct {
    20  	cmd.CommandBase
    21  	ctx Context
    22  
    23  	owner        string
    24  	rotatePolicy string
    25  	description  string
    26  	label        string
    27  	fileName     string
    28  
    29  	expireSpec string
    30  	expireTime time.Time
    31  
    32  	data map[string]string
    33  }
    34  
    35  type secretAddCommand struct {
    36  	secretUpsertCommand
    37  }
    38  
    39  // NewSecretAddCommand returns a command to add a secret.
    40  func NewSecretAddCommand(ctx Context) (cmd.Command, error) {
    41  	return &secretAddCommand{
    42  		secretUpsertCommand{ctx: ctx},
    43  	}, nil
    44  }
    45  
    46  // Info implements cmd.Command.
    47  func (c *secretAddCommand) Info() *cmd.Info {
    48  	doc := `
    49  Add a secret with a list of key values.
    50  
    51  If a key has the '#base64' suffix, the value is already in base64 format and no
    52  encoding will be performed, otherwise the value will be base64 encoded
    53  prior to being stored.
    54  
    55  If a key has the '#file' suffix, the value is read from the corresponding file.
    56  
    57  By default, a secret is owned by the application, meaning only the unit
    58  leader can manage it. Use "--owner unit" to create a secret owned by the
    59  specific unit which created it.
    60  
    61  Examples:
    62      secret-add token=34ae35facd4
    63      secret-add key#base64=AA==
    64      secret-add key#file=/path/to/file another-key=s3cret
    65      secret-add --owner unit token=s3cret 
    66      secret-add --rotate monthly token=s3cret 
    67      secret-add --expire 24h token=s3cret 
    68      secret-add --expire 2025-01-01T06:06:06 token=s3cret 
    69      secret-add --label db-password \
    70          --description "my database password" \
    71          data#base64=s3cret== 
    72      secret-add --label db-password \
    73          --description "my database password" \
    74          --file=/path/to/file
    75  `
    76  	return jujucmd.Info(&cmd.Info{
    77  		Name:    "secret-add",
    78  		Args:    "[key[#base64|#file]=value...]",
    79  		Purpose: "add a new secret",
    80  		Doc:     doc,
    81  	})
    82  }
    83  
    84  // SetFlags implements cmd.Command.
    85  func (c *secretUpsertCommand) SetFlags(f *gnuflag.FlagSet) {
    86  	f.StringVar(&c.expireSpec, "expire", "", "either a duration or time when the secret should expire")
    87  	f.StringVar(&c.rotatePolicy, "rotate", "", "the secret rotation policy")
    88  	f.StringVar(&c.description, "description", "", "the secret description")
    89  	f.StringVar(&c.label, "label", "", "a label used to identify the secret in hooks")
    90  	f.StringVar(&c.fileName, "file", "", "a YAML file containing secret key values")
    91  	f.StringVar(&c.owner, "owner", "application", "the owner of the secret, either the application or unit")
    92  }
    93  
    94  const rcf3339NoTZ = "2006-01-02T15:04:05"
    95  
    96  // Init implements cmd.Command.
    97  func (c *secretUpsertCommand) Init(args []string) error {
    98  	if c.expireSpec != "" {
    99  		expireTime, err := time.Parse(time.RFC3339, c.expireSpec)
   100  		if err != nil {
   101  			expireTime, err = time.Parse(rcf3339NoTZ, c.expireSpec)
   102  		}
   103  		if err != nil {
   104  			d, err := time.ParseDuration(c.expireSpec)
   105  			if err != nil {
   106  				return errors.NotValidf("expire time or duration %q", c.expireSpec)
   107  			}
   108  			if d <= 0 {
   109  				return errors.NotValidf("negative expire duration %q", c.expireSpec)
   110  			}
   111  			expireTime = time.Now().Add(d)
   112  		}
   113  		c.expireTime = expireTime.UTC()
   114  	}
   115  	if c.rotatePolicy != "" && !secrets.RotatePolicy(c.rotatePolicy).IsValid() {
   116  		return errors.NotValidf("rotate policy %q", c.rotatePolicy)
   117  	}
   118  	if c.owner != "application" && c.owner != "unit" {
   119  		return errors.NotValidf("secret owner %q", c.owner)
   120  	}
   121  
   122  	var err error
   123  	c.data, err = secrets.CreateSecretData(args)
   124  	if err != nil {
   125  		return errors.Trace(err)
   126  	}
   127  	if c.fileName == "" {
   128  		return nil
   129  	}
   130  	dataFromFile, err := secrets.ReadSecretData(c.fileName)
   131  	if err != nil {
   132  		return errors.Trace(err)
   133  	}
   134  	for k, v := range dataFromFile {
   135  		c.data[k] = v
   136  	}
   137  	return nil
   138  }
   139  
   140  func (c *secretUpsertCommand) marshallArg() *SecretUpdateArgs {
   141  	value := secrets.NewSecretValue(c.data)
   142  	arg := &SecretUpdateArgs{
   143  		Value: value,
   144  	}
   145  	if c.rotatePolicy != "" {
   146  		p := secrets.RotatePolicy(c.rotatePolicy)
   147  		arg.RotatePolicy = &p
   148  	}
   149  	if !c.expireTime.IsZero() {
   150  		arg.ExpireTime = &c.expireTime
   151  	}
   152  	if c.description != "" {
   153  		arg.Description = &c.description
   154  	}
   155  	if c.label != "" {
   156  		arg.Label = &c.label
   157  	}
   158  	return arg
   159  }
   160  
   161  // Init implements cmd.Command.
   162  func (c *secretAddCommand) Init(args []string) error {
   163  	if len(args) < 1 && c.fileName == "" {
   164  		return errors.New("missing secret value or filename")
   165  	}
   166  	return c.secretUpsertCommand.Init(args)
   167  }
   168  
   169  // Run implements cmd.Command.
   170  func (c *secretAddCommand) Run(ctx *cmd.Context) error {
   171  	unitName := c.ctx.UnitName()
   172  	var ownerTag names.Tag
   173  	appName, _ := names.UnitApplication(unitName)
   174  	ownerTag = names.NewApplicationTag(appName)
   175  	if c.owner == "unit" {
   176  		ownerTag = names.NewUnitTag(unitName)
   177  	}
   178  	updateArgs := c.marshallArg()
   179  	if updateArgs.Value.IsEmpty() {
   180  		return errors.NotValidf("empty secret value")
   181  	}
   182  	arg := &SecretCreateArgs{
   183  		SecretUpdateArgs: *updateArgs,
   184  		OwnerTag:         ownerTag,
   185  	}
   186  	uri, err := c.ctx.CreateSecret(arg)
   187  	if err != nil {
   188  		return err
   189  	}
   190  	fmt.Fprintln(ctx.Stdout, uri.String())
   191  	return nil
   192  }