Skip to content

Commit 47f322d

Browse files
Feat/stackitcli 318 secret flags 3 (#1377)
* refac(io) set IO once in main to allow overriding with in memory io in tests `print.Printer` had a reference to a `cobra.Command` for using its IO streams. Each Command also used a Printer, resulting in an awkward circular dependency. Refactored Printer to use IO streams directly. When using the application these are set in `main`, when used in tests these can be set to `bytes.Buffer`s. Also replaced usages of `os.Args` with just a string slice. Also set in `main`. `cobra.Command`s `Args` and IO-streams are set in `NewRootCmd` with `traverseCommands`. `CmdParams` also has an `fs.FS`, currently unused but will allow using the real FS during regular use and an in-memory-FS during tests. This change prepares the application for integrative testing while keeping good isolation. Generally speaking the goal is to move all things with side effects into main (compare with https://grafana.com/blog/how-i-write-http-services-in-go-after-13-years/#func-main-only-calls-run) * fix(fmt) run formatter * fix(test) adapt cli args after changing arg slicing * feat(secret flag) add reusable secret flag implementation, update guide Had to split implementation into two parts: `Set()` and `SecretFlagToSP`. Set is only called when an argument is specified on the command line. So moving the `PromptForPassword` logic into `Set` does not work. I'm not sure if the current solution with a specialized `*ToStringPointer` func is the way to go. So I've only refactored `obs-credentials add` as an example. * fix(docs) generate docs * fix(lint) fix linting errors * fix(secretflag) use title case when printing flag usage * fix(fmt) run formatter * feat(secretflags) refactor password flags to use SecretFlag - searched for 'password' and 'secret' to find usages - checked usages of printer.PromptForPassword
1 parent f52c9ac commit 47f322d

15 files changed

Lines changed: 67 additions & 83 deletions

docs/stackit_beta_alb_observability-credentials_update.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ stackit beta alb observability-credentials update CREDENTIAL_REF_ARG [flags]
2222
```
2323
-d, --displayname string Displayname for the credentials
2424
-h, --help Help for "stackit beta alb observability-credentials update"
25-
--password string Password. Can be a string or a file path, if prefixed with "@" (example: @./password.txt).
25+
--password string Password. Can be a string (deprecated) or a file path, if prefixed with '@' (example: @./secret.txt). Will be read from stdin when empty.
2626
-u, --username string Username for the credentials
2727
```
2828

docs/stackit_beta_cdn_distribution_create.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ stackit beta cdn distribution create [flags]
3535
--blocked-ips strings Comma-separated list of IPv4 addresses to block (e.g., '10.0.0.8,127.0.0.1')
3636
--bucket Use Object Storage backend
3737
--bucket-credentials-access-key-id string Access Key ID for Object Storage backend
38+
--bucket-password string Bucket-Password. Can be a string (deprecated) or a file path, if prefixed with '@' (example: @./secret.txt). Will be read from stdin when empty.
3839
--bucket-region string Region for Object Storage backend
3940
--bucket-url string Bucket URL for Object Storage backend
4041
--default-cache-duration string ISO8601 duration string for default cache duration (e.g., 'PT1H30M' for 1 hour and 30 minutes)
@@ -44,6 +45,7 @@ stackit beta cdn distribution create [flags]
4445
--http-origin-request-headers strings Origin request headers for HTTP backend in the format 'HeaderName: HeaderValue', repeatable. WARNING: do not store sensitive values in the headers!
4546
--http-origin-url string Origin URL for HTTP backend
4647
--loki Enable Loki log sink for the CDN distribution
48+
--loki-password string Loki-Password. Can be a string (deprecated) or a file path, if prefixed with '@' (example: @./secret.txt). Will be read from stdin when empty.
4749
--loki-push-url string Push URL for log sink
4850
--loki-username string Username for log sink
4951
--monthly-limit-bytes int Monthly limit in bytes for the CDN distribution

docs/stackit_beta_cdn_distribution_update.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ stackit beta cdn distribution update [flags]
2424
--blocked-ips strings Comma-separated list of IPv4 addresses to block (e.g., '10.0.0.8,127.0.0.1')
2525
--bucket Use Object Storage backend
2626
--bucket-credentials-access-key-id string Access Key ID for Object Storage backend
27+
--bucket-password string Bucket-Password. Can be a string (deprecated) or a file path, if prefixed with '@' (example: @./secret.txt). Will be read from stdin when empty.
2728
--bucket-region string Region for Object Storage backend
2829
--bucket-url string Bucket URL for Object Storage backend
2930
--default-cache-duration string ISO8601 duration string for default cache duration (e.g., 'PT1H30M' for 1 hour and 30 minutes)
@@ -33,6 +34,7 @@ stackit beta cdn distribution update [flags]
3334
--http-origin-request-headers strings Origin request headers for HTTP backend in the format 'HeaderName: HeaderValue', repeatable. WARNING: do not store sensitive values in the headers!
3435
--http-origin-url string Origin URL for HTTP backend
3536
--loki Enable Loki log sink for the CDN distribution
37+
--loki-password string Loki-Password. Can be a string (deprecated) or a file path, if prefixed with '@' (example: @./secret.txt). Will be read from stdin when empty.
3638
--loki-push-url string Push URL for log sink
3739
--loki-username string Username for log sink
3840
--monthly-limit-bytes int Monthly limit in bytes for the CDN distribution

docs/stackit_beta_intake_user_create.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ stackit beta intake user create [flags]
2828
-h, --help Help for "stackit beta intake user create"
2929
--intake-id string The UUID of the Intake to associate the user with
3030
--labels stringToString Labels in key=value format, separated by commas (default [])
31-
--password string Password for the user. Must contain lower, upper, number, and special characters (min 12 chars)
31+
--password string Password. Can be a string (deprecated) or a file path, if prefixed with '@' (example: @./secret.txt). Will be read from stdin when empty. Must contain lower, upper, number, and special characters (min 12 chars)
3232
--type string Type of user. One of 'intake' (default) or 'dead-letter' (default "intake")
3333
```
3434

docs/stackit_beta_intake_user_update.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ stackit beta intake user update USER_ID [flags]
2828
-h, --help Help for "stackit beta intake user update"
2929
--intake-id string Intake ID
3030
--labels stringToString Labels in key=value format, separated by commas. Example: --labels "key1=value1,key2=value2". (default [])
31-
--password string Password for the user. Must contain lower, upper, number, and special characters (min 12 chars)
31+
--password string Password. Can be a string (deprecated) or a file path, if prefixed with '@' (example: @./secret.txt). Will be read from stdin when empty. Must contain lower, upper, number, and special characters (min 12 chars)
3232
--type string Type of user. One of 'intake' or 'dead-letter'
3333
```
3434

docs/stackit_load-balancer_observability-credentials_add.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ stackit load-balancer observability-credentials add [flags]
2525
```
2626
--display-name string Credentials display name
2727
-h, --help Help for "stackit load-balancer observability-credentials add"
28-
--password string Password. Can be a string or a file path, if prefixed with "@" (example: @./password.txt).
28+
--password string Password. Can be a string (deprecated) or a file path, if prefixed with '@' (example: @./secret.txt). Will be read from stdin when empty.
2929
--username string Username
3030
```
3131

docs/stackit_load-balancer_observability-credentials_update.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ stackit load-balancer observability-credentials update CREDENTIALS_REF [flags]
2525
```
2626
--display-name string Credentials name
2727
-h, --help Help for "stackit load-balancer observability-credentials update"
28-
--password string Password. Can be a string or a file path, if prefixed with "@" (example: @./password.txt).
28+
--password string Password. Can be a string (deprecated) or a file path, if prefixed with '@' (example: @./secret.txt). Will be read from stdin when empty.
2929
--username string Username
3030
```
3131

internal/cmd/beta/alb/observability-credentials/update/update.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,14 +82,15 @@ func NewCmd(params *types.CmdParams) *cobra.Command {
8282
return outputResult(params.Printer, model, resp)
8383
},
8484
}
85-
configureFlags(cmd)
85+
configureFlags(cmd, params)
8686
return cmd
8787
}
8888

