package workspace import ( "context" "encoding/json" "fmt" "sort" "strings" "github.com/pulumi/esc" "github.com/pulumi/pulumi/sdk/v3/go/common/resource/config" "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" ) func formatMissingKeys(missingKeys []string) string { if len(missingKeys) == 1 { return fmt.Sprintf("'%v'", missingKeys[0]) } sort.Strings(missingKeys) formattedMissingKeys := "" for index, key := range missingKeys { // if last index, then use and before the key if index == len(missingKeys)-1 { formattedMissingKeys += fmt.Sprintf("and '%s'", key) } else if index == len(missingKeys)-2 { // no comma before the last key formattedMissingKeys += fmt.Sprintf("'%s' ", key) } else { formattedMissingKeys += fmt.Sprintf("'%s', ", key) } } return formattedMissingKeys } func missingStackConfigurationKeysError(missingKeys []string, stackName string) error { valueOrValues := "value" if len(missingKeys) > 1 { valueOrValues = "values" } return fmt.Errorf( "Stack '%v' is missing configuration %v %v", stackName, valueOrValues, formatMissingKeys(missingKeys)) } type ( StackName = string ProjectConfigKey = string ) func validateStackConfigValue( stackName string, projectConfigKey string, projectConfigType ProjectConfigType, stackValue config.Value, dec config.Decrypter, ) error { if dec == nil { return nil } // First check if the project says this should be secret, and if so that the stack value is // secure. if projectConfigType.Secret && !stackValue.Secure() { validationError := fmt.Errorf( "Stack '%v' with configuration key '%v' must be encrypted as it's secret", stackName, projectConfigKey) return validationError } value, err := stackValue.Value(dec) if err != nil { return err } // Content will be a JSON string if object is true, so marshal that back into an actual structure var content interface{} = value if stackValue.Object() { err = json.Unmarshal([]byte(value), &content) if err != nil { return err } } if !ValidateConfigValue(*projectConfigType.Type, projectConfigType.Items, content) { typeName := InferFullTypeName(*projectConfigType.Type, projectConfigType.Items) validationError := fmt.Errorf( "Stack '%v' with configuration key '%v' must be of type '%v'", stackName, projectConfigKey, typeName) return validationError } return nil } func parseConfigKey(projectName, key string) (config.Key, error) { if strings.Contains(key, ":") { // key is already namespaced return config.ParseKey(key) } // key is not namespaced // use the project as default namespace return config.MustMakeKey(projectName, key), nil } func createConfigValue(rawValue interface{}) (config.Value, error) { if isPrimitiveValue(rawValue) { configValueContent := fmt.Sprintf("%v", rawValue) return config.NewValue(configValueContent), nil } value, err := SimplifyMarshalledValue(rawValue) if err != nil { return config.Value{}, err } configValueJSON, jsonError := json.Marshal(value) if jsonError != nil { return config.Value{}, jsonError } return config.NewObjectValue(string(configValueJSON)), nil } func envConfigValue(v esc.Value) config.Plaintext { if v.Unknown { if v.Secret { return config.NewSecurePlaintext("[unknown]") } return config.NewPlaintext("[unknown]") } switch repr := v.Value.(type) { case nil: return config.Plaintext{} case bool: return config.NewPlaintext(repr) case json.Number: if i, err := repr.Int64(); err == nil { return config.NewPlaintext(i) } else if f, err := repr.Float64(); err == nil { return config.NewPlaintext(f) } // TODO(pdg): this disagrees with config unmarshaling semantics. Should probably fail. return config.NewPlaintext(string(repr)) case string: if v.Secret { return config.NewSecurePlaintext(repr) } return config.NewPlaintext(repr) case []esc.Value: vs := make([]config.Plaintext, len(repr)) for i, v := range repr { vs[i] = envConfigValue(v) } return config.NewPlaintext(vs) case map[string]esc.Value: vs := make(map[string]config.Plaintext, len(repr)) for k, v := range repr { vs[k] = envConfigValue(v) } return config.NewPlaintext(vs) default: contract.Failf("unexpected environments value of type %T", repr) return config.Plaintext{} } } func mergeConfig( ctx context.Context, stackName string, project *Project, stackEnv esc.Value, stackConfig config.Map, encrypter config.Encrypter, decrypter config.Decrypter, validate bool, ) error { missingConfigurationKeys := make([]string, 0) projectName := project.Name.String() keys := make([]string, 0, len(project.Config)) for k := range project.Config { keys = append(keys, k) } sort.Strings(keys) // First merge the stack environment and the stack config together. if envMap, ok := stackEnv.Value.(map[string]esc.Value); ok { for rawKey, value := range envMap { key, err := parseConfigKey(projectName, rawKey) if err != nil { return err } envValue, err := envConfigValue(value).Encrypt(ctx, encrypter) if err != nil { return err } stackValue, foundOnStack, err := stackConfig.Get(key, false) if err != nil { return fmt.Errorf("getting stack config value for key '%v': %w", key.String(), err) } if !foundOnStack { err = stackConfig.Set(key, envValue, false) } else { merged, mergeErr := stackValue.Merge(envValue) if mergeErr != nil { return fmt.Errorf("merging environment config for key '%v': %w", key.String(), err) } err = stackConfig.Set(key, merged, false) } if err != nil { return fmt.Errorf("setting merged config value for key '%v': %w", key.String(), err) } } } // Next validate the merged config and merge in the project config. for _, projectConfigKey := range keys { projectConfigType := project.Config[projectConfigKey] key, err := parseConfigKey(projectName, projectConfigKey) if err != nil { return err } stackValue, foundOnStack, err := stackConfig.Get(key, true) if err != nil { return fmt.Errorf("getting stack config value for key '%v': %w", key.String(), err) } hasDefault := projectConfigType.Default != nil hasValue := projectConfigType.Value != nil if !foundOnStack && !hasValue && !hasDefault && key.Namespace() == projectName { // add it to the list of missing project configuration keys in the stack // which are required by the project // then return them as a single error missingConfigurationKeys = append(missingConfigurationKeys, projectConfigKey) continue } if !foundOnStack && (hasValue || hasDefault) { // either value or default value is provided var value interface{} if hasValue { value = projectConfigType.Value } if hasDefault { value = projectConfigType.Default } // it is not found on the stack we are currently validating / merging values with // then we assign the value to that stack whatever that value is configValue, err := createConfigValue(value) if err != nil { return err } setError := stackConfig.Set(key, configValue, true) if setError != nil { return setError } continue } // Validate stack level value against the config defined at the project level if validate && projectConfigType.IsExplicitlyTyped() { err := validateStackConfigValue(stackName, projectConfigKey, projectConfigType, stackValue, decrypter) if err != nil { return err } } } if len(missingConfigurationKeys) > 0 { // there are missing configuration keys in the stack // return them as a single error. return missingStackConfigurationKeysError(missingConfigurationKeys, stackName) } return nil } func ValidateStackConfigAndApplyProjectConfig( ctx context.Context, stackName string, project *Project, stackEnv esc.Value, stackConfig config.Map, encrypter config.Encrypter, decrypter config.Decrypter, ) error { return mergeConfig(ctx, stackName, project, stackEnv, stackConfig, encrypter, decrypter, true) } // ApplyConfigDefaults applies the default values for the project configuration onto the stack configuration // without validating the contents of stack config values. // This is because sometimes during pulumi config ls and pulumi config get, if users are // using PassphraseDecrypter, we don't want to always prompt for the values when not necessary func ApplyProjectConfig( ctx context.Context, stackName string, project *Project, stackEnv esc.Value, stackConfig config.Map, encrypter config.Encrypter, ) error { return mergeConfig(ctx, stackName, project, stackEnv, stackConfig, encrypter, nil, false) }