2020-04-03 06:27:05 +00:00
|
|
|
// 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 (
|
2020-04-07 02:43:16 +00:00
|
|
|
"bytes"
|
2020-04-03 06:27:05 +00:00
|
|
|
"fmt"
|
|
|
|
"io"
|
2023-01-06 22:39:16 +00:00
|
|
|
"os"
|
2020-04-21 17:24:42 +00:00
|
|
|
"path"
|
2023-03-08 23:34:15 +00:00
|
|
|
"path/filepath"
|
2020-04-03 06:27:05 +00:00
|
|
|
"sort"
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
2021-03-17 13:20:05 +00:00
|
|
|
"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"
|
2021-09-30 03:11:56 +00:00
|
|
|
"github.com/pulumi/pulumi/pkg/v3/codegen/pcl"
|
2021-04-08 11:47:08 +00:00
|
|
|
"github.com/pulumi/pulumi/pkg/v3/codegen/schema"
|
2022-04-29 16:04:04 +00:00
|
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/encoding"
|
2023-06-28 16:02:04 +00:00
|
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/slice"
|
2021-03-17 13:20:05 +00:00
|
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
|
2022-04-29 16:04:04 +00:00
|
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"
|
2020-05-04 22:04:35 +00:00
|
|
|
"github.com/zclconf/go-cty/cty"
|
2020-04-03 06:27:05 +00:00
|
|
|
)
|
|
|
|
|
2022-08-19 17:27:05 +00:00
|
|
|
const PulumiToken = "pulumi"
|
|
|
|
|
2020-04-03 06:27:05 +00:00
|
|
|
type generator struct {
|
|
|
|
// The formatter to use when generating code.
|
|
|
|
*format.Formatter
|
|
|
|
|
2021-09-30 03:11:56 +00:00
|
|
|
program *pcl.Program
|
2020-04-07 02:43:16 +00:00
|
|
|
diagnostics hcl.Diagnostics
|
2020-04-21 17:24:42 +00:00
|
|
|
|
2020-05-04 22:04:35 +00:00
|
|
|
asyncMain bool
|
2020-04-21 17:24:42 +00:00
|
|
|
configCreated bool
|
2023-03-08 23:34:15 +00:00
|
|
|
isComponent bool
|
2020-04-03 06:27:05 +00:00
|
|
|
}
|
|
|
|
|
2021-09-30 03:11:56 +00:00
|
|
|
func GenerateProgram(program *pcl.Program) (map[string][]byte, hcl.Diagnostics, error) {
|
2022-07-19 16:26:40 +00:00
|
|
|
pcl.MapProvidersAsResources(program)
|
2020-04-03 06:27:05 +00:00
|
|
|
// Linearize the nodes into an order appropriate for procedural code generation.
|
2021-09-30 03:11:56 +00:00
|
|
|
nodes := pcl.Linearize(program)
|
2020-04-03 06:27:05 +00:00
|
|
|
|
|
|
|
g := &generator{
|
2020-04-16 23:44:34 +00:00
|
|
|
program: program,
|
2020-04-03 06:27:05 +00:00
|
|
|
}
|
|
|
|
g.Formatter = format.NewFormatter(g)
|
|
|
|
|
2022-05-23 22:44:35 +00:00
|
|
|
packages, err := program.PackageSnapshots()
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
for _, p := range packages {
|
2020-08-05 23:27:17 +00:00
|
|
|
if err := p.ImportLanguages(map[string]schema.Language{"nodejs": Importer}); err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-07 02:43:16 +00:00
|
|
|
var index bytes.Buffer
|
2023-03-08 23:34:15 +00:00
|
|
|
err = g.genPreamble(&index, program)
|
2022-12-08 13:36:44 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
2023-06-07 00:37:40 +00:00
|
|
|
// 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{}
|
2020-04-03 06:27:05 +00:00
|
|
|
for _, n := range nodes {
|
2022-04-25 22:07:25 +00:00
|
|
|
if g.asyncMain {
|
2020-05-04 22:04:35 +00:00
|
|
|
break
|
|
|
|
}
|
2022-04-25 22:07:25 +00:00
|
|
|
switch x := n.(type) {
|
|
|
|
case *pcl.Resource:
|
|
|
|
if resourceRequiresAsyncMain(x) {
|
|
|
|
g.asyncMain = true
|
|
|
|
}
|
2023-06-07 00:37:40 +00:00
|
|
|
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
|
2022-04-25 22:07:25 +00:00
|
|
|
case *pcl.OutputVariable:
|
|
|
|
if outputRequiresAsyncMain(x) {
|
|
|
|
g.asyncMain = true
|
|
|
|
}
|
2023-06-07 00:37:40 +00:00
|
|
|
|
|
|
|
outputIdentifier := makeValidIdentifier(x.Name())
|
|
|
|
if _, alreadyDeclared := declaredNodeIdentifiers[outputIdentifier]; alreadyDeclared {
|
|
|
|
g.asyncMain = true
|
|
|
|
}
|
2022-04-25 22:07:25 +00:00
|
|
|
}
|
2020-05-04 22:04:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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 {
|
2021-09-30 03:11:56 +00:00
|
|
|
if o, ok := n.(*pcl.OutputVariable); ok {
|
2020-05-04 22:04:35 +00:00
|
|
|
if result == nil {
|
|
|
|
result = &model.ObjectConsExpression{}
|
|
|
|
}
|
2022-04-25 22:07:25 +00:00
|
|
|
name := o.LogicalName()
|
2020-05-04 22:04:35 +00:00
|
|
|
result.Items = append(result.Items, model.ObjectConsItem{
|
2023-06-07 00:37:40 +00:00
|
|
|
Key: &model.LiteralValueExpression{Value: cty.StringVal(name)},
|
|
|
|
Value: g.lowerExpression(o.Value, o.Type()),
|
2020-05-04 22:04:35 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if result != nil {
|
|
|
|
g.Fgenf(&index, "%sreturn %v;\n", g.Indent, result)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
if g.asyncMain {
|
|
|
|
g.Fgenf(&index, "}\n")
|
2020-04-03 06:27:05 +00:00
|
|
|
}
|
|
|
|
|
2020-04-07 02:43:16 +00:00
|
|
|
files := map[string][]byte{
|
|
|
|
"index.ts": index.Bytes(),
|
|
|
|
}
|
2023-03-08 23:34:15 +00:00
|
|
|
|
|
|
|
for componentDir, component := range program.CollectComponents() {
|
2023-07-27 13:15:27 +00:00
|
|
|
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)
|
2023-07-27 13:15:27 +00:00
|
|
|
files[componentFilename+".ts"] = componentBuffer.Bytes()
|
2023-03-08 23:34:15 +00:00
|
|
|
}
|
|
|
|
|
2020-04-07 02:43:16 +00:00
|
|
|
return files, g.diagnostics, nil
|
2020-04-03 06:27:05 +00:00
|
|
|
}
|
|
|
|
|
2023-08-03 10:40:05 +00:00
|
|
|
func GenerateProject(
|
|
|
|
directory string, project workspace.Project,
|
|
|
|
program *pcl.Program, localDependencies map[string]string,
|
2024-03-28 15:44:25 +00:00
|
|
|
forceTsc bool,
|
2023-08-03 10:40:05 +00:00
|
|
|
) error {
|
2022-04-29 16:04:04 +00:00
|
|
|
files, diagnostics, err := GenerateProgram(program)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if diagnostics.HasErrors() {
|
|
|
|
return diagnostics
|
|
|
|
}
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-29 16:04:04 +00:00
|
|
|
// Set the runtime to "nodejs" then marshal to Pulumi.yaml
|
2024-03-28 15:44:25 +00:00
|
|
|
runtime := workspace.NewProjectRuntimeInfo("nodejs", nil)
|
|
|
|
if forceTsc {
|
|
|
|
runtime.SetOption("typescript", false)
|
|
|
|
}
|
|
|
|
project.Runtime = runtime
|
|
|
|
|
2022-04-29 16:04:04 +00:00
|
|
|
projectBytes, err := encoding.YAML.Marshal(project)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
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)
|
|
|
|
}
|
2022-04-29 16:04:04 +00:00
|
|
|
|
2023-08-08 14:03:03 +00:00
|
|
|
// Build the package.json
|
2022-04-29 16:04:04 +00:00
|
|
|
var packageJSON bytes.Buffer
|
2023-02-23 21:14:41 +00:00
|
|
|
fmt.Fprintf(&packageJSON, `{
|
2023-06-20 21:28:12 +00:00
|
|
|
"name": "%s",
|
|
|
|
"devDependencies": {
|
|
|
|
"@types/node": "^14"
|
|
|
|
},
|
|
|
|
"dependencies": {
|
|
|
|
"typescript": "^4.0.0",
|
2023-08-08 14:03:03 +00:00
|
|
|
`, 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"`)
|
|
|
|
}
|
2023-02-23 21:14:41 +00:00
|
|
|
|
2022-04-29 16:04:04 +00:00
|
|
|
// For each package add a dependency line
|
2023-03-09 13:28:55 +00:00
|
|
|
packages, err := program.CollectNestedPackageSnapshots()
|
2022-05-23 22:44:35 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2024-06-07 08:57:46 +00:00
|
|
|
// 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
|
|
|
|
}
|
2022-05-24 01:11:20 +00:00
|
|
|
if err := p.ImportLanguages(map[string]schema.Language{"nodejs": Importer}); err != nil {
|
2022-04-29 16:04:04 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
packageName := "@pulumi/" + p.Name
|
2022-07-20 22:12:02 +00:00
|
|
|
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
|
|
|
|
}
|
2022-04-29 16:04:04 +00:00
|
|
|
}
|
2023-08-08 14:03:03 +00:00
|
|
|
|
2023-06-20 21:28:12 +00:00
|
|
|
dependencyTemplate := ",\n \"%s\": \"%s\""
|
2023-08-08 14:03:03 +00:00
|
|
|
if path, has := localDependencies[p.Name]; has {
|
|
|
|
fmt.Fprintf(&packageJSON, dependencyTemplate, packageName, path)
|
2022-04-30 00:11:37 +00:00
|
|
|
} else {
|
2023-08-08 14:03:03 +00:00
|
|
|
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
|
|
|
}
|
2022-04-29 16:04:04 +00:00
|
|
|
}
|
|
|
|
packageJSON.WriteString(`
|
2023-06-20 21:28:12 +00:00
|
|
|
}
|
2022-04-29 16:04:04 +00:00
|
|
|
}`)
|
|
|
|
|
|
|
|
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(`{
|
2023-06-20 21:28:12 +00:00
|
|
|
"compilerOptions": {
|
|
|
|
"strict": true,
|
|
|
|
"outDir": "bin",
|
|
|
|
"target": "es2016",
|
|
|
|
"module": "commonjs",
|
|
|
|
"moduleResolution": "node",
|
|
|
|
"sourceMap": true,
|
|
|
|
"experimentalDecorators": true,
|
|
|
|
"pretty": true,
|
|
|
|
"noFallthroughCasesInSwitch": true,
|
|
|
|
"noImplicitReturns": true,
|
|
|
|
"forceConsistentCasingInFileNames": true
|
|
|
|
},
|
|
|
|
"files": [
|
2022-04-29 16:04:04 +00:00
|
|
|
`)
|
|
|
|
|
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))
|
2022-04-29 16:04:04 +00:00
|
|
|
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 {
|
2022-04-29 16:04:04 +00:00
|
|
|
if strings.HasSuffix(file, ".ts") {
|
2023-06-20 21:28:12 +00:00
|
|
|
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
|
2023-03-09 13:28:55 +00:00
|
|
|
if !lastFile {
|
|
|
|
tsConfig.WriteString(",\n")
|
|
|
|
} else {
|
|
|
|
tsConfig.WriteString("\n")
|
|
|
|
}
|
2022-04-29 16:04:04 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-06-20 21:28:12 +00:00
|
|
|
tsConfig.WriteString(` ]
|
2022-04-29 16:04:04 +00:00
|
|
|
}`)
|
|
|
|
files["tsconfig.json"] = tsConfig.Bytes()
|
|
|
|
|
|
|
|
for filename, data := range files {
|
|
|
|
outPath := path.Join(directory, filename)
|
2023-03-03 16:36:39 +00:00
|
|
|
err := os.WriteFile(outPath, data, 0o600)
|
2022-04-29 16:04:04 +00:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("could not write output program: %w", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-04-03 06:27:05 +00:00
|
|
|
// 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
|
|
|
|
}
|
2020-04-03 06:27:05 +00:00
|
|
|
|
2023-03-08 23:34:15 +00:00
|
|
|
func (g *generator) collectProgramImports(program *pcl.Program) programImports {
|
2020-04-21 17:24:42 +00:00
|
|
|
importSet := codegen.NewStringSet("@pulumi/pulumi")
|
2023-03-08 23:34:15 +00:00
|
|
|
preambleHelperMethods := codegen.NewStringSet()
|
2023-03-09 13:28:55 +00:00
|
|
|
var componentImports []string
|
2023-03-08 23:34:15 +00:00
|
|
|
|
2022-08-29 18:38:41 +00:00
|
|
|
npmToPuPkgName := make(map[string]string)
|
2023-12-14 15:43:27 +00:00
|
|
|
seenComponentImports := map[string]bool{}
|
2020-04-03 06:27:05 +00:00
|
|
|
for _, n := range program.Nodes {
|
2023-03-09 13:28:55 +00:00
|
|
|
switch n := n.(type) {
|
|
|
|
case *pcl.Resource:
|
|
|
|
pkg, _, _, _ := n.DecomposeToken()
|
2022-08-19 17:27:05 +00:00
|
|
|
if pkg == PulumiToken {
|
|
|
|
continue
|
|
|
|
}
|
2022-01-31 20:48:32 +00:00
|
|
|
pkgName := "@pulumi/" + pkg
|
2023-03-09 13:28:55 +00:00
|
|
|
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)
|
2022-12-08 13:36:44 +00:00
|
|
|
if info, ok := def.Language["nodejs"].(NodePackageInfo); ok && info.PackageName != "" {
|
2022-01-31 20:48:32 +00:00
|
|
|
pkgName = info.PackageName
|
|
|
|
}
|
2022-08-29 18:38:41 +00:00
|
|
|
npmToPuPkgName[pkgName] = pkg
|
2022-01-31 20:48:32 +00:00
|
|
|
}
|
|
|
|
importSet.Add(pkgName)
|
2023-03-09 13:28:55 +00:00
|
|
|
case *pcl.Component:
|
|
|
|
componentDir := filepath.Base(n.DirPath())
|
2023-07-27 13:15:27 +00:00
|
|
|
componentName := n.DeclarationName()
|
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
|
|
|
|
}
|
2020-04-03 06:27:05 +00:00
|
|
|
}
|
2020-04-21 17:24:42 +00:00
|
|
|
diags := n.VisitExpressions(nil, func(n model.Expression) (model.Expression, hcl.Diagnostics) {
|
|
|
|
if call, ok := n.(*model.FunctionCallExpression); ok {
|
2022-01-21 14:03:25 +00:00
|
|
|
if i := g.getFunctionImports(call); len(i) > 0 && i[0] != "" {
|
|
|
|
for _, importPackage := range i {
|
|
|
|
importSet.Add(importPackage)
|
|
|
|
}
|
|
|
|
}
|
2023-03-10 11:14:28 +00:00
|
|
|
if helperMethodBody, ok := getHelperMethodIfNeeded(call.Name, g.Indent); ok {
|
2022-01-21 14:03:25 +00:00
|
|
|
preambleHelperMethods.Add(helperMethodBody)
|
2020-04-21 17:24:42 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return n, nil
|
|
|
|
})
|
2023-02-23 20:46:42 +00:00
|
|
|
contract.Assertf(len(diags) == 0, "unexpected diagnostics: %v", diags)
|
2020-04-03 06:27:05 +00:00
|
|
|
}
|
|
|
|
|
2023-03-08 23:34:15 +00:00
|
|
|
sortedValues := importSet.SortedValues()
|
2023-06-28 16:02:04 +00:00
|
|
|
imports := slice.Prealloc[string](len(sortedValues))
|
2023-03-08 23:34:15 +00:00
|
|
|
for _, pkg := range sortedValues {
|
2020-04-21 17:24:42 +00:00
|
|
|
if pkg == "@pulumi/pulumi" {
|
|
|
|
continue
|
|
|
|
}
|
2022-08-29 18:38:41 +00:00
|
|
|
var as string
|
|
|
|
if puPkg, ok := npmToPuPkgName[pkg]; ok {
|
|
|
|
as = makeValidIdentifier(puPkg)
|
|
|
|
} else {
|
|
|
|
as = makeValidIdentifier(path.Base(pkg))
|
|
|
|
}
|
2022-05-20 18:26:48 +00:00
|
|
|
imports = append(imports, fmt.Sprintf("import * as %v from \"%v\";", as, pkg))
|
2020-04-21 17:24:42 +00:00
|
|
|
}
|
2023-03-09 13:28:55 +00:00
|
|
|
|
|
|
|
imports = append(imports, componentImports...)
|
2020-04-03 06:27:05 +00:00
|
|
|
sort.Strings(imports)
|
2020-04-21 17:24:42 +00:00
|
|
|
|
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)
|
|
|
|
|
2020-04-21 17:24:42 +00:00
|
|
|
// Now sort the imports and emit them.
|
2023-03-08 23:34:15 +00:00
|
|
|
for _, i := range programImports.importStatements {
|
2020-04-21 17:24:42 +00:00
|
|
|
g.Fprintln(w, i)
|
2020-04-03 06:27:05 +00:00
|
|
|
}
|
|
|
|
g.Fprint(w, "\n")
|
2022-01-21 14:03:25 +00:00
|
|
|
|
|
|
|
// 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() {
|
2022-01-21 14:03:25 +00:00
|
|
|
g.Fprintf(w, "%s\n\n", preambleHelperMethodBody)
|
|
|
|
}
|
2022-12-08 13:36:44 +00:00
|
|
|
return nil
|
2020-04-03 06:27:05 +00:00
|
|
|
}
|
|
|
|
|
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)
|
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)
|
2023-04-05 11:19:52 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2023-03-23 22:25:21 +00:00
|
|
|
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 {
|
2023-07-27 13:15:27 +00:00
|
|
|
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 = ""
|
|
|
|
}
|
2023-03-21 14:01:16 +00:00
|
|
|
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)
|
2023-03-23 22:25:21 +00:00
|
|
|
switch configVarType := configVar.Type().(type) {
|
|
|
|
case *model.ObjectType:
|
2023-03-24 08:51:56 +00:00
|
|
|
// generate {...}
|
2023-03-23 22:25:21 +00:00
|
|
|
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 {...}[]
|
2023-03-23 22:25:21 +00:00
|
|
|
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, {...}>
|
2023-03-23 22:25:21 +00:00
|
|
|
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()
|
|
|
|
|
2023-07-27 13:15:27 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
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)
|
2023-03-09 13:28:55 +00:00
|
|
|
g.Fgenf(w, "super(\"%s\", name, {}, opts);\n", token)
|
2023-03-08 23:34:15 +00:00
|
|
|
})
|
|
|
|
} else {
|
|
|
|
g.Fgenf(w, "%s", g.Indent)
|
2023-07-27 13:15:27 +00:00
|
|
|
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)
|
2023-03-09 13:28:55 +00:00
|
|
|
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() {
|
2023-03-09 13:28:55 +00:00
|
|
|
// 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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-14 11:22:53 +00:00
|
|
|
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)
|
2023-03-09 13:28:55 +00:00
|
|
|
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")
|
|
|
|
}
|
|
|
|
}
|
2023-03-09 13:28:55 +00:00
|
|
|
|
|
|
|
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")
|
|
|
|
}
|
|
|
|
|
2021-09-30 03:11:56 +00:00
|
|
|
func (g *generator) genNode(w io.Writer, n pcl.Node) {
|
2020-04-03 06:27:05 +00:00
|
|
|
switch n := n.(type) {
|
2021-09-30 03:11:56 +00:00
|
|
|
case *pcl.Resource:
|
2020-04-03 06:27:05 +00:00
|
|
|
g.genResource(w, n)
|
2021-09-30 03:11:56 +00:00
|
|
|
case *pcl.ConfigVariable:
|
2020-04-03 06:27:05 +00:00
|
|
|
g.genConfigVariable(w, n)
|
2021-09-30 03:11:56 +00:00
|
|
|
case *pcl.LocalVariable:
|
2020-04-03 06:27:05 +00:00
|
|
|
g.genLocalVariable(w, n)
|
2021-09-30 03:11:56 +00:00
|
|
|
case *pcl.OutputVariable:
|
2020-04-03 06:27:05 +00:00
|
|
|
g.genOutputVariable(w, n)
|
2023-03-09 13:28:55 +00:00
|
|
|
case *pcl.Component:
|
|
|
|
g.genComponent(w, n)
|
2020-04-03 06:27:05 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-25 22:07:25 +00:00
|
|
|
func resourceRequiresAsyncMain(r *pcl.Resource) bool {
|
2020-05-04 22:04:35 +00:00
|
|
|
if r.Options == nil || r.Options.Range == nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2020-05-22 09:47:34 +00:00
|
|
|
return model.ContainsPromises(r.Options.Range.Type())
|
2020-05-04 22:04:35 +00:00
|
|
|
}
|
|
|
|
|
2022-04-25 22:07:25 +00:00
|
|
|
func outputRequiresAsyncMain(ov *pcl.OutputVariable) bool {
|
|
|
|
outputName := ov.LogicalName()
|
2023-01-11 15:59:43 +00:00
|
|
|
return makeValidIdentifier(outputName) != outputName
|
2022-04-25 22:07:25 +00:00
|
|
|
}
|
|
|
|
|
2020-04-03 06:27:05 +00:00
|
|
|
// resourceTypeName computes the NodeJS package, module, and type name for the given resource.
|
2021-09-30 03:11:56 +00:00
|
|
|
func resourceTypeName(r *pcl.Resource) (string, string, string, hcl.Diagnostics) {
|
2020-04-03 06:27:05 +00:00
|
|
|
// Compute the resource type from the Pulumi type token.
|
2022-08-19 17:27:05 +00:00
|
|
|
pcl.FixupPulumiPackageTokens(r)
|
2020-04-03 06:27:05 +00:00
|
|
|
pkg, module, member, diagnostics := r.DecomposeToken()
|
2020-08-05 23:27:17 +00:00
|
|
|
|
|
|
|
if r.Schema != nil {
|
2022-12-08 13:36:44 +00:00
|
|
|
module = moduleName(module, r.Schema.PackageReference)
|
2022-04-29 16:56:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return makeValidIdentifier(pkg), module, title(member), diagnostics
|
|
|
|
}
|
|
|
|
|
2022-12-08 13:36:44 +00:00
|
|
|
func moduleName(module string, pkg schema.PackageReference) string {
|
2022-04-29 16:56:02 +00:00
|
|
|
// Normalize module.
|
|
|
|
if pkg != nil {
|
2022-12-08 13:36:44 +00:00
|
|
|
def, err := pkg.Definition()
|
2023-02-23 20:46:42 +00:00
|
|
|
contract.AssertNoErrorf(err, "error loading package definition for %q", pkg.Name())
|
2022-12-08 13:36:44 +00:00
|
|
|
err = def.ImportLanguages(map[string]schema.Language{"nodejs": Importer})
|
2023-02-23 20:46:42 +00:00
|
|
|
contract.AssertNoErrorf(err, "error importing nodejs language for %q", pkg.Name())
|
2022-12-08 13:36:44 +00:00
|
|
|
if lang, ok := def.Language["nodejs"]; ok {
|
2020-08-05 23:27:17 +00:00
|
|
|
pkgInfo := lang.(NodePackageInfo)
|
|
|
|
if m, ok := pkgInfo.ModuleToPackage[module]; ok {
|
|
|
|
module = m
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-04-29 16:56:02 +00:00
|
|
|
return strings.ToLower(strings.ReplaceAll(module, "/", "."))
|
2020-04-03 06:27:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
}
|
2020-04-03 06:27:05 +00:00
|
|
|
return fmt.Sprintf(`"%s"`, baseName)
|
|
|
|
}
|
2023-03-08 23:34:15 +00:00
|
|
|
|
|
|
|
if g.isComponent {
|
|
|
|
return fmt.Sprintf("`${name}-%s-${%s}`", baseName, count)
|
|
|
|
}
|
2020-04-03 06:27:05 +00:00
|
|
|
return fmt.Sprintf("`%s-${%s}`", baseName, count)
|
|
|
|
}
|
|
|
|
|
2021-09-30 03:11:56 +00:00
|
|
|
func (g *generator) genResourceOptions(opts *pcl.ResourceOptions) string {
|
2020-06-29 23:33:52 +00:00
|
|
|
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)
|
|
|
|
}
|
2023-02-28 20:50:51 +00:00
|
|
|
if opts.RetainOnDelete != nil {
|
|
|
|
appendOption("retainOnDelete", opts.RetainOnDelete)
|
|
|
|
}
|
2020-06-29 23:33:52 +00:00
|
|
|
if opts.IgnoreChanges != nil {
|
|
|
|
appendOption("ignoreChanges", opts.IgnoreChanges)
|
|
|
|
}
|
2024-07-19 14:17:45 +00:00
|
|
|
if opts.DeletedWith != nil {
|
|
|
|
appendOption("deletedWith", opts.DeletedWith)
|
|
|
|
}
|
2020-06-29 23:33:52 +00:00
|
|
|
|
|
|
|
if object == nil {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
var buffer bytes.Buffer
|
2022-04-18 09:03:42 +00:00
|
|
|
g.Fgenf(&buffer, ", %v", g.lowerExpression(object, nil))
|
2020-06-29 23:33:52 +00:00
|
|
|
return buffer.String()
|
|
|
|
}
|
|
|
|
|
2023-04-26 18:00:32 +00:00
|
|
|
// genResourceDeclaration handles the generation of instantiations of resources.
|
|
|
|
func (g *generator) genResourceDeclaration(w io.Writer, r *pcl.Resource, needsDefinition bool) {
|
2020-04-03 06:27:05 +00:00
|
|
|
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)
|
|
|
|
|
2020-06-29 23:33:52 +00:00
|
|
|
optionsBag := g.genResourceOptions(r.Options)
|
2020-04-03 06:27:05 +00:00
|
|
|
|
2022-04-25 22:07:25 +00:00
|
|
|
name := r.LogicalName()
|
|
|
|
variableName := makeValidIdentifier(r.Name())
|
2020-04-03 06:27:05 +00:00
|
|
|
|
2023-04-26 18:00:32 +00:00
|
|
|
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())
|
2020-04-03 06:27:05 +00:00
|
|
|
}
|
|
|
|
|
2020-04-07 02:43:16 +00:00
|
|
|
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() {
|
2022-04-18 09:03:42 +00:00
|
|
|
fmtString := "%s: %.v"
|
|
|
|
if len(r.Inputs) > 1 {
|
|
|
|
fmtString = "\n" + g.Indent + "%s: %.v,"
|
|
|
|
}
|
|
|
|
|
2020-04-07 02:43:16 +00:00
|
|
|
for _, attr := range r.Inputs {
|
|
|
|
propertyName := attr.Name
|
|
|
|
if !isLegalIdentifier(propertyName) {
|
|
|
|
propertyName = fmt.Sprintf("%q", propertyName)
|
|
|
|
}
|
|
|
|
|
2023-06-14 17:02:56 +00:00
|
|
|
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)
|
|
|
|
}
|
2020-04-03 06:27:05 +00:00
|
|
|
}
|
2020-04-07 02:43:16 +00:00
|
|
|
})
|
|
|
|
if len(r.Inputs) > 1 {
|
|
|
|
g.Fgenf(w, "\n%s", g.Indent)
|
2020-04-03 06:27:05 +00:00
|
|
|
}
|
2020-04-07 02:43:16 +00:00
|
|
|
g.Fgenf(w, "}%s)", optionsBag)
|
2020-04-03 06:27:05 +00:00
|
|
|
}
|
2020-04-07 02:43:16 +00:00
|
|
|
|
|
|
|
if r.Options != nil && r.Options.Range != nil {
|
2023-04-26 18:00:32 +00:00
|
|
|
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
|
2020-04-16 23:44:34 +00:00
|
|
|
|
2023-04-26 18:00:32 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
2020-05-04 22:04:35 +00:00
|
|
|
if model.InputType(model.BoolType).ConversionFrom(rangeType) == model.SafeConversion {
|
2023-04-26 18:00:32 +00:00
|
|
|
if needsDefinition {
|
|
|
|
g.Fgenf(w, "%slet %s: %s | undefined;\n", g.Indent, variableName, qualifiedMemberName)
|
|
|
|
}
|
|
|
|
|
2020-04-16 23:44:34 +00:00
|
|
|
g.Fgenf(w, "%sif (%.v) {\n", g.Indent, rangeExpr)
|
|
|
|
g.Indented(func() {
|
2020-05-22 06:46:25 +00:00
|
|
|
g.Fgenf(w, "%s%s = ", g.Indent, variableName)
|
2020-04-16 23:44:34 +00:00
|
|
|
instantiate(g.makeResourceName(name, ""))
|
|
|
|
g.Fgenf(w, ";\n")
|
|
|
|
})
|
|
|
|
g.Fgenf(w, "%s}\n", g.Indent)
|
|
|
|
} else {
|
2023-04-26 18:00:32 +00:00
|
|
|
if needsDefinition {
|
|
|
|
g.Fgenf(w, "%sconst %s: %s[] = [];\n", g.Indent, variableName, qualifiedMemberName)
|
|
|
|
}
|
2020-05-04 22:04:35 +00:00
|
|
|
resKey := "key"
|
2020-04-16 23:44:34 +00:00
|
|
|
if model.InputType(model.NumberType).ConversionFrom(rangeExpr.Type()) != model.NoConversion {
|
2022-10-09 21:03:03 +00:00
|
|
|
g.Fgenf(w, "%sfor (const range = {value: 0}; range.value < %.12o; range.value++) {\n", g.Indent, rangeExpr)
|
2020-04-16 23:44:34 +00:00
|
|
|
resKey = "value"
|
|
|
|
} else {
|
2020-05-04 22:04:35 +00:00
|
|
|
rangeExpr := &model.FunctionCallExpression{
|
|
|
|
Name: "entries",
|
|
|
|
Args: []model.Expression{rangeExpr},
|
|
|
|
}
|
|
|
|
g.Fgenf(w, "%sfor (const range of %.v) {\n", g.Indent, rangeExpr)
|
2020-04-16 23:44:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
resName := g.makeResourceName(name, "range."+resKey)
|
|
|
|
g.Indented(func() {
|
2020-05-22 06:46:25 +00:00
|
|
|
g.Fgenf(w, "%s%s.push(", g.Indent, variableName)
|
2020-04-16 23:44:34 +00:00
|
|
|
instantiate(resName)
|
|
|
|
g.Fgenf(w, ");\n")
|
|
|
|
})
|
|
|
|
g.Fgenf(w, "%s}\n", g.Indent)
|
|
|
|
}
|
2020-04-07 02:43:16 +00:00
|
|
|
} else {
|
2020-05-22 06:46:25 +00:00
|
|
|
g.Fgenf(w, "%sconst %s = ", g.Indent, variableName)
|
2020-04-07 02:43:16 +00:00
|
|
|
instantiate(g.makeResourceName(name, ""))
|
|
|
|
g.Fgenf(w, ";\n")
|
|
|
|
}
|
|
|
|
|
2020-04-21 17:24:42 +00:00
|
|
|
g.genTrivia(w, r.Definition.Tokens.GetCloseBrace())
|
2020-04-03 06:27:05 +00:00
|
|
|
}
|
|
|
|
|
2023-04-26 18:00:32 +00:00
|
|
|
func (g *generator) genResource(w io.Writer, r *pcl.Resource) {
|
|
|
|
g.genResourceDeclaration(w, r, true)
|
|
|
|
}
|
|
|
|
|
2023-03-09 13:28:55 +00:00
|
|
|
// genResource handles the generation of instantiations of non-builtin resources.
|
|
|
|
func (g *generator) genComponent(w io.Writer, component *pcl.Component) {
|
2023-07-27 13:15:27 +00:00
|
|
|
componentName := component.DeclarationName()
|
2023-03-09 13:28:55 +00:00
|
|
|
|
|
|
|
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())
|
|
|
|
}
|
|
|
|
|
2023-07-14 22:15:28 +00:00
|
|
|
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
|
|
|
|
2023-07-17 22:26:28 +00:00
|
|
|
var elementTypes []string
|
|
|
|
for _, propertyName := range attributeKeys {
|
2023-07-14 22:15:28 +00:00
|
|
|
propertyType := complexType.Properties[propertyName]
|
2023-07-17 22:26:28 +00:00
|
|
|
elementType := fmt.Sprintf("%s?: %s", propertyName, computeConfigTypeParam(propertyType))
|
|
|
|
elementTypes = append(elementTypes, elementType)
|
2023-07-14 22:15:28 +00:00
|
|
|
}
|
2023-07-17 22:26:28 +00:00
|
|
|
|
|
|
|
return fmt.Sprintf("{%s}", strings.Join(elementTypes, ", "))
|
2023-07-14 22:15:28 +00:00
|
|
|
default:
|
|
|
|
return "any"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-04-03 06:27:05 +00:00
|
|
|
|
2023-07-14 22:15:28 +00:00
|
|
|
func (g *generator) genConfigVariable(w io.Writer, v *pcl.ConfigVariable) {
|
2020-04-21 17:24:42 +00:00
|
|
|
if !g.configCreated {
|
|
|
|
g.Fprintf(w, "%sconst config = new pulumi.Config();\n", g.Indent)
|
|
|
|
g.configCreated = true
|
|
|
|
}
|
|
|
|
|
2020-04-03 06:27:05 +00:00
|
|
|
getType := "Object"
|
2023-07-14 22:15:28 +00:00
|
|
|
switch pcl.UnwrapOption(v.Type()) {
|
2020-04-03 06:27:05 +00:00
|
|
|
case model.StringType:
|
|
|
|
getType = ""
|
|
|
|
case model.NumberType, model.IntType:
|
|
|
|
getType = "Number"
|
|
|
|
case model.BoolType:
|
|
|
|
getType = "Boolean"
|
|
|
|
}
|
|
|
|
|
2023-07-14 22:15:28 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-03 06:27:05 +00:00
|
|
|
getOrRequire := "get"
|
2023-07-14 22:15:28 +00:00
|
|
|
if v.DefaultValue == nil && !model.IsOptionalType(v.Type()) {
|
2020-04-03 06:27:05 +00:00
|
|
|
getOrRequire = "require"
|
|
|
|
}
|
|
|
|
|
2023-03-21 14:01:16 +00:00
|
|
|
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())
|
2023-07-14 22:15:28 +00:00
|
|
|
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()) {
|
2022-04-18 09:03:42 +00:00
|
|
|
g.Fgenf(w, " || %.v", g.lowerExpression(v.DefaultValue, v.DefaultValue.Type()))
|
2020-04-03 06:27:05 +00:00
|
|
|
}
|
|
|
|
g.Fgenf(w, ";\n")
|
|
|
|
}
|
|
|
|
|
2021-09-30 03:11:56 +00:00
|
|
|
func (g *generator) genLocalVariable(w io.Writer, v *pcl.LocalVariable) {
|
2024-02-11 17:02:12 +00:00
|
|
|
g.genTrivia(w, v.Definition.Tokens.Name)
|
2022-04-18 09:03:42 +00:00
|
|
|
g.Fgenf(w, "%sconst %s = %.3v;\n", g.Indent, v.Name(), g.lowerExpression(v.Definition.Value, v.Type()))
|
2020-04-03 06:27:05 +00:00
|
|
|
}
|
|
|
|
|
2021-09-30 03:11:56 +00:00
|
|
|
func (g *generator) genOutputVariable(w io.Writer, v *pcl.OutputVariable) {
|
2020-05-04 22:04:35 +00:00
|
|
|
if g.asyncMain {
|
2023-06-07 00:37:40 +00:00
|
|
|
// 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
|
2020-05-04 22:04:35 +00:00
|
|
|
}
|
2023-06-07 00:37:40 +00:00
|
|
|
|
|
|
|
// TODO(pdg): trivia
|
|
|
|
g.Fgenf(w, "%sexport const %s = %.3v;\n", g.Indent,
|
2022-04-18 09:03:42 +00:00
|
|
|
makeValidIdentifier(v.Name()), g.lowerExpression(v.Value, v.Type()))
|
2020-04-03 06:27:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (g *generator) genNYI(w io.Writer, reason string, vs ...interface{}) {
|
2023-12-12 12:19:42 +00:00
|
|
|
message := "not yet implemented: " + fmt.Sprintf(reason, vs...)
|
2020-06-10 17:21:53 +00:00
|
|
|
g.diagnostics = append(g.diagnostics, &hcl.Diagnostic{
|
|
|
|
Severity: hcl.DiagError,
|
|
|
|
Summary: message,
|
|
|
|
Detail: message,
|
|
|
|
})
|
2020-04-03 06:27:05 +00:00
|
|
|
g.Fgenf(w, "(() => throw new Error(%q))()", fmt.Sprintf(reason, vs...))
|
|
|
|
}
|