89-
func configureFlags(cmd *cobra.Command) {
89+
func configureFlags(cmd *cobra.Command, params *types.CmdParams) {
9090
cmd.Flags().StringP(usernameFlag, "u", "", "Username for the credentials")
9191
cmd.Flags().StringP(displaynameFlag, "d", "", "Displayname for the credentials")
92-
cmd.Flags().Var(flags.ReadFromFileFlag(), passwordFlag, `Password. Can be a string or a file path, if prefixed with "@" (example: @./password.txt).`)
92+
password := flags.SecretFlag(passwordFlag, params)
93+
cmd.Flags().Var(password, passwordFlag, password.Usage())
9394

9495
cobra.CheckErr(flags.MarkFlagsRequired(cmd, displaynameFlag, usernameFlag))
9596
}
@@ -116,7 +117,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) inputM
116117
Username: flags.FlagToStringPointer(p, cmd, usernameFlag),
117118
Displayname: flags.FlagToStringPointer(p, cmd, displaynameFlag),
118119
CredentialsRef: &inputArgs[0],
119-
Password: flags.FlagToStringPointer(p, cmd, passwordFlag),
120+
Password: flags.SecretFlagToStringPointer(p, cmd, passwordFlag),
120121
}
121122

122123
p.DebugInputModel(model)

