Skip to content
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

Hierarchical and structured config implementation: the initial pass #10832

Merged
merged 36 commits into from Oct 18, 2022

Conversation

Zaid-Ajaj
Copy link
Contributor

@Zaid-Ajaj Zaid-Ajaj commented Sep 22, 2022

Description

This implements the initial pass of hierarchical and structured config which fixes #10602.

This changes the CLI such that configuration can now be defined at the project level using a config block. The configuration values defined here are inherited by all the stacks and made available to the Pulumi program without having to duplicate values in every stack (hence hierarchical) and the values are also typed / structured.

Example Project.yaml syntax:

name: config-test
runtime: dotnet
config:
  instanceSize:
    type: string
    default: t3.micro
  instanceCount: 
    type: integer
    default: 5

This can also be rewritten using short-hand syntax and will be equivalent to the above

name: config-test
runtime: dotnet
config:
  instanceSize: t3.micro
  instanceCount: 5

The complex types allowed for now are only arrays and nested arrays:

name: config-test
runtime: dotnet
config:
  availabilityZones:
    type: array
    items: 
      type: string
    default: [us-east-1-atl-1a, us-east-1-chi-1a]
  • Project-level configuration values that do not have a default value MUST be defined at the stack level
  • Stack configuration values are type-checked against their defined type in the project file i.e. Pulumi.yaml
  • Short-hand syntax only accepts primitive values (no arrays for now)
  • Accepted config types are a subset of a JSON schema where the property type: string | integer | boolean | array is expected. When type: array then a config block must also have property items which defines the type of array elements (can be nested)
  • Running pulumi config will list the configuration values from the selected stack AND the values inherited from the project
  • After a successful pulumi up run using hierarchical config from the project, pulumi config refresh will write ALL the used config back to the refreshed stack
  • pulumi config set/rm only applies to the selected stack

Checklist

  • I have added tests that prove my fix is effective or that my feature works
  • I have run make changelog and committed the changelog/pending/<file> documenting my change
  • Yes, there are changes in this PR that warrants bumping the Pulumi Service API version

@Zaid-Ajaj Zaid-Ajaj added area/cli UX of using the CLI (args, output, logs) impact/no-changelog-required This issue doesn't require a CHANGELOG update labels Sep 22, 2022
@Zaid-Ajaj Zaid-Ajaj requested a review from a team September 22, 2022 16:10
@pulumi-bot
Copy link
Contributor

pulumi-bot commented Sep 22, 2022

Changelog

[uncommitted] (2022-10-17)

Features

  • [cli] Implement initial MVP for hierarchical and structured project configuration.
    #10832
    #10832

@Frassle Frassle removed the impact/no-changelog-required This issue doesn't require a CHANGELOG update label Sep 22, 2022
changes:
- type: feat
scope: cli
description: Implement initial MVP for hierarchical and structured project configuration
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
description: Implement initial MVP for hierarchical and structured project configuration
description: Implement initial MVP for hierarchical and structured project configuration.

@@ -492,7 +492,11 @@ func newImportCmd() *cobra.Command {
return result.FromError(fmt.Errorf("getting secrets manager: %w", err))
}

cfg, err := getStackConfiguration(ctx, s, sm)
cfg, err := getStackConfiguration(ctx, s, sm, StackConfigOptions{
// we don't need project config here
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really? Maybe for this first mvp, but as soon as we allow things like "aws:region" at project level even import is going to need it. I'd think it simpler to just plumb projects through everywhere and not bother introducing this StackConfigOptions type which we'll probably remove later.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't sure in this case, so I changed this to inherit project-level config too ✅

pkg/cmd/pulumi/config.go Show resolved Hide resolved
sdk/go/common/resource/config/value.go Outdated Show resolved Hide resolved
sdk/go/common/workspace/project.go Show resolved Hide resolved
return errors.New("project is missing a 'runtime' attribute")
}

project = RewriteShorthandConfigValues(project)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really the JSON schema ought to be written such that the short-hand config values are validated ok. People could use that schema directly in vscode for example. So I feel this rewrite ought to happen after schema validation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The project schema now validates short-hand config syntax ✅

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great but the rewrite here should still happen after the Validate call below.
Validate the users input as is, then rewrite to the internal domain model.

}

fileName := fmt.Sprintf("%s.%s%s", ProjectFile, qnameFileName(stackName), filepath.Ext(projPath))

