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 }