internal/cmd/beta/cdn/distribution/create/create.go

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,14 @@ const (
3030
flagBucket = "bucket"
3131
flagBucketURL = "bucket-url"
3232
flagBucketCredentialsAccessKeyID = "bucket-credentials-access-key-id" //nolint:gosec // linter false positive
33+
flagBucketPassword = "bucket-password"
3334
flagBucketRegion = "bucket-region"
3435
flagBlockedCountries = "blocked-countries"
3536
flagBlockedIPs = "blocked-ips"
3637
flagDefaultCacheDuration = "default-cache-duration"
3738
flagLoki = "loki"
3839
flagLokiUsername = "loki-username"
40+
flagLokiPassword = "loki-password"
3941
flagLokiPushURL = "loki-push-url"
4042
flagMonthlyLimitBytes = "monthly-limit-bytes"
4143
flagOptimizer = "optimizer"
@@ -119,20 +121,6 @@ func NewCmd(params *types.CmdParams) *cobra.Command {
119121
if err != nil {
120122
return err
121123
}
122-
if model.Bucket != nil {
123-
pw, err := params.Printer.PromptForPassword("enter your secret access key for the object storage bucket: ")
124-
if err != nil {
125-
return fmt.Errorf("reading secret access key: %w", err)
126-
}
127-
model.Bucket.Password = pw
128-
}
129-
if model.Loki != nil {
130-
pw, err := params.Printer.PromptForPassword("enter your password for the loki log sink: ")
131-
if err != nil {
132-
return fmt.Errorf("reading loki password: %w", err)
133-
}
134-
model.Loki.Password = pw
135-
}
136124

137125
apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion)
138126
if err != nil {
@@ -161,11 +149,11 @@ func NewCmd(params *types.CmdParams) *cobra.Command {
161149
return outputResult(params.Printer, model.OutputFormat, projectLabel, resp)
162150
},
163151
}
164-
configureFlags(cmd)
152+
configureFlags(cmd, params)
165153
return cmd
166154
}
167155

