-
Notifications
You must be signed in to change notification settings - Fork 894
chore: color value_source
for deployment values
#9922
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 13 commits
ac997a9
3748430
07588ec
e1a8997
a6cc10e
be1aec3
dc67bb7
53b147b
b1058a6
6ce3af7
5049884
9e68d53
27cc9f3
a5029d1
ab2ecd9
1afa4fe
03a7883
93aa426
42fb5d0
cbb69bc
b5a9164
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,8 @@ | ||
package clibase | ||
|
||
import ( | ||
"bytes" | ||
"encoding/json" | ||
"os" | ||
"strings" | ||
|
||
|
@@ -65,6 +67,20 @@ type Option struct { | |
ValueSource ValueSource `json:"value_source,omitempty"` | ||
} | ||
|
||
// optionNoMethods is just a wrapper around Option so we can defer to the | ||
// default json.Unmarshaler behavior. | ||
type optionNoMethods Option | ||
|
||
func (o *Option) UnmarshalJSON(data []byte) error { | ||
// If an option has no values, we have no idea how to unmarshal it. | ||
// So just discard the json data. | ||
if o.Value == nil { | ||
o.Value = &DiscardValue | ||
} | ||
|
||
return json.Unmarshal(data, (*optionNoMethods)(o)) | ||
} | ||
|
||
func (o Option) YAMLPath() string { | ||
if o.YAML == "" { | ||
return "" | ||
|
@@ -79,15 +95,94 @@ func (o Option) YAMLPath() string { | |
// OptionSet is a group of options that can be applied to a command. | ||
type OptionSet []Option | ||
|
||
// UnmarshalJSON implements json.Unmarshaler for OptionSets. Options have an | ||
// interface Value type that cannot handle unmarshalling because the types cannot | ||
// be inferred. Since it is a slice, instantiating the Options first does not | ||
// help. | ||
// | ||
// However, we typically do instantiate the slice to have the correct types. | ||
// So this unmarshaller will attempt to find the named option in the existing | ||
// set, if it cannot, the value is discarded. If the option exists, the value | ||
// is unmarshalled into the existing option, and replaces the existing option. | ||
// | ||
// The value is discarded if it's type cannot be inferred. This behavior just | ||
// feels "safer", although it should never happen if the correct option set | ||
// is passed in. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure under what circumstances this Unmarshal is run? Is there a chance of conflicts between server versions (API response) and CLI version (decode)? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yup exactly. The new version can have more deployment options, so the old version has nothing to match it against. |
||
func (optSet *OptionSet) UnmarshalJSON(data []byte) error { | ||
dec := json.NewDecoder(bytes.NewBuffer(data)) | ||
// Should be a json array, so consume the starting open bracket. | ||
t, err := dec.Token() | ||
if err != nil { | ||
return xerrors.Errorf("read array open bracket: %w", err) | ||
} | ||
if t != json.Delim('[') { | ||
return xerrors.Errorf("expected array open bracket, got %q", t) | ||
} | ||
|
||
// As long as json elements exist, consume them. The counter is used for | ||
// better errors. | ||
var i int | ||
OptionSetDecodeLoop: | ||
for dec.More() { | ||
var opt Option | ||
// jValue is a placeholder value that allows us to capture the | ||
// raw json for the value to attempt to unmarshal later. | ||
var jValue jsonValue | ||
opt.Value = &jValue | ||
err := dec.Decode(&opt) | ||
if err != nil { | ||
return xerrors.Errorf("decode %d option: %w", i, err) | ||
} | ||
|
||
// Try to see if the option already exists in the option set. | ||
// If it does, just update the existing option. | ||
for i, have := range *optSet { | ||
if have.Name == opt.Name { | ||
if jValue != nil { | ||
err := json.Unmarshal(jValue, &(*optSet)[i].Value) | ||
if err != nil { | ||
return xerrors.Errorf("decode option %q value: %w", have.Name, err) | ||
} | ||
// Set the opt's value | ||
opt.Value = (*optSet)[i].Value | ||
} else { | ||
// Discard the value if it's nil. | ||
opt.Value = DiscardValue | ||
} | ||
// Override the existing. | ||
(*optSet)[i] = opt | ||
// Go to the next option to decode. | ||
continue OptionSetDecodeLoop | ||
Emyrk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} | ||
|
||
// If the option doesn't exist, the value will be discarded. | ||
// We do this because we cannot infer the type of the value. | ||
opt.Value = DiscardValue | ||
*optSet = append(*optSet, opt) | ||
i++ | ||
} | ||
|
||
t, err = dec.Token() | ||
if err != nil { | ||
return xerrors.Errorf("read array close bracket: %w", err) | ||
} | ||
if t != json.Delim(']') { | ||
return xerrors.Errorf("expected array close bracket, got %q", t) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// Add adds the given Options to the OptionSet. | ||
func (s *OptionSet) Add(opts ...Option) { | ||
*s = append(*s, opts...) | ||
func (optSet *OptionSet) Add(opts ...Option) { | ||
*optSet = append(*optSet, opts...) | ||
} | ||
|
||
// Filter will only return options that match the given filter. (return true) | ||
func (s OptionSet) Filter(filter func(opt Option) bool) OptionSet { | ||
func (optSet OptionSet) Filter(filter func(opt Option) bool) OptionSet { | ||
cpy := make(OptionSet, 0) | ||
for _, opt := range s { | ||
for _, opt := range optSet { | ||
if filter(opt) { | ||
cpy = append(cpy, opt) | ||
} | ||
|
@@ -96,13 +191,13 @@ func (s OptionSet) Filter(filter func(opt Option) bool) OptionSet { | |
} | ||
|
||
// FlagSet returns a pflag.FlagSet for the OptionSet. | ||
func (s *OptionSet) FlagSet() *pflag.FlagSet { | ||
if s == nil { | ||
func (optSet *OptionSet) FlagSet() *pflag.FlagSet { | ||
if optSet == nil { | ||
return &pflag.FlagSet{} | ||
} | ||
|
||
fs := pflag.NewFlagSet("", pflag.ContinueOnError) | ||
for _, opt := range *s { | ||
for _, opt := range *optSet { | ||
if opt.Flag == "" { | ||
continue | ||
} | ||
|
@@ -139,8 +234,8 @@ func (s *OptionSet) FlagSet() *pflag.FlagSet { | |
|
||
// ParseEnv parses the given environment variables into the OptionSet. | ||
// Use EnvsWithPrefix to filter out prefixes. | ||
func (s *OptionSet) ParseEnv(vs []EnvVar) error { | ||
if s == nil { | ||
func (optSet *OptionSet) ParseEnv(vs []EnvVar) error { | ||
if optSet == nil { | ||
return nil | ||
} | ||
|
||
|
@@ -154,7 +249,7 @@ func (s *OptionSet) ParseEnv(vs []EnvVar) error { | |
envs[v.Name] = v.Value | ||
} | ||
|
||
for i, opt := range *s { | ||
for i, opt := range *optSet { | ||
if opt.Env == "" { | ||
continue | ||
} | ||
|
@@ -172,7 +267,7 @@ func (s *OptionSet) ParseEnv(vs []EnvVar) error { | |
continue | ||
} | ||
|
||
(*s)[i].ValueSource = ValueSourceEnv | ||
(*optSet)[i].ValueSource = ValueSourceEnv | ||
if err := opt.Value.Set(envVal); err != nil { | ||
merr = multierror.Append( | ||
merr, xerrors.Errorf("parse %q: %w", opt.Name, err), | ||
|
@@ -185,14 +280,14 @@ func (s *OptionSet) ParseEnv(vs []EnvVar) error { | |
|
||
// SetDefaults sets the default values for each Option, skipping values | ||
// that already have a value source. | ||
func (s *OptionSet) SetDefaults() error { | ||
if s == nil { | ||
func (optSet *OptionSet) SetDefaults() error { | ||
if optSet == nil { | ||
return nil | ||
} | ||
|
||
var merr *multierror.Error | ||
|
||
for i, opt := range *s { | ||
for i, opt := range *optSet { | ||
// Skip values that may have already been set by the user. | ||
if opt.ValueSource != ValueSourceNone { | ||
continue | ||
|
@@ -212,7 +307,7 @@ func (s *OptionSet) SetDefaults() error { | |
) | ||
continue | ||
} | ||
(*s)[i].ValueSource = ValueSourceDefault | ||
(*optSet)[i].ValueSource = ValueSourceDefault | ||
if err := opt.Value.Set(opt.Default); err != nil { | ||
merr = multierror.Append( | ||
merr, xerrors.Errorf("parse %q: %w", opt.Name, err), | ||
|
@@ -224,9 +319,9 @@ func (s *OptionSet) SetDefaults() error { | |
|
||
// ByName returns the Option with the given name, or nil if no such option | ||
// exists. | ||
func (s *OptionSet) ByName(name string) *Option { | ||
for i := range *s { | ||
opt := &(*s)[i] | ||
func (optSet *OptionSet) ByName(name string) *Option { | ||
for i := range *optSet { | ||
opt := &(*optSet)[i] | ||
if opt.Name == name { | ||
return opt | ||
} | ||
|
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
@@ -40,6 +40,7 @@ import ( | |||||||||
|
||||||||||
"cdr.dev/slog" | ||||||||||
"github.com/coder/coder/v2/buildinfo" | ||||||||||
"github.com/coder/coder/v2/cli/clibase" | ||||||||||
"github.com/coder/coder/v2/coderd/audit" | ||||||||||
"github.com/coder/coder/v2/coderd/awsidentity" | ||||||||||
"github.com/coder/coder/v2/coderd/batchstats" | ||||||||||
|
@@ -152,7 +153,12 @@ type Options struct { | |||||||||
MetricsCacheRefreshInterval time.Duration | ||||||||||
AgentStatsRefreshInterval time.Duration | ||||||||||
DeploymentValues *codersdk.DeploymentValues | ||||||||||
UpdateCheckOptions *updatecheck.Options // Set non-nil to enable update checking. | ||||||||||
// DeploymentOptions do contain the copy of DeploymentValues, but contain | ||||||||||
// contextual information about how the values were set. | ||||||||||
Emyrk marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
// Do not use DeploymentOptions to retrieve values, use DeploymentValues instead. | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not just strip all values from There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wanted to go this route, but the UI grabs the value from the option. Values are returned in the parsed form, and the option form: Lines 34 to 37 in b1058a6
There is no way to associate the values between the two, so the value needs to be present on the Option for the UI to know. I feel like this could use a bit of a refactor, but it is working and not really that important. |
||||||||||
// All secrets values are stripped. | ||||||||||
DeploymentOptions clibase.OptionSet | ||||||||||
UpdateCheckOptions *updatecheck.Options // Set non-nil to enable update checking. | ||||||||||
|
||||||||||
// SSHConfig is the response clients use to configure config-ssh locally. | ||||||||||
SSHConfig codersdk.SSHConfigResponse | ||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1762,6 +1762,26 @@ type LinkConfig struct { | |
Icon string `json:"icon" yaml:"icon"` | ||
} | ||
|
||
// DeploymentOptionsWithoutSecrets returns a copy of the OptionSet with secret values omitted. | ||
func DeploymentOptionsWithoutSecrets(set clibase.OptionSet) clibase.OptionSet { | ||
cpy := make(clibase.OptionSet, 0, len(set)) | ||
for _, opt := range set { | ||
cpyOpt := opt | ||
if IsSecretDeploymentOption(cpyOpt) { | ||
switch cpyOpt.Value.Type() { | ||
case "string-array": | ||
var empty clibase.StringArray | ||
cpyOpt.Value = &empty | ||
default: | ||
var empty clibase.String | ||
cpyOpt.Value = &empty | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oooh this is where I broke the So So this made a So I can delete all the json unmarshal stuff I wrote, but my json unmarshal handles all the edge cases that would usually just fail (see https://goplay.tools/snippet/GPxfjtnfvys). So I can delete my unmarshal code, and if your client and version have different option sets, the call will fail. 🤔 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yes. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd rather have a custom UnmarshalJSON method than a potential regression between versions :-) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should keep this complicated unmarshal logic. It is more complicated, but with testing, it is safe and only affects the |
||
} | ||
cpy = append(cpy, cpyOpt) | ||
} | ||
return cpy | ||
} | ||
johnstcn marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// WithoutSecrets returns a copy of the config without secret values. | ||
func (c *DeploymentValues) WithoutSecrets() (*DeploymentValues, error) { | ||
var ff DeploymentValues | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the values are behind an interface, if we do not know how to implement the
.String()
method, then having the value in an unknown form is pointless.We could add some logic to try and detect primitive options, but I do not see the value. We should always have the full set of Options when we unmarshal, so we should always get the correct types and values.