// Back compat: StackConfigDir used to be called Config.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We still need to support this. People could still have projects using "config: some_path". Ripping this out is another 4.0 change (maybe we should start leaving "TODO(4.0)" comments around for stuff like this)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Brought back support for defining config: some_path by rewriting it to stackConfigDir when it is a string. Erroring out when both are defined ✅

},
"config": {
"description": "A map of configuration keys to their types",
"type": "object",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"type": "object",
"type": ["object", "string", "null"]

Still need to support the old string path here, and it's optional so a null is also valid.

]
},
"config": {
"description": "A map of configuration keys to their types",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a bit to the description about how the "Config directory location relative to the location of Pulumi.yaml." is a deprecated use of this key.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added that to the description

"$ref": "#/$defs/configItemsType"
}
},
"if": {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if a "oneOf" results in better errors here than "if" 🤔
Not needed for this first pass but I might have a play with that idea later, so which is nicer UX.

@Zaid-Ajaj
Copy link
Contributor Author

Can't loadProjectStack do this?

The reason loadProjectStack doesn't already apply inherited configuration values is because we do not always want to populate the configuration: especially for pulumi config set and pulumi config rm because when we save the config to disk, it will have the configuration from the project written out too and we don't want that.

We could remove the project *Project parameter but I think it is needed to remove the requirement of name-spacing configuration values with the project name: i.e. project:config-key is now required but I think it shouldn't be. Only config-key is needed and the project name should be auto detected

@Zaid-Ajaj
Copy link
Contributor Author

I still think loadProjectStack should be handling this

@Frassle As discussed, because of projectStack.Save() we cannot always load the config into the project stack so we only apply where needed.

I think we can just get rid of StackConfigOptions, I don't forsee any options here and applyProjectConfig should always be true for now.

@Frassle Done ✅

Would it be possible to collect these [missing keys error]? It would be a much better UX if we showed users all of the missing keys, instead of just the first one encountered.

@iwahbe Nice catch! Done ✅

I've addressed your comments in the latest update and added moaar tests, please have a second look when you have time 🙏 @Frassle @iwahbe

Comment on lines 101 to 112
type ProjectConfigItemsType struct {
Type string `json:"type" yaml:"type"`
Items *ProjectConfigItemsType `json:"items" yaml:"items"`
}

type ProjectConfigType struct {
Type string `json:"type" yaml:"type"`
Description string `json:"description" yaml:"description"`
Items *ProjectConfigItemsType `json:"items" yaml:"items"`
Default interface{} `json:"default" yaml:"default"`
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably want some omitemptys here.

sdk/go/common/workspace/project.go Show resolved Hide resolved
sdk/go/common/workspace/project.go Show resolved Hide resolved
Comment on lines 101 to 104
type ProjectConfigItemsType struct {
Type string `json:"type" yaml:"type"`
Items *ProjectConfigItemsType `json:"items" yaml:"items"`
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to express this as a schema.Type? We already have a lot of logic to work on these, and we will need to translate them into schema types anyways for entry into the Pulumi YAML type checker.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's possible to write a function to translate a config type into schema.Type but I wouldn't want to start with this right off the bat and only support what is needed because it would seem that a config type can be any schema.Type but that is not the case

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree with Zaid here, config types != schema types, it will probably end up being a subset but possibly a non-intersecting set.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We will definitely need the config type -> schema type function when we incorporate this change into Pulumi YAML. We can wait for this change to settle before we unify.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point that this will probably have to be a subset otherwise there would be config values that YAML wouldn't be able to use, but I think given the subset nature of it I'm going to continue err'ing towards us not making it a schema.Type for now but it's own thing.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually see above, I think it might end up being non-intersecting still if we end up with config types that YAML doesn't support.

return typeName
}

func ValidateConfigValue(typeName string, itemsType *ProjectConfigItemsType, value interface{}) bool {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would appreciate a doc comment explaining what constitutes a valid config value.

if err != nil {
return nil, err
}

if configFile == "" {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right I've cleaned up configFile now on master, so it should always set here.

@@ -25,7 +25,16 @@ import (
func NewServiceSecretsManager(s Stack, stackName tokens.Name, configFile string) (secrets.Manager, error) {
contract.Assertf(stackName != "", "stackName %s", "!= \"\"")

info, err := workspace.LoadProjectStack(configFile)
project, path, err := workspace.DetectProjectStackPath(stackName.Q())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As above, configFile should always be set here.

@@ -986,7 +986,6 @@ func TestDestroyStackRef(t *testing.T) {

e.RunCommand("pulumi", "up", "--skip-preview", "--yes")

e.CWD = os.TempDir()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh this was by design so that destroy didn't need a Pulumi.yaml file. We should see if we can maintain that support for now.

@Zaid-Ajaj
Copy link
Contributor Author

@Frassle I've fixed the issue about pulumi destroy requiring a project file, that is no longer the case and TestDestroyStackRef works nice as it did before. Please have another look when you have time 🙏

sdk/go/common/resource/config/value.go Outdated Show resolved Hide resolved
@@ -1,180 +1,307 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The indent change in this file makes diff compare very hard :( can we revert to 4 space tab?

sdk/go/common/workspace/paths_test.go Show resolved Hide resolved
sdk/go/common/workspace/project.go Show resolved Hide resolved
return errors.New("project is missing a 'runtime' attribute")
}

project = RewriteShorthandConfigValues(project)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great but the rewrite here should still happen after the Validate call below.
Validate the users input as is, then rewrite to the internal domain model.

…r test to assert full stackConfigDir error message
… is it: allow config to be string for validation
changelog/pending/20220922--cli-initial-mvp-config.yaml Outdated Show resolved Hide resolved
return nil, err
}

if projectStackRaw == nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am very suspect of this. I assume this was to fix the "destory works without a Pulumi.yaml" but fixing it at this level means that any command using projects will try to use an empty project rather than failing validation, that feels wrong.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was not the fix for "destory works without a Pulumi.yaml", the fix was in removing readProject() from getStackConfiguration and refactor getStackConfiguration to accept a project from the outer scope (which in case of destroy, accounts for non-existing Pulumi.yaml)

This however, I believe is a check for when there is a stack file Pulumi.<stack>.yaml but it is empty so the unmarshalled projectStackRaw becomes nil. In this case, I treat it the same way we do when the file does not exist by returning a default project stack

@@ -105,8 +106,27 @@ func (singleton *projectLoader) load(path string) (*Project, error) {
return nil, fmt.Errorf("could not validate '%s': %w", path, err)
}

// just before marshalling, we will rewrite the config values
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still think this rewrite should happen after validation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the rewriting from inside the validation code but didn't realize it was duplicated in the loader code. I will adjust it

@Zaid-Ajaj
Copy link
Contributor Author

bors merge

@bors
Copy link
Contributor

bors bot commented Oct 18, 2022

Build succeeded:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area/cli UX of using the CLI (args, output, logs)
Projects
None yet
Development

Successfully merging this pull request may close these issues.

MVP for project level config
4 participants