168-
func configureFlags(cmd *cobra.Command) {
156+
func configureFlags(cmd *cobra.Command, params *types.CmdParams) {
169157
cmd.Flags().Var(flags.EnumSliceFlag(false, []string{}, sdkUtils.EnumSliceToStringSlice(cdn.AllowedRegionEnumValues)...), flagRegion, fmt.Sprintf("Regions in which content should be cached, multiple of: %q", cdn.AllowedRegionEnumValues))
170158
cmd.Flags().Bool(flagHTTP, false, "Use HTTP backend")
171159
cmd.Flags().String(flagHTTPOriginURL, "", "Origin URL for HTTP backend")
@@ -174,12 +162,16 @@ func configureFlags(cmd *cobra.Command) {
174162
cmd.Flags().Bool(flagBucket, false, "Use Object Storage backend")
175163
cmd.Flags().String(flagBucketURL, "", "Bucket URL for Object Storage backend")
176164
cmd.Flags().String(flagBucketCredentialsAccessKeyID, "", "Access Key ID for Object Storage backend")
165+
bucketPassword := flags.SecretFlag(flagBucketPassword, params)
166+
cmd.Flags().Var(bucketPassword, flagBucketPassword, bucketPassword.Usage())
177167
cmd.Flags().String(flagBucketRegion, "", "Region for Object Storage backend")
178168
cmd.Flags().StringSlice(flagBlockedCountries, []string{}, "Comma-separated list of ISO 3166-1 alpha-2 country codes to block (e.g., 'US,DE,FR')")
179169
cmd.Flags().StringSlice(flagBlockedIPs, []string{}, "Comma-separated list of IPv4 addresses to block (e.g., '10.0.0.8,127.0.0.1')")
180170
cmd.Flags().String(flagDefaultCacheDuration, "", "ISO8601 duration string for default cache duration (e.g., 'PT1H30M' for 1 hour and 30 minutes)")
181171
cmd.Flags().Bool(flagLoki, false, "Enable Loki log sink for the CDN distribution")
182172
cmd.Flags().String(flagLokiUsername, "", "Username for log sink")
173+
lokiPassword := flags.SecretFlag(flagLokiPassword, params)
174+
cmd.Flags().Var(lokiPassword, flagLokiPassword, lokiPassword.Usage())
183175
cmd.Flags().String(flagLokiPushURL, "", "Push URL for log sink")
184176
cmd.Flags().Int64(flagMonthlyLimitBytes, 0, "Monthly limit in bytes for the CDN distribution")
185177
cmd.Flags().Bool(flagOptimizer, false, "Enable optimizer for the CDN distribution (paid feature).")
@@ -229,11 +221,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel,
229221
bucketURL := flags.FlagToStringValue(p, cmd, flagBucketURL)
230222
accessKeyID := flags.FlagToStringValue(p, cmd, flagBucketCredentialsAccessKeyID)
231223
region := flags.FlagToStringValue(p, cmd, flagBucketRegion)
224+
password := flags.SecretFlagToString(p, cmd, flagBucketPassword)
232225

233226
bucket = &bucketInputModel{
234227
URL: bucketURL,
235228
AccessKeyID: accessKeyID,
236-
Password: "",
229+
Password: password,
237230
Region: region,
238231
}
239232
}
@@ -248,7 +241,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel,
248241
loki = &lokiInputModel{
249242
Username: flags.FlagToStringValue(p, cmd, flagLokiUsername),
250243
PushURL: flags.FlagToStringValue(p, cmd, flagLokiPushURL),
251-
Password: "",
244+
Password: flags.SecretFlagToString(p, cmd, flagLokiPassword),
252245
}
253246
}
254247

internal/cmd/beta/cdn/distribution/update/update.go

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,14 @@ const (
3131
flagBucket = "bucket"
3232
flagBucketURL = "bucket-url"
3333
flagBucketCredentialsAccessKeyID = "bucket-credentials-access-key-id" //nolint:gosec // linter false positive
34+
flagBucketPassword = "bucket-password"
3435
flagBucketRegion = "bucket-region"
3536
flagBlockedCountries = "blocked-countries"
3637
flagBlockedIPs = "blocked-ips"
3738
flagDefaultCacheDuration = "default-cache-duration"
3839
flagLoki = "loki"
3940
flagLokiUsername = "loki-username"
41+
flagLokiPassword = "loki-password"
4042
flagLokiPushURL = "loki-push-url"
4143
flagMonthlyLimitBytes = "monthly-limit-bytes"
4244
flagOptimizer = "optimizer"
@@ -93,20 +95,6 @@ func NewCmd(params *types.CmdParams) *cobra.Command {
9395
if err != nil {
9496
return err
9597
}
96-
if model.Bucket != nil {
97-
pw, err := params.Printer.PromptForPassword("enter your secret access key for the object storage bucket: ")
98-
if err != nil {
99-
return fmt.Errorf("reading secret access key: %w", err)
100-
}
101-
model.Bucket.Password = pw
102-
}
103-
if model.Loki != nil {
104-
pw, err := params.Printer.PromptForPassword("enter your password for the loki log sink: ")
105-
if err != nil {
106-
return fmt.Errorf("reading loki password: %w", err)
107-
}
108-
model.Loki.Password = pw
109-
}
11098

11199
apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion)
112100
if err != nil {
@@ -134,11 +122,11 @@ func NewCmd(params *types.CmdParams) *cobra.Command {
134122
return outputResult(params.Printer, model.OutputFormat, projectLabel, resp)
135123
},
136124
}
137-
configureFlags(cmd)
125+
configureFlags(cmd, params)
138126
return cmd
139127
}
140128

141-
func configureFlags(cmd *cobra.Command) {
129+
func configureFlags(cmd *cobra.Command, params *types.CmdParams) {
142130
cmd.Flags().Var(flags.EnumSliceFlag(false, []string{}, sdkUtils.EnumSliceToStringSlice(cdn.AllowedRegionEnumValues)...), flagRegions, fmt.Sprintf("Regions in which content should be cached, multiple of: %q", cdn.AllowedRegionEnumValues))
143131
cmd.Flags().Bool(flagHTTP, false, "Use HTTP backend")
144132
cmd.Flags().String(flagHTTPOriginURL, "", "Origin URL for HTTP backend")
@@ -147,12 +135,16 @@ func configureFlags(cmd *cobra.Command) {
147135
cmd.Flags().Bool(flagBucket, false, "Use Object Storage backend")
148136
cmd.Flags().String(flagBucketURL, "", "Bucket URL for Object Storage backend")
149137
cmd.Flags().String(flagBucketCredentialsAccessKeyID, "", "Access Key ID for Object Storage backend")
138+
bucketPassword := flags.SecretFlag(flagBucketPassword, params)
139+
cmd.Flags().Var(bucketPassword, flagBucketPassword, bucketPassword.Usage())
150140
cmd.Flags().String(flagBucketRegion, "", "Region for Object Storage backend")
151141
cmd.Flags().StringSlice(flagBlockedCountries, []string{}, "Comma-separated list of ISO 3166-1 alpha-2 country codes to block (e.g., 'US,DE,FR')")
152142
cmd.Flags().StringSlice(flagBlockedIPs, []string{}, "Comma-separated list of IPv4 addresses to block (e.g., '10.0.0.8,127.0.0.1')")
153143
cmd.Flags().String(flagDefaultCacheDuration, "", "ISO8601 duration string for default cache duration (e.g., 'PT1H30M' for 1 hour and 30 minutes)")
154144
cmd.Flags().Bool(flagLoki, false, "Enable Loki log sink for the CDN distribution")
155145
cmd.Flags().String(flagLokiUsername, "", "Username for log sink")
146+
lokiPassword := flags.SecretFlag(flagLokiPassword, params)
147+
cmd.Flags().Var(lokiPassword, flagLokiPassword, lokiPassword.Usage())
156148
cmd.Flags().String(flagLokiPushURL, "", "Push URL for log sink")
157149
cmd.Flags().Int64(flagMonthlyLimitBytes, 0, "Monthly limit in bytes for the CDN distribution")
158150
cmd.Flags().Bool(flagOptimizer, false, "Enable optimizer for the CDN distribution (paid feature).")
@@ -200,11 +192,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu
200192
bucketURL := flags.FlagToStringValue(p, cmd, flagBucketURL)
201193
accessKeyID := flags.FlagToStringValue(p, cmd, flagBucketCredentialsAccessKeyID)
202194
region := flags.FlagToStringValue(p, cmd, flagBucketRegion)
195+
password := flags.SecretFlagToString(p, cmd, flagBucketPassword)
203196

204197
bucket = &bucketInputModel{
205198
URL: bucketURL,
206199
AccessKeyID: accessKeyID,
207-
Password: "",
200+
Password: password,
208201
Region: region,
209202
}
210203
}
@@ -219,7 +212,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu
219212
loki = &lokiInputModel{
220213
Username: flags.FlagToStringValue(p, cmd, flagLokiUsername),
221214
PushURL: flags.FlagToStringValue(p, cmd, flagLokiPushURL),
222-
Password: "",
215+
Password: flags.SecretFlagToString(p, cmd, flagLokiPassword),
223216
}
224217
}
225218

0 commit comments

Comments
 (0)