pulumi/pkg/codegen/nodejs/gen_program.go

1280 lines
38 KiB
Go
Raw Permalink Normal View History

// Copyright 2016-2020, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package nodejs
import (
"bytes"
"fmt"
"io"
"os"
"path"
2023-03-08 23:34:15 +00:00
"path/filepath"
"sort"
"strings"
"github.com/hashicorp/hcl/v2"
"github.com/pulumi/pulumi/pkg/v3/codegen"
"github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/model"
"github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/model/format"
"github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/syntax"
"github.com/pulumi/pulumi/pkg/v3/codegen/pcl"
"github.com/pulumi/pulumi/pkg/v3/codegen/schema"
"github.com/pulumi/pulumi/sdk/v3/go/common/encoding"
"github.com/pulumi/pulumi/sdk/v3/go/common/slice"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"
"github.com/zclconf/go-cty/cty"
)
2022-08-19 17:27:05 +00:00
const PulumiToken = "pulumi"
type generator struct {
// The formatter to use when generating code.
*format.Formatter
program *pcl.Program
diagnostics hcl.Diagnostics
asyncMain bool
configCreated bool
2023-03-08 23:34:15 +00:00
isComponent bool
}
func GenerateProgram(program *pcl.Program) (map[string][]byte, hcl.Diagnostics, error) {
pcl.MapProvidersAsResources(program)
// Linearize the nodes into an order appropriate for procedural code generation.
nodes := pcl.Linearize(program)
g := &generator{
program: program,
}
g.Formatter = format.NewFormatter(g)
packages, err := program.PackageSnapshots()
if err != nil {
return nil, nil, err
}
for _, p := range packages {
if err := p.ImportLanguages(map[string]schema.Language{"nodejs": Importer}); err != nil {
return nil, nil, err
}
}
var index bytes.Buffer
2023-03-08 23:34:15 +00:00
err = g.genPreamble(&index, program)
if err != nil {
return nil, nil, err
}
// used to track declared variables in the main program
// since outputs have identifiers which can conflict with other program nodes' identifiers
// we switch the entry point to async which allows for declaring arbitrary output names
declaredNodeIdentifiers := map[string]bool{}
for _, n := range nodes {
if g.asyncMain {
break
}
switch x := n.(type) {
case *pcl.Resource:
if resourceRequiresAsyncMain(x) {
g.asyncMain = true
}
declaredNodeIdentifiers[makeValidIdentifier(x.Name())] = true
case *pcl.ConfigVariable:
declaredNodeIdentifiers[makeValidIdentifier(x.Name())] = true
case *pcl.LocalVariable:
declaredNodeIdentifiers[makeValidIdentifier(x.Name())] = true
case *pcl.Component:
declaredNodeIdentifiers[makeValidIdentifier(x.Name())] = true
case *pcl.OutputVariable:
if outputRequiresAsyncMain(x) {
g.asyncMain = true
}
outputIdentifier := makeValidIdentifier(x.Name())
if _, alreadyDeclared := declaredNodeIdentifiers[outputIdentifier]; alreadyDeclared {
g.asyncMain = true
}
}
}
indenter := func(f func()) { f() }
if g.asyncMain {
indenter = g.Indented
g.Fgenf(&index, "export = async () => {\n")
}
indenter(func() {
for _, n := range nodes {
g.genNode(&index, n)
}
if g.asyncMain {
var result *model.ObjectConsExpression
for _, n := range nodes {
if o, ok := n.(*pcl.OutputVariable); ok {
if result == nil {
result = &model.ObjectConsExpression{}
}
name := o.LogicalName()
result.Items = append(result.Items, model.ObjectConsItem{
Key: &model.LiteralValueExpression{Value: cty.StringVal(name)},
Value: g.lowerExpression(o.Value, o.Type()),
})
}
}
if result != nil {
g.Fgenf(&index, "%sreturn %v;\n", g.Indent, result)
}
}
})
if g.asyncMain {
g.Fgenf(&index, "}\n")
}
files := map[string][]byte{
"index.ts": index.Bytes(),
}
2023-03-08 23:34:15 +00:00
for componentDir, component := range program.CollectComponents() {
componentFilename := filepath.Base(componentDir)
componentName := component.DeclarationName()
2023-03-08 23:34:15 +00:00
componentGenerator := &generator{
program: component.Program,
isComponent: true,
}
componentGenerator.Formatter = format.NewFormatter(componentGenerator)
var componentBuffer bytes.Buffer
componentGenerator.genComponentResourceDefinition(&componentBuffer, componentName, component)
files[componentFilename+".ts"] = componentBuffer.Bytes()
2023-03-08 23:34:15 +00:00
}
return files, g.diagnostics, nil
}
func GenerateProject(
directory string, project workspace.Project,
program *pcl.Program, localDependencies map[string]string,
Run conformance tests with and without ts-node (#15809) <!--- Thanks so much for your contribution! If this is your first time contributing, please ensure that you have read the [CONTRIBUTING](https://github.com/pulumi/pulumi/blob/master/CONTRIBUTING.md) documentation. --> # Description <!--- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. --> Fixes https://github.com/pulumi/pulumi/issues/15075. This doubles up the conformance tests to run with `tsc` instead of `ts-node`. Getting this to work in the conformance framework required a bit of pushing the envelope for what it means to "install dependencies" and "run", but it works for testing, and isn't horribly exposed to users. It also points out a couple of things that we might want to clean up for users wanting the same workflow. 1. Adding a `Build` command (which we might want for the type checker work added in https://github.com/pulumi/pulumi/pull/15725 as well). 2. If you set "main" to ./bin because you're using tsc, you can't then use InstallDependencies because to start with the "bin" folder is missing. ## Checklist - [x] I have run `make tidy` to update any new dependencies - [x] I have run `make lint` to verify my code passes the lint check - [x] I have formatted my code using `gofumpt` <!--- Please provide details if the checkbox below is to be left unchecked. --> - [x] I have added tests that prove my fix is effective or that my feature works <!--- User-facing changes require a CHANGELOG entry. --> - [ ] I have run `make changelog` and committed the `changelog/pending/<file>` documenting my change <!-- If the change(s) in this PR is a modification of an existing call to the Pulumi Cloud, then the service should honor older versions of the CLI where this change would not exist. You must then bump the API version in /pkg/backend/httpstate/client/api.go, as well as add it to the service. --> - [ ] Yes, there are changes in this PR that warrants bumping the Pulumi Cloud API version <!-- @Pulumi employees: If yes, you must submit corresponding changes in the service repo. -->
2024-03-28 15:44:25 +00:00
forceTsc bool,
) error {
files, diagnostics, err := GenerateProgram(program)
if err != nil {
return err
}
if diagnostics.HasErrors() {
return diagnostics
}
Test main in conformance tests (#15287) <!--- Thanks so much for your contribution! If this is your first time contributing, please ensure that you have read the [CONTRIBUTING](https://github.com/pulumi/pulumi/blob/master/CONTRIBUTING.md) documentation. --> # Description <!--- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. --> This adds a main option to the test, that is put into the Pulumi.yaml and sent to codegen. This allows us to write a test that sets up "main" in a Pulumi.yaml file and test that "program directory" and "entry point" are respected. Doing this correctly also required some bug fixes in the conformance interface itself. ## Checklist - [x] I have run `make tidy` to update any new dependencies - [x] I have run `make lint` to verify my code passes the lint check - [x] I have formatted my code using `gofumpt` <!--- Please provide details if the checkbox below is to be left unchecked. --> - [x] I have added tests that prove my fix is effective or that my feature works <!--- User-facing changes require a CHANGELOG entry. --> - [ ] I have run `make changelog` and committed the `changelog/pending/<file>` documenting my change <!-- If the change(s) in this PR is a modification of an existing call to the Pulumi Cloud, then the service should honor older versions of the CLI where this change would not exist. You must then bump the API version in /pkg/backend/httpstate/client/api.go, as well as add it to the service. --> - [ ] Yes, there are changes in this PR that warrants bumping the Pulumi Cloud API version <!-- @Pulumi employees: If yes, you must submit corresponding changes in the service repo. -->
2024-01-28 20:11:52 +00:00
// Check the project for "main" as that changes where we write out files and some relative paths.
rootDirectory := directory
if project.Main != "" {
directory = filepath.Join(rootDirectory, project.Main)
// mkdir -p the subdirectory
err = os.MkdirAll(directory, 0o700)
if err != nil {
return fmt.Errorf("create main directory: %w", err)
}
}
// Set the runtime to "nodejs" then marshal to Pulumi.yaml
Run conformance tests with and without ts-node (#15809) <!--- Thanks so much for your contribution! If this is your first time contributing, please ensure that you have read the [CONTRIBUTING](https://github.com/pulumi/pulumi/blob/master/CONTRIBUTING.md) documentation. --> # Description <!--- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. --> Fixes https://github.com/pulumi/pulumi/issues/15075. This doubles up the conformance tests to run with `tsc` instead of `ts-node`. Getting this to work in the conformance framework required a bit of pushing the envelope for what it means to "install dependencies" and "run", but it works for testing, and isn't horribly exposed to users. It also points out a couple of things that we might want to clean up for users wanting the same workflow. 1. Adding a `Build` command (which we might want for the type checker work added in https://github.com/pulumi/pulumi/pull/15725 as well). 2. If you set "main" to ./bin because you're using tsc, you can't then use InstallDependencies because to start with the "bin" folder is missing. ## Checklist - [x] I have run `make tidy` to update any new dependencies - [x] I have run `make lint` to verify my code passes the lint check - [x] I have formatted my code using `gofumpt` <!--- Please provide details if the checkbox below is to be left unchecked. --> - [x] I have added tests that prove my fix is effective or that my feature works <!--- User-facing changes require a CHANGELOG entry. --> - [ ] I have run `make changelog` and committed the `changelog/pending/<file>` documenting my change <!-- If the change(s) in this PR is a modification of an existing call to the Pulumi Cloud, then the service should honor older versions of the CLI where this change would not exist. You must then bump the API version in /pkg/backend/httpstate/client/api.go, as well as add it to the service. --> - [ ] Yes, there are changes in this PR that warrants bumping the Pulumi Cloud API version <!-- @Pulumi employees: If yes, you must submit corresponding changes in the service repo. -->
2024-03-28 15:44:25 +00:00
runtime := workspace.NewProjectRuntimeInfo("nodejs", nil)
if forceTsc {
runtime.SetOption("typescript", false)
}
project.Runtime = runtime
projectBytes, err := encoding.YAML.Marshal(project)
if err != nil {
return err
}
Test main in conformance tests (#15287) <!--- Thanks so much for your contribution! If this is your first time contributing, please ensure that you have read the [CONTRIBUTING](https://github.com/pulumi/pulumi/blob/master/CONTRIBUTING.md) documentation. --> # Description <!--- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. --> This adds a main option to the test, that is put into the Pulumi.yaml and sent to codegen. This allows us to write a test that sets up "main" in a Pulumi.yaml file and test that "program directory" and "entry point" are respected. Doing this correctly also required some bug fixes in the conformance interface itself. ## Checklist - [x] I have run `make tidy` to update any new dependencies - [x] I have run `make lint` to verify my code passes the lint check - [x] I have formatted my code using `gofumpt` <!--- Please provide details if the checkbox below is to be left unchecked. --> - [x] I have added tests that prove my fix is effective or that my feature works <!--- User-facing changes require a CHANGELOG entry. --> - [ ] I have run `make changelog` and committed the `changelog/pending/<file>` documenting my change <!-- If the change(s) in this PR is a modification of an existing call to the Pulumi Cloud, then the service should honor older versions of the CLI where this change would not exist. You must then bump the API version in /pkg/backend/httpstate/client/api.go, as well as add it to the service. --> - [ ] Yes, there are changes in this PR that warrants bumping the Pulumi Cloud API version <!-- @Pulumi employees: If yes, you must submit corresponding changes in the service repo. -->
2024-01-28 20:11:52 +00:00
err = os.WriteFile(path.Join(rootDirectory, "Pulumi.yaml"), projectBytes, 0o600)
if err != nil {
return fmt.Errorf("write Pulumi.yaml: %w", err)
}
// Build the package.json
var packageJSON bytes.Buffer
fmt.Fprintf(&packageJSON, `{
"name": "%s",
"devDependencies": {
"@types/node": "^14"
},
"dependencies": {
"typescript": "^4.0.0",
`, project.Name.String())
// Check if pulumi is a local dependency, else add it as a normal range dependency
if pulumiArtifact, has := localDependencies[PulumiToken]; has {
fmt.Fprintf(&packageJSON, `"@pulumi/pulumi": "%s"`, pulumiArtifact)
} else {
fmt.Fprintf(&packageJSON, `"@pulumi/pulumi": "^3.0.0"`)
}
// For each package add a dependency line
packages, err := program.CollectNestedPackageSnapshots()
if err != nil {
return err
}
// Sort the dependencies to ensure a deterministic package.json. Note that the typescript and
// @pulumi/pulumi dependencies are already added above and not sorted.
sortedPackageNames := make([]string, 0, len(packages))
for k := range packages {
sortedPackageNames = append(sortedPackageNames, k)
}
sort.Strings(sortedPackageNames)
for _, k := range sortedPackageNames {
p := packages[k]
2022-08-19 17:27:05 +00:00
if p.Name == PulumiToken {
continue
}
if err := p.ImportLanguages(map[string]schema.Language{"nodejs": Importer}); err != nil {
return err
}
packageName := "@pulumi/" + p.Name
err := p.ImportLanguages(map[string]schema.Language{"nodejs": Importer})
if err != nil {
return err
}
2022-04-30 00:11:37 +00:00
if langInfo, found := p.Language["nodejs"]; found {
nodeInfo, ok := langInfo.(NodePackageInfo)
if ok && nodeInfo.PackageName != "" {
packageName = nodeInfo.PackageName
}
}
dependencyTemplate := ",\n \"%s\": \"%s\""
if path, has := localDependencies[p.Name]; has {
fmt.Fprintf(&packageJSON, dependencyTemplate, packageName, path)
2022-04-30 00:11:37 +00:00
} else {
if p.Version != nil {
fmt.Fprintf(&packageJSON, dependencyTemplate, packageName, p.Version.String())
} else {
fmt.Fprintf(&packageJSON, dependencyTemplate, packageName, "*")
}
2022-04-30 00:11:37 +00:00
}
}
packageJSON.WriteString(`
}
}`)
files["package.json"] = packageJSON.Bytes()
// Add the language specific .gitignore
files[".gitignore"] = []byte(`/bin/
/node_modules/`)
// Add the basic tsconfig
var tsConfig bytes.Buffer
tsConfig.WriteString(`{
"compilerOptions": {
"strict": true,
"outDir": "bin",
"target": "es2016",
"module": "commonjs",
"moduleResolution": "node",
"sourceMap": true,
"experimentalDecorators": true,
"pretty": true,
"noFallthroughCasesInSwitch": true,
"noImplicitReturns": true,
"forceConsistentCasingInFileNames": true
},
"files": [
`)
Add matrix testing (#13705) <!--- Thanks so much for your contribution! If this is your first time contributing, please ensure that you have read the [CONTRIBUTING](https://github.com/pulumi/pulumi/blob/master/CONTRIBUTING.md) documentation. --> # Description <!--- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. --> Adds the first pass of matrix testing. Matrix testing allows us to define tests once in pulumi/pulumi via PCL and then run those tests against each language plugin to verify code generation and runtime correctness. Rather than packing matrix tests and all the associated data and machinery into the CLI itself we define a new Go package at cmd/pulumi-test-lanaguage. This depends on pkg and runs the deployment engine in a unique way for matrix tests but it is running the proper deployment engine with a proper backend (always filestate, using $TEMP). Currently only NodeJS is hooked up to run these tests, and all the code for that currently lives in sdk/nodejs/cmd/pulumi-language-nodejs/language_test.go. I expect we'll move that helper code to sdk/go/common and use it in each language plugin to run the tests in the same way. This first pass includes 3 simple tests: * l1-empty that runs an empty PCL file and checks just a stack is created * l1-output-bool that runs a PCL program that returns two stack outputs of `true` and `false * l2-resource-simple that runs a PCL program creating a simple resource with a single bool property These tests are themselves tested with a mock language runtime. This verifies the behavior of the matrix test framework for both correct and incorrect language hosts (that is some the mock language runtimes purposefully cause errors or compute the wrong result). There are a number of things missing from from the core framework still, but I feel don't block getting this first pass merged and starting to be used. 1. The tests can not currently run in parallel. That is calling RunLanguageTest in parallel will break things. This is due to two separate problems. Firstly is that the SDK snapshot's are not safe to write in parallel (when PULUMI_ACCEPT is true), this should be fairly easy to fix by doing a write to dst-{random} and them atomic move to dst. Secondly is that the deployment engine itself has mutable global state, short term we should probably just lock around that part RunLanguageTest, long term it would be good to clean that up. 2. We need a way to verify "preview" behavior, I think this is probably just a variation of the tests that would call `stack.Preview` and not pass a snapshot to `assert`. 3. stdout, stderr and log messages are returned in bulk at the end of the test. Plus there are a couple of calls to the language runtime that don't correctly thread stdout/stderr to use and so default to the process `os.Stdout/Stderr`. stdout/stderr streaming shows up in a load of other places as well so I'm thinking of a clean way to handle all of them together. Log message streaming we can probably do by just turning RunLanguageTest to a streaming grpc call. ## Checklist - [x] I have run `make tidy` to update any new dependencies - [x] I have run `make lint` to verify my code passes the lint check - [ ] I have formatted my code using `gofumpt` <!--- Please provide details if the checkbox below is to be left unchecked. --> - [x] I have added tests that prove my fix is effective or that my feature works <!--- User-facing changes require a CHANGELOG entry. --> - [x] I have run `make changelog` and committed the `changelog/pending/<file>` documenting my change <!-- If the change(s) in this PR is a modification of an existing call to the Pulumi Cloud, then the service should honor older versions of the CLI where this change would not exist. You must then bump the API version in /pkg/backend/httpstate/client/api.go, as well as add it to the service. --> - [ ] Yes, there are changes in this PR that warrants bumping the Pulumi Cloud API version <!-- @Pulumi employees: If yes, you must submit corresponding changes in the service repo. --> --------- Co-authored-by: Abhinav Gupta <abhinav@pulumi.com>
2023-09-13 15:17:46 +00:00
fileNames := make([]string, 0, len(files))
for file := range files {
Add matrix testing (#13705) <!--- Thanks so much for your contribution! If this is your first time contributing, please ensure that you have read the [CONTRIBUTING](https://github.com/pulumi/pulumi/blob/master/CONTRIBUTING.md) documentation. --> # Description <!--- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. --> Adds the first pass of matrix testing. Matrix testing allows us to define tests once in pulumi/pulumi via PCL and then run those tests against each language plugin to verify code generation and runtime correctness. Rather than packing matrix tests and all the associated data and machinery into the CLI itself we define a new Go package at cmd/pulumi-test-lanaguage. This depends on pkg and runs the deployment engine in a unique way for matrix tests but it is running the proper deployment engine with a proper backend (always filestate, using $TEMP). Currently only NodeJS is hooked up to run these tests, and all the code for that currently lives in sdk/nodejs/cmd/pulumi-language-nodejs/language_test.go. I expect we'll move that helper code to sdk/go/common and use it in each language plugin to run the tests in the same way. This first pass includes 3 simple tests: * l1-empty that runs an empty PCL file and checks just a stack is created * l1-output-bool that runs a PCL program that returns two stack outputs of `true` and `false * l2-resource-simple that runs a PCL program creating a simple resource with a single bool property These tests are themselves tested with a mock language runtime. This verifies the behavior of the matrix test framework for both correct and incorrect language hosts (that is some the mock language runtimes purposefully cause errors or compute the wrong result). There are a number of things missing from from the core framework still, but I feel don't block getting this first pass merged and starting to be used. 1. The tests can not currently run in parallel. That is calling RunLanguageTest in parallel will break things. This is due to two separate problems. Firstly is that the SDK snapshot's are not safe to write in parallel (when PULUMI_ACCEPT is true), this should be fairly easy to fix by doing a write to dst-{random} and them atomic move to dst. Secondly is that the deployment engine itself has mutable global state, short term we should probably just lock around that part RunLanguageTest, long term it would be good to clean that up. 2. We need a way to verify "preview" behavior, I think this is probably just a variation of the tests that would call `stack.Preview` and not pass a snapshot to `assert`. 3. stdout, stderr and log messages are returned in bulk at the end of the test. Plus there are a couple of calls to the language runtime that don't correctly thread stdout/stderr to use and so default to the process `os.Stdout/Stderr`. stdout/stderr streaming shows up in a load of other places as well so I'm thinking of a clean way to handle all of them together. Log message streaming we can probably do by just turning RunLanguageTest to a streaming grpc call. ## Checklist - [x] I have run `make tidy` to update any new dependencies - [x] I have run `make lint` to verify my code passes the lint check - [ ] I have formatted my code using `gofumpt` <!--- Please provide details if the checkbox below is to be left unchecked. --> - [x] I have added tests that prove my fix is effective or that my feature works <!--- User-facing changes require a CHANGELOG entry. --> - [x] I have run `make changelog` and committed the `changelog/pending/<file>` documenting my change <!-- If the change(s) in this PR is a modification of an existing call to the Pulumi Cloud, then the service should honor older versions of the CLI where this change would not exist. You must then bump the API version in /pkg/backend/httpstate/client/api.go, as well as add it to the service. --> - [ ] Yes, there are changes in this PR that warrants bumping the Pulumi Cloud API version <!-- @Pulumi employees: If yes, you must submit corresponding changes in the service repo. --> --------- Co-authored-by: Abhinav Gupta <abhinav@pulumi.com>
2023-09-13 15:17:46 +00:00
fileNames = append(fileNames, file)
}
sort.Strings(fileNames)
for i, file := range fileNames {
if strings.HasSuffix(file, ".ts") {
tsConfig.WriteString(" \"" + file + "\"")
Add matrix testing (#13705) <!--- Thanks so much for your contribution! If this is your first time contributing, please ensure that you have read the [CONTRIBUTING](https://github.com/pulumi/pulumi/blob/master/CONTRIBUTING.md) documentation. --> # Description <!--- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. --> Adds the first pass of matrix testing. Matrix testing allows us to define tests once in pulumi/pulumi via PCL and then run those tests against each language plugin to verify code generation and runtime correctness. Rather than packing matrix tests and all the associated data and machinery into the CLI itself we define a new Go package at cmd/pulumi-test-lanaguage. This depends on pkg and runs the deployment engine in a unique way for matrix tests but it is running the proper deployment engine with a proper backend (always filestate, using $TEMP). Currently only NodeJS is hooked up to run these tests, and all the code for that currently lives in sdk/nodejs/cmd/pulumi-language-nodejs/language_test.go. I expect we'll move that helper code to sdk/go/common and use it in each language plugin to run the tests in the same way. This first pass includes 3 simple tests: * l1-empty that runs an empty PCL file and checks just a stack is created * l1-output-bool that runs a PCL program that returns two stack outputs of `true` and `false * l2-resource-simple that runs a PCL program creating a simple resource with a single bool property These tests are themselves tested with a mock language runtime. This verifies the behavior of the matrix test framework for both correct and incorrect language hosts (that is some the mock language runtimes purposefully cause errors or compute the wrong result). There are a number of things missing from from the core framework still, but I feel don't block getting this first pass merged and starting to be used. 1. The tests can not currently run in parallel. That is calling RunLanguageTest in parallel will break things. This is due to two separate problems. Firstly is that the SDK snapshot's are not safe to write in parallel (when PULUMI_ACCEPT is true), this should be fairly easy to fix by doing a write to dst-{random} and them atomic move to dst. Secondly is that the deployment engine itself has mutable global state, short term we should probably just lock around that part RunLanguageTest, long term it would be good to clean that up. 2. We need a way to verify "preview" behavior, I think this is probably just a variation of the tests that would call `stack.Preview` and not pass a snapshot to `assert`. 3. stdout, stderr and log messages are returned in bulk at the end of the test. Plus there are a couple of calls to the language runtime that don't correctly thread stdout/stderr to use and so default to the process `os.Stdout/Stderr`. stdout/stderr streaming shows up in a load of other places as well so I'm thinking of a clean way to handle all of them together. Log message streaming we can probably do by just turning RunLanguageTest to a streaming grpc call. ## Checklist - [x] I have run `make tidy` to update any new dependencies - [x] I have run `make lint` to verify my code passes the lint check - [ ] I have formatted my code using `gofumpt` <!--- Please provide details if the checkbox below is to be left unchecked. --> - [x] I have added tests that prove my fix is effective or that my feature works <!--- User-facing changes require a CHANGELOG entry. --> - [x] I have run `make changelog` and committed the `changelog/pending/<file>` documenting my change <!-- If the change(s) in this PR is a modification of an existing call to the Pulumi Cloud, then the service should honor older versions of the CLI where this change would not exist. You must then bump the API version in /pkg/backend/httpstate/client/api.go, as well as add it to the service. --> - [ ] Yes, there are changes in this PR that warrants bumping the Pulumi Cloud API version <!-- @Pulumi employees: If yes, you must submit corresponding changes in the service repo. --> --------- Co-authored-by: Abhinav Gupta <abhinav@pulumi.com>
2023-09-13 15:17:46 +00:00
lastFile := i == len(files)-1
if !lastFile {
tsConfig.WriteString(",\n")
} else {
tsConfig.WriteString("\n")
}
}
}
tsConfig.WriteString(` ]
}`)
files["tsconfig.json"] = tsConfig.Bytes()
for filename, data := range files {
outPath := path.Join(directory, filename)
all: Reformat with gofumpt Per team discussion, switching to gofumpt. [gofumpt][1] is an alternative, stricter alternative to gofmt. It addresses other stylistic concerns that gofmt doesn't yet cover. [1]: https://github.com/mvdan/gofumpt See the full list of [Added rules][2], but it includes: - Dropping empty lines around function bodies - Dropping unnecessary variable grouping when there's only one variable - Ensuring an empty line between multi-line functions - simplification (`-s` in gofmt) is always enabled - Ensuring multi-line function signatures end with `) {` on a separate line. [2]: https://github.com/mvdan/gofumpt#Added-rules gofumpt is stricter, but there's no lock-in. All gofumpt output is valid gofmt output, so if we decide we don't like it, it's easy to switch back without any code changes. gofumpt support is built into the tooling we use for development so this won't change development workflows. - golangci-lint includes a gofumpt check (enabled in this PR) - gopls, the LSP for Go, includes a gofumpt option (see [installation instrutions][3]) [3]: https://github.com/mvdan/gofumpt#installation This change was generated by running: ```bash gofumpt -w $(rg --files -g '*.go' | rg -v testdata | rg -v compilation_error) ``` The following files were manually tweaked afterwards: - pkg/cmd/pulumi/stack_change_secrets_provider.go: one of the lines overflowed and had comments in an inconvenient place - pkg/cmd/pulumi/destroy.go: `var x T = y` where `T` wasn't necessary - pkg/cmd/pulumi/policy_new.go: long line because of error message - pkg/backend/snapshot_test.go: long line trying to assign three variables in the same assignment I have included mention of gofumpt in the CONTRIBUTING.md.
2023-03-03 16:36:39 +00:00
err := os.WriteFile(outPath, data, 0o600)
if err != nil {
return fmt.Errorf("could not write output program: %w", err)
}
}
return nil
}
// genLeadingTrivia generates the list of leading trivia assicated with a given token.
func (g *generator) genLeadingTrivia(w io.Writer, token syntax.Token) {
// TODO(pdg): whitespace?
for _, t := range token.LeadingTrivia {
if c, ok := t.(syntax.Comment); ok {
g.genComment(w, c)
}
}
}
// genTrailingTrivia generates the list of trailing trivia assicated with a given token.
func (g *generator) genTrailingTrivia(w io.Writer, token syntax.Token) {
// TODO(pdg): whitespace
for _, t := range token.TrailingTrivia {
if c, ok := t.(syntax.Comment); ok {
g.genComment(w, c)
}
}
}
// genTrivia generates the list of trivia assicated with a given token.
func (g *generator) genTrivia(w io.Writer, token syntax.Token) {
g.genLeadingTrivia(w, token)
g.genTrailingTrivia(w, token)
}
// genComment generates a comment into the output.
func (g *generator) genComment(w io.Writer, comment syntax.Comment) {
for _, l := range comment.Lines {
g.Fgenf(w, "%s//%s\n", g.Indent, l)
}
}
2023-03-08 23:34:15 +00:00
type programImports struct {
importStatements []string
preambleHelperMethods codegen.StringSet
}
2023-03-08 23:34:15 +00:00
func (g *generator) collectProgramImports(program *pcl.Program) programImports {
importSet := codegen.NewStringSet("@pulumi/pulumi")
2023-03-08 23:34:15 +00:00
preambleHelperMethods := codegen.NewStringSet()
var componentImports []string
2023-03-08 23:34:15 +00:00
npmToPuPkgName := make(map[string]string)
[program-gen] Fix duplicated import statements when instantiating a component multiple times (#14829) # Description This PR extends the `components` test program in PCL so that it instantiates a component with `options { range = <expr> }` to test that it is generating the right thing and increase code coverage in program-gen which has a special handling for the `range` option. Found a small bug where we duplicate imports for components if they are instantiated multiple times in the program and fixed it as well ## Checklist - [ ] I have run `make tidy` to update any new dependencies - [ ] I have run `make lint` to verify my code passes the lint check - [ ] I have formatted my code using `gofumpt` <!--- Please provide details if the checkbox below is to be left unchecked. --> - [x] I have added tests that prove my fix is effective or that my feature works <!--- User-facing changes require a CHANGELOG entry. --> - [x] I have run `make changelog` and committed the `changelog/pending/<file>` documenting my change <!-- If the change(s) in this PR is a modification of an existing call to the Pulumi Cloud, then the service should honor older versions of the CLI where this change would not exist. You must then bump the API version in /pkg/backend/httpstate/client/api.go, as well as add it to the service. --> - [ ] Yes, there are changes in this PR that warrants bumping the Pulumi Cloud API version <!-- @Pulumi employees: If yes, you must submit corresponding changes in the service repo. -->
2023-12-14 15:43:27 +00:00
seenComponentImports := map[string]bool{}
for _, n := range program.Nodes {
switch n := n.(type) {
case *pcl.Resource:
pkg, _, _, _ := n.DecomposeToken()
2022-08-19 17:27:05 +00:00
if pkg == PulumiToken {
continue
}
pkgName := "@pulumi/" + pkg
if n.Schema != nil && n.Schema.PackageReference != nil {
def, err := n.Schema.PackageReference.Definition()
contract.AssertNoErrorf(err, "Should be able to retrieve definition for %s", n.Schema.Token)
if info, ok := def.Language["nodejs"].(NodePackageInfo); ok && info.PackageName != "" {
pkgName = info.PackageName
}
npmToPuPkgName[pkgName] = pkg
}
importSet.Add(pkgName)
case *pcl.Component:
componentDir := filepath.Base(n.DirPath())
componentName := n.DeclarationName()
[program-gen] Fix duplicated import statements when instantiating a component multiple times (#14829) # Description This PR extends the `components` test program in PCL so that it instantiates a component with `options { range = <expr> }` to test that it is generating the right thing and increase code coverage in program-gen which has a special handling for the `range` option. Found a small bug where we duplicate imports for components if they are instantiated multiple times in the program and fixed it as well ## Checklist - [ ] I have run `make tidy` to update any new dependencies - [ ] I have run `make lint` to verify my code passes the lint check - [ ] I have formatted my code using `gofumpt` <!--- Please provide details if the checkbox below is to be left unchecked. --> - [x] I have added tests that prove my fix is effective or that my feature works <!--- User-facing changes require a CHANGELOG entry. --> - [x] I have run `make changelog` and committed the `changelog/pending/<file>` documenting my change <!-- If the change(s) in this PR is a modification of an existing call to the Pulumi Cloud, then the service should honor older versions of the CLI where this change would not exist. You must then bump the API version in /pkg/backend/httpstate/client/api.go, as well as add it to the service. --> - [ ] Yes, there are changes in this PR that warrants bumping the Pulumi Cloud API version <!-- @Pulumi employees: If yes, you must submit corresponding changes in the service repo. -->
2023-12-14 15:43:27 +00:00
dirAndName := componentDir + "-" + componentName
if _, ok := seenComponentImports[dirAndName]; !ok {
importStatement := fmt.Sprintf("import { %s } from \"./%s\";", componentName, componentDir)
componentImports = append(componentImports, importStatement)
seenComponentImports[dirAndName] = true
}
}
diags := n.VisitExpressions(nil, func(n model.Expression) (model.Expression, hcl.Diagnostics) {
if call, ok := n.(*model.FunctionCallExpression); ok {
if i := g.getFunctionImports(call); len(i) > 0 && i[0] != "" {
for _, importPackage := range i {
importSet.Add(importPackage)
}
}
if helperMethodBody, ok := getHelperMethodIfNeeded(call.Name, g.Indent); ok {
preambleHelperMethods.Add(helperMethodBody)
}
}
return n, nil
})
contract.Assertf(len(diags) == 0, "unexpected diagnostics: %v", diags)
}
2023-03-08 23:34:15 +00:00
sortedValues := importSet.SortedValues()
imports := slice.Prealloc[string](len(sortedValues))
2023-03-08 23:34:15 +00:00
for _, pkg := range sortedValues {
if pkg == "@pulumi/pulumi" {
continue
}
var as string
if puPkg, ok := npmToPuPkgName[pkg]; ok {
as = makeValidIdentifier(puPkg)
} else {
as = makeValidIdentifier(path.Base(pkg))
}
imports = append(imports, fmt.Sprintf("import * as %v from \"%v\";", as, pkg))
}
imports = append(imports, componentImports...)
sort.Strings(imports)
2023-03-08 23:34:15 +00:00
return programImports{
importStatements: imports,
preambleHelperMethods: preambleHelperMethods,
}
}
func (g *generator) genPreamble(w io.Writer, program *pcl.Program) error {
// Print the @pulumi/pulumi import at the top.
g.Fprintln(w, `import * as pulumi from "@pulumi/pulumi";`)
programImports := g.collectProgramImports(program)
// Now sort the imports and emit them.
2023-03-08 23:34:15 +00:00
for _, i := range programImports.importStatements {
g.Fprintln(w, i)
}
g.Fprint(w, "\n")
// If we collected any helper methods that should be added, write them just before the main func
2023-03-08 23:34:15 +00:00
for _, preambleHelperMethodBody := range programImports.preambleHelperMethods.SortedValues() {
g.Fprintf(w, "%s\n\n", preambleHelperMethodBody)
}
return nil
}
2023-03-08 23:34:15 +00:00
func componentElementType(pclType model.Type) string {
switch pclType {
case model.BoolType:
2023-03-20 19:56:24 +00:00
return "boolean"
case model.IntType, model.NumberType:
return "number"
2023-03-08 23:34:15 +00:00
case model.StringType:
return "string"
default:
switch pclType := pclType.(type) {
case *model.ListType:
elementType := componentElementType(pclType.ElementType)
Enable perfsprint linter (#14813) <!--- Thanks so much for your contribution! If this is your first time contributing, please ensure that you have read the [CONTRIBUTING](https://github.com/pulumi/pulumi/blob/master/CONTRIBUTING.md) documentation. --> # Description <!--- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. --> Prompted by a comment in another review: https://github.com/pulumi/pulumi/pull/14654#discussion_r1419995945 This lints that we don't use `fmt.Errorf` when `errors.New` will suffice, it also covers a load of other cases where `Sprintf` is sub-optimal. Most of these edits were made by running `perfsprint --fix`. ## Checklist - [x] I have run `make tidy` to update any new dependencies - [x] I have run `make lint` to verify my code passes the lint check - [x] I have formatted my code using `gofumpt` <!--- Please provide details if the checkbox below is to be left unchecked. --> - [ ] I have added tests that prove my fix is effective or that my feature works <!--- User-facing changes require a CHANGELOG entry. --> - [ ] I have run `make changelog` and committed the `changelog/pending/<file>` documenting my change <!-- If the change(s) in this PR is a modification of an existing call to the Pulumi Cloud, then the service should honor older versions of the CLI where this change would not exist. You must then bump the API version in /pkg/backend/httpstate/client/api.go, as well as add it to the service. --> - [ ] Yes, there are changes in this PR that warrants bumping the Pulumi Cloud API version <!-- @Pulumi employees: If yes, you must submit corresponding changes in the service repo. -->
2023-12-12 12:19:42 +00:00
return elementType + "[]"
2023-03-08 23:34:15 +00:00
case *model.MapType:
elementType := componentElementType(pclType.ElementType)
2023-03-20 19:56:24 +00:00
return fmt.Sprintf("Record<string, pulumi.Input<%s>>", elementType)
2023-03-08 23:34:15 +00:00
case *model.OutputType:
// something is already an output
// get only the element type because we are wrapping these in Output<T> anyway
return componentElementType(pclType.ElementType)
case *model.UnionType:
if len(pclType.ElementTypes) == 2 && pclType.ElementTypes[0] == model.NoneType {
return componentElementType(pclType.ElementTypes[1])
} else if len(pclType.ElementTypes) == 2 && pclType.ElementTypes[1] == model.NoneType {
return componentElementType(pclType.ElementTypes[0])
} else {
return "any"
}
2023-03-08 23:34:15 +00:00
default:
return "any"
}
}
}
func componentInputType(pclType model.Type) string {
elementType := componentElementType(pclType)
return fmt.Sprintf("pulumi.Input<%s>", elementType)
}
func componentOutputType(pclType model.Type) string {
elementType := componentElementType(pclType)
return fmt.Sprintf("pulumi.Output<%s>", elementType)
}
func (g *generator) genObjectTypedConfig(w io.Writer, objectType *model.ObjectType) {
attributeKeys := []string{}
for attributeKey := range objectType.Properties {
attributeKeys = append(attributeKeys, attributeKey)
}
// get deterministically sorted keys
sort.Strings(attributeKeys)
g.Fgenf(w, "{\n")
g.Indented(func() {
for _, attributeKey := range attributeKeys {
attributeType := objectType.Properties[attributeKey]
optional := "?"
g.Fgenf(w, "%s", g.Indent)
typeName := componentInputType(attributeType)
g.Fgenf(w, "%s%s: %s,\n", attributeKey, optional, typeName)
}
})
g.Fgenf(w, "%s}", g.Indent)
}
2023-03-08 23:34:15 +00:00
func (g *generator) genComponentResourceDefinition(w io.Writer, componentName string, component *pcl.Component) {
// Print the @pulumi/pulumi import at the top.
g.Fprintln(w, `import * as pulumi from "@pulumi/pulumi";`)
programImports := g.collectProgramImports(component.Program)
// Now sort the imports and emit them.
for _, i := range programImports.importStatements {
g.Fprintln(w, i)
}
g.Fprint(w, "\n")
// If we collected any helper methods that should be added, write them just before the main func
for _, preambleHelperMethodBody := range programImports.preambleHelperMethods.SortedValues() {
g.Fprintf(w, "%s\n\n", preambleHelperMethodBody)
}
configVars := component.Program.ConfigVariables()
if len(configVars) > 0 {
g.Fgenf(w, "interface %sArgs {\n", componentName)
2023-03-08 23:34:15 +00:00
g.Indented(func() {
for _, configVar := range configVars {
optional := "?"
if configVar.DefaultValue == nil {
optional = ""
}
if configVar.Description != "" {
g.Fgenf(w, "%s/**\n", g.Indent)
for _, line := range strings.Split(configVar.Description, "\n") {
g.Fgenf(w, "%s * %s\n", g.Indent, line)
}
g.Fgenf(w, "%s */\n", g.Indent)
}
2023-03-08 23:34:15 +00:00
g.Fgenf(w, "%s", g.Indent)
switch configVarType := configVar.Type().(type) {
case *model.ObjectType:
2023-03-24 08:51:56 +00:00
// generate {...}
g.Fgenf(w, "%s%s: ", configVar.Name(), optional)
g.genObjectTypedConfig(w, configVarType)
g.Fgen(w, ",\n")
case *model.ListType:
switch elementType := configVarType.ElementType.(type) {
case *model.ObjectType:
2023-03-24 08:51:56 +00:00
// generate {...}[]
g.Fgenf(w, "%s%s: ", configVar.Name(), optional)
g.genObjectTypedConfig(w, elementType)
g.Fgen(w, "[],\n")
default:
typeName := componentInputType(configVar.Type())
g.Fgenf(w, "%s%s: %s,\n", configVar.Name(), optional, typeName)
}
case *model.MapType:
switch elementType := configVarType.ElementType.(type) {
case *model.ObjectType:
2023-03-24 08:51:56 +00:00
// generate Record<string, {...}>
g.Fgenf(w, "%s%s: Record<string, ", configVar.Name(), optional)
g.genObjectTypedConfig(w, elementType)
g.Fgen(w, ">,\n")
default:
typeName := componentInputType(configVar.Type())
g.Fgenf(w, "%s%s: %s,\n", configVar.Name(), optional, typeName)
}
default:
typeName := componentInputType(configVar.Type())
g.Fgenf(w, "%s%s: %s,\n", configVar.Name(), optional, typeName)
}
2023-03-08 23:34:15 +00:00
}
})
g.Fgenf(w, "}\n\n")
}
outputs := component.Program.OutputVariables()
g.Fgenf(w, "export class %s extends pulumi.ComponentResource {\n", componentName)
2023-03-08 23:34:15 +00:00
g.Indented(func() {
for _, output := range outputs {
var outputType string
switch expr := output.Value.(type) {
case *model.ScopeTraversalExpression:
resource, ok := expr.Parts[0].(*pcl.Resource)
if ok && len(expr.Parts) == 1 {
pkg, module, memberName, diagnostics := resourceTypeName(resource)
g.diagnostics = append(g.diagnostics, diagnostics...)
if module != "" {
module = "." + module
}
qualifiedMemberName := fmt.Sprintf("%s%s.%s", pkg, module, memberName)
// special case: the output is a Resource type
outputType = fmt.Sprintf("pulumi.Output<%s>", qualifiedMemberName)
} else {
outputType = componentOutputType(expr.Type())
}
default:
outputType = componentOutputType(expr.Type())
}
g.Fgenf(w, "%s", g.Indent)
g.Fgenf(w, "public %s: %s;\n", output.Name(), outputType)
}
Enable perfsprint linter (#14813) <!--- Thanks so much for your contribution! If this is your first time contributing, please ensure that you have read the [CONTRIBUTING](https://github.com/pulumi/pulumi/blob/master/CONTRIBUTING.md) documentation. --> # Description <!--- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. --> Prompted by a comment in another review: https://github.com/pulumi/pulumi/pull/14654#discussion_r1419995945 This lints that we don't use `fmt.Errorf` when `errors.New` will suffice, it also covers a load of other cases where `Sprintf` is sub-optimal. Most of these edits were made by running `perfsprint --fix`. ## Checklist - [x] I have run `make tidy` to update any new dependencies - [x] I have run `make lint` to verify my code passes the lint check - [x] I have formatted my code using `gofumpt` <!--- Please provide details if the checkbox below is to be left unchecked. --> - [ ] I have added tests that prove my fix is effective or that my feature works <!--- User-facing changes require a CHANGELOG entry. --> - [ ] I have run `make changelog` and committed the `changelog/pending/<file>` documenting my change <!-- If the change(s) in this PR is a modification of an existing call to the Pulumi Cloud, then the service should honor older versions of the CLI where this change would not exist. You must then bump the API version in /pkg/backend/httpstate/client/api.go, as well as add it to the service. --> - [ ] Yes, there are changes in this PR that warrants bumping the Pulumi Cloud API version <!-- @Pulumi employees: If yes, you must submit corresponding changes in the service repo. -->
2023-12-12 12:19:42 +00:00
token := "components:index:" + componentName
2023-03-08 23:34:15 +00:00
if len(configVars) == 0 {
g.Fgenf(w, "%s", g.Indent)
g.Fgen(w, "constructor(name: string, opts?: pulumi.ComponentResourceOptions) {\n")
g.Indented(func() {
g.Fgenf(w, "%s", g.Indent)
g.Fgenf(w, "super(\"%s\", name, {}, opts);\n", token)
2023-03-08 23:34:15 +00:00
})
} else {
g.Fgenf(w, "%s", g.Indent)
argsTypeName := componentName + "Args"
2023-03-08 23:34:15 +00:00
g.Fgenf(w, "constructor(name: string, args: %s, opts?: pulumi.ComponentResourceOptions) {\n",
argsTypeName)
g.Indented(func() {
g.Fgenf(w, "%s", g.Indent)
g.Fgenf(w, "super(\"%s\", name, args, opts);\n", token)
2023-03-08 23:34:15 +00:00
})
}
// generate component resources and local variables
g.Indented(func() {
// assign default values to config inputs
for _, configVar := range configVars {
if configVar.DefaultValue != nil {
g.Fgenf(w, "%sargs.%s = args.%s || %v;\n",
g.Indent,
configVar.Name(),
configVar.Name(),
configVar.DefaultValue)
}
}
for _, node := range pcl.Linearize(component.Program) {
2023-03-08 23:34:15 +00:00
switch node := node.(type) {
case *pcl.LocalVariable:
g.genLocalVariable(w, node)
g.Fgen(w, "\n")
case *pcl.Component:
if node.Options == nil {
node.Options = &pcl.ResourceOptions{}
}
if node.Options.Parent == nil {
node.Options.Parent = model.ConstantReference(&model.Constant{
Name: "this",
})
}
g.genComponent(w, node)
g.Fgen(w, "\n")
2023-03-08 23:34:15 +00:00
case *pcl.Resource:
if node.Options == nil {
node.Options = &pcl.ResourceOptions{}
}
if node.Options.Parent == nil {
node.Options.Parent = model.ConstantReference(&model.Constant{
Name: "this",
})
}
g.genResource(w, node)
g.Fgen(w, "\n")
}
}
registeredOutputs := &model.ObjectConsExpression{}
for _, output := range outputs {
// assign the output fields
outputProperty := output.Name()
switch expr := output.Value.(type) {
case *model.ScopeTraversalExpression:
_, ok := expr.Parts[0].(*pcl.Resource)
if ok && len(expr.Parts) == 1 {
// special case: the output is a Resource type
g.Fgenf(w, "%sthis.%s = pulumi.output(%v);\n",
g.Indent, outputProperty,
g.lowerExpression(output.Value, output.Type()))
} else {
g.Fgenf(w, "%sthis.%s = %v;\n",
g.Indent, outputProperty,
g.lowerExpression(output.Value, output.Type()))
}
default:
g.Fgenf(w, "%sthis.%s = %v;\n",
g.Indent, outputProperty,
g.lowerExpression(output.Value, output.Type()))
}
// add the outputs to abject for registration
registeredOutputs.Items = append(registeredOutputs.Items, model.ObjectConsItem{
Key: &model.LiteralValueExpression{
Tokens: syntax.NewLiteralValueTokens(cty.StringVal(output.Name())),
Value: cty.StringVal(output.Name()),
},
Value: output.Value,
})
}
if len(outputs) == 0 {
g.Fgenf(w, "%sthis.registerOutputs();\n", g.Indent)
} else {
g.Fgenf(w, "%sthis.registerOutputs(%v);\n", g.Indent, registeredOutputs)
}
2023-03-08 23:34:15 +00:00
})
g.Fgenf(w, "%s}\n", g.Indent)
})
g.Fgen(w, "}\n")
}
func (g *generator) genNode(w io.Writer, n pcl.Node) {
switch n := n.(type) {
case *pcl.Resource:
g.genResource(w, n)
case *pcl.ConfigVariable:
g.genConfigVariable(w, n)
case *pcl.LocalVariable:
g.genLocalVariable(w, n)
case *pcl.OutputVariable:
g.genOutputVariable(w, n)
case *pcl.Component:
g.genComponent(w, n)
}
}
func resourceRequiresAsyncMain(r *pcl.Resource) bool {
if r.Options == nil || r.Options.Range == nil {
return false
}
return model.ContainsPromises(r.Options.Range.Type())
}
func outputRequiresAsyncMain(ov *pcl.OutputVariable) bool {
outputName := ov.LogicalName()
return makeValidIdentifier(outputName) != outputName
}
// resourceTypeName computes the NodeJS package, module, and type name for the given resource.
func resourceTypeName(r *pcl.Resource) (string, string, string, hcl.Diagnostics) {
// Compute the resource type from the Pulumi type token.
2022-08-19 17:27:05 +00:00
pcl.FixupPulumiPackageTokens(r)
pkg, module, member, diagnostics := r.DecomposeToken()
if r.Schema != nil {
module = moduleName(module, r.Schema.PackageReference)
}
return makeValidIdentifier(pkg), module, title(member), diagnostics
}
func moduleName(module string, pkg schema.PackageReference) string {
// Normalize module.
if pkg != nil {
def, err := pkg.Definition()
contract.AssertNoErrorf(err, "error loading package definition for %q", pkg.Name())
err = def.ImportLanguages(map[string]schema.Language{"nodejs": Importer})
contract.AssertNoErrorf(err, "error importing nodejs language for %q", pkg.Name())
if lang, ok := def.Language["nodejs"]; ok {
pkgInfo := lang.(NodePackageInfo)
if m, ok := pkgInfo.ModuleToPackage[module]; ok {
module = m
}
}
}
return strings.ToLower(strings.ReplaceAll(module, "/", "."))
}
// makeResourceName returns the expression that should be emitted for a resource's "name" parameter given its base name
// and the count variable name, if any.
func (g *generator) makeResourceName(baseName, count string) string {
if count == "" {
2023-03-08 23:34:15 +00:00
if g.isComponent {
return fmt.Sprintf("`${name}-%s`", baseName)
}
return fmt.Sprintf(`"%s"`, baseName)
}
2023-03-08 23:34:15 +00:00
if g.isComponent {
return fmt.Sprintf("`${name}-%s-${%s}`", baseName, count)
}
return fmt.Sprintf("`%s-${%s}`", baseName, count)
}
func (g *generator) genResourceOptions(opts *pcl.ResourceOptions) string {
if opts == nil {
return ""
}
// Turn the resource options into an ObjectConsExpression and generate it.
var object *model.ObjectConsExpression
appendOption := func(name string, value model.Expression) {
if object == nil {
object = &model.ObjectConsExpression{}
}
object.Items = append(object.Items, model.ObjectConsItem{
Key: &model.LiteralValueExpression{
Tokens: syntax.NewLiteralValueTokens(cty.StringVal(name)),
Value: cty.StringVal(name),
},
Value: value,
})
}
if opts.Parent != nil {
appendOption("parent", opts.Parent)
}
if opts.Provider != nil {
appendOption("provider", opts.Provider)
}
if opts.DependsOn != nil {
appendOption("dependsOn", opts.DependsOn)
}
if opts.Protect != nil {
appendOption("protect", opts.Protect)
}
if opts.RetainOnDelete != nil {
appendOption("retainOnDelete", opts.RetainOnDelete)
}
if opts.IgnoreChanges != nil {
appendOption("ignoreChanges", opts.IgnoreChanges)
}
if opts.DeletedWith != nil {
appendOption("deletedWith", opts.DeletedWith)
}
if object == nil {
return ""
}
var buffer bytes.Buffer
g.Fgenf(&buffer, ", %v", g.lowerExpression(object, nil))
return buffer.String()
}
// genResourceDeclaration handles the generation of instantiations of resources.
func (g *generator) genResourceDeclaration(w io.Writer, r *pcl.Resource, needsDefinition bool) {
pkg, module, memberName, diagnostics := resourceTypeName(r)
g.diagnostics = append(g.diagnostics, diagnostics...)
if module != "" {
module = "." + module
}
qualifiedMemberName := fmt.Sprintf("%s%s.%s", pkg, module, memberName)
optionsBag := g.genResourceOptions(r.Options)
name := r.LogicalName()
variableName := makeValidIdentifier(r.Name())
if needsDefinition {
g.genTrivia(w, r.Definition.Tokens.GetType(""))
for _, l := range r.Definition.Tokens.GetLabels(nil) {
g.genTrivia(w, l)
}
g.genTrivia(w, r.Definition.Tokens.GetOpenBrace())
}
instantiate := func(resName string) {
g.Fgenf(w, "new %s(%s, {", qualifiedMemberName, resName)
indenter := func(f func()) { f() }
if len(r.Inputs) > 1 {
indenter = g.Indented
}
indenter(func() {
fmtString := "%s: %.v"
if len(r.Inputs) > 1 {
fmtString = "\n" + g.Indent + "%s: %.v,"
}
for _, attr := range r.Inputs {
propertyName := attr.Name
if !isLegalIdentifier(propertyName) {
propertyName = fmt.Sprintf("%q", propertyName)
}
if r.Schema != nil {
destType, diagnostics := r.InputType.Traverse(hcl.TraverseAttr{Name: attr.Name})
g.diagnostics = append(g.diagnostics, diagnostics...)
g.Fgenf(w, fmtString, propertyName,
g.lowerExpression(attr.Value, destType.(model.Type)))
} else {
g.Fgenf(w, fmtString, propertyName, attr.Value)
}
}
})
if len(r.Inputs) > 1 {
g.Fgenf(w, "\n%s", g.Indent)
}
g.Fgenf(w, "}%s)", optionsBag)
}
if r.Options != nil && r.Options.Range != nil {
rangeType := r.Options.Range.Type()
rangeExpr := r.Options.Range
if model.ContainsOutputs(r.Options.Range.Type()) {
rangeExpr = g.lowerExpression(rangeExpr, rangeType)
if model.InputType(model.BoolType).ConversionFrom(rangeType) == model.SafeConversion {
g.Fgenf(w, "%slet %s: %s | undefined;\n", g.Indent, variableName, qualifiedMemberName)
} else {
g.Fgenf(w, "%sconst %s: %s[] = [];\n", g.Indent, variableName, qualifiedMemberName)
}
switch expr := rangeExpr.(type) {
case *model.FunctionCallExpression:
if expr.Name == pcl.IntrinsicApply {
applyArgs, applyLambda := pcl.ParseApplyCall(expr)
// Step 1: generate the apply function call:
if len(applyArgs) == 1 {
// If we only have a single output, just generate a normal `.apply`
g.Fgenf(w, "%.20v.apply(", applyArgs[0])
} else {
// Otherwise, generate a call to `pulumi.all([]).apply()`.
g.Fgen(w, "pulumi.all([")
for i, o := range applyArgs {
if i > 0 {
g.Fgen(w, ", ")
}
g.Fgenf(w, "%v", o)
}
g.Fgen(w, "]).apply(")
}
// Step 2: apply lambda function arguments
switch len(applyLambda.Signature.Parameters) {
case 0:
g.Fgen(w, "()")
case 1:
g.Fgenf(w, "%s", applyLambda.Signature.Parameters[0].Name)
default:
g.Fgen(w, "([")
for i, p := range applyLambda.Signature.Parameters {
if i > 0 {
g.Fgen(w, ", ")
}
g.Fgenf(w, "%s", p.Name)
}
g.Fgen(w, "])")
}
// Step 3: The function body is where the resources are generated:
// The function body is also a non-output value so we rewrite the range of
// the resource declaration to this non-output value
g.Fgen(w, " => {\n")
g.Indented(func() {
r.Options.Range = applyLambda.Body
g.genResourceDeclaration(w, r, false)
})
g.Fgenf(w, "%s});\n", g.Indent)
return
}
// If we have anything else that returns output, just generate a normal `.apply`
g.Fgenf(w, "%.20v.apply(rangeBody => {\n", rangeExpr)
g.Indented(func() {
r.Options.Range = model.VariableReference(&model.Variable{
Name: "rangeBody",
VariableType: model.ResolveOutputs(rangeExpr.Type()),
})
g.genResourceDeclaration(w, r, false)
})
g.Fgenf(w, "%s});\n", g.Indent)
return
case *model.TupleConsExpression, *model.ForExpression:
// A list or list generator that contains outputs looks like list(output(T))
// ideally we want this to be output(list(T)) and then call apply:
// so we call pulumi.all to lift the elements of the list, then call apply
g.Fgenf(w, "pulumi.all(%.20v).apply(rangeBody => {\n", rangeExpr)
g.Indented(func() {
r.Options.Range = model.VariableReference(&model.Variable{
Name: "rangeBody",
VariableType: model.ResolveOutputs(rangeExpr.Type()),
})
g.genResourceDeclaration(w, r, false)
})
g.Fgenf(w, "%s});\n", g.Indent)
return
default:
// If we have anything else that returns output, just generate a normal `.apply`
g.Fgenf(w, "%.20v.apply(rangeBody => {\n", rangeExpr)
g.Indented(func() {
r.Options.Range = model.VariableReference(&model.Variable{
Name: "rangeBody",
VariableType: model.ResolveOutputs(rangeExpr.Type()),
})
g.genResourceDeclaration(w, r, false)
})
g.Fgenf(w, "%s});\n", g.Indent)
return
}
}
if model.InputType(model.BoolType).ConversionFrom(rangeType) == model.SafeConversion {
if needsDefinition {
g.Fgenf(w, "%slet %s: %s | undefined;\n", g.Indent, variableName, qualifiedMemberName)
}
g.Fgenf(w, "%sif (%.v) {\n", g.Indent, rangeExpr)
g.Indented(func() {
g.Fgenf(w, "%s%s = ", g.Indent, variableName)
instantiate(g.makeResourceName(name, ""))
g.Fgenf(w, ";\n")
})
g.Fgenf(w, "%s}\n", g.Indent)
} else {
if needsDefinition {
g.Fgenf(w, "%sconst %s: %s[] = [];\n", g.Indent, variableName, qualifiedMemberName)
}
resKey := "key"
if model.InputType(model.NumberType).ConversionFrom(rangeExpr.Type()) != model.NoConversion {
g.Fgenf(w, "%sfor (const range = {value: 0}; range.value < %.12o; range.value++) {\n", g.Indent, rangeExpr)
resKey = "value"
} else {
rangeExpr := &model.FunctionCallExpression{
Name: "entries",
Args: []model.Expression{rangeExpr},
}
g.Fgenf(w, "%sfor (const range of %.v) {\n", g.Indent, rangeExpr)
}
resName := g.makeResourceName(name, "range."+resKey)
g.Indented(func() {
g.Fgenf(w, "%s%s.push(", g.Indent, variableName)
instantiate(resName)
g.Fgenf(w, ");\n")
})
g.Fgenf(w, "%s}\n", g.Indent)
}
} else {
g.Fgenf(w, "%sconst %s = ", g.Indent, variableName)
instantiate(g.makeResourceName(name, ""))
g.Fgenf(w, ";\n")
}
g.genTrivia(w, r.Definition.Tokens.GetCloseBrace())
}
func (g *generator) genResource(w io.Writer, r *pcl.Resource) {
g.genResourceDeclaration(w, r, true)
}
// genResource handles the generation of instantiations of non-builtin resources.
func (g *generator) genComponent(w io.Writer, component *pcl.Component) {
componentName := component.DeclarationName()
optionsBag := g.genResourceOptions(component.Options)
name := component.LogicalName()
variableName := makeValidIdentifier(component.Name())
g.genTrivia(w, component.Definition.Tokens.GetType(""))
for _, l := range component.Definition.Tokens.GetLabels(nil) {
g.genTrivia(w, l)
}
g.genTrivia(w, component.Definition.Tokens.GetOpenBrace())
configVars := component.Program.ConfigVariables()
instantiate := func(resName string) {
if len(configVars) == 0 {
g.Fgenf(w, "new %s(%s%s)", componentName, resName, optionsBag)
return
}
g.Fgenf(w, "new %s(%s, {", componentName, resName)
indenter := func(f func()) { f() }
if len(component.Inputs) > 1 {
indenter = g.Indented
}
indenter(func() {
fmtString := "%s: %.v"
if len(component.Inputs) > 1 {
fmtString = "\n" + g.Indent + "%s: %.v,"
}
for _, attr := range component.Inputs {
propertyName := attr.Name
if !isLegalIdentifier(propertyName) {
propertyName = fmt.Sprintf("%q", propertyName)
}
g.Fgenf(w, fmtString, propertyName,
g.lowerExpression(attr.Value, attr.Value.Type()))
}
})
if len(component.Inputs) > 1 {
g.Fgenf(w, "\n%s", g.Indent)
}
g.Fgenf(w, "}%s)", optionsBag)
}
if component.Options != nil && component.Options.Range != nil {
rangeType := model.ResolveOutputs(component.Options.Range.Type())
rangeExpr := g.lowerExpression(component.Options.Range, rangeType)
if model.InputType(model.BoolType).ConversionFrom(rangeType) == model.SafeConversion {
g.Fgenf(w, "%slet %s: %s | undefined;\n", g.Indent, variableName, componentName)
g.Fgenf(w, "%sif (%.v) {\n", g.Indent, rangeExpr)
g.Indented(func() {
g.Fgenf(w, "%s%s = ", g.Indent, variableName)
instantiate(g.makeResourceName(name, ""))
g.Fgenf(w, ";\n")
})
g.Fgenf(w, "%s}\n", g.Indent)
} else {
g.Fgenf(w, "%sconst %s: %s[] = [];\n", g.Indent, variableName, componentName)
resKey := "key"
if model.InputType(model.NumberType).ConversionFrom(rangeExpr.Type()) != model.NoConversion {
g.Fgenf(w, "%sfor (const range = {value: 0}; range.value < %.12o; range.value++) {\n", g.Indent, rangeExpr)
resKey = "value"
} else {
rangeExpr := &model.FunctionCallExpression{
Name: "entries",
Args: []model.Expression{rangeExpr},
}
g.Fgenf(w, "%sfor (const range of %.v) {\n", g.Indent, rangeExpr)
}
resName := g.makeResourceName(name, "range."+resKey)
g.Indented(func() {
g.Fgenf(w, "%s%s.push(", g.Indent, variableName)
instantiate(resName)
g.Fgenf(w, ");\n")
})
g.Fgenf(w, "%s}\n", g.Indent)
}
} else {
g.Fgenf(w, "%sconst %s = ", g.Indent, variableName)
instantiate(g.makeResourceName(name, ""))
g.Fgenf(w, ";\n")
}
g.genTrivia(w, component.Definition.Tokens.GetCloseBrace())
}
func computeConfigTypeParam(configType model.Type) string {
switch pcl.UnwrapOption(configType) {
case model.StringType:
return "string"
case model.NumberType, model.IntType:
return "number"
case model.BoolType:
return "boolean"
case model.DynamicType:
return "any"
default:
switch complexType := pcl.UnwrapOption(configType).(type) {
case *model.ListType:
return fmt.Sprintf("Array<%s>", computeConfigTypeParam(complexType.ElementType))
case *model.MapType:
return fmt.Sprintf("Record<string, %s>", computeConfigTypeParam(complexType.ElementType))
case *model.ObjectType:
if len(complexType.Properties) == 0 {
return "any"
}
attributeKeys := []string{}
for attributeKey := range complexType.Properties {
attributeKeys = append(attributeKeys, attributeKey)
}
// get deterministically sorted attribute keys
sort.Strings(attributeKeys)
2023-07-17 22:48:55 +00:00
var elementTypes []string
for _, propertyName := range attributeKeys {
propertyType := complexType.Properties[propertyName]
elementType := fmt.Sprintf("%s?: %s", propertyName, computeConfigTypeParam(propertyType))
elementTypes = append(elementTypes, elementType)
}
return fmt.Sprintf("{%s}", strings.Join(elementTypes, ", "))
default:
return "any"
}
}
}
func (g *generator) genConfigVariable(w io.Writer, v *pcl.ConfigVariable) {
if !g.configCreated {
g.Fprintf(w, "%sconst config = new pulumi.Config();\n", g.Indent)
g.configCreated = true
}
getType := "Object"
switch pcl.UnwrapOption(v.Type()) {
case model.StringType:
getType = ""
case model.NumberType, model.IntType:
getType = "Number"
case model.BoolType:
getType = "Boolean"
}
typeParam := ""
if getType == "Object" {
// compute the type parameter T for the call to config.getObject<T>(...)
computedTypeParam := computeConfigTypeParam(v.Type())
if computedTypeParam != "any" {
// any is redundant
typeParam = fmt.Sprintf("<%s>", computedTypeParam)
}
}
getOrRequire := "get"
if v.DefaultValue == nil && !model.IsOptionalType(v.Type()) {
getOrRequire = "require"
}
if v.Description != "" {
for _, line := range strings.Split(v.Description, "\n") {
g.Fgenf(w, "%s// %s\n", g.Indent, line)
}
}
2022-11-02 19:39:57 +00:00
name := makeValidIdentifier(v.Name())
g.Fgenf(w, "%[1]sconst %[2]s = config.%[3]s%[4]s%[5]s(\"%[6]s\")",
g.Indent, name, getOrRequire, getType, typeParam, v.LogicalName())
if v.DefaultValue != nil && !model.IsOptionalType(v.Type()) {
g.Fgenf(w, " || %.v", g.lowerExpression(v.DefaultValue, v.DefaultValue.Type()))
}
g.Fgenf(w, ";\n")
}
func (g *generator) genLocalVariable(w io.Writer, v *pcl.LocalVariable) {
g.genTrivia(w, v.Definition.Tokens.Name)
g.Fgenf(w, "%sconst %s = %.3v;\n", g.Indent, v.Name(), g.lowerExpression(v.Definition.Value, v.Type()))
}
func (g *generator) genOutputVariable(w io.Writer, v *pcl.OutputVariable) {
if g.asyncMain {
// skip generating the output variables as export constants
// when we are inside an async main program because we export them as a single object
return
}
// TODO(pdg): trivia
g.Fgenf(w, "%sexport const %s = %.3v;\n", g.Indent,
makeValidIdentifier(v.Name()), g.lowerExpression(v.Value, v.Type()))
}
func (g *generator) genNYI(w io.Writer, reason string, vs ...interface{}) {
Enable perfsprint linter (#14813) <!--- Thanks so much for your contribution! If this is your first time contributing, please ensure that you have read the [CONTRIBUTING](https://github.com/pulumi/pulumi/blob/master/CONTRIBUTING.md) documentation. --> # Description <!--- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. --> Prompted by a comment in another review: https://github.com/pulumi/pulumi/pull/14654#discussion_r1419995945 This lints that we don't use `fmt.Errorf` when `errors.New` will suffice, it also covers a load of other cases where `Sprintf` is sub-optimal. Most of these edits were made by running `perfsprint --fix`. ## Checklist - [x] I have run `make tidy` to update any new dependencies - [x] I have run `make lint` to verify my code passes the lint check - [x] I have formatted my code using `gofumpt` <!--- Please provide details if the checkbox below is to be left unchecked. --> - [ ] I have added tests that prove my fix is effective or that my feature works <!--- User-facing changes require a CHANGELOG entry. --> - [ ] I have run `make changelog` and committed the `changelog/pending/<file>` documenting my change <!-- If the change(s) in this PR is a modification of an existing call to the Pulumi Cloud, then the service should honor older versions of the CLI where this change would not exist. You must then bump the API version in /pkg/backend/httpstate/client/api.go, as well as add it to the service. --> - [ ] Yes, there are changes in this PR that warrants bumping the Pulumi Cloud API version <!-- @Pulumi employees: If yes, you must submit corresponding changes in the service repo. -->
2023-12-12 12:19:42 +00:00
message := "not yet implemented: " + fmt.Sprintf(reason, vs...)
g.diagnostics = append(g.diagnostics, &hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: message,
Detail: message,
})
g.Fgenf(w, "(() => throw new Error(%q))()", fmt.Sprintf(reason, vs...))
}