pulumi/pkg/testing/integration/program.go

2677 lines
86 KiB
Go
Raw Normal View History

2018-05-22 19:43:36 +00:00
// Copyright 2016-2018, 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 integration
import (
"context"
cryptorand "crypto/rand"
sha256 "crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"flag"
"fmt"
"hash/fnv"
"io"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"strconv"
"strings"
"testing"
"time"
multierror "github.com/hashicorp/go-multierror"
"golang.org/x/mod/modfile"
"golang.org/x/mod/module"
"gopkg.in/yaml.v3"
"github.com/pulumi/pulumi/pkg/v3/engine"
"github.com/pulumi/pulumi/pkg/v3/operations"
"github.com/pulumi/pulumi/pkg/v3/resource/stack"
"github.com/pulumi/pulumi/sdk/v3/go/common/apitype"
"github.com/pulumi/pulumi/sdk/v3/go/common/env"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/config"
Fix merge failures #2 (#15543) <!--- 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/15528 See https://github.com/pulumi/pulumi/pull/15540 & https://github.com/pulumi/pulumi/pull/15531 Re-creating this as a PR with `ci/test` label so we can get it merged. ## 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. --> - [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: Justin Van Patten <jvp@justinvp.com> Co-authored-by: Anton Tayanovskyy <anton@pulumi.com> Co-authored-by: Thomas Gummerer <t.gummerer@gmail.com>
2024-02-29 21:06:24 +00:00
ptesting "github.com/pulumi/pulumi/sdk/v3/go/common/testing"
"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
"github.com/pulumi/pulumi/sdk/v3/go/common/tools"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/fsutil"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/retry"
"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"
"github.com/pulumi/pulumi/sdk/v3/nodejs/npm"
"github.com/stretchr/testify/assert"
user "github.com/tweekmonster/luser"
)
const (
PythonRuntime = "python"
NodeJSRuntime = "nodejs"
GoRuntime = "go"
DotNetRuntime = "dotnet"
YAMLRuntime = "yaml"
JavaRuntime = "java"
)
const windowsOS = "windows"
[Test] Eager cancelation for ProgramTester (#14126) <!--- 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 PR changes the behavior of `ProgramTester` in `pkg/testing/integration` package, to detect test failures more eagerly and then to skip remaining steps. Specifically it checks whether the test been marked as failed by a validation function (as defined by [`t.Failed()`](https://pkg.go.dev/testing#T.Failed)). The rationale is that, for most integration tests, each step assumes that the previous step was successful. It is simply noisy to continue with the subsequent steps. A workaround is to call `t.FailNow()` in the validation function (or use `require`). An option is provided `ExpectTestFailure` to continue with steps after test failure (as is the current behavior). There exists an option `ExpectFailures` to continue with steps after a pulumi deployment failure. I took such failures to be orthogonal to test failure. ## 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. --> - [ ] 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. --> ## Example Here's a snippet of output in the event that a validation function marks the test as failed (e.g. using an assertion). ``` ... /Users/eronwright/Pulumi/pulumi/tests/examples/program.go:1742: Performing extra runtime validation. /Users/eronwright/Pulumi/pulumi/tests/examples/examples_test.go:44: Error Trace: ... Error: Expected nil, but got ... Test: TestAccMinimal /Users/eronwright/Pulumi/pulumi/tests/examples/program.go:1744: Extra runtime validation complete. /Users/eronwright/Pulumi/pulumi/tests/examples/program.go:1164: Canceling further steps due to test failure /Users/eronwright/Pulumi/pulumi/tests/examples/program.go:1349: Destroying stack ... /Users/eronwright/Pulumi/pulumi/tests/examples/program.go:1365: Test failed, retaining stack '...' --- FAIL: TestAccMinimal (5.61s) ```
2023-10-11 08:07:44 +00:00
var ErrTestFailed = errors.New("test failed")
// RuntimeValidationStackInfo contains details related to the stack that runtime validation logic may want to use.
type RuntimeValidationStackInfo struct {
StackName tokens.QName
Implement more precise delete-before-replace semantics. (#2369) This implements the new algorithm for deciding which resources must be deleted due to a delete-before-replace operation. We need to compute the set of resources that may be replaced by a change to the resource under consideration. We do this by taking the complete set of transitive dependents on the resource under consideration and removing any resources that would not be replaced by changes to their dependencies. We determine whether or not a resource may be replaced by substituting unknowns for input properties that may change due to deletion of the resources their value depends on and calling the resource provider's Diff method. This is perhaps clearer when described by example. Consider the following dependency graph: A __|__ B C | _|_ D E F In this graph, all of B, C, D, E, and F transitively depend on A. It may be the case, however, that changes to the specific properties of any of those resources R that would occur if a resource on the path to A were deleted and recreated may not cause R to be replaced. For example, the edge from B to A may be a simple dependsOn edge such that a change to B does not actually influence any of B's input properties. In that case, neither B nor D would need to be deleted before A could be deleted. In order to make the above algorithm a reality, the resource monitor interface has been updated to include a map that associates an input property key with the list of resources that input property depends on. Older clients of the resource monitor will leave this map empty, in which case all input properties will be treated as depending on all dependencies of the resource. This is probably overly conservative, but it is less conservative than what we currently implement, and is certainly correct.
2019-01-28 17:46:30 +00:00
Deployment *apitype.DeploymentV3
RootResource apitype.ResourceV3
Outputs map[string]interface{}
Events []apitype.EngineEvent
}
// EditDir is an optional edit to apply to the example, as subsequent deployments.
type EditDir struct {
Dir string
ExtraRuntimeValidation func(t *testing.T, stack RuntimeValidationStackInfo)
// Additive is true if Dir should be copied *on top* of the test directory.
// Otherwise Dir *replaces* the test directory, except we keep .pulumi/ and Pulumi.yaml and Pulumi.<stack>.yaml.
Additive bool
// ExpectFailure is true if we expect this test to fail. This is very coarse grained, and will essentially
// tolerate *any* failure in the program (IDEA: in the future, offer a way to narrow this down more).
ExpectFailure bool
Support aliases for renaming, re-typing, or re-parenting resources (#2774) Adds a new resource option `aliases` which can be used to rename a resource. When making a breaking change to the name or type of a resource or component, the old name can be added to the list of `aliases` for a resource to ensure that existing resources will be migrated to the new name instead of being deleted and replaced with the new named resource. There are two key places this change is implemented. The first is the step generator in the engine. When computing whether there is an old version of a registered resource, we now take into account the aliases specified on the registered resource. That is, we first look up the resource by its new URN in the old state, and then by any aliases provided (in order). This can allow the resource to be matched as a (potential) update to an existing resource with a different URN. The second is the core `Resource` constructor in the JavaScript (and soon Python) SDKs. This change ensures that when a parent resource is aliased, that all children implicitly inherit corresponding aliases. It is similar to how many other resource options are "inherited" implicitly from the parent. Four specific scenarios are explicitly tested as part of this PR: 1. Renaming a resource 2. Adopting a resource into a component (as the owner of both component and consumption codebases) 3. Renaming a component instance (as the owner of the consumption codebase without changes to the component) 4. Changing the type of a component (as the owner of the component codebase without changes to the consumption codebase) 4. Combining (1) and (3) to make both changes to a resource at the same time
2019-06-01 06:01:01 +00:00
// ExpectNoChanges is true if the edit is expected to not propose any changes.
ExpectNoChanges bool
// Stdout is the writer to use for all stdout messages.
Stdout io.Writer
// Stderr is the writer to use for all stderr messages.
Stderr io.Writer
// Verbose may be set to true to print messages as they occur, rather than buffering and showing upon failure.
Verbose bool
// Run program directory in query mode.
QueryMode bool
}
// TestCommandStats is a collection of data related to running a single command during a test.
type TestCommandStats struct {
// StartTime is the time at which the command was started
StartTime string `json:"startTime"`
// EndTime is the time at which the command exited
EndTime string `json:"endTime"`
// ElapsedSeconds is the time at which the command exited
ElapsedSeconds float64 `json:"elapsedSeconds"`
// StackName is the name of the stack
StackName string `json:"stackName"`
// TestId is the unique ID of the test run
TestID string `json:"testId"`
// StepName is the command line which was invoked
StepName string `json:"stepName"`
// CommandLine is the command line which was invoked
CommandLine string `json:"commandLine"`
// TestName is the name of the directory in which the test was executed
TestName string `json:"testName"`
// IsError is true if the command failed
IsError bool `json:"isError"`
// The Cloud that the test was run against, or empty for local deployments
CloudURL string `json:"cloudURL"`
}
// TestStatsReporter reports results and metadata from a test run.
type TestStatsReporter interface {
ReportCommand(stats TestCommandStats)
}
// Environment is used to create environments for use by test programs.
type Environment struct {
// The name of the environment.
Name string
// The definition of the environment.
Definition map[string]any
}
Support lists and maps in config (#3342) This change adds support for lists and maps in config. We now allow lists/maps (and nested structures) in `Pulumi.<stack>.yaml` (or `Pulumi.<stack>.json`; yes, we currently support that). For example: ```yaml config: proj:blah: - a - b - c proj:hello: world proj:outer: inner: value proj:servers: - port: 80 ``` While such structures could be specified in the `.yaml` file manually, we support setting values in maps/lists from the command line. As always, you can specify single values with: ```shell $ pulumi config set hello world ``` Which results in the following YAML: ```yaml proj:hello world ``` And single value secrets via: ```shell $ pulumi config set --secret token shhh ``` Which results in the following YAML: ```yaml proj:token: secure: v1:VZAhuroR69FkEPTk:isKafsoZVMWA9pQayGzbWNynww== ``` Values in a list can be set from the command line using the new `--path` flag, which indicates the config key contains a path to a property in a map or list: ```shell $ pulumi config set --path names[0] a $ pulumi config set --path names[1] b $ pulumi config set --path names[2] c ``` Which results in: ```yaml proj:names - a - b - c ``` Values can be obtained similarly: ```shell $ pulumi config get --path names[1] b ``` Or setting values in a map: ```shell $ pulumi config set --path outer.inner value ``` Which results in: ```yaml proj:outer: inner: value ``` Of course, setting values in nested structures is supported: ```shell $ pulumi config set --path servers[0].port 80 ``` Which results in: ```yaml proj:servers: - port: 80 ``` If you want to include a period in the name of a property, it can be specified as: ``` $ pulumi config set --path 'nested["foo.bar"]' baz ``` Which results in: ```yaml proj:nested: foo.bar: baz ``` Examples of valid paths: - root - root.nested - 'root["nested"]' - root.double.nest - 'root["double"].nest' - 'root["double"]["nest"]' - root.array[0] - root.array[100] - root.array[0].nested - root.array[0][1].nested - root.nested.array[0].double[1] - 'root["key with \"escaped\" quotes"]' - 'root["key with a ."]' - '["root key with \"escaped\" quotes"].nested' - '["root key with a ."][100]' Note: paths that contain quotes can be surrounded by single quotes. When setting values with `--path`, if the value is `"false"` or `"true"`, it will be saved as the boolean value, and if it is convertible to an integer, it will be saved as an integer. Secure values are supported in lists/maps as well: ```shell $ pulumi config set --path --secret tokens[0] shh ``` Will result in: ```yaml proj:tokens: - secure: v1:wpZRCe36sFg1RxwG:WzPeQrCn4n+m4Ks8ps15MxvFXg== ``` Note: maps of length 1 with a key of “secure” and string value are reserved for storing secret values. Attempting to create such a value manually will result in an error: ```shell $ pulumi config set --path parent.secure foo error: "secure" key in maps of length 1 are reserved ``` **Accessing config values from the command line with JSON** ```shell $ pulumi config --json ``` Will output: ```json { "proj:hello": { "value": "world", "secret": false, "object": false }, "proj:names": { "value": "[\"a\",\"b\",\"c\"]", "secret": false, "object": true, "objectValue": [ "a", "b", "c" ] }, "proj:nested": { "value": "{\"foo.bar\":\"baz\"}", "secret": false, "object": true, "objectValue": { "foo.bar": "baz" } }, "proj:outer": { "value": "{\"inner\":\"value\"}", "secret": false, "object": true, "objectValue": { "inner": "value" } }, "proj:servers": { "value": "[{\"port\":80}]", "secret": false, "object": true, "objectValue": [ { "port": 80 } ] }, "proj:token": { "secret": true, "object": false }, "proj:tokens": { "secret": true, "object": true } } ``` If the value is a map or list, `"object"` will be `true`. `"value"` will contain the object as serialized JSON and a new `"objectValue"` property will be available containing the value of the object. If the object contains any secret values, `"secret"` will be `true`, and just like with scalar values, the value will not be outputted unless `--show-secrets` is specified. **Accessing config values from Pulumi programs** Map/list values are available to Pulumi programs as serialized JSON, so the existing `getObject`/`requireObject`/`getSecretObject`/`requireSecretObject` functions can be used to retrieve such values, e.g.: ```typescript import * as pulumi from "@pulumi/pulumi"; interface Server { port: number; } const config = new pulumi.Config(); const names = config.requireObject<string[]>("names"); for (const n of names) { console.log(n); } const servers = config.requireObject<Server[]>("servers"); for (const s of servers) { console.log(s.port); } ```
2019-11-01 20:41:27 +00:00
// ConfigValue is used to provide config values to a test program.
type ConfigValue struct {
// The config key to pass to `pulumi config`.
Key string
// The config value to pass to `pulumi config`.
Value string
// Secret indicates that the `--secret` flag should be specified when calling `pulumi config`.
Secret bool
// Path indicates that the `--path` flag should be specified when calling `pulumi config`.
Path bool
}
// ProgramTestOptions provides options for ProgramTest
type ProgramTestOptions struct {
// Dir is the program directory to test.
Dir string
// Array of NPM packages which must be `yarn linked` (e.g. {"pulumi", "@pulumi/aws"})
Dependencies []string
// Map of package names to versions. The test will use the specified versions of these packages instead of what
// is declared in `package.json`.
Overrides map[string]string
// Automatically use the latest dev version of pulumi SDKs if available.
InstallDevReleases bool
// List of environments to create in order.
CreateEnvironments []Environment
// List of environments to use.
Environments []string
Support lists and maps in config (#3342) This change adds support for lists and maps in config. We now allow lists/maps (and nested structures) in `Pulumi.<stack>.yaml` (or `Pulumi.<stack>.json`; yes, we currently support that). For example: ```yaml config: proj:blah: - a - b - c proj:hello: world proj:outer: inner: value proj:servers: - port: 80 ``` While such structures could be specified in the `.yaml` file manually, we support setting values in maps/lists from the command line. As always, you can specify single values with: ```shell $ pulumi config set hello world ``` Which results in the following YAML: ```yaml proj:hello world ``` And single value secrets via: ```shell $ pulumi config set --secret token shhh ``` Which results in the following YAML: ```yaml proj:token: secure: v1:VZAhuroR69FkEPTk:isKafsoZVMWA9pQayGzbWNynww== ``` Values in a list can be set from the command line using the new `--path` flag, which indicates the config key contains a path to a property in a map or list: ```shell $ pulumi config set --path names[0] a $ pulumi config set --path names[1] b $ pulumi config set --path names[2] c ``` Which results in: ```yaml proj:names - a - b - c ``` Values can be obtained similarly: ```shell $ pulumi config get --path names[1] b ``` Or setting values in a map: ```shell $ pulumi config set --path outer.inner value ``` Which results in: ```yaml proj:outer: inner: value ``` Of course, setting values in nested structures is supported: ```shell $ pulumi config set --path servers[0].port 80 ``` Which results in: ```yaml proj:servers: - port: 80 ``` If you want to include a period in the name of a property, it can be specified as: ``` $ pulumi config set --path 'nested["foo.bar"]' baz ``` Which results in: ```yaml proj:nested: foo.bar: baz ``` Examples of valid paths: - root - root.nested - 'root["nested"]' - root.double.nest - 'root["double"].nest' - 'root["double"]["nest"]' - root.array[0] - root.array[100] - root.array[0].nested - root.array[0][1].nested - root.nested.array[0].double[1] - 'root["key with \"escaped\" quotes"]' - 'root["key with a ."]' - '["root key with \"escaped\" quotes"].nested' - '["root key with a ."][100]' Note: paths that contain quotes can be surrounded by single quotes. When setting values with `--path`, if the value is `"false"` or `"true"`, it will be saved as the boolean value, and if it is convertible to an integer, it will be saved as an integer. Secure values are supported in lists/maps as well: ```shell $ pulumi config set --path --secret tokens[0] shh ``` Will result in: ```yaml proj:tokens: - secure: v1:wpZRCe36sFg1RxwG:WzPeQrCn4n+m4Ks8ps15MxvFXg== ``` Note: maps of length 1 with a key of “secure” and string value are reserved for storing secret values. Attempting to create such a value manually will result in an error: ```shell $ pulumi config set --path parent.secure foo error: "secure" key in maps of length 1 are reserved ``` **Accessing config values from the command line with JSON** ```shell $ pulumi config --json ``` Will output: ```json { "proj:hello": { "value": "world", "secret": false, "object": false }, "proj:names": { "value": "[\"a\",\"b\",\"c\"]", "secret": false, "object": true, "objectValue": [ "a", "b", "c" ] }, "proj:nested": { "value": "{\"foo.bar\":\"baz\"}", "secret": false, "object": true, "objectValue": { "foo.bar": "baz" } }, "proj:outer": { "value": "{\"inner\":\"value\"}", "secret": false, "object": true, "objectValue": { "inner": "value" } }, "proj:servers": { "value": "[{\"port\":80}]", "secret": false, "object": true, "objectValue": [ { "port": 80 } ] }, "proj:token": { "secret": true, "object": false }, "proj:tokens": { "secret": true, "object": true } } ``` If the value is a map or list, `"object"` will be `true`. `"value"` will contain the object as serialized JSON and a new `"objectValue"` property will be available containing the value of the object. If the object contains any secret values, `"secret"` will be `true`, and just like with scalar values, the value will not be outputted unless `--show-secrets` is specified. **Accessing config values from Pulumi programs** Map/list values are available to Pulumi programs as serialized JSON, so the existing `getObject`/`requireObject`/`getSecretObject`/`requireSecretObject` functions can be used to retrieve such values, e.g.: ```typescript import * as pulumi from "@pulumi/pulumi"; interface Server { port: number; } const config = new pulumi.Config(); const names = config.requireObject<string[]>("names"); for (const n of names) { console.log(n); } const servers = config.requireObject<Server[]>("servers"); for (const s of servers) { console.log(s.port); } ```
2019-11-01 20:41:27 +00:00
// Map of config keys and values to set (e.g. {"aws:region": "us-east-2"}).
Config map[string]string
Support lists and maps in config (#3342) This change adds support for lists and maps in config. We now allow lists/maps (and nested structures) in `Pulumi.<stack>.yaml` (or `Pulumi.<stack>.json`; yes, we currently support that). For example: ```yaml config: proj:blah: - a - b - c proj:hello: world proj:outer: inner: value proj:servers: - port: 80 ``` While such structures could be specified in the `.yaml` file manually, we support setting values in maps/lists from the command line. As always, you can specify single values with: ```shell $ pulumi config set hello world ``` Which results in the following YAML: ```yaml proj:hello world ``` And single value secrets via: ```shell $ pulumi config set --secret token shhh ``` Which results in the following YAML: ```yaml proj:token: secure: v1:VZAhuroR69FkEPTk:isKafsoZVMWA9pQayGzbWNynww== ``` Values in a list can be set from the command line using the new `--path` flag, which indicates the config key contains a path to a property in a map or list: ```shell $ pulumi config set --path names[0] a $ pulumi config set --path names[1] b $ pulumi config set --path names[2] c ``` Which results in: ```yaml proj:names - a - b - c ``` Values can be obtained similarly: ```shell $ pulumi config get --path names[1] b ``` Or setting values in a map: ```shell $ pulumi config set --path outer.inner value ``` Which results in: ```yaml proj:outer: inner: value ``` Of course, setting values in nested structures is supported: ```shell $ pulumi config set --path servers[0].port 80 ``` Which results in: ```yaml proj:servers: - port: 80 ``` If you want to include a period in the name of a property, it can be specified as: ``` $ pulumi config set --path 'nested["foo.bar"]' baz ``` Which results in: ```yaml proj:nested: foo.bar: baz ``` Examples of valid paths: - root - root.nested - 'root["nested"]' - root.double.nest - 'root["double"].nest' - 'root["double"]["nest"]' - root.array[0] - root.array[100] - root.array[0].nested - root.array[0][1].nested - root.nested.array[0].double[1] - 'root["key with \"escaped\" quotes"]' - 'root["key with a ."]' - '["root key with \"escaped\" quotes"].nested' - '["root key with a ."][100]' Note: paths that contain quotes can be surrounded by single quotes. When setting values with `--path`, if the value is `"false"` or `"true"`, it will be saved as the boolean value, and if it is convertible to an integer, it will be saved as an integer. Secure values are supported in lists/maps as well: ```shell $ pulumi config set --path --secret tokens[0] shh ``` Will result in: ```yaml proj:tokens: - secure: v1:wpZRCe36sFg1RxwG:WzPeQrCn4n+m4Ks8ps15MxvFXg== ``` Note: maps of length 1 with a key of “secure” and string value are reserved for storing secret values. Attempting to create such a value manually will result in an error: ```shell $ pulumi config set --path parent.secure foo error: "secure" key in maps of length 1 are reserved ``` **Accessing config values from the command line with JSON** ```shell $ pulumi config --json ``` Will output: ```json { "proj:hello": { "value": "world", "secret": false, "object": false }, "proj:names": { "value": "[\"a\",\"b\",\"c\"]", "secret": false, "object": true, "objectValue": [ "a", "b", "c" ] }, "proj:nested": { "value": "{\"foo.bar\":\"baz\"}", "secret": false, "object": true, "objectValue": { "foo.bar": "baz" } }, "proj:outer": { "value": "{\"inner\":\"value\"}", "secret": false, "object": true, "objectValue": { "inner": "value" } }, "proj:servers": { "value": "[{\"port\":80}]", "secret": false, "object": true, "objectValue": [ { "port": 80 } ] }, "proj:token": { "secret": true, "object": false }, "proj:tokens": { "secret": true, "object": true } } ``` If the value is a map or list, `"object"` will be `true`. `"value"` will contain the object as serialized JSON and a new `"objectValue"` property will be available containing the value of the object. If the object contains any secret values, `"secret"` will be `true`, and just like with scalar values, the value will not be outputted unless `--show-secrets` is specified. **Accessing config values from Pulumi programs** Map/list values are available to Pulumi programs as serialized JSON, so the existing `getObject`/`requireObject`/`getSecretObject`/`requireSecretObject` functions can be used to retrieve such values, e.g.: ```typescript import * as pulumi from "@pulumi/pulumi"; interface Server { port: number; } const config = new pulumi.Config(); const names = config.requireObject<string[]>("names"); for (const n of names) { console.log(n); } const servers = config.requireObject<Server[]>("servers"); for (const s of servers) { console.log(s.port); } ```
2019-11-01 20:41:27 +00:00
// Map of secure config keys and values to set (e.g. {"aws:region": "us-east-2"}).
Secrets map[string]string
Support lists and maps in config (#3342) This change adds support for lists and maps in config. We now allow lists/maps (and nested structures) in `Pulumi.<stack>.yaml` (or `Pulumi.<stack>.json`; yes, we currently support that). For example: ```yaml config: proj:blah: - a - b - c proj:hello: world proj:outer: inner: value proj:servers: - port: 80 ``` While such structures could be specified in the `.yaml` file manually, we support setting values in maps/lists from the command line. As always, you can specify single values with: ```shell $ pulumi config set hello world ``` Which results in the following YAML: ```yaml proj:hello world ``` And single value secrets via: ```shell $ pulumi config set --secret token shhh ``` Which results in the following YAML: ```yaml proj:token: secure: v1:VZAhuroR69FkEPTk:isKafsoZVMWA9pQayGzbWNynww== ``` Values in a list can be set from the command line using the new `--path` flag, which indicates the config key contains a path to a property in a map or list: ```shell $ pulumi config set --path names[0] a $ pulumi config set --path names[1] b $ pulumi config set --path names[2] c ``` Which results in: ```yaml proj:names - a - b - c ``` Values can be obtained similarly: ```shell $ pulumi config get --path names[1] b ``` Or setting values in a map: ```shell $ pulumi config set --path outer.inner value ``` Which results in: ```yaml proj:outer: inner: value ``` Of course, setting values in nested structures is supported: ```shell $ pulumi config set --path servers[0].port 80 ``` Which results in: ```yaml proj:servers: - port: 80 ``` If you want to include a period in the name of a property, it can be specified as: ``` $ pulumi config set --path 'nested["foo.bar"]' baz ``` Which results in: ```yaml proj:nested: foo.bar: baz ``` Examples of valid paths: - root - root.nested - 'root["nested"]' - root.double.nest - 'root["double"].nest' - 'root["double"]["nest"]' - root.array[0] - root.array[100] - root.array[0].nested - root.array[0][1].nested - root.nested.array[0].double[1] - 'root["key with \"escaped\" quotes"]' - 'root["key with a ."]' - '["root key with \"escaped\" quotes"].nested' - '["root key with a ."][100]' Note: paths that contain quotes can be surrounded by single quotes. When setting values with `--path`, if the value is `"false"` or `"true"`, it will be saved as the boolean value, and if it is convertible to an integer, it will be saved as an integer. Secure values are supported in lists/maps as well: ```shell $ pulumi config set --path --secret tokens[0] shh ``` Will result in: ```yaml proj:tokens: - secure: v1:wpZRCe36sFg1RxwG:WzPeQrCn4n+m4Ks8ps15MxvFXg== ``` Note: maps of length 1 with a key of “secure” and string value are reserved for storing secret values. Attempting to create such a value manually will result in an error: ```shell $ pulumi config set --path parent.secure foo error: "secure" key in maps of length 1 are reserved ``` **Accessing config values from the command line with JSON** ```shell $ pulumi config --json ``` Will output: ```json { "proj:hello": { "value": "world", "secret": false, "object": false }, "proj:names": { "value": "[\"a\",\"b\",\"c\"]", "secret": false, "object": true, "objectValue": [ "a", "b", "c" ] }, "proj:nested": { "value": "{\"foo.bar\":\"baz\"}", "secret": false, "object": true, "objectValue": { "foo.bar": "baz" } }, "proj:outer": { "value": "{\"inner\":\"value\"}", "secret": false, "object": true, "objectValue": { "inner": "value" } }, "proj:servers": { "value": "[{\"port\":80}]", "secret": false, "object": true, "objectValue": [ { "port": 80 } ] }, "proj:token": { "secret": true, "object": false }, "proj:tokens": { "secret": true, "object": true } } ``` If the value is a map or list, `"object"` will be `true`. `"value"` will contain the object as serialized JSON and a new `"objectValue"` property will be available containing the value of the object. If the object contains any secret values, `"secret"` will be `true`, and just like with scalar values, the value will not be outputted unless `--show-secrets` is specified. **Accessing config values from Pulumi programs** Map/list values are available to Pulumi programs as serialized JSON, so the existing `getObject`/`requireObject`/`getSecretObject`/`requireSecretObject` functions can be used to retrieve such values, e.g.: ```typescript import * as pulumi from "@pulumi/pulumi"; interface Server { port: number; } const config = new pulumi.Config(); const names = config.requireObject<string[]>("names"); for (const n of names) { console.log(n); } const servers = config.requireObject<Server[]>("servers"); for (const s of servers) { console.log(s.port); } ```
2019-11-01 20:41:27 +00:00
// List of config keys and values to set in order, including Secret and Path options.
OrderedConfig []ConfigValue
// SecretsProvider is the optional custom secrets provider to use instead of the default.
SecretsProvider string
// EditDirs is an optional list of edits to apply to the example, as subsequent deployments.
EditDirs []EditDir
// ExtraRuntimeValidation is an optional callback for additional validation, called before applying edits.
ExtraRuntimeValidation func(t *testing.T, stack RuntimeValidationStackInfo)
// RelativeWorkDir is an optional path relative to `Dir` which should be used as working directory during tests.
RelativeWorkDir string
// AllowEmptyPreviewChanges is true if we expect that this test's no-op preview may propose changes (e.g.
// because the test is sensitive to the exact contents of its working directory and those contents change
// incidentally between the initial update and the empty update).
AllowEmptyPreviewChanges bool
// AllowEmptyUpdateChanges is true if we expect that this test's no-op update may perform changes (e.g.
// because the test is sensitive to the exact contents of its working directory and those contents change
// incidentally between the initial update and the empty update).
AllowEmptyUpdateChanges bool
// ExpectFailure is true if we expect this test to fail. This is very coarse grained, and will essentially
// tolerate *any* failure in the program (IDEA: in the future, offer a way to narrow this down more).
ExpectFailure bool
// ExpectRefreshChanges may be set to true if a test is expected to have changes yielded by an immediate refresh.
// This could occur, for example, is a resource's state is constantly changing outside of Pulumi (e.g., timestamps).
ExpectRefreshChanges bool
// RetryFailedSteps indicates that failed updates, refreshes, and destroys should be retried after a brief
// intermission. A maximum of 3 retries will be attempted.
RetryFailedSteps bool
// SkipRefresh indicates that the refresh step should be skipped entirely.
SkipRefresh bool
Add RequireEmptyPreviewAfterRefresh option to ProgramTest (#15309) <!--- 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 While working on [this issue in Azure Native](https://github.com/pulumi/pulumi-azure-native/issues/2798), I realized that I don't see how to test the scenario that I care about. I need the following workflow: 1. Run `pulumi up` for the initial deployment. 2. Run `pulumi refresh` to pull changes from API. 3. Run `pulumi preview --expect-no-changes` to guarantee that there is no loop of `refresh/up` stepping over each other. The existing `ExpectRefreshChanges` option is different. If it's set to false, the test would require no changes on step (2) above, which is a stronger requirement. Unfortunately, it's not always true in Azure Native, e.g. when a subresource changes properties of its parent resource. I want to enable "ExpectRefreshChanges: false" where possible, but I also want to require step (3) to succeed for all tests in Azure Native (and potentially everywhere, eventually). I've already found three issues while drafting that. Therefore, this PR proposes a new option for ProgramTest: `RequireEmptyPreviewAfterRefresh`. It defaults to false, so no behavior changes unless users opt in. If set to true, it runs an extra preview after refreshing, i.e. the step (3). Let me know if I missed a better way of achieving the same. ## 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. --> - [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. -->
2024-01-31 09:17:37 +00:00
// Require a preview after refresh to be a no-op (expect no changes). Has no effect if SkipRefresh is true.
RequireEmptyPreviewAfterRefresh bool
// SkipPreview indicates that the preview step should be skipped entirely.
SkipPreview bool
// SkipUpdate indicates that the update step should be skipped entirely.
SkipUpdate bool
// SkipExportImport skips testing that exporting and importing the stack works properly.
SkipExportImport bool
// SkipEmptyPreviewUpdate skips the no-change preview/update that is performed that validates
// that no changes happen.
SkipEmptyPreviewUpdate bool
// SkipStackRemoval indicates that the stack should not be removed. (And so the test's results could be inspected
// in the Pulumi Service after the test has completed.)
SkipStackRemoval bool
// Destroy on cleanup defers stack destruction until the test cleanup step, rather than after
// program test execution. This is useful for more realistic stack reference testing, allowing one
// project and stack to be stood up and a second to be run before the first is destroyed.
//
// Implies NoParallel because we expect that another caller to ProgramTest will set that
DestroyOnCleanup bool
// DestroyExcludeProtected indicates that when the test stack is destroyed,
// protected resources should be excluded from the destroy operation.
DestroyExcludeProtected bool
// Quick implies SkipPreview, SkipExportImport and SkipEmptyPreviewUpdate
Quick bool
// RequireService indicates that the test must be run against the Pulumi Service
RequireService bool
// PreviewCommandlineFlags specifies flags to add to the `pulumi preview` command line (e.g. "--color=raw")
PreviewCommandlineFlags []string
// UpdateCommandlineFlags specifies flags to add to the `pulumi up` command line (e.g. "--color=raw")
UpdateCommandlineFlags []string
// QueryCommandlineFlags specifies flags to add to the `pulumi query` command line (e.g. "--color=raw")
QueryCommandlineFlags []string
// RunBuild indicates that the build step should be run (e.g. run `yarn build` for `nodejs` programs)
RunBuild bool
// RunUpdateTest will ensure that updates to the package version can test for spurious diffs
RunUpdateTest bool
// DecryptSecretsInOutput will ensure that stack output is passed `--show-secrets` parameter
// Used in conjunction with ExtraRuntimeValidation
DecryptSecretsInOutput bool
// CloudURL is an optional URL to override the default Pulumi Service API (https://api.pulumi-staging.io). The
// PULUMI_ACCESS_TOKEN environment variable must also be set to a valid access token for the target cloud.
CloudURL string
// StackName allows the stack name to be explicitly provided instead of computed from the
// environment during tests.
StackName string
// If non-empty, specifies the value of the `--tracing` flag to pass
// to Pulumi CLI, which may be a Zipkin endpoint or a
// `file:./local.trace` style url for AppDash tracing.
//
// Template `{command}` syntax will be expanded to the current
// command name such as `pulumi-stack-rm`. This is useful for
// file-based tracing since `ProgramTest` performs multiple
// CLI invocations that can inadvertently overwrite the trace
// file.
Tracing string
// NoParallel will opt the test out of being ran in parallel.
NoParallel bool
// PrePulumiCommand specifies a callback that will be executed before each `pulumi` invocation. This callback may
// optionally return another callback to be invoked after the `pulumi` invocation completes.
PrePulumiCommand func(verb string) (func(err error) error, error)
// ReportStats optionally specifies how to report results from the test for external collection.
ReportStats TestStatsReporter
// Stdout is the writer to use for all stdout messages.
Stdout io.Writer
// Stderr is the writer to use for all stderr messages.
Stderr io.Writer
// Verbose may be set to true to print messages as they occur, rather than buffering and showing upon failure.
Verbose bool
2022-09-20 15:58:12 +00:00
// DebugLogging may be set to anything >0 to enable excessively verbose debug logging from `pulumi`. This
// is equivalent to `--logflow --logtostderr -v=N`, where N is the value of DebugLogLevel. This may also
// be enabled by setting the environment variable PULUMI_TEST_DEBUG_LOG_LEVEL.
DebugLogLevel int
// DebugUpdates may be set to true to enable debug logging from `pulumi preview`, `pulumi up`, and
// `pulumi destroy`. This may also be enabled by setting the environment variable PULUMI_TEST_DEBUG_UPDATES.
DebugUpdates bool
// Bin is a location of a `pulumi` executable to be run. Taken from the $PATH if missing.
Bin string
// YarnBin is a location of a `yarn` executable to be run. Taken from the $PATH if missing.
YarnBin string
// GoBin is a location of a `go` executable to be run. Taken from the $PATH if missing.
GoBin string
// PythonBin is a location of a `python` executable to be run. Taken from the $PATH if missing.
PythonBin string
// PipenvBin is a location of a `pipenv` executable to run. Taken from the $PATH if missing.
PipenvBin string
// DotNetBin is a location of a `dotnet` executable to be run. Taken from the $PATH if missing.
DotNetBin string
// Additional environment variables to pass for each command we run.
Env []string
// Automatically create and use a virtual environment, rather than using the Pipenv tool. This is now the default
// behavior, so this option no longer has any affect. To go back to the old behavior use the `UsePipenv` option.
UseAutomaticVirtualEnv bool
// Use the Pipenv tool to manage the virtual environment.
UsePipenv bool
// Use a shared virtual environment for tests based on the contents of the requirements file. Defaults to false.
UseSharedVirtualEnv *bool
// Shared venv path when UseSharedVirtualEnv is true. Defaults to $HOME/.pulumi-test-venvs.
SharedVirtualEnvPath string
// Refers to the shared venv directory when UseSharedVirtualEnv is true. Otherwise defaults to venv
virtualEnvDir string
// If set, this hook is called after the `pulumi preview` command has completed.
PreviewCompletedHook func(dir string) error
// JSONOutput indicates that the `--json` flag should be passed to `up`, `preview`,
// `refresh` and `destroy` commands.
JSONOutput bool
// If set, this hook is called after `pulumi stack export` on the exported file. If `SkipExportImport` is set, this
// hook is ignored.
ExportStateValidator func(t *testing.T, stack []byte)
// If not nil, specifies the logic of preparing a project by
// ensuring dependencies. If left as nil, runs default
// preparation logic by dispatching on whether the project
// uses Node, Python, .NET or Go.
PrepareProject func(*engine.Projinfo) error
2023-02-28 17:06:15 +00:00
// If not nil, will be run after the project has been prepared.
PostPrepareProject func(*engine.Projinfo) error
// Array of provider plugin dependencies which come from local packages.
LocalProviders []LocalDependency
}
func (opts *ProgramTestOptions) GetUseSharedVirtualEnv() bool {
if opts.UseSharedVirtualEnv != nil {
return *opts.UseSharedVirtualEnv
}
return false
}
type LocalDependency struct {
Package string
Path string
}
func (opts *ProgramTestOptions) GetDebugLogLevel() int {
if opts.DebugLogLevel > 0 {
return opts.DebugLogLevel
}
if du := os.Getenv("PULUMI_TEST_DEBUG_LOG_LEVEL"); du != "" {
2018-03-29 22:15:52 +00:00
if n, e := strconv.Atoi(du); e != nil {
panic(e)
} else if n > 0 {
return n
}
}
return 0
}
func (opts *ProgramTestOptions) GetDebugUpdates() bool {
return opts.DebugUpdates || os.Getenv("PULUMI_TEST_DEBUG_UPDATES") != ""
}
// GetStackName returns a stack name to use for this test.
func (opts *ProgramTestOptions) GetStackName() tokens.QName {
if opts.StackName == "" {
// Fetch the host and test dir names, cleaned so to contain just [a-zA-Z0-9-_] chars.
hostname, err := os.Hostname()
contract.AssertNoErrorf(err, "failure to fetch hostname for stack prefix")
var host string
for _, c := range hostname {
if len(host) >= 10 {
break
}
if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') || c == '-' || c == '_' {
host += string(c)
}
}
var test string
for _, c := range filepath.Base(opts.Dir) {
if len(test) >= 10 {
break
}
if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') || c == '-' || c == '_' {
test += string(c)
}
}
b := make([]byte, 4)
_, err = cryptorand.Read(b)
contract.AssertNoErrorf(err, "failure to generate random stack suffix")
opts.StackName = strings.ToLower("p-it-" + host + "-" + test + "-" + hex.EncodeToString(b))
}
return tokens.QName(opts.StackName)
}
// getEnvName returns the uniquified name for the given environment. The name is made unique by appending the FNV hash
// of the associated stack's name. This ensures that the name is both unique and deterministic. The name must be
// deterministic because it is computed by both LifeCycleInitialize and TestLifeCycleDestroy.
func (opts *ProgramTestOptions) getEnvName(name string) string {
h := fnv.New32()
_, err := h.Write([]byte(opts.GetStackName()))
contract.IgnoreError(err)
suffix := hex.EncodeToString(h.Sum(nil))
return fmt.Sprintf("%v-%v", name, suffix)
}
func (opts *ProgramTestOptions) getEnvNameWithOwner(name string) string {
owner := os.Getenv("PULUMI_TEST_OWNER")
if opts.RequireService && owner != "" {
return fmt.Sprintf("%v/%v", owner, opts.getEnvName(name))
}
return opts.getEnvName(name)
}
// Returns the md5 hash of the file at the given path as a string
func hashFile(path string) (string, error) {
file, err := os.Open(path)
if err != nil {
return "", err
}
defer file.Close()
buf := make([]byte, 32*1024)
hash := sha256.New()
for {
n, err := file.Read(buf)
if n > 0 {
_, err := hash.Write(buf[:n])
if err != nil {
return "", err
}
}
if err == io.EOF {
break
}
if err != nil {
return "", err
}
}
sum := string(hash.Sum(nil))
return sum, nil
}
// GetStackNameWithOwner gets the name of the stack prepended with an owner, if PULUMI_TEST_OWNER is set.
// We use this in CI to create test stacks in an organization that all developers have access to, for debugging.
func (opts *ProgramTestOptions) GetStackNameWithOwner() tokens.QName {
owner := os.Getenv("PULUMI_TEST_OWNER")
if opts.RequireService && owner != "" {
return tokens.QName(fmt.Sprintf("%s/%s", owner, opts.GetStackName()))
}
return opts.GetStackName()
}
// With combines a source set of options with a set of overrides.
func (opts ProgramTestOptions) With(overrides ProgramTestOptions) ProgramTestOptions {
if overrides.Dir != "" {
opts.Dir = overrides.Dir
}
if overrides.Dependencies != nil {
opts.Dependencies = overrides.Dependencies
}
if overrides.Overrides != nil {
opts.Overrides = overrides.Overrides
}
if overrides.InstallDevReleases {
opts.InstallDevReleases = overrides.InstallDevReleases
}
if len(overrides.CreateEnvironments) != 0 {
opts.CreateEnvironments = append(opts.CreateEnvironments, overrides.CreateEnvironments...)
}
if len(overrides.Environments) != 0 {
opts.Environments = append(opts.Environments, overrides.Environments...)
}
for k, v := range overrides.Config {
if opts.Config == nil {
opts.Config = make(map[string]string)
}
opts.Config[k] = v
}
for k, v := range overrides.Secrets {
if opts.Secrets == nil {
opts.Secrets = make(map[string]string)
}
opts.Secrets[k] = v
}
if overrides.OrderedConfig != nil {
opts.OrderedConfig = append(opts.OrderedConfig, overrides.OrderedConfig...)
}
if overrides.SecretsProvider != "" {
opts.SecretsProvider = overrides.SecretsProvider
}
if overrides.EditDirs != nil {
opts.EditDirs = overrides.EditDirs
}
if overrides.ExtraRuntimeValidation != nil {
opts.ExtraRuntimeValidation = overrides.ExtraRuntimeValidation
}
if overrides.RelativeWorkDir != "" {
opts.RelativeWorkDir = overrides.RelativeWorkDir
}
if overrides.AllowEmptyPreviewChanges {
opts.AllowEmptyPreviewChanges = overrides.AllowEmptyPreviewChanges
}
if overrides.AllowEmptyUpdateChanges {
opts.AllowEmptyUpdateChanges = overrides.AllowEmptyUpdateChanges
}
if overrides.ExpectFailure {
opts.ExpectFailure = overrides.ExpectFailure
}
if overrides.ExpectRefreshChanges {
opts.ExpectRefreshChanges = overrides.ExpectRefreshChanges
}
2020-01-29 19:57:45 +00:00
if overrides.RetryFailedSteps {
opts.RetryFailedSteps = overrides.RetryFailedSteps
}
if overrides.SkipRefresh {
opts.SkipRefresh = overrides.SkipRefresh
}
Add RequireEmptyPreviewAfterRefresh option to ProgramTest (#15309) <!--- 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 While working on [this issue in Azure Native](https://github.com/pulumi/pulumi-azure-native/issues/2798), I realized that I don't see how to test the scenario that I care about. I need the following workflow: 1. Run `pulumi up` for the initial deployment. 2. Run `pulumi refresh` to pull changes from API. 3. Run `pulumi preview --expect-no-changes` to guarantee that there is no loop of `refresh/up` stepping over each other. The existing `ExpectRefreshChanges` option is different. If it's set to false, the test would require no changes on step (2) above, which is a stronger requirement. Unfortunately, it's not always true in Azure Native, e.g. when a subresource changes properties of its parent resource. I want to enable "ExpectRefreshChanges: false" where possible, but I also want to require step (3) to succeed for all tests in Azure Native (and potentially everywhere, eventually). I've already found three issues while drafting that. Therefore, this PR proposes a new option for ProgramTest: `RequireEmptyPreviewAfterRefresh`. It defaults to false, so no behavior changes unless users opt in. If set to true, it runs an extra preview after refreshing, i.e. the step (3). Let me know if I missed a better way of achieving the same. ## 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. --> - [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. -->
2024-01-31 09:17:37 +00:00
if overrides.RequireEmptyPreviewAfterRefresh {
opts.RequireEmptyPreviewAfterRefresh = overrides.RequireEmptyPreviewAfterRefresh
}
if overrides.SkipPreview {
opts.SkipPreview = overrides.SkipPreview
}
if overrides.SkipUpdate {
opts.SkipUpdate = overrides.SkipUpdate
}
if overrides.SkipExportImport {
opts.SkipExportImport = overrides.SkipExportImport
}
if overrides.SkipEmptyPreviewUpdate {
opts.SkipEmptyPreviewUpdate = overrides.SkipEmptyPreviewUpdate
}
if overrides.SkipStackRemoval {
opts.SkipStackRemoval = overrides.SkipStackRemoval
}
if overrides.DestroyOnCleanup {
opts.DestroyOnCleanup = overrides.DestroyOnCleanup
}
if overrides.DestroyExcludeProtected {
opts.DestroyExcludeProtected = overrides.DestroyExcludeProtected
}
if overrides.Quick {
opts.Quick = overrides.Quick
}
if overrides.RequireService {
opts.RequireService = overrides.RequireService
}
if overrides.PreviewCommandlineFlags != nil {
opts.PreviewCommandlineFlags = append(opts.PreviewCommandlineFlags, overrides.PreviewCommandlineFlags...)
}
if overrides.UpdateCommandlineFlags != nil {
opts.UpdateCommandlineFlags = append(opts.UpdateCommandlineFlags, overrides.UpdateCommandlineFlags...)
}
if overrides.QueryCommandlineFlags != nil {
opts.QueryCommandlineFlags = append(opts.QueryCommandlineFlags, overrides.QueryCommandlineFlags...)
}
if overrides.RunBuild {
opts.RunBuild = overrides.RunBuild
}
if overrides.RunUpdateTest {
opts.RunUpdateTest = overrides.RunUpdateTest
}
if overrides.DecryptSecretsInOutput {
opts.DecryptSecretsInOutput = overrides.DecryptSecretsInOutput
}
if overrides.CloudURL != "" {
opts.CloudURL = overrides.CloudURL
}
if overrides.StackName != "" {
opts.StackName = overrides.StackName
}
if overrides.Tracing != "" {
opts.Tracing = overrides.Tracing
}
if overrides.NoParallel {
opts.NoParallel = overrides.NoParallel
}
if overrides.PrePulumiCommand != nil {
opts.PrePulumiCommand = overrides.PrePulumiCommand
}
if overrides.ReportStats != nil {
opts.ReportStats = overrides.ReportStats
}
if overrides.Stdout != nil {
opts.Stdout = overrides.Stdout
}
if overrides.Stderr != nil {
opts.Stderr = overrides.Stderr
}
if overrides.Verbose {
opts.Verbose = overrides.Verbose
}
if overrides.DebugLogLevel != 0 {
opts.DebugLogLevel = overrides.DebugLogLevel
}
if overrides.DebugUpdates {
opts.DebugUpdates = overrides.DebugUpdates
}
if overrides.Bin != "" {
opts.Bin = overrides.Bin
}
if overrides.YarnBin != "" {
opts.YarnBin = overrides.YarnBin
}
if overrides.GoBin != "" {
opts.GoBin = overrides.GoBin
}
if overrides.PipenvBin != "" {
opts.PipenvBin = overrides.PipenvBin
}
if overrides.DotNetBin != "" {
opts.DotNetBin = overrides.DotNetBin
}
if overrides.Env != nil {
opts.Env = append(opts.Env, overrides.Env...)
}
if overrides.UseAutomaticVirtualEnv {
opts.UseAutomaticVirtualEnv = overrides.UseAutomaticVirtualEnv
}
if overrides.UsePipenv {
opts.UsePipenv = overrides.UsePipenv
}
if overrides.UseSharedVirtualEnv != nil {
opts.UseSharedVirtualEnv = overrides.UseSharedVirtualEnv
}
if overrides.SharedVirtualEnvPath != "" {
opts.SharedVirtualEnvPath = overrides.SharedVirtualEnvPath
}
if overrides.PreviewCompletedHook != nil {
opts.PreviewCompletedHook = overrides.PreviewCompletedHook
}
if overrides.JSONOutput {
opts.JSONOutput = overrides.JSONOutput
}
if overrides.ExportStateValidator != nil {
opts.ExportStateValidator = overrides.ExportStateValidator
}
if overrides.PrepareProject != nil {
opts.PrepareProject = overrides.PrepareProject
}
[ProgramTest] Fix for PostPrepareProject option (#14742) <!--- 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 This PR fixes a small problem with ProgramTest, that the `With` function doesn't propagate the `PostPrepareProject` option as it should. e.g. ```go options := baseOptions.With(integration.ProgramTestOptions{ PostPrepareProject: func(p *engine.Projinfo) error { // ... }, }) ``` A follow-up to: https://github.com/pulumi/pulumi/pull/12302 ## 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. --> - [ ] 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-04 22:49:38 +00:00
if overrides.PostPrepareProject != nil {
opts.PostPrepareProject = overrides.PostPrepareProject
}
2022-09-21 08:14:06 +00:00
if overrides.LocalProviders != nil {
opts.LocalProviders = append(opts.LocalProviders, overrides.LocalProviders...)
}
return opts
}
type regexFlag struct {
re *regexp.Regexp
}
func (rf *regexFlag) String() string {
if rf.re == nil {
return ""
}
return rf.re.String()
}
func (rf *regexFlag) Set(v string) error {
r, err := regexp.Compile(v)
if err != nil {
return err
}
rf.re = r
return nil
}
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
var (
directoryMatcher regexFlag
listDirs bool
pipMutex *fsutil.FileMutex
)
func init() {
flag.Var(&directoryMatcher, "dirs", "optional list of regexes to use to select integration tests to run")
flag.BoolVar(&listDirs, "list-dirs", false, "list available integration tests without running them")
mutexPath := filepath.Join(os.TempDir(), "pip-mutex.lock")
pipMutex = fsutil.NewFileMutex(mutexPath)
}
2018-11-28 19:46:10 +00:00
// GetLogs retrieves the logs for a given stack in a particular region making the query provided.
//
// [provider] should be one of "aws" or "azure"
func GetLogs(
t *testing.T,
provider, region string,
stackInfo RuntimeValidationStackInfo,
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
query operations.LogQuery,
) *[]operations.LogEntry {
snap, err := stack.DeserializeDeploymentV3(
context.Background(),
*stackInfo.Deployment,
stack.DefaultSecretsProvider)
assert.NoError(t, err)
2018-11-28 19:46:10 +00:00
tree := operations.NewResourceTree(snap.Resources)
2018-11-28 19:46:10 +00:00
if !assert.NotNil(t, tree) {
return nil
}
cfg := map[config.Key]string{
config.MustMakeKey(provider, "region"): region,
}
ops := tree.OperationsProvider(cfg)
// Validate logs from example
logs, err := ops.GetLogs(query)
if !assert.NoError(t, err) {
return nil
}
return logs
}
func prepareProgram(t *testing.T, opts *ProgramTestOptions) {
// If we're just listing tests, simply print this test's directory.
if listDirs {
fmt.Printf("%s\n", opts.Dir)
}
// If we have a matcher, ensure that this test matches its pattern.
if directoryMatcher.re != nil && !directoryMatcher.re.Match([]byte(opts.Dir)) {
t.Skipf("Skipping: '%v' does not match '%v'", opts.Dir, directoryMatcher.re)
}
// Disable stack backups for tests to avoid filling up ~/.pulumi/backups with unnecessary
// backups of test stacks.
disableCheckpointBackups := env.DIYBackendDisableCheckpointBackups.Var().Name()
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
opts.Env = append(opts.Env, disableCheckpointBackups+"=1")
// We want tests to default into being ran in parallel, hence the odd double negative.
if !opts.NoParallel && !opts.DestroyOnCleanup {
t.Parallel()
}
if os.Getenv("PULUMI_TEST_USE_SERVICE") == "true" {
opts.RequireService = true
}
if opts.RequireService {
// This token is set in CI jobs, so this escape hatch is here to enable a smooth local dev
// experience, i.e.: running "make" and not seeing many failures due to a missing token.
if os.Getenv("PULUMI_ACCESS_TOKEN") == "" {
t.Skipf("Skipping: PULUMI_ACCESS_TOKEN is not set")
}
} else if opts.CloudURL == "" {
opts.CloudURL = MakeTempBackend(t)
}
// If the test panics, recover and log instead of letting the panic escape the test. Even though *this* test will
// have run deferred functions and cleaned up, if the panic reaches toplevel it will kill the process and prevent
// other tests running in parallel from cleaning up.
defer func() {
if failure := recover(); failure != nil {
t.Errorf("panic testing %v: %v", opts.Dir, failure)
}
}()
// Set up some default values for sending test reports and tracing data. We use environment varaiables to
// control these globally and set reasonable values for our own use in CI.
if opts.ReportStats == nil {
if v := os.Getenv("PULUMI_TEST_REPORT_CONFIG"); v != "" {
splits := strings.Split(v, ":")
if len(splits) != 3 {
t.Errorf("report config should be set to a value of the form: <aws-region>:<bucket-name>:<keyPrefix>")
}
opts.ReportStats = NewS3Reporter(splits[0], splits[1], splits[2])
}
}
if opts.Tracing == "" {
opts.Tracing = os.Getenv("PULUMI_TEST_TRACE_ENDPOINT")
}
if opts.UseSharedVirtualEnv == nil {
if sharedVenv := os.Getenv("PULUMI_TEST_PYTHON_SHARED_VENV"); sharedVenv != "" {
useSharedVenvBool := sharedVenv == "true"
opts.UseSharedVirtualEnv = &useSharedVenvBool
}
}
if opts.virtualEnvDir == "" && !opts.GetUseSharedVirtualEnv() {
opts.virtualEnvDir = "venv"
}
if opts.SharedVirtualEnvPath == "" {
opts.SharedVirtualEnvPath = filepath.Join(os.Getenv("HOME"), ".pulumi-test-venvs")
if sharedVenvPath := os.Getenv("PULUMI_TEST_PYTHON_SHARED_VENV_PATH"); sharedVenvPath != "" {
opts.SharedVirtualEnvPath = sharedVenvPath
}
}
if opts.Quick {
opts.SkipPreview = true
opts.SkipExportImport = true
opts.SkipEmptyPreviewUpdate = true
}
}
// ProgramTest runs a lifecycle of Pulumi commands in a program working directory, using the `pulumi` and `yarn`
// binaries available on PATH. It essentially executes the following workflow:
//
2022-09-14 02:12:02 +00:00
// yarn install
// yarn link <each opts.Depencies>
// (+) yarn run build
// pulumi init
// (*) pulumi login
// pulumi stack init integrationtesting
// pulumi config set <each opts.Config>
// pulumi config set --secret <each opts.Secrets>
// pulumi preview
// pulumi up
// pulumi stack export --file stack.json
// pulumi stack import --file stack.json
// pulumi preview (expected to be empty)
// pulumi up (expected to be empty)
// pulumi destroy --yes
// pulumi stack rm --yes integrationtesting
//
2022-09-14 02:12:02 +00:00
// (*) Only if PULUMI_ACCESS_TOKEN is set.
// (+) Only if `opts.RunBuild` is true.
//
// All commands must return success return codes for the test to succeed, unless ExpectFailure is true.
func ProgramTest(t *testing.T, opts *ProgramTestOptions) {
prepareProgram(t, opts)
pt := newProgramTester(t, opts)
err := pt.TestLifeCycleInitAndDestroy()
[Test] Eager cancelation for ProgramTester (#14126) <!--- 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 PR changes the behavior of `ProgramTester` in `pkg/testing/integration` package, to detect test failures more eagerly and then to skip remaining steps. Specifically it checks whether the test been marked as failed by a validation function (as defined by [`t.Failed()`](https://pkg.go.dev/testing#T.Failed)). The rationale is that, for most integration tests, each step assumes that the previous step was successful. It is simply noisy to continue with the subsequent steps. A workaround is to call `t.FailNow()` in the validation function (or use `require`). An option is provided `ExpectTestFailure` to continue with steps after test failure (as is the current behavior). There exists an option `ExpectFailures` to continue with steps after a pulumi deployment failure. I took such failures to be orthogonal to test failure. ## 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. --> - [ ] 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. --> ## Example Here's a snippet of output in the event that a validation function marks the test as failed (e.g. using an assertion). ``` ... /Users/eronwright/Pulumi/pulumi/tests/examples/program.go:1742: Performing extra runtime validation. /Users/eronwright/Pulumi/pulumi/tests/examples/examples_test.go:44: Error Trace: ... Error: Expected nil, but got ... Test: TestAccMinimal /Users/eronwright/Pulumi/pulumi/tests/examples/program.go:1744: Extra runtime validation complete. /Users/eronwright/Pulumi/pulumi/tests/examples/program.go:1164: Canceling further steps due to test failure /Users/eronwright/Pulumi/pulumi/tests/examples/program.go:1349: Destroying stack ... /Users/eronwright/Pulumi/pulumi/tests/examples/program.go:1365: Test failed, retaining stack '...' --- FAIL: TestAccMinimal (5.61s) ```
2023-10-11 08:07:44 +00:00
if !errors.Is(err, ErrTestFailed) {
assert.NoError(t, err)
}
}
// ProgramTestManualLifeCycle returns a ProgramTester than must be manually controlled in terms of its lifecycle
func ProgramTestManualLifeCycle(t *testing.T, opts *ProgramTestOptions) *ProgramTester {
prepareProgram(t, opts)
pt := newProgramTester(t, opts)
return pt
}
// ProgramTester contains state associated with running a single test pass.
type ProgramTester struct {
t *testing.T // the Go tester for this run.
opts *ProgramTestOptions // options that control this test run.
bin string // the `pulumi` binary we are using.
yarnBin string // the `yarn` binary we are using.
goBin string // the `go` binary we are using.
pythonBin string // the `python` binary we are using.
pipenvBin string // The `pipenv` binary we are using.
dotNetBin string // the `dotnet` binary we are using.
updateEventLog string // The path to the engine event log for `pulumi up` in this test.
maxStepTries int // The maximum number of times to retry a failed pulumi step.
tmpdir string // the temporary directory we use for our test environment
projdir string // the project directory we use for this run
TestFinished bool // whether or not the test if finished
pulumiHome string // The directory PULUMI_HOME will be set to
2018-01-06 01:40:41 +00:00
}
func newProgramTester(t *testing.T, opts *ProgramTestOptions) *ProgramTester {
stackName := opts.GetStackName()
maxStepTries := 1
if opts.RetryFailedSteps {
maxStepTries = 3
}
home, err := os.MkdirTemp("", "test-env-home")
assert.NoError(t, err, "creating temp PULUMI_HOME directory")
return &ProgramTester{
t: t,
opts: opts,
updateEventLog: filepath.Join(os.TempDir(), string(stackName)+"-events.json"),
maxStepTries: maxStepTries,
pulumiHome: home,
}
}
2022-09-14 21:02:36 +00:00
// MakeTempBackend creates a temporary backend directory which will clean up on test exit.
func MakeTempBackend(t *testing.T) string {
tempDir := t.TempDir()
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 "file://" + filepath.ToSlash(tempDir)
}
func (pt *ProgramTester) getBin() (string, error) {
return getCmdBin(&pt.bin, "pulumi", pt.opts.Bin)
}
func (pt *ProgramTester) getYarnBin() (string, error) {
return getCmdBin(&pt.yarnBin, "yarn", pt.opts.YarnBin)
}
func (pt *ProgramTester) getGoBin() (string, error) {
return getCmdBin(&pt.goBin, "go", pt.opts.GoBin)
}
// getPythonBin returns a path to the currently-installed `python` binary, or an error if it could not be found.
func (pt *ProgramTester) getPythonBin() (string, error) {
if pt.pythonBin == "" {
pt.pythonBin = pt.opts.PythonBin
if pt.opts.PythonBin == "" {
var err error
// Look for `python3` by default, but fallback to `python` if not found, except on Windows
// where we look for these in the reverse order because the default python.org Windows
// installation does not include a `python3` binary, and the existence of a `python3.exe`
// symlink to `python.exe` on some systems does not work correctly with the Python `venv`
// module.
pythonCmds := []string{"python3", "python"}
if runtime.GOOS == windowsOS {
pythonCmds = []string{"python", "python3"}
}
for _, bin := range pythonCmds {
pt.pythonBin, err = exec.LookPath(bin)
// Break on the first cmd we find on the path (if any).
if err == nil {
break
}
}
if err != nil {
return "", fmt.Errorf("Expected to find one of %q on $PATH: %w", pythonCmds, err)
}
}
}
return pt.pythonBin, nil
}
// getPipenvBin returns a path to the currently-installed Pipenv tool, or an error if the tool could not be found.
func (pt *ProgramTester) getPipenvBin() (string, error) {
return getCmdBin(&pt.pipenvBin, "pipenv", pt.opts.PipenvBin)
}
func (pt *ProgramTester) getDotNetBin() (string, error) {
return getCmdBin(&pt.dotNetBin, "dotnet", pt.opts.DotNetBin)
}
func (pt *ProgramTester) pulumiCmd(name string, args []string) ([]string, error) {
bin, err := pt.getBin()
if err != nil {
return nil, err
}
cmd := []string{bin}
if du := pt.opts.GetDebugLogLevel(); du > 0 {
cmd = append(cmd, "--logflow", "--logtostderr", "-v="+strconv.Itoa(du))
}
cmd = append(cmd, args...)
if tracing := pt.opts.Tracing; tracing != "" {
cmd = append(cmd, "--tracing", strings.ReplaceAll(tracing, "{command}", name))
}
return cmd, nil
}
func (pt *ProgramTester) yarnCmd(args []string) ([]string, error) {
bin, err := pt.getYarnBin()
if err != nil {
return nil, err
}
result := []string{bin}
result = append(result, args...)
return withOptionalYarnFlags(result), nil
}
func (pt *ProgramTester) pythonCmd(args []string) ([]string, error) {
bin, err := pt.getPythonBin()
if err != nil {
return nil, err
}
cmd := []string{bin}
return append(cmd, args...), nil
}
func (pt *ProgramTester) pipenvCmd(args []string) ([]string, error) {
bin, err := pt.getPipenvBin()
if err != nil {
return nil, err
}
cmd := []string{bin}
return append(cmd, args...), nil
}
func (pt *ProgramTester) runCommand(name string, args []string, wd string) error {
return RunCommandPulumiHome(pt.t, name, args, wd, pt.opts, pt.pulumiHome)
}
// RunPulumiCommand runs a Pulumi command in the project directory.
// For example:
//
// pt.RunPulumiCommand("preview", "--stack", "dev")
func (pt *ProgramTester) RunPulumiCommand(name string, args ...string) error {
// pt.runPulumiCommand uses 'name' for logging only.
// We want it to be part of the actual command.
args = append([]string{name}, args...)
return pt.runPulumiCommand(name, args, pt.projdir, false /* expectFailure */)
}
func (pt *ProgramTester) runPulumiCommand(name string, args []string, wd string, expectFailure bool) error {
cmd, err := pt.pulumiCmd(name, args)
if err != nil {
return err
}
var postFn func(error) error
if pt.opts.PrePulumiCommand != nil {
postFn, err = pt.opts.PrePulumiCommand(args[0])
if err != nil {
return err
}
}
isUpdate := args[0] == "preview" || args[0] == "up" || args[0] == "destroy" || args[0] == "refresh"
Python Dynamic Providers (#2900) Dynamic providers in Python. This PR uses [dill](https://pypi.org/project/dill/) for code serialization, along with a customization to help ensure deterministic serialization results. One notable limitation - which I believe is a general requirement of Python - is that any serialization of Python functions must serialize byte code, and byte code is not safely versioned across Python versions. So any resource created with Python `3.x.y` can only be updated by exactly the same version of Python. This is very constraining, but it's not clear there is any other option within the realm of what "dynamic providers" are as a feature. It is plausible that we could ensure that updates which only update the serialized provider can avoid calling the dynamic provider operations, so that version updates could still be accomplished. We can explore this separately. ```py from pulumi import ComponentResource, export, Input, Output from pulumi.dynamic import Resource, ResourceProvider, CreateResult, UpdateResult from typing import Optional from github import Github, GithubObject auth = "<auth token>" g = Github(auth) class GithubLabelArgs(object): owner: Input[str] repo: Input[str] name: Input[str] color: Input[str] description: Optional[Input[str]] def __init__(self, owner, repo, name, color, description=None): self.owner = owner self.repo = repo self.name = name self.color = color self.description = description class GithubLabelProvider(ResourceProvider): def create(self, props): l = g.get_user(props["owner"]).get_repo(props["repo"]).create_label( name=props["name"], color=props["color"], description=props.get("description", GithubObject.NotSet)) return CreateResult(l.name, {**props, **l.raw_data}) def update(self, id, _olds, props): l = g.get_user(props["owner"]).get_repo(props["repo"]).get_label(id) l.edit(name=props["name"], color=props["color"], description=props.get("description", GithubObject.NotSet)) return UpdateResult({**props, **l.raw_data}) def delete(self, id, props): l = g.get_user(props["owner"]).get_repo(props["repo"]).get_label(id) l.delete() class GithubLabel(Resource): name: Output[str] color: Output[str] url: Output[str] description: Output[str] def __init__(self, name, args: GithubLabelArgs, opts = None): full_args = {'url':None, 'description':None, 'name':None, 'color':None, **vars(args)} super().__init__(GithubLabelProvider(), name, full_args, opts) label = GithubLabel("foo", GithubLabelArgs("lukehoban", "todo", "mylabel", "d94f0b")) export("label_color", label.color) export("label_url", label.url) ``` Fixes https://github.com/pulumi/pulumi/issues/2902.
2019-07-19 17:18:25 +00:00
// If we're doing a preview or an update and this project is a Python project, we need to run
// the command in the context of the virtual environment that Pipenv created in order to pick up
// the correct version of Python. We also need to do this for destroy and refresh so that
// dynamic providers are run in the right virtual environment.
// This is only necessary when not using automatic virtual environment support.
if pt.opts.UsePipenv && isUpdate {
projinfo, err := pt.getProjinfo(wd)
if err != nil {
return nil
}
if projinfo.Proj.Runtime.Name() == "python" {
pipenvBin, err := pt.getPipenvBin()
if err != nil {
return err
}
// "pipenv run" activates the current virtual environment and runs the remainder of the arguments as if it
// were a command.
cmd = append([]string{pipenvBin, "run"}, cmd...)
}
}
_, _, err = retry.Until(context.Background(), retry.Acceptor{
Accept: func(try int, nextRetryTime time.Duration) (bool, interface{}, error) {
runerr := pt.runCommand(name, cmd, wd)
if runerr == nil {
return true, nil, nil
} else if _, ok := runerr.(*exec.ExitError); ok && isUpdate && !expectFailure {
// the update command failed, let's try again, assuming we haven't failed a few times.
if try+1 >= pt.maxStepTries {
return false, nil, fmt.Errorf("%v did not succeed after %v tries", cmd, try+1)
}
pt.t.Logf("%v failed: %v; retrying...", cmd, runerr)
return false, nil, nil
}
// some other error, fail
return false, nil, runerr
},
})
if postFn != nil {
if postErr := postFn(err); postErr != nil {
return multierror.Append(err, postErr)
}
}
return err
}
func (pt *ProgramTester) runYarnCommand(name string, args []string, wd string) error {
// Yarn will time out if multiple processes are trying to install packages at the same time.
Fix merge failures #2 (#15543) <!--- 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/15528 See https://github.com/pulumi/pulumi/pull/15540 & https://github.com/pulumi/pulumi/pull/15531 Re-creating this as a PR with `ci/test` label so we can get it merged. ## 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. --> - [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: Justin Van Patten <jvp@justinvp.com> Co-authored-by: Anton Tayanovskyy <anton@pulumi.com> Co-authored-by: Thomas Gummerer <t.gummerer@gmail.com>
2024-02-29 21:06:24 +00:00
ptesting.YarnInstallMutex.Lock()
defer ptesting.YarnInstallMutex.Unlock()
pt.t.Log("acquired yarn install lock")
defer pt.t.Log("released yarn install lock")
cmd, err := pt.yarnCmd(args)
if err != nil {
return err
}
_, _, err = retry.Until(context.Background(), retry.Acceptor{
Accept: func(try int, nextRetryTime time.Duration) (bool, interface{}, error) {
runerr := pt.runCommand(name, cmd, wd)
if runerr == nil {
return true, nil, nil
} else if _, ok := runerr.(*exec.ExitError); ok {
// yarn failed, let's try again, assuming we haven't failed a few times.
if try+1 >= 3 {
return false, nil, fmt.Errorf("%v did not complete after %v tries", cmd, try+1)
}
return false, nil, nil
}
// someother error, fail
return false, nil, runerr
},
})
return err
}
func (pt *ProgramTester) runPythonCommand(name string, args []string, wd string) error {
cmd, err := pt.pythonCmd(args)
if err != nil {
return err
}
return pt.runCommand(name, cmd, wd)
}
func (pt *ProgramTester) runVirtualEnvCommand(name string, args []string, wd string) error {
// When installing with `pip install -e`, a PKG-INFO file is created. If two packages are being installed
// this way simultaneously (which happens often, when running tests), both installations will be writing the
// same file simultaneously. If one process catches "PKG-INFO" in a half-written state, the one process that
// observed the torn write will fail to install the package.
//
// To avoid this problem, we use pipMutex to explicitly serialize installation operations. Doing so avoids
// the problem of multiple processes stomping on the same files in the source tree. Note that pipMutex is a
// file mutex, so this strategy works even if the go test runner chooses to split up text execution across
// multiple processes. (Furthermore, each test gets an instance of ProgramTester and thus the mutex, so we'd
// need to be sharing the mutex globally in each test process if we weren't using the file system to lock.)
if name == "virtualenv-pip-install-package" {
if err := pipMutex.Lock(); err != nil {
panic(err)
}
if pt.opts.Verbose {
pt.t.Log("acquired pip install lock")
defer pt.t.Log("released pip install lock")
}
defer func() {
if err := pipMutex.Unlock(); err != nil {
panic(err)
}
}()
}
virtualenvBinPath, err := getVirtualenvBinPath(wd, args[0], pt)
if err != nil {
return err
}
cmd := append([]string{virtualenvBinPath}, args[1:]...)
return pt.runCommand(name, cmd, wd)
}
func (pt *ProgramTester) runPipenvCommand(name string, args []string, wd string) error {
// Pipenv uses setuptools to install and uninstall packages. Setuptools has an installation mode called "develop"
// that we use to install the package being tested, since it is 1) lightweight and 2) not doing so has its own set
// of annoying problems.
//
// Setuptools develop does three things:
// 1. It invokes the "egg_info" command in the target package,
// 2. It creates a special `.egg-link` sentinel file in the current site-packages folder, pointing to the package
// being installed's path on disk
// 3. It updates easy-install.pth in site-packages so that pip understand that this package has been installed.
//
// Steps 2 and 3 operate entirely within the context of a virtualenv. The state that they mutate is fully contained
// within the current virtualenv. However, step 1 operates in the context of the package's source tree. Egg info
// is responsible for producing a minimal "egg" for a particular package, and its largest responsibility is creating
// a PKG-INFO file for a package. PKG-INFO contains, among other things, the version of the package being installed.
//
// If two packages are being installed in "develop" mode simultaneously (which happens often, when running tests),
// both installations will run "egg_info" on the source tree and both processes will be writing the same files
// simultaneously. If one process catches "PKG-INFO" in a half-written state, the one process that observed the
// torn write will fail to install the package (setuptools crashes).
//
// To avoid this problem, we use pipMutex to explicitly serialize installation operations. Doing so avoids the
// problem of multiple processes stomping on the same files in the source tree. Note that pipMutex is a file
// mutex, so this strategy works even if the go test runner chooses to split up text execution across multiple
// processes. (Furthermore, each test gets an instance of ProgramTester and thus the mutex, so we'd need to be
// sharing the mutex globally in each test process if we weren't using the file system to lock.)
if name == "pipenv-install-package" {
if err := pipMutex.Lock(); err != nil {
panic(err)
}
if pt.opts.Verbose {
pt.t.Log("acquired pip install lock")
defer pt.t.Log("released pip install lock")
}
defer func() {
if err := pipMutex.Unlock(); err != nil {
panic(err)
}
}()
}
cmd, err := pt.pipenvCmd(args)
if err != nil {
return err
}
return pt.runCommand(name, cmd, wd)
}
// TestLifeCyclePrepare prepares a test by creating a temporary directory
func (pt *ProgramTester) TestLifeCyclePrepare() error {
tmpdir, projdir, err := pt.copyTestToTemporaryDirectory()
pt.tmpdir = tmpdir
pt.projdir = projdir
return err
}
[Test] Eager cancelation for ProgramTester (#14126) <!--- 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 PR changes the behavior of `ProgramTester` in `pkg/testing/integration` package, to detect test failures more eagerly and then to skip remaining steps. Specifically it checks whether the test been marked as failed by a validation function (as defined by [`t.Failed()`](https://pkg.go.dev/testing#T.Failed)). The rationale is that, for most integration tests, each step assumes that the previous step was successful. It is simply noisy to continue with the subsequent steps. A workaround is to call `t.FailNow()` in the validation function (or use `require`). An option is provided `ExpectTestFailure` to continue with steps after test failure (as is the current behavior). There exists an option `ExpectFailures` to continue with steps after a pulumi deployment failure. I took such failures to be orthogonal to test failure. ## 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. --> - [ ] 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. --> ## Example Here's a snippet of output in the event that a validation function marks the test as failed (e.g. using an assertion). ``` ... /Users/eronwright/Pulumi/pulumi/tests/examples/program.go:1742: Performing extra runtime validation. /Users/eronwright/Pulumi/pulumi/tests/examples/examples_test.go:44: Error Trace: ... Error: Expected nil, but got ... Test: TestAccMinimal /Users/eronwright/Pulumi/pulumi/tests/examples/program.go:1744: Extra runtime validation complete. /Users/eronwright/Pulumi/pulumi/tests/examples/program.go:1164: Canceling further steps due to test failure /Users/eronwright/Pulumi/pulumi/tests/examples/program.go:1349: Destroying stack ... /Users/eronwright/Pulumi/pulumi/tests/examples/program.go:1365: Test failed, retaining stack '...' --- FAIL: TestAccMinimal (5.61s) ```
2023-10-11 08:07:44 +00:00
func (pt *ProgramTester) checkTestFailure() error {
if pt.t.Failed() {
pt.t.Logf("Canceling further steps due to test failure")
return ErrTestFailed
}
return nil
}
// TestCleanUp cleans up the temporary directory that a test used
func (pt *ProgramTester) TestCleanUp() {
testFinished := pt.TestFinished
if pt.tmpdir != "" {
if !testFinished || pt.t.Failed() {
// Test aborted or failed. Maybe copy to "failed tests" directory.
failedTestsDir := os.Getenv("PULUMI_FAILED_TESTS_DIR")
if failedTestsDir != "" {
dest := filepath.Join(failedTestsDir, pt.t.Name()+uniqueSuffix())
contract.IgnoreError(fsutil.CopyFile(dest, pt.tmpdir, nil))
}
} else {
contract.IgnoreError(os.RemoveAll(pt.tmpdir))
}
} else {
// When tmpdir is empty, we ran "in tree", which means we wrote output
// to the "command-output" folder in the projdir, and we should clean
// it up if the test passed
if testFinished && !pt.t.Failed() {
contract.IgnoreError(os.RemoveAll(filepath.Join(pt.projdir, commandOutputFolderName)))
}
}
}
// TestLifeCycleInitAndDestroy executes the test and cleans up
func (pt *ProgramTester) TestLifeCycleInitAndDestroy() error {
err := pt.TestLifeCyclePrepare()
if err != nil {
return fmt.Errorf("copying test to temp dir %s: %w", pt.tmpdir, err)
}
pt.TestFinished = false
2022-02-03 07:40:12 +00:00
if pt.opts.DestroyOnCleanup {
pt.t.Cleanup(pt.TestCleanUp)
} else {
defer pt.TestCleanUp()
}
err = pt.TestLifeCycleInitialize()
if err != nil {
return fmt.Errorf("initializing test project: %w", err)
}
destroyStack := func() {
destroyErr := pt.TestLifeCycleDestroy()
assert.NoError(pt.t, destroyErr)
}
if pt.opts.DestroyOnCleanup {
// Allow other tests to refer to this stack until the test is complete.
pt.t.Cleanup(destroyStack)
} else {
// Ensure that before we exit, we attempt to destroy and remove the stack.
defer destroyStack()
}
if err = pt.TestPreviewUpdateAndEdits(); err != nil {
return fmt.Errorf("running test preview, update, and edits: %w", err)
}
if pt.opts.RunUpdateTest {
err = upgradeProjectDeps(pt.projdir, pt)
if err != nil {
return fmt.Errorf("upgrading project dependencies: %w", err)
}
if err = pt.TestPreviewUpdateAndEdits(); err != nil {
2022-10-04 21:33:07 +00:00
return fmt.Errorf("running test preview, update, and edits (updateTest): %w", err)
}
}
pt.TestFinished = true
return nil
}
func upgradeProjectDeps(projectDir string, pt *ProgramTester) error {
projInfo, err := pt.getProjinfo(projectDir)
if err != nil {
return fmt.Errorf("getting project info: %w", err)
}
switch rt := projInfo.Proj.Runtime.Name(); rt {
case NodeJSRuntime:
if err = pt.yarnLinkPackageDeps(projectDir); err != nil {
return err
}
case PythonRuntime:
if err = pt.installPipPackageDeps(projectDir); err != nil {
return err
}
default:
return fmt.Errorf("unrecognized project runtime: %s", rt)
}
return nil
}
// TestLifeCycleInitialize initializes the project directory and stack along with any configuration
func (pt *ProgramTester) TestLifeCycleInitialize() error {
dir := pt.projdir
stackName := pt.opts.GetStackName()
// Set the default target Pulumi API if not overridden in options.
if pt.opts.CloudURL == "" {
pulumiAPI := os.Getenv("PULUMI_API")
if pulumiAPI != "" {
pt.opts.CloudURL = pulumiAPI
}
}
// Ensure all links are present, the stack is created, and all configs are applied.
pt.t.Logf("Initializing project (dir %s; stack %s)", dir, stackName)
// Login as needed.
stackInitName := string(pt.opts.GetStackNameWithOwner())
if os.Getenv("PULUMI_ACCESS_TOKEN") == "" && pt.opts.CloudURL == "" {
fmt.Printf("Using existing logged in user for tests. Set PULUMI_ACCESS_TOKEN and/or PULUMI_API to override.\n")
} else {
// Set PulumiCredentialsPathEnvVar to our CWD, so we use credentials specific to just this
// test.
pt.opts.Env = append(pt.opts.Env, fmt.Sprintf("%s=%s", workspace.PulumiCredentialsPathEnvVar, dir))
loginArgs := []string{"login"}
loginArgs = addFlagIfNonNil(loginArgs, "--cloud-url", pt.opts.CloudURL)
// If this is a local OR cloud login, then don't attach the owner to the stack-name.
if pt.opts.CloudURL != "" {
stackInitName = string(pt.opts.GetStackName())
}
if err := pt.runPulumiCommand("pulumi-login", loginArgs, dir, false); err != nil {
return err
}
}
// Stack init
stackInitArgs := []string{"stack", "init", stackInitName}
if pt.opts.SecretsProvider != "" {
stackInitArgs = append(stackInitArgs, "--secrets-provider", pt.opts.SecretsProvider)
}
if err := pt.runPulumiCommand("pulumi-stack-init", stackInitArgs, dir, false); err != nil {
return err
}
2022-09-14 20:04:00 +00:00
if len(pt.opts.Config)+len(pt.opts.Secrets) > 0 {
setAllArgs := []string{"config", "set-all"}
for key, value := range pt.opts.Config {
setAllArgs = append(setAllArgs, "--plaintext", fmt.Sprintf("%s=%s", key, value))
}
for key, value := range pt.opts.Secrets {
setAllArgs = append(setAllArgs, "--secret", fmt.Sprintf("%s=%s", key, value))
}
2022-09-14 20:04:00 +00:00
if err := pt.runPulumiCommand("pulumi-config", setAllArgs, dir, false); err != nil {
return err
}
}
Support lists and maps in config (#3342) This change adds support for lists and maps in config. We now allow lists/maps (and nested structures) in `Pulumi.<stack>.yaml` (or `Pulumi.<stack>.json`; yes, we currently support that). For example: ```yaml config: proj:blah: - a - b - c proj:hello: world proj:outer: inner: value proj:servers: - port: 80 ``` While such structures could be specified in the `.yaml` file manually, we support setting values in maps/lists from the command line. As always, you can specify single values with: ```shell $ pulumi config set hello world ``` Which results in the following YAML: ```yaml proj:hello world ``` And single value secrets via: ```shell $ pulumi config set --secret token shhh ``` Which results in the following YAML: ```yaml proj:token: secure: v1:VZAhuroR69FkEPTk:isKafsoZVMWA9pQayGzbWNynww== ``` Values in a list can be set from the command line using the new `--path` flag, which indicates the config key contains a path to a property in a map or list: ```shell $ pulumi config set --path names[0] a $ pulumi config set --path names[1] b $ pulumi config set --path names[2] c ``` Which results in: ```yaml proj:names - a - b - c ``` Values can be obtained similarly: ```shell $ pulumi config get --path names[1] b ``` Or setting values in a map: ```shell $ pulumi config set --path outer.inner value ``` Which results in: ```yaml proj:outer: inner: value ``` Of course, setting values in nested structures is supported: ```shell $ pulumi config set --path servers[0].port 80 ``` Which results in: ```yaml proj:servers: - port: 80 ``` If you want to include a period in the name of a property, it can be specified as: ``` $ pulumi config set --path 'nested["foo.bar"]' baz ``` Which results in: ```yaml proj:nested: foo.bar: baz ``` Examples of valid paths: - root - root.nested - 'root["nested"]' - root.double.nest - 'root["double"].nest' - 'root["double"]["nest"]' - root.array[0] - root.array[100] - root.array[0].nested - root.array[0][1].nested - root.nested.array[0].double[1] - 'root["key with \"escaped\" quotes"]' - 'root["key with a ."]' - '["root key with \"escaped\" quotes"].nested' - '["root key with a ."][100]' Note: paths that contain quotes can be surrounded by single quotes. When setting values with `--path`, if the value is `"false"` or `"true"`, it will be saved as the boolean value, and if it is convertible to an integer, it will be saved as an integer. Secure values are supported in lists/maps as well: ```shell $ pulumi config set --path --secret tokens[0] shh ``` Will result in: ```yaml proj:tokens: - secure: v1:wpZRCe36sFg1RxwG:WzPeQrCn4n+m4Ks8ps15MxvFXg== ``` Note: maps of length 1 with a key of “secure” and string value are reserved for storing secret values. Attempting to create such a value manually will result in an error: ```shell $ pulumi config set --path parent.secure foo error: "secure" key in maps of length 1 are reserved ``` **Accessing config values from the command line with JSON** ```shell $ pulumi config --json ``` Will output: ```json { "proj:hello": { "value": "world", "secret": false, "object": false }, "proj:names": { "value": "[\"a\",\"b\",\"c\"]", "secret": false, "object": true, "objectValue": [ "a", "b", "c" ] }, "proj:nested": { "value": "{\"foo.bar\":\"baz\"}", "secret": false, "object": true, "objectValue": { "foo.bar": "baz" } }, "proj:outer": { "value": "{\"inner\":\"value\"}", "secret": false, "object": true, "objectValue": { "inner": "value" } }, "proj:servers": { "value": "[{\"port\":80}]", "secret": false, "object": true, "objectValue": [ { "port": 80 } ] }, "proj:token": { "secret": true, "object": false }, "proj:tokens": { "secret": true, "object": true } } ``` If the value is a map or list, `"object"` will be `true`. `"value"` will contain the object as serialized JSON and a new `"objectValue"` property will be available containing the value of the object. If the object contains any secret values, `"secret"` will be `true`, and just like with scalar values, the value will not be outputted unless `--show-secrets` is specified. **Accessing config values from Pulumi programs** Map/list values are available to Pulumi programs as serialized JSON, so the existing `getObject`/`requireObject`/`getSecretObject`/`requireSecretObject` functions can be used to retrieve such values, e.g.: ```typescript import * as pulumi from "@pulumi/pulumi"; interface Server { port: number; } const config = new pulumi.Config(); const names = config.requireObject<string[]>("names"); for (const n of names) { console.log(n); } const servers = config.requireObject<Server[]>("servers"); for (const s of servers) { console.log(s.port); } ```
2019-11-01 20:41:27 +00:00
for _, cv := range pt.opts.OrderedConfig {
configArgs := []string{"config", "set", cv.Key, cv.Value}
if cv.Secret {
configArgs = append(configArgs, "--secret")
}
if cv.Path {
configArgs = append(configArgs, "--path")
}
if err := pt.runPulumiCommand("pulumi-config", configArgs, dir, false); err != nil {
Support lists and maps in config (#3342) This change adds support for lists and maps in config. We now allow lists/maps (and nested structures) in `Pulumi.<stack>.yaml` (or `Pulumi.<stack>.json`; yes, we currently support that). For example: ```yaml config: proj:blah: - a - b - c proj:hello: world proj:outer: inner: value proj:servers: - port: 80 ``` While such structures could be specified in the `.yaml` file manually, we support setting values in maps/lists from the command line. As always, you can specify single values with: ```shell $ pulumi config set hello world ``` Which results in the following YAML: ```yaml proj:hello world ``` And single value secrets via: ```shell $ pulumi config set --secret token shhh ``` Which results in the following YAML: ```yaml proj:token: secure: v1:VZAhuroR69FkEPTk:isKafsoZVMWA9pQayGzbWNynww== ``` Values in a list can be set from the command line using the new `--path` flag, which indicates the config key contains a path to a property in a map or list: ```shell $ pulumi config set --path names[0] a $ pulumi config set --path names[1] b $ pulumi config set --path names[2] c ``` Which results in: ```yaml proj:names - a - b - c ``` Values can be obtained similarly: ```shell $ pulumi config get --path names[1] b ``` Or setting values in a map: ```shell $ pulumi config set --path outer.inner value ``` Which results in: ```yaml proj:outer: inner: value ``` Of course, setting values in nested structures is supported: ```shell $ pulumi config set --path servers[0].port 80 ``` Which results in: ```yaml proj:servers: - port: 80 ``` If you want to include a period in the name of a property, it can be specified as: ``` $ pulumi config set --path 'nested["foo.bar"]' baz ``` Which results in: ```yaml proj:nested: foo.bar: baz ``` Examples of valid paths: - root - root.nested - 'root["nested"]' - root.double.nest - 'root["double"].nest' - 'root["double"]["nest"]' - root.array[0] - root.array[100] - root.array[0].nested - root.array[0][1].nested - root.nested.array[0].double[1] - 'root["key with \"escaped\" quotes"]' - 'root["key with a ."]' - '["root key with \"escaped\" quotes"].nested' - '["root key with a ."][100]' Note: paths that contain quotes can be surrounded by single quotes. When setting values with `--path`, if the value is `"false"` or `"true"`, it will be saved as the boolean value, and if it is convertible to an integer, it will be saved as an integer. Secure values are supported in lists/maps as well: ```shell $ pulumi config set --path --secret tokens[0] shh ``` Will result in: ```yaml proj:tokens: - secure: v1:wpZRCe36sFg1RxwG:WzPeQrCn4n+m4Ks8ps15MxvFXg== ``` Note: maps of length 1 with a key of “secure” and string value are reserved for storing secret values. Attempting to create such a value manually will result in an error: ```shell $ pulumi config set --path parent.secure foo error: "secure" key in maps of length 1 are reserved ``` **Accessing config values from the command line with JSON** ```shell $ pulumi config --json ``` Will output: ```json { "proj:hello": { "value": "world", "secret": false, "object": false }, "proj:names": { "value": "[\"a\",\"b\",\"c\"]", "secret": false, "object": true, "objectValue": [ "a", "b", "c" ] }, "proj:nested": { "value": "{\"foo.bar\":\"baz\"}", "secret": false, "object": true, "objectValue": { "foo.bar": "baz" } }, "proj:outer": { "value": "{\"inner\":\"value\"}", "secret": false, "object": true, "objectValue": { "inner": "value" } }, "proj:servers": { "value": "[{\"port\":80}]", "secret": false, "object": true, "objectValue": [ { "port": 80 } ] }, "proj:token": { "secret": true, "object": false }, "proj:tokens": { "secret": true, "object": true } } ``` If the value is a map or list, `"object"` will be `true`. `"value"` will contain the object as serialized JSON and a new `"objectValue"` property will be available containing the value of the object. If the object contains any secret values, `"secret"` will be `true`, and just like with scalar values, the value will not be outputted unless `--show-secrets` is specified. **Accessing config values from Pulumi programs** Map/list values are available to Pulumi programs as serialized JSON, so the existing `getObject`/`requireObject`/`getSecretObject`/`requireSecretObject` functions can be used to retrieve such values, e.g.: ```typescript import * as pulumi from "@pulumi/pulumi"; interface Server { port: number; } const config = new pulumi.Config(); const names = config.requireObject<string[]>("names"); for (const n of names) { console.log(n); } const servers = config.requireObject<Server[]>("servers"); for (const s of servers) { console.log(s.port); } ```
2019-11-01 20:41:27 +00:00
return err
}
}
// Environments
for _, env := range pt.opts.CreateEnvironments {
name := pt.opts.getEnvNameWithOwner(env.Name)
envFile, err := func() (string, error) {
temp, err := os.CreateTemp(pt.t.TempDir(), fmt.Sprintf("pulumi-env-%v-*", env.Name))
if err != nil {
return "", err
}
defer contract.IgnoreClose(temp)
enc := yaml.NewEncoder(temp)
enc.SetIndent(2)
if err = enc.Encode(env.Definition); err != nil {
return "", err
}
return temp.Name(), nil
}()
if err != nil {
return err
}
initArgs := []string{"env", "init", name, "-f", envFile}
if err := pt.runPulumiCommand("pulumi-env-init", initArgs, dir, false); err != nil {
return err
}
}
if len(pt.opts.Environments) != 0 {
envs := make([]string, len(pt.opts.Environments))
for i, e := range pt.opts.Environments {
envs[i] = pt.opts.getEnvName(e)
}
stackFile := filepath.Join(dir, fmt.Sprintf("Pulumi.%v.yaml", stackName))
bytes, err := os.ReadFile(stackFile)
if err != nil && !os.IsNotExist(err) {
return err
}
var stack workspace.ProjectStack
if err := yaml.Unmarshal(bytes, &stack); err != nil {
return err
}
stack.Environment = workspace.NewEnvironment(envs)
bytes, err = yaml.Marshal(stack)
if err != nil {
return err
}
if err = os.WriteFile(stackFile, bytes, 0o600); err != nil {
return err
}
}
return nil
}
// TestLifeCycleDestroy destroys a stack and removes it
func (pt *ProgramTester) TestLifeCycleDestroy() error {
if pt.projdir != "" {
// Destroy and remove the stack.
pt.t.Log("Destroying stack")
destroy := []string{"destroy", "--non-interactive", "--yes", "--skip-preview"}
if pt.opts.GetDebugUpdates() {
destroy = append(destroy, "-d")
}
if pt.opts.JSONOutput {
destroy = append(destroy, "--json")
}
if pt.opts.DestroyExcludeProtected {
destroy = append(destroy, "--exclude-protected")
}
if err := pt.runPulumiCommand("pulumi-destroy", destroy, pt.projdir, false); err != nil {
return err
}
if pt.t.Failed() {
pt.t.Logf("Test failed, retaining stack '%s'", pt.opts.GetStackNameWithOwner())
return nil
}
if !pt.opts.SkipStackRemoval {
err := pt.runPulumiCommand("pulumi-stack-rm", []string{"stack", "rm", "--yes"}, pt.projdir, false)
if err != nil {
return err
}
}
for _, env := range pt.opts.CreateEnvironments {
name := pt.opts.getEnvNameWithOwner(env.Name)
err := pt.runPulumiCommand("pulumi-env-rm", []string{"env", "rm", "--yes", name}, pt.projdir, false)
if err != nil {
return err
}
}
}
return nil
}
// TestPreviewUpdateAndEdits runs the preview, update, and any relevant edits
func (pt *ProgramTester) TestPreviewUpdateAndEdits() error {
dir := pt.projdir
// Now preview and update the real changes.
pt.t.Log("Performing primary preview and update")
initErr := pt.PreviewAndUpdate(dir, "initial", pt.opts.ExpectFailure, false, false)
// If the initial preview/update failed, just exit without trying the rest (but make sure to destroy).
if initErr != nil {
2022-10-04 21:33:07 +00:00
return fmt.Errorf("initial failure: %w", initErr)
}
// Perform an empty preview and update; nothing is expected to happen here.
if !pt.opts.SkipExportImport {
pt.t.Log("Roundtripping checkpoint via stack export and stack import")
if err := pt.exportImport(dir); err != nil {
2022-10-04 21:33:07 +00:00
return fmt.Errorf("empty preview + update: %w", err)
}
}
if !pt.opts.SkipEmptyPreviewUpdate {
msg := ""
if !pt.opts.AllowEmptyUpdateChanges {
msg = "(no changes expected)"
}
pt.t.Logf("Performing empty preview and update%s", msg)
2022-10-04 23:48:22 +00:00
if err := pt.PreviewAndUpdate(dir, "empty", pt.opts.ExpectFailure,
!pt.opts.AllowEmptyPreviewChanges, !pt.opts.AllowEmptyUpdateChanges); err != nil {
2022-10-04 21:33:07 +00:00
return fmt.Errorf("empty preview: %w", err)
}
}
// Run additional validation provided by the test options, passing in the checkpoint info.
if err := pt.performExtraRuntimeValidation(pt.opts.ExtraRuntimeValidation, dir); err != nil {
return err
}
if !pt.opts.SkipRefresh {
// Perform a refresh and ensure it doesn't yield changes.
refresh := []string{"refresh", "--non-interactive", "--yes", "--skip-preview"}
if pt.opts.GetDebugUpdates() {
refresh = append(refresh, "-d")
}
if pt.opts.JSONOutput {
refresh = append(refresh, "--json")
}
if !pt.opts.ExpectRefreshChanges {
refresh = append(refresh, "--expect-no-changes")
}
if err := pt.runPulumiCommand("pulumi-refresh", refresh, dir, false); err != nil {
return err
}
Add RequireEmptyPreviewAfterRefresh option to ProgramTest (#15309) <!--- 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 While working on [this issue in Azure Native](https://github.com/pulumi/pulumi-azure-native/issues/2798), I realized that I don't see how to test the scenario that I care about. I need the following workflow: 1. Run `pulumi up` for the initial deployment. 2. Run `pulumi refresh` to pull changes from API. 3. Run `pulumi preview --expect-no-changes` to guarantee that there is no loop of `refresh/up` stepping over each other. The existing `ExpectRefreshChanges` option is different. If it's set to false, the test would require no changes on step (2) above, which is a stronger requirement. Unfortunately, it's not always true in Azure Native, e.g. when a subresource changes properties of its parent resource. I want to enable "ExpectRefreshChanges: false" where possible, but I also want to require step (3) to succeed for all tests in Azure Native (and potentially everywhere, eventually). I've already found three issues while drafting that. Therefore, this PR proposes a new option for ProgramTest: `RequireEmptyPreviewAfterRefresh`. It defaults to false, so no behavior changes unless users opt in. If set to true, it runs an extra preview after refreshing, i.e. the step (3). Let me know if I missed a better way of achieving the same. ## 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. --> - [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. -->
2024-01-31 09:17:37 +00:00
// Perform another preview and expect no changes in it.
if pt.opts.RequireEmptyPreviewAfterRefresh {
preview := []string{"preview", "--non-interactive", "--expect-no-changes"}
if pt.opts.GetDebugUpdates() {
preview = append(preview, "-d")
}
if pt.opts.JSONOutput {
preview = append(preview, "--json")
}
if err := pt.runPulumiCommand("pulumi-preview-after-refresh", preview, dir, false); err != nil {
return err
}
}
}
// If there are any edits, apply them and run a preview and update for each one.
return pt.testEdits(dir)
}
func (pt *ProgramTester) exportImport(dir string) error {
exportCmd := []string{"stack", "export", "--file", "stack.json"}
importCmd := []string{"stack", "import", "--file", "stack.json"}
defer func() {
contract.IgnoreError(os.Remove(filepath.Join(dir, "stack.json")))
}()
if err := pt.runPulumiCommand("pulumi-stack-export", exportCmd, dir, false); err != nil {
return err
}
if f := pt.opts.ExportStateValidator; f != nil {
bytes, err := os.ReadFile(filepath.Join(dir, "stack.json"))
if err != nil {
pt.t.Logf("Failed to read stack.json: %s", err)
return err
}
pt.t.Logf("Calling ExportStateValidator")
f(pt.t, bytes)
[Test] Eager cancelation for ProgramTester (#14126) <!--- 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 PR changes the behavior of `ProgramTester` in `pkg/testing/integration` package, to detect test failures more eagerly and then to skip remaining steps. Specifically it checks whether the test been marked as failed by a validation function (as defined by [`t.Failed()`](https://pkg.go.dev/testing#T.Failed)). The rationale is that, for most integration tests, each step assumes that the previous step was successful. It is simply noisy to continue with the subsequent steps. A workaround is to call `t.FailNow()` in the validation function (or use `require`). An option is provided `ExpectTestFailure` to continue with steps after test failure (as is the current behavior). There exists an option `ExpectFailures` to continue with steps after a pulumi deployment failure. I took such failures to be orthogonal to test failure. ## 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. --> - [ ] 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. --> ## Example Here's a snippet of output in the event that a validation function marks the test as failed (e.g. using an assertion). ``` ... /Users/eronwright/Pulumi/pulumi/tests/examples/program.go:1742: Performing extra runtime validation. /Users/eronwright/Pulumi/pulumi/tests/examples/examples_test.go:44: Error Trace: ... Error: Expected nil, but got ... Test: TestAccMinimal /Users/eronwright/Pulumi/pulumi/tests/examples/program.go:1744: Extra runtime validation complete. /Users/eronwright/Pulumi/pulumi/tests/examples/program.go:1164: Canceling further steps due to test failure /Users/eronwright/Pulumi/pulumi/tests/examples/program.go:1349: Destroying stack ... /Users/eronwright/Pulumi/pulumi/tests/examples/program.go:1365: Test failed, retaining stack '...' --- FAIL: TestAccMinimal (5.61s) ```
2023-10-11 08:07:44 +00:00
if err := pt.checkTestFailure(); err != nil {
return err
}
}
return pt.runPulumiCommand("pulumi-stack-import", importCmd, dir, false)
}
// PreviewAndUpdate runs pulumi preview followed by pulumi up
func (pt *ProgramTester) PreviewAndUpdate(dir string, name string, shouldFail, expectNopPreview,
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
expectNopUpdate bool,
) error {
preview := []string{"preview", "--non-interactive", "--diff"}
update := []string{"up", "--non-interactive", "--yes", "--skip-preview", "--event-log", pt.updateEventLog}
if pt.opts.GetDebugUpdates() {
preview = append(preview, "-d")
update = append(update, "-d")
}
if pt.opts.JSONOutput {
preview = append(preview, "--json")
update = append(update, "--json")
}
if expectNopPreview {
preview = append(preview, "--expect-no-changes")
}
if expectNopUpdate {
update = append(update, "--expect-no-changes")
}
if pt.opts.PreviewCommandlineFlags != nil {
preview = append(preview, pt.opts.PreviewCommandlineFlags...)
}
if pt.opts.UpdateCommandlineFlags != nil {
update = append(update, pt.opts.UpdateCommandlineFlags...)
}
// If not in quick mode, run an explicit preview.
if !pt.opts.SkipPreview {
if err := pt.runPulumiCommand("pulumi-preview-"+name, preview, dir, shouldFail); err != nil {
if shouldFail {
pt.t.Log("Permitting failure (ExpectFailure=true for this preview)")
return nil
}
return err
}
if pt.opts.PreviewCompletedHook != nil {
if err := pt.opts.PreviewCompletedHook(dir); err != nil {
return err
}
}
}
// Now run an update.
if !pt.opts.SkipUpdate {
if err := pt.runPulumiCommand("pulumi-update-"+name, update, dir, shouldFail); err != nil {
if shouldFail {
pt.t.Log("Permitting failure (ExpectFailure=true for this update)")
return nil
}
return err
}
}
// If we expected a failure, but none occurred, return an error.
if shouldFail {
return errors.New("expected this step to fail, but it succeeded")
}
return nil
}
func (pt *ProgramTester) query(dir string, name string, shouldFail bool) error {
query := []string{"query", "--non-interactive"}
if pt.opts.GetDebugUpdates() {
query = append(query, "-d")
}
if pt.opts.QueryCommandlineFlags != nil {
query = append(query, pt.opts.QueryCommandlineFlags...)
}
// Now run a query.
if err := pt.runPulumiCommand("pulumi-query-"+name, query, dir, shouldFail); err != nil {
if shouldFail {
pt.t.Log("Permitting failure (ExpectFailure=true for this update)")
return nil
}
return err
}
// If we expected a failure, but none occurred, return an error.
if shouldFail {
return errors.New("expected this step to fail, but it succeeded")
}
return nil
}
func (pt *ProgramTester) testEdits(dir string) error {
for i, edit := range pt.opts.EditDirs {
var err error
if err = pt.testEdit(dir, i, edit); err != nil {
return err
}
}
return nil
}
func (pt *ProgramTester) testEdit(dir string, i int, edit EditDir) error {
pt.t.Logf("Applying edit '%v' and rerunning preview and update", edit.Dir)
if edit.Additive {
// Just copy new files into dir
if err := fsutil.CopyFile(dir, edit.Dir, nil); err != nil {
return fmt.Errorf("Couldn't copy %v into %v: %w", edit.Dir, dir, err)
}
} else {
// Create a new temporary directory
newDir, err := os.MkdirTemp("", pt.opts.StackName+"-")
if err != nil {
return fmt.Errorf("Couldn't create new temporary directory: %w", err)
}
// Delete whichever copy of the test is unused when we return
dirToDelete := newDir
defer func() {
contract.IgnoreError(os.RemoveAll(dirToDelete))
}()
// Copy everything except Pulumi.yaml, Pulumi.<stack-name>.yaml, and .pulumi from source into new directory
exclusions := make(map[string]bool)
projectYaml := workspace.ProjectFile + ".yaml"
configYaml := workspace.ProjectFile + "." + pt.opts.StackName + ".yaml"
exclusions[workspace.BookkeepingDir] = true
exclusions[projectYaml] = true
exclusions[configYaml] = true
if err := fsutil.CopyFile(newDir, edit.Dir, exclusions); err != nil {
return fmt.Errorf("Couldn't copy %v into %v: %w", edit.Dir, newDir, err)
}
// Copy Pulumi.yaml, Pulumi.<stack-name>.yaml, and .pulumi from old directory to new directory
oldProjectYaml := filepath.Join(dir, projectYaml)
newProjectYaml := filepath.Join(newDir, projectYaml)
oldConfigYaml := filepath.Join(dir, configYaml)
newConfigYaml := filepath.Join(newDir, configYaml)
oldProjectDir := filepath.Join(dir, workspace.BookkeepingDir)
newProjectDir := filepath.Join(newDir, workspace.BookkeepingDir)
if err := fsutil.CopyFile(newProjectYaml, oldProjectYaml, nil); err != nil {
return fmt.Errorf("Couldn't copy Pulumi.yaml: %w", err)
}
Allow missing stack config and bookkeeping files (#15863) Pulumi isn't required to write out these files, so valid tests fail when it doesn't. Previously this behavior wasn't a problem because the return values from fsutil.CopyFile were not checked. This regression was changed in https://github.com/pulumi/pulumi/pull/14695. <!--- 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 # (issue) ## 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. --> - [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. -->
2024-04-05 11:57:00 +00:00
// Copy the config file over if it exists.
//
// Pulumi is not required to write a config file if there is no config, so
// it might not.
if _, err := os.Stat(oldConfigYaml); !os.IsNotExist(err) {
if err := fsutil.CopyFile(newConfigYaml, oldConfigYaml, nil); err != nil {
return fmt.Errorf("Couldn't copy Pulumi.%s.yaml: %w", pt.opts.StackName, err)
}
}
Allow missing stack config and bookkeeping files (#15863) Pulumi isn't required to write out these files, so valid tests fail when it doesn't. Previously this behavior wasn't a problem because the return values from fsutil.CopyFile were not checked. This regression was changed in https://github.com/pulumi/pulumi/pull/14695. <!--- 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 # (issue) ## 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. --> - [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. -->
2024-04-05 11:57:00 +00:00
// Likewise, pulumi is not required to write a book-keeping (.pulumi) file.
if _, err := os.Stat(oldProjectDir); !os.IsNotExist(err) {
if err := fsutil.CopyFile(newProjectDir, oldProjectDir, nil); err != nil {
return fmt.Errorf("Couldn't copy .pulumi: %w", err)
}
}
// Finally, replace our current temp directory with the new one.
dirOld := dir + ".old"
if err := os.Rename(dir, dirOld); err != nil {
return fmt.Errorf("Couldn't rename %v to %v: %w", dir, dirOld, err)
}
// There's a brief window here where the old temp dir name could be taken from us.
if err := os.Rename(newDir, dir); err != nil {
return fmt.Errorf("Couldn't rename %v to %v: %w", newDir, dir, err)
}
// Keep dir, delete oldDir
dirToDelete = dirOld
}
err := pt.prepareProjectDir(dir)
if err != nil {
return fmt.Errorf("Couldn't prepare project in %v: %w", dir, err)
}
oldStdOut := pt.opts.Stdout
oldStderr := pt.opts.Stderr
oldVerbose := pt.opts.Verbose
if edit.Stdout != nil {
pt.opts.Stdout = edit.Stdout
}
if edit.Stderr != nil {
pt.opts.Stderr = edit.Stderr
}
if edit.Verbose {
pt.opts.Verbose = true
}
defer func() {
pt.opts.Stdout = oldStdOut
pt.opts.Stderr = oldStderr
pt.opts.Verbose = oldVerbose
}()
if !edit.QueryMode {
if err = pt.PreviewAndUpdate(dir, fmt.Sprintf("edit-%d", i),
edit.ExpectFailure, edit.ExpectNoChanges, edit.ExpectNoChanges); err != nil {
return err
}
} else {
if err = pt.query(dir, fmt.Sprintf("query-%d", i), edit.ExpectFailure); err != nil {
return err
}
}
return pt.performExtraRuntimeValidation(edit.ExtraRuntimeValidation, dir)
}
func (pt *ProgramTester) performExtraRuntimeValidation(
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
extraRuntimeValidation func(t *testing.T, stack RuntimeValidationStackInfo), dir string,
) error {
if extraRuntimeValidation == nil {
return nil
}
stackName := pt.opts.GetStackName()
// Create a temporary file name for the stack export
tempDir, err := os.MkdirTemp("", string(stackName))
if err != nil {
return err
}
fileName := filepath.Join(tempDir, "stack.json")
// Invoke `pulumi stack export`
// There are situations where we want to get access to the secrets in the validation
// this will allow us to get access to them as part of running ExtraRuntimeValidation
var pulumiCommand []string
if pt.opts.DecryptSecretsInOutput {
pulumiCommand = append(pulumiCommand, "stack", "export", "--show-secrets", "--file", fileName)
} else {
pulumiCommand = append(pulumiCommand, "stack", "export", "--file", fileName)
}
if err = pt.runPulumiCommand("pulumi-export",
pulumiCommand, dir, false); err != nil {
return fmt.Errorf("expected to export stack to file: %s: %w", fileName, err)
}
// Open the exported JSON file
f, err := os.Open(fileName)
if err != nil {
return fmt.Errorf("expected to be able to open file with stack exports: %s: %w", fileName, err)
}
defer func() {
contract.IgnoreClose(f)
contract.IgnoreError(os.RemoveAll(tempDir))
}()
// Unmarshal the Deployment
var untypedDeployment apitype.UntypedDeployment
if err = json.NewDecoder(f).Decode(&untypedDeployment); err != nil {
return err
}
Implement more precise delete-before-replace semantics. (#2369) This implements the new algorithm for deciding which resources must be deleted due to a delete-before-replace operation. We need to compute the set of resources that may be replaced by a change to the resource under consideration. We do this by taking the complete set of transitive dependents on the resource under consideration and removing any resources that would not be replaced by changes to their dependencies. We determine whether or not a resource may be replaced by substituting unknowns for input properties that may change due to deletion of the resources their value depends on and calling the resource provider's Diff method. This is perhaps clearer when described by example. Consider the following dependency graph: A __|__ B C | _|_ D E F In this graph, all of B, C, D, E, and F transitively depend on A. It may be the case, however, that changes to the specific properties of any of those resources R that would occur if a resource on the path to A were deleted and recreated may not cause R to be replaced. For example, the edge from B to A may be a simple dependsOn edge such that a change to B does not actually influence any of B's input properties. In that case, neither B nor D would need to be deleted before A could be deleted. In order to make the above algorithm a reality, the resource monitor interface has been updated to include a map that associates an input property key with the list of resources that input property depends on. Older clients of the resource monitor will leave this map empty, in which case all input properties will be treated as depending on all dependencies of the resource. This is probably overly conservative, but it is less conservative than what we currently implement, and is certainly correct.
2019-01-28 17:46:30 +00:00
var deployment apitype.DeploymentV3
if err = json.Unmarshal(untypedDeployment.Deployment, &deployment); err != nil {
return err
}
// Get the root resource and outputs from the deployment
Implement more precise delete-before-replace semantics. (#2369) This implements the new algorithm for deciding which resources must be deleted due to a delete-before-replace operation. We need to compute the set of resources that may be replaced by a change to the resource under consideration. We do this by taking the complete set of transitive dependents on the resource under consideration and removing any resources that would not be replaced by changes to their dependencies. We determine whether or not a resource may be replaced by substituting unknowns for input properties that may change due to deletion of the resources their value depends on and calling the resource provider's Diff method. This is perhaps clearer when described by example. Consider the following dependency graph: A __|__ B C | _|_ D E F In this graph, all of B, C, D, E, and F transitively depend on A. It may be the case, however, that changes to the specific properties of any of those resources R that would occur if a resource on the path to A were deleted and recreated may not cause R to be replaced. For example, the edge from B to A may be a simple dependsOn edge such that a change to B does not actually influence any of B's input properties. In that case, neither B nor D would need to be deleted before A could be deleted. In order to make the above algorithm a reality, the resource monitor interface has been updated to include a map that associates an input property key with the list of resources that input property depends on. Older clients of the resource monitor will leave this map empty, in which case all input properties will be treated as depending on all dependencies of the resource. This is probably overly conservative, but it is less conservative than what we currently implement, and is certainly correct.
2019-01-28 17:46:30 +00:00
var rootResource apitype.ResourceV3
var outputs map[string]interface{}
for _, res := range deployment.Resources {
if res.Type == resource.RootStackType && res.Parent == "" {
rootResource = res
outputs = res.Outputs
}
}
events, err := pt.readUpdateEventLog()
if err != nil {
return err
}
// Populate stack info object with all of this data to pass to the validation function
stackInfo := RuntimeValidationStackInfo{
StackName: pt.opts.GetStackName(),
Deployment: &deployment,
RootResource: rootResource,
Outputs: outputs,
Events: events,
}
2019-01-29 21:14:06 +00:00
pt.t.Log("Performing extra runtime validation.")
extraRuntimeValidation(pt.t, stackInfo)
pt.t.Log("Extra runtime validation complete.")
[Test] Eager cancelation for ProgramTester (#14126) <!--- 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 PR changes the behavior of `ProgramTester` in `pkg/testing/integration` package, to detect test failures more eagerly and then to skip remaining steps. Specifically it checks whether the test been marked as failed by a validation function (as defined by [`t.Failed()`](https://pkg.go.dev/testing#T.Failed)). The rationale is that, for most integration tests, each step assumes that the previous step was successful. It is simply noisy to continue with the subsequent steps. A workaround is to call `t.FailNow()` in the validation function (or use `require`). An option is provided `ExpectTestFailure` to continue with steps after test failure (as is the current behavior). There exists an option `ExpectFailures` to continue with steps after a pulumi deployment failure. I took such failures to be orthogonal to test failure. ## 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. --> - [ ] 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. --> ## Example Here's a snippet of output in the event that a validation function marks the test as failed (e.g. using an assertion). ``` ... /Users/eronwright/Pulumi/pulumi/tests/examples/program.go:1742: Performing extra runtime validation. /Users/eronwright/Pulumi/pulumi/tests/examples/examples_test.go:44: Error Trace: ... Error: Expected nil, but got ... Test: TestAccMinimal /Users/eronwright/Pulumi/pulumi/tests/examples/program.go:1744: Extra runtime validation complete. /Users/eronwright/Pulumi/pulumi/tests/examples/program.go:1164: Canceling further steps due to test failure /Users/eronwright/Pulumi/pulumi/tests/examples/program.go:1349: Destroying stack ... /Users/eronwright/Pulumi/pulumi/tests/examples/program.go:1365: Test failed, retaining stack '...' --- FAIL: TestAccMinimal (5.61s) ```
2023-10-11 08:07:44 +00:00
return pt.checkTestFailure()
}
func (pt *ProgramTester) readUpdateEventLog() ([]apitype.EngineEvent, error) {
events := []apitype.EngineEvent{}
eventsFile, err := os.Open(pt.updateEventLog)
if err != nil {
if os.IsNotExist(err) {
return events, nil
}
return events, fmt.Errorf("expected to be able to open event log file %s: %w",
pt.updateEventLog, err)
}
defer contract.IgnoreClose(eventsFile)
decoder := json.NewDecoder(eventsFile)
for {
var event apitype.EngineEvent
if err = decoder.Decode(&event); err != nil {
if err == io.EOF {
break
}
return events, fmt.Errorf("failed decoding engine event from log file %s: %w",
pt.updateEventLog, err)
}
events = append(events, event)
}
return events, nil
}
// copyTestToTemporaryDirectory creates a temporary directory to run the test in and copies the test to it.
func (pt *ProgramTester) copyTestToTemporaryDirectory() (string, string, error) {
// Get the source dir and project info.
sourceDir := pt.opts.Dir
projSourceDir := sourceDir
if wd := pt.opts.RelativeWorkDir; wd != "" {
projSourceDir = filepath.Join(projSourceDir, wd)
}
projinfo, err := pt.getProjinfo(projSourceDir)
if err != nil {
return "", "", fmt.Errorf("could not get project info from source: %w", err)
}
if pt.opts.Stdout == nil {
pt.opts.Stdout = os.Stdout
}
if pt.opts.Stderr == nil {
pt.opts.Stderr = os.Stderr
}
pt.t.Logf("sample: %v", sourceDir)
bin, err := pt.getBin()
if err != nil {
return "", "", err
}
pt.t.Logf("pulumi: %v\n", bin)
stackName := string(pt.opts.GetStackName())
// For most projects, we will copy to a temporary directory. For Go projects, however, we must create
// a folder structure that adheres to GOPATH requirements
var tmpdir, projdir string
if projinfo.Proj.Runtime.Name() == "go" {
targetDir, err := tools.CreateTemporaryGoFolder("stackName")
if err != nil {
return "", "", fmt.Errorf("Couldn't create temporary directory: %w", err)
}
tmpdir = targetDir
projdir = targetDir
} else {
targetDir, tempErr := os.MkdirTemp("", stackName+"-")
if tempErr != nil {
return "", "", fmt.Errorf("Couldn't create temporary directory: %w", tempErr)
}
tmpdir = targetDir
projdir = targetDir
}
if wd := pt.opts.RelativeWorkDir; wd != "" {
projdir = filepath.Join(projdir, wd)
}
// Copy the source project.
if copyErr := fsutil.CopyFile(tmpdir, sourceDir, nil); copyErr != nil {
return "", "", copyErr
}
2022-09-22 14:58:57 +00:00
// Reload the projinfo before making mutating changes (workspace.LoadProject caches the in-memory Project by path)
projinfo, err = pt.getProjinfo(projdir)
if err != nil {
return "", "", fmt.Errorf("could not get project info: %w", err)
2022-09-22 14:58:57 +00:00
}
// Add dynamic plugin paths from ProgramTester
if (projinfo.Proj.Plugins == nil || projinfo.Proj.Plugins.Providers == nil) && pt.opts.LocalProviders != nil {
projinfo.Proj.Plugins = &workspace.Plugins{
Providers: make([]workspace.PluginOptions, 0),
}
}
if pt.opts.LocalProviders != nil {
for _, provider := range pt.opts.LocalProviders {
// LocalProviders are relative to the working directory when running tests, NOT relative to the
// Pulumi.yaml. This is a bit odd, but makes it easier to construct the required paths in each
// test.
absPath, err := filepath.Abs(provider.Path)
if err != nil {
return "", "", fmt.Errorf("could not get absolute path for plugin %s: %w", provider.Path, err)
}
projinfo.Proj.Plugins.Providers = append(projinfo.Proj.Plugins.Providers, workspace.PluginOptions{
Name: provider.Package,
Path: absPath,
})
}
}
// Absolute path of the source directory, for fixupPath to use below
absSource, err := filepath.Abs(sourceDir)
if err != nil {
return "", "", fmt.Errorf("could not get absolute path for source directory %s: %w", sourceDir, err)
}
// Return a fixed up path if it's relative to sourceDir but not beneath it, else just returns the input
fixupPath := func(path string) (string, error) {
if filepath.IsAbs(path) {
return path, nil
}
absPlugin := filepath.Join(absSource, path)
if !strings.HasPrefix(absPlugin, absSource+string(filepath.Separator)) {
return absPlugin, nil
}
return path, nil
}
if projinfo.Proj.Plugins != nil {
optionSets := [][]workspace.PluginOptions{
projinfo.Proj.Plugins.Providers,
projinfo.Proj.Plugins.Languages,
projinfo.Proj.Plugins.Analyzers,
}
for _, options := range optionSets {
for i, opt := range options {
path, err := fixupPath(opt.Path)
2022-09-22 14:58:57 +00:00
if err != nil {
return "", "", fmt.Errorf("could not get fixed path for plugin %s: %w", opt.Path, err)
2022-09-22 14:58:57 +00:00
}
options[i].Path = path
}
}
}
projfile := filepath.Join(projdir, workspace.ProjectFile+".yaml")
bytes, err := yaml.Marshal(projinfo.Proj)
if err != nil {
return "", "", fmt.Errorf("error marshalling project %q: %w", projfile, err)
}
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
if err := os.WriteFile(projfile, bytes, 0o600); err != nil {
return "", "", fmt.Errorf("error writing project: %w", err)
}
err = pt.prepareProject(projinfo)
if err != nil {
return "", "", fmt.Errorf("Failed to prepare %v: %w", projdir, err)
}
2023-02-28 17:06:15 +00:00
if pt.opts.PostPrepareProject != nil {
err = pt.opts.PostPrepareProject(projinfo)
if err != nil {
return "", "", fmt.Errorf("Failed to post-prepare %v: %w", projdir, err)
}
}
// TODO[pulumi/pulumi#5455]: Dynamic providers fail to load when used from multi-lang components.
// Until that's been fixed, this environment variable can be set by a test, which results in
// a package.json being emitted in the project directory and `yarn install && yarn link @pulumi/pulumi`
// being run.
// When the underlying issue has been fixed, the use of this environment variable should be removed.
var yarnLinkPulumi bool
for _, env := range pt.opts.Env {
if env == "PULUMI_TEST_YARN_LINK_PULUMI=true" {
yarnLinkPulumi = true
break
}
}
if yarnLinkPulumi {
const packageJSON = `{
"name": "test",
"peerDependencies": {
"@pulumi/pulumi": "latest"
}
}`
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
if err := os.WriteFile(filepath.Join(projdir, "package.json"), []byte(packageJSON), 0o600); err != nil {
return "", "", err
}
if err := pt.runYarnCommand("yarn-link", []string{"link", "@pulumi/pulumi"}, projdir); err != nil {
return "", "", err
}
if err = pt.runYarnCommand("yarn-install", []string{"install"}, projdir); err != nil {
return "", "", err
}
}
pt.t.Logf("projdir: %v", projdir)
return tmpdir, projdir, nil
}
func (pt *ProgramTester) getProjinfo(projectDir string) (*engine.Projinfo, error) {
Get the empty Python program working This change gets enough of the Python SDK up and running that the empty Python program will work. Mostly just scaffolding, but the basic structure is now in place. The primary remaining work is to wire up resource creation to the gRPC interfaces. In summary: * The basic structure is as follows: - Everything goes into sdk/python/. - sdk/python/cmd/pulumi-langhost-python is a Go language host that simply knows how to spawn Python processes to run out entrypoint in response to requests by the engine. - sdk/python/cmd/pulumi-langhost-python-exec is a little Python shim that is invoked by the language host to run Python programs, and is responsible for setting up the minimal goo before we can do so (RPC connections and the like). - sdk/python/lib/ contains a Python Pip package suitable for PyPi. - In there, we have two packages: the root pulumi package that contains all of the basic Pulumi programming model abstractions, and pulumi.runtime, which contains the implementation of resource registration, RPC interfacing with the engine, and so on. * Add logic in our test framework to conditionalize on the language type and react accordingly. This will allow us to skip Yarn for Python projects and eventually run Pip if there's a requirements.txt. * Created the basic project structure, including all of the usual Make targets for installing into the proper places. * Building also runs Pylint and we are clean. There are a few other minor things in here: * Add an "empty" test for both Node.js and Python. These pass. * Fix an existing bug in plugin shutdown logic. At some point, we started waiting for stderr/stdout to flush before shutting down the plugin; but if certain failures happen "early" during the plugin launch process, these channels will never get initialized and so waiting for them deadlocks. * Recently we seem to have added logic to delete test temp directories if a failure happened during initialization of said temp directories. This is unfortunate, because you often need to look at the temp directory to see what failed. We already clean them up elsewhere after the full test completes successfully, so I don't think we need to be doing this, and I've removed it. Still many loose ends (config, resources, etc), but it's a start!
2018-01-13 18:29:34 +00:00
// Load up the package so we know things like what language the project is.
projfile := filepath.Join(projectDir, workspace.ProjectFile+".yaml")
proj, err := workspace.LoadProject(projfile)
if err != nil {
return nil, err
Get the empty Python program working This change gets enough of the Python SDK up and running that the empty Python program will work. Mostly just scaffolding, but the basic structure is now in place. The primary remaining work is to wire up resource creation to the gRPC interfaces. In summary: * The basic structure is as follows: - Everything goes into sdk/python/. - sdk/python/cmd/pulumi-langhost-python is a Go language host that simply knows how to spawn Python processes to run out entrypoint in response to requests by the engine. - sdk/python/cmd/pulumi-langhost-python-exec is a little Python shim that is invoked by the language host to run Python programs, and is responsible for setting up the minimal goo before we can do so (RPC connections and the like). - sdk/python/lib/ contains a Python Pip package suitable for PyPi. - In there, we have two packages: the root pulumi package that contains all of the basic Pulumi programming model abstractions, and pulumi.runtime, which contains the implementation of resource registration, RPC interfacing with the engine, and so on. * Add logic in our test framework to conditionalize on the language type and react accordingly. This will allow us to skip Yarn for Python projects and eventually run Pip if there's a requirements.txt. * Created the basic project structure, including all of the usual Make targets for installing into the proper places. * Building also runs Pylint and we are clean. There are a few other minor things in here: * Add an "empty" test for both Node.js and Python. These pass. * Fix an existing bug in plugin shutdown logic. At some point, we started waiting for stderr/stdout to flush before shutting down the plugin; but if certain failures happen "early" during the plugin launch process, these channels will never get initialized and so waiting for them deadlocks. * Recently we seem to have added logic to delete test temp directories if a failure happened during initialization of said temp directories. This is unfortunate, because you often need to look at the temp directory to see what failed. We already clean them up elsewhere after the full test completes successfully, so I don't think we need to be doing this, and I've removed it. Still many loose ends (config, resources, etc), but it's a start!
2018-01-13 18:29:34 +00:00
}
return &engine.Projinfo{Proj: proj, Root: projectDir}, nil
}
Get the empty Python program working This change gets enough of the Python SDK up and running that the empty Python program will work. Mostly just scaffolding, but the basic structure is now in place. The primary remaining work is to wire up resource creation to the gRPC interfaces. In summary: * The basic structure is as follows: - Everything goes into sdk/python/. - sdk/python/cmd/pulumi-langhost-python is a Go language host that simply knows how to spawn Python processes to run out entrypoint in response to requests by the engine. - sdk/python/cmd/pulumi-langhost-python-exec is a little Python shim that is invoked by the language host to run Python programs, and is responsible for setting up the minimal goo before we can do so (RPC connections and the like). - sdk/python/lib/ contains a Python Pip package suitable for PyPi. - In there, we have two packages: the root pulumi package that contains all of the basic Pulumi programming model abstractions, and pulumi.runtime, which contains the implementation of resource registration, RPC interfacing with the engine, and so on. * Add logic in our test framework to conditionalize on the language type and react accordingly. This will allow us to skip Yarn for Python projects and eventually run Pip if there's a requirements.txt. * Created the basic project structure, including all of the usual Make targets for installing into the proper places. * Building also runs Pylint and we are clean. There are a few other minor things in here: * Add an "empty" test for both Node.js and Python. These pass. * Fix an existing bug in plugin shutdown logic. At some point, we started waiting for stderr/stdout to flush before shutting down the plugin; but if certain failures happen "early" during the plugin launch process, these channels will never get initialized and so waiting for them deadlocks. * Recently we seem to have added logic to delete test temp directories if a failure happened during initialization of said temp directories. This is unfortunate, because you often need to look at the temp directory to see what failed. We already clean them up elsewhere after the full test completes successfully, so I don't think we need to be doing this, and I've removed it. Still many loose ends (config, resources, etc), but it's a start!
2018-01-13 18:29:34 +00:00
// prepareProject runs setup necessary to get the project ready for `pulumi` commands.
func (pt *ProgramTester) prepareProject(projinfo *engine.Projinfo) error {
if pt.opts.PrepareProject != nil {
return pt.opts.PrepareProject(projinfo)
Get the empty Python program working This change gets enough of the Python SDK up and running that the empty Python program will work. Mostly just scaffolding, but the basic structure is now in place. The primary remaining work is to wire up resource creation to the gRPC interfaces. In summary: * The basic structure is as follows: - Everything goes into sdk/python/. - sdk/python/cmd/pulumi-langhost-python is a Go language host that simply knows how to spawn Python processes to run out entrypoint in response to requests by the engine. - sdk/python/cmd/pulumi-langhost-python-exec is a little Python shim that is invoked by the language host to run Python programs, and is responsible for setting up the minimal goo before we can do so (RPC connections and the like). - sdk/python/lib/ contains a Python Pip package suitable for PyPi. - In there, we have two packages: the root pulumi package that contains all of the basic Pulumi programming model abstractions, and pulumi.runtime, which contains the implementation of resource registration, RPC interfacing with the engine, and so on. * Add logic in our test framework to conditionalize on the language type and react accordingly. This will allow us to skip Yarn for Python projects and eventually run Pip if there's a requirements.txt. * Created the basic project structure, including all of the usual Make targets for installing into the proper places. * Building also runs Pylint and we are clean. There are a few other minor things in here: * Add an "empty" test for both Node.js and Python. These pass. * Fix an existing bug in plugin shutdown logic. At some point, we started waiting for stderr/stdout to flush before shutting down the plugin; but if certain failures happen "early" during the plugin launch process, these channels will never get initialized and so waiting for them deadlocks. * Recently we seem to have added logic to delete test temp directories if a failure happened during initialization of said temp directories. This is unfortunate, because you often need to look at the temp directory to see what failed. We already clean them up elsewhere after the full test completes successfully, so I don't think we need to be doing this, and I've removed it. Still many loose ends (config, resources, etc), but it's a start!
2018-01-13 18:29:34 +00:00
}
return pt.defaultPrepareProject(projinfo)
Get the empty Python program working This change gets enough of the Python SDK up and running that the empty Python program will work. Mostly just scaffolding, but the basic structure is now in place. The primary remaining work is to wire up resource creation to the gRPC interfaces. In summary: * The basic structure is as follows: - Everything goes into sdk/python/. - sdk/python/cmd/pulumi-langhost-python is a Go language host that simply knows how to spawn Python processes to run out entrypoint in response to requests by the engine. - sdk/python/cmd/pulumi-langhost-python-exec is a little Python shim that is invoked by the language host to run Python programs, and is responsible for setting up the minimal goo before we can do so (RPC connections and the like). - sdk/python/lib/ contains a Python Pip package suitable for PyPi. - In there, we have two packages: the root pulumi package that contains all of the basic Pulumi programming model abstractions, and pulumi.runtime, which contains the implementation of resource registration, RPC interfacing with the engine, and so on. * Add logic in our test framework to conditionalize on the language type and react accordingly. This will allow us to skip Yarn for Python projects and eventually run Pip if there's a requirements.txt. * Created the basic project structure, including all of the usual Make targets for installing into the proper places. * Building also runs Pylint and we are clean. There are a few other minor things in here: * Add an "empty" test for both Node.js and Python. These pass. * Fix an existing bug in plugin shutdown logic. At some point, we started waiting for stderr/stdout to flush before shutting down the plugin; but if certain failures happen "early" during the plugin launch process, these channels will never get initialized and so waiting for them deadlocks. * Recently we seem to have added logic to delete test temp directories if a failure happened during initialization of said temp directories. This is unfortunate, because you often need to look at the temp directory to see what failed. We already clean them up elsewhere after the full test completes successfully, so I don't think we need to be doing this, and I've removed it. Still many loose ends (config, resources, etc), but it's a start!
2018-01-13 18:29:34 +00:00
}
// prepareProjectDir runs setup necessary to get the project ready for `pulumi` commands.
func (pt *ProgramTester) prepareProjectDir(projectDir string) error {
projinfo, err := pt.getProjinfo(projectDir)
if err != nil {
return err
}
return pt.prepareProject(projinfo)
}
Get the empty Python program working This change gets enough of the Python SDK up and running that the empty Python program will work. Mostly just scaffolding, but the basic structure is now in place. The primary remaining work is to wire up resource creation to the gRPC interfaces. In summary: * The basic structure is as follows: - Everything goes into sdk/python/. - sdk/python/cmd/pulumi-langhost-python is a Go language host that simply knows how to spawn Python processes to run out entrypoint in response to requests by the engine. - sdk/python/cmd/pulumi-langhost-python-exec is a little Python shim that is invoked by the language host to run Python programs, and is responsible for setting up the minimal goo before we can do so (RPC connections and the like). - sdk/python/lib/ contains a Python Pip package suitable for PyPi. - In there, we have two packages: the root pulumi package that contains all of the basic Pulumi programming model abstractions, and pulumi.runtime, which contains the implementation of resource registration, RPC interfacing with the engine, and so on. * Add logic in our test framework to conditionalize on the language type and react accordingly. This will allow us to skip Yarn for Python projects and eventually run Pip if there's a requirements.txt. * Created the basic project structure, including all of the usual Make targets for installing into the proper places. * Building also runs Pylint and we are clean. There are a few other minor things in here: * Add an "empty" test for both Node.js and Python. These pass. * Fix an existing bug in plugin shutdown logic. At some point, we started waiting for stderr/stdout to flush before shutting down the plugin; but if certain failures happen "early" during the plugin launch process, these channels will never get initialized and so waiting for them deadlocks. * Recently we seem to have added logic to delete test temp directories if a failure happened during initialization of said temp directories. This is unfortunate, because you often need to look at the temp directory to see what failed. We already clean them up elsewhere after the full test completes successfully, so I don't think we need to be doing this, and I've removed it. Still many loose ends (config, resources, etc), but it's a start!
2018-01-13 18:29:34 +00:00
// prepareNodeJSProject runs setup necessary to get a Node.js project ready for `pulumi` commands.
func (pt *ProgramTester) prepareNodeJSProject(projinfo *engine.Projinfo) error {
Fix merge failures #2 (#15543) <!--- 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/15528 See https://github.com/pulumi/pulumi/pull/15540 & https://github.com/pulumi/pulumi/pull/15531 Re-creating this as a PR with `ci/test` label so we can get it merged. ## 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. --> - [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: Justin Van Patten <jvp@justinvp.com> Co-authored-by: Anton Tayanovskyy <anton@pulumi.com> Co-authored-by: Thomas Gummerer <t.gummerer@gmail.com>
2024-02-29 21:06:24 +00:00
if err := ptesting.WriteYarnRCForTest(projinfo.Root); err != nil {
return err
}
Get the empty Python program working This change gets enough of the Python SDK up and running that the empty Python program will work. Mostly just scaffolding, but the basic structure is now in place. The primary remaining work is to wire up resource creation to the gRPC interfaces. In summary: * The basic structure is as follows: - Everything goes into sdk/python/. - sdk/python/cmd/pulumi-langhost-python is a Go language host that simply knows how to spawn Python processes to run out entrypoint in response to requests by the engine. - sdk/python/cmd/pulumi-langhost-python-exec is a little Python shim that is invoked by the language host to run Python programs, and is responsible for setting up the minimal goo before we can do so (RPC connections and the like). - sdk/python/lib/ contains a Python Pip package suitable for PyPi. - In there, we have two packages: the root pulumi package that contains all of the basic Pulumi programming model abstractions, and pulumi.runtime, which contains the implementation of resource registration, RPC interfacing with the engine, and so on. * Add logic in our test framework to conditionalize on the language type and react accordingly. This will allow us to skip Yarn for Python projects and eventually run Pip if there's a requirements.txt. * Created the basic project structure, including all of the usual Make targets for installing into the proper places. * Building also runs Pylint and we are clean. There are a few other minor things in here: * Add an "empty" test for both Node.js and Python. These pass. * Fix an existing bug in plugin shutdown logic. At some point, we started waiting for stderr/stdout to flush before shutting down the plugin; but if certain failures happen "early" during the plugin launch process, these channels will never get initialized and so waiting for them deadlocks. * Recently we seem to have added logic to delete test temp directories if a failure happened during initialization of said temp directories. This is unfortunate, because you often need to look at the temp directory to see what failed. We already clean them up elsewhere after the full test completes successfully, so I don't think we need to be doing this, and I've removed it. Still many loose ends (config, resources, etc), but it's a start!
2018-01-13 18:29:34 +00:00
// Get the correct pwd to run Yarn in.
cwd, _, err := projinfo.GetPwdMain()
if err != nil {
return err
}
workspaceRoot, err := npm.FindWorkspaceRoot(cwd)
if err != nil {
if !errors.Is(err, npm.ErrNotInWorkspace) {
return err
}
// Not in a workspace, don't updated cwd.
} else {
pt.t.Logf("detected yarn/npm workspace root at %s", workspaceRoot)
cwd = workspaceRoot
}
// If dev versions were requested, we need to update the
// package.json to use them. Note that Overrides take
// priority over installing dev versions.
if pt.opts.InstallDevReleases {
err := pt.runYarnCommand("yarn-add", []string{"add", "@pulumi/pulumi@dev"}, cwd)
if err != nil {
return err
}
}
// If the test requested some packages to be overridden, we do two things. First, if the package is listed as a
// direct dependency of the project, we change the version constraint in the package.json. For transitive
// dependencies, we use yarn's "resolutions" feature to force them to a specific version.
if len(pt.opts.Overrides) > 0 {
packageJSON, err := readPackageJSON(cwd)
if err != nil {
return err
}
resolutions := make(map[string]interface{})
for packageName, packageVersion := range pt.opts.Overrides {
for _, section := range []string{"dependencies", "devDependencies"} {
if _, has := packageJSON[section]; has {
entry := packageJSON[section].(map[string]interface{})
if _, has := entry[packageName]; has {
entry[packageName] = packageVersion
}
}
}
pt.t.Logf("adding resolution for %s to version %s", packageName, packageVersion)
resolutions["**/"+packageName] = packageVersion
}
// Wack any existing resolutions section with our newly computed one.
packageJSON["resolutions"] = resolutions
if err := writePackageJSON(cwd, packageJSON); err != nil {
return err
}
}
// Now ensure dependencies are present.
if err = pt.runYarnCommand("yarn-install", []string{"install"}, cwd); err != nil {
Get the empty Python program working This change gets enough of the Python SDK up and running that the empty Python program will work. Mostly just scaffolding, but the basic structure is now in place. The primary remaining work is to wire up resource creation to the gRPC interfaces. In summary: * The basic structure is as follows: - Everything goes into sdk/python/. - sdk/python/cmd/pulumi-langhost-python is a Go language host that simply knows how to spawn Python processes to run out entrypoint in response to requests by the engine. - sdk/python/cmd/pulumi-langhost-python-exec is a little Python shim that is invoked by the language host to run Python programs, and is responsible for setting up the minimal goo before we can do so (RPC connections and the like). - sdk/python/lib/ contains a Python Pip package suitable for PyPi. - In there, we have two packages: the root pulumi package that contains all of the basic Pulumi programming model abstractions, and pulumi.runtime, which contains the implementation of resource registration, RPC interfacing with the engine, and so on. * Add logic in our test framework to conditionalize on the language type and react accordingly. This will allow us to skip Yarn for Python projects and eventually run Pip if there's a requirements.txt. * Created the basic project structure, including all of the usual Make targets for installing into the proper places. * Building also runs Pylint and we are clean. There are a few other minor things in here: * Add an "empty" test for both Node.js and Python. These pass. * Fix an existing bug in plugin shutdown logic. At some point, we started waiting for stderr/stdout to flush before shutting down the plugin; but if certain failures happen "early" during the plugin launch process, these channels will never get initialized and so waiting for them deadlocks. * Recently we seem to have added logic to delete test temp directories if a failure happened during initialization of said temp directories. This is unfortunate, because you often need to look at the temp directory to see what failed. We already clean them up elsewhere after the full test completes successfully, so I don't think we need to be doing this, and I've removed it. Still many loose ends (config, resources, etc), but it's a start!
2018-01-13 18:29:34 +00:00
return err
}
if !pt.opts.RunUpdateTest {
if err = pt.yarnLinkPackageDeps(cwd); err != nil {
Get the empty Python program working This change gets enough of the Python SDK up and running that the empty Python program will work. Mostly just scaffolding, but the basic structure is now in place. The primary remaining work is to wire up resource creation to the gRPC interfaces. In summary: * The basic structure is as follows: - Everything goes into sdk/python/. - sdk/python/cmd/pulumi-langhost-python is a Go language host that simply knows how to spawn Python processes to run out entrypoint in response to requests by the engine. - sdk/python/cmd/pulumi-langhost-python-exec is a little Python shim that is invoked by the language host to run Python programs, and is responsible for setting up the minimal goo before we can do so (RPC connections and the like). - sdk/python/lib/ contains a Python Pip package suitable for PyPi. - In there, we have two packages: the root pulumi package that contains all of the basic Pulumi programming model abstractions, and pulumi.runtime, which contains the implementation of resource registration, RPC interfacing with the engine, and so on. * Add logic in our test framework to conditionalize on the language type and react accordingly. This will allow us to skip Yarn for Python projects and eventually run Pip if there's a requirements.txt. * Created the basic project structure, including all of the usual Make targets for installing into the proper places. * Building also runs Pylint and we are clean. There are a few other minor things in here: * Add an "empty" test for both Node.js and Python. These pass. * Fix an existing bug in plugin shutdown logic. At some point, we started waiting for stderr/stdout to flush before shutting down the plugin; but if certain failures happen "early" during the plugin launch process, these channels will never get initialized and so waiting for them deadlocks. * Recently we seem to have added logic to delete test temp directories if a failure happened during initialization of said temp directories. This is unfortunate, because you often need to look at the temp directory to see what failed. We already clean them up elsewhere after the full test completes successfully, so I don't think we need to be doing this, and I've removed it. Still many loose ends (config, resources, etc), but it's a start!
2018-01-13 18:29:34 +00:00
return err
}
}
if pt.opts.RunBuild {
// And finally compile it using whatever build steps are in the package.json file.
if err = pt.runYarnCommand("yarn-build", []string{"run", "build"}, cwd); err != nil {
return err
}
}
return nil
}
Get the empty Python program working This change gets enough of the Python SDK up and running that the empty Python program will work. Mostly just scaffolding, but the basic structure is now in place. The primary remaining work is to wire up resource creation to the gRPC interfaces. In summary: * The basic structure is as follows: - Everything goes into sdk/python/. - sdk/python/cmd/pulumi-langhost-python is a Go language host that simply knows how to spawn Python processes to run out entrypoint in response to requests by the engine. - sdk/python/cmd/pulumi-langhost-python-exec is a little Python shim that is invoked by the language host to run Python programs, and is responsible for setting up the minimal goo before we can do so (RPC connections and the like). - sdk/python/lib/ contains a Python Pip package suitable for PyPi. - In there, we have two packages: the root pulumi package that contains all of the basic Pulumi programming model abstractions, and pulumi.runtime, which contains the implementation of resource registration, RPC interfacing with the engine, and so on. * Add logic in our test framework to conditionalize on the language type and react accordingly. This will allow us to skip Yarn for Python projects and eventually run Pip if there's a requirements.txt. * Created the basic project structure, including all of the usual Make targets for installing into the proper places. * Building also runs Pylint and we are clean. There are a few other minor things in here: * Add an "empty" test for both Node.js and Python. These pass. * Fix an existing bug in plugin shutdown logic. At some point, we started waiting for stderr/stdout to flush before shutting down the plugin; but if certain failures happen "early" during the plugin launch process, these channels will never get initialized and so waiting for them deadlocks. * Recently we seem to have added logic to delete test temp directories if a failure happened during initialization of said temp directories. This is unfortunate, because you often need to look at the temp directory to see what failed. We already clean them up elsewhere after the full test completes successfully, so I don't think we need to be doing this, and I've removed it. Still many loose ends (config, resources, etc), but it's a start!
2018-01-13 18:29:34 +00:00
// readPackageJSON unmarshals the package.json file located in pathToPackage.
func readPackageJSON(pathToPackage string) (map[string]interface{}, error) {
f, err := os.Open(filepath.Join(pathToPackage, "package.json"))
if err != nil {
return nil, fmt.Errorf("opening package.json: %w", err)
}
defer contract.IgnoreClose(f)
var ret map[string]interface{}
if err := json.NewDecoder(f).Decode(&ret); err != nil {
return nil, fmt.Errorf("decoding package.json: %w", err)
}
return ret, nil
}
func writePackageJSON(pathToPackage string, metadata map[string]interface{}) error {
// os.Create truncates the already existing file.
f, err := os.Create(filepath.Join(pathToPackage, "package.json"))
if err != nil {
return fmt.Errorf("opening package.json: %w", err)
}
defer contract.IgnoreClose(f)
encoder := json.NewEncoder(f)
encoder.SetEscapeHTML(false)
encoder.SetIndent("", " ")
if err := encoder.Encode(metadata); err != nil {
return fmt.Errorf("writing package.json: %w", err)
}
return nil
}
Get the empty Python program working This change gets enough of the Python SDK up and running that the empty Python program will work. Mostly just scaffolding, but the basic structure is now in place. The primary remaining work is to wire up resource creation to the gRPC interfaces. In summary: * The basic structure is as follows: - Everything goes into sdk/python/. - sdk/python/cmd/pulumi-langhost-python is a Go language host that simply knows how to spawn Python processes to run out entrypoint in response to requests by the engine. - sdk/python/cmd/pulumi-langhost-python-exec is a little Python shim that is invoked by the language host to run Python programs, and is responsible for setting up the minimal goo before we can do so (RPC connections and the like). - sdk/python/lib/ contains a Python Pip package suitable for PyPi. - In there, we have two packages: the root pulumi package that contains all of the basic Pulumi programming model abstractions, and pulumi.runtime, which contains the implementation of resource registration, RPC interfacing with the engine, and so on. * Add logic in our test framework to conditionalize on the language type and react accordingly. This will allow us to skip Yarn for Python projects and eventually run Pip if there's a requirements.txt. * Created the basic project structure, including all of the usual Make targets for installing into the proper places. * Building also runs Pylint and we are clean. There are a few other minor things in here: * Add an "empty" test for both Node.js and Python. These pass. * Fix an existing bug in plugin shutdown logic. At some point, we started waiting for stderr/stdout to flush before shutting down the plugin; but if certain failures happen "early" during the plugin launch process, these channels will never get initialized and so waiting for them deadlocks. * Recently we seem to have added logic to delete test temp directories if a failure happened during initialization of said temp directories. This is unfortunate, because you often need to look at the temp directory to see what failed. We already clean them up elsewhere after the full test completes successfully, so I don't think we need to be doing this, and I've removed it. Still many loose ends (config, resources, etc), but it's a start!
2018-01-13 18:29:34 +00:00
// preparePythonProject runs setup necessary to get a Python project ready for `pulumi` commands.
func (pt *ProgramTester) preparePythonProject(projinfo *engine.Projinfo) error {
cwd, _, err := projinfo.GetPwdMain()
if err != nil {
return err
}
if pt.opts.UsePipenv {
if err = pt.preparePythonProjectWithPipenv(cwd); err != nil {
return err
}
} else {
venvPath := "venv"
ProgramTest: Python: Use correct venv path for projects with `main` option (#15225) In preparation for supporting Python 3.12... I'm planning to remove `pulumi` from `requirements.txt` in a bunch of the integration tests (at least temporarily) because the current published version of `pulumi` can't be installed on Python 3.12 due to its dependency on `grpcio` which is broken on Python 3.12, and the tests specify a dependency on the locally built Pulumi Python SDK via `ProgramTestOptions.Dependencies`. However, after removing `pulumi` from `requirements.txt` for `TestPythonStackTruncate/main_dir_specified`, the test started to fail due to a missing `pulumi` package. It shouldn't fail because the test additionally requires the locally built SDK via `ProgramTestOptions.Dependencies`. The reason for the failure is because this test had a `main` specified in the project to a sub directory (`bar/`). Because of this, the venv created by `ProgramTest` is inside the sub directory. It would install the `requirements.txt` in that venv, and then install any `ProgramTestOptions.Dependencies` deps. Then it would record the `virtualenv` option in the project as just `venv`. But this option is relative to the project file and a `venv` directory next to the project doesn't exist -- it exists in the sub directory. So when the first `pulumi` operation was run, `pulumi` would see that the `venv` directory next to the project doesn't exist, and create it, installing the dependencies from `requirements.txt` in it and carrying on. With the change to remove `pulumi` from `requirements.txt` for this test, this would no longer work. When `pulumi` created the venv, it'd install an empty `requirements.txt` in the venv it created. And since it didn't have a `pulumi` package, it fails with a missing module. This change fixes `ProgramTest` to record the correct path to the venv in the project file. In most cases, this continues to be just `venv`. But if `main` is specified in the project such that the "cwd" path is different from the project file root, then that path is used for the venv. This way, the CLI will use the correct venv that includes dependencies from `ProgramTestOptions.Dependencies`.
2024-01-23 23:21:02 +00:00
if cwd != projinfo.Root {
venvPath = filepath.Join(cwd, "venv")
}
if pt.opts.GetUseSharedVirtualEnv() {
requirementsPath := filepath.Join(cwd, "requirements.txt")
requirementsmd5, err := hashFile(requirementsPath)
if err != nil {
return err
}
pt.opts.virtualEnvDir = fmt.Sprintf("pulumi-venv-%x", requirementsmd5)
venvPath = filepath.Join(pt.opts.SharedVirtualEnvPath, pt.opts.virtualEnvDir)
}
if err = pt.runPythonCommand("python-venv", []string{"-m", "venv", venvPath}, cwd); err != nil {
return err
}
projinfo.Proj.Runtime.SetOption("virtualenv", venvPath)
projfile := filepath.Join(projinfo.Root, workspace.ProjectFile+".yaml")
if err = projinfo.Proj.Save(projfile); err != nil {
return fmt.Errorf("saving project: %w", err)
}
if pt.opts.InstallDevReleases {
command := []string{"python", "-m", "pip", "install", "--pre", "pulumi"}
if err := pt.runVirtualEnvCommand("virtualenv-pip-install", command, cwd); err != nil {
return err
}
}
command := []string{"python", "-m", "pip", "install", "-r", "requirements.txt"}
if err := pt.runVirtualEnvCommand("virtualenv-pip-install", command, cwd); err != nil {
return err
}
}
if !pt.opts.RunUpdateTest {
if err = pt.installPipPackageDeps(cwd); err != nil {
return err
}
}
return nil
}
func (pt *ProgramTester) preparePythonProjectWithPipenv(cwd string) error {
// Allow ENV var based overload of desired Python version for
// the Pipenv environment. This is useful in CI scenarios that
// need to pin a specific version such as 3.9.x vs 3.10.x.
pythonVersion := os.Getenv("PYTHON_VERSION")
if pythonVersion == "" {
pythonVersion = "3"
}
// Create a new Pipenv environment. This bootstraps a new virtual environment containing the version of Python that
// we requested. Note that this version of Python is sourced from the machine, so you must first install the version
// of Python that you are requesting on the host machine before building a virtualenv for it.
if err := pt.runPipenvCommand("pipenv-new", []string{"--python", pythonVersion}, cwd); err != nil {
return err
}
// Install the package's dependencies. We do this by running `pip` inside the virtualenv that `pipenv` has created.
// We don't use `pipenv install` because we don't want a lock file and prefer the similar model of `pip install`
// which matches what our customers do
command := []string{"run", "pip", "install", "-r", "requirements.txt"}
if pt.opts.InstallDevReleases {
command = []string{"run", "pip", "install", "--pre", "-r", "requirements.txt"}
}
err := pt.runPipenvCommand("pipenv-install", command, cwd)
if err != nil {
return err
}
return nil
}
// YarnLinkPackageDeps bring in package dependencies via yarn
func (pt *ProgramTester) yarnLinkPackageDeps(cwd string) error {
for _, dependency := range pt.opts.Dependencies {
if err := pt.runYarnCommand("yarn-link", []string{"link", dependency}, cwd); err != nil {
return err
}
}
return nil
}
// InstallPipPackageDeps brings in package dependencies via pip install
func (pt *ProgramTester) installPipPackageDeps(cwd string) error {
var err error
for _, dep := range pt.opts.Dependencies {
// If the given filepath isn't absolute, make it absolute. We're about to pass it to pipenv and pipenv is
// operating inside of a random folder in /tmp.
if !filepath.IsAbs(dep) {
dep, err = filepath.Abs(dep)
if err != nil {
return err
}
}
if pt.opts.UsePipenv {
if err := pt.runPipenvCommand("pipenv-install-package",
[]string{"run", "pip", "install", "-e", dep}, cwd); err != nil {
return err
}
} else {
if err := pt.runVirtualEnvCommand("virtualenv-pip-install-package",
[]string{"python", "-m", "pip", "install", "-e", dep}, cwd); err != nil {
return err
}
}
}
Get the empty Python program working This change gets enough of the Python SDK up and running that the empty Python program will work. Mostly just scaffolding, but the basic structure is now in place. The primary remaining work is to wire up resource creation to the gRPC interfaces. In summary: * The basic structure is as follows: - Everything goes into sdk/python/. - sdk/python/cmd/pulumi-langhost-python is a Go language host that simply knows how to spawn Python processes to run out entrypoint in response to requests by the engine. - sdk/python/cmd/pulumi-langhost-python-exec is a little Python shim that is invoked by the language host to run Python programs, and is responsible for setting up the minimal goo before we can do so (RPC connections and the like). - sdk/python/lib/ contains a Python Pip package suitable for PyPi. - In there, we have two packages: the root pulumi package that contains all of the basic Pulumi programming model abstractions, and pulumi.runtime, which contains the implementation of resource registration, RPC interfacing with the engine, and so on. * Add logic in our test framework to conditionalize on the language type and react accordingly. This will allow us to skip Yarn for Python projects and eventually run Pip if there's a requirements.txt. * Created the basic project structure, including all of the usual Make targets for installing into the proper places. * Building also runs Pylint and we are clean. There are a few other minor things in here: * Add an "empty" test for both Node.js and Python. These pass. * Fix an existing bug in plugin shutdown logic. At some point, we started waiting for stderr/stdout to flush before shutting down the plugin; but if certain failures happen "early" during the plugin launch process, these channels will never get initialized and so waiting for them deadlocks. * Recently we seem to have added logic to delete test temp directories if a failure happened during initialization of said temp directories. This is unfortunate, because you often need to look at the temp directory to see what failed. We already clean them up elsewhere after the full test completes successfully, so I don't think we need to be doing this, and I've removed it. Still many loose ends (config, resources, etc), but it's a start!
2018-01-13 18:29:34 +00:00
return nil
}
func getVirtualenvBinPath(cwd, bin string, pt *ProgramTester) (string, error) {
virtualEnvBasePath := filepath.Join(cwd, pt.opts.virtualEnvDir)
if pt.opts.GetUseSharedVirtualEnv() {
virtualEnvBasePath = filepath.Join(pt.opts.SharedVirtualEnvPath, pt.opts.virtualEnvDir)
}
virtualenvBinPath := filepath.Join(virtualEnvBasePath, "bin", bin)
if runtime.GOOS == windowsOS {
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
virtualenvBinPath = filepath.Join(virtualEnvBasePath, "Scripts", bin+".exe")
}
if info, err := os.Stat(virtualenvBinPath); err != nil || info.IsDir() {
return "", fmt.Errorf("Expected %s to exist in virtual environment at %q", bin, virtualenvBinPath)
}
return virtualenvBinPath, nil
}
2020-08-31 23:08:15 +00:00
// getSanitizedPkg strips the version string from a go dep
2020-09-01 01:57:05 +00:00
// Note: most of the pulumi modules don't use major version subdirectories for modules
func getSanitizedModulePath(pkg string) string {
2020-08-31 23:08:15 +00:00
re := regexp.MustCompile(`v\d`)
v := re.FindString(pkg)
if v != "" {
return strings.TrimSuffix(strings.ReplaceAll(pkg, v, ""), "/")
2020-08-31 23:08:15 +00:00
}
return pkg
}
func getRewritePath(pkg string, gopath string, depRoot string) string {
var depParts []string
2020-09-01 01:57:05 +00:00
sanitizedPkg := getSanitizedModulePath(pkg)
2020-08-31 23:08:15 +00:00
splitPkg := strings.Split(sanitizedPkg, "/")
if depRoot != "" {
// Get the package name
// This is the value after "github.com/foo/bar"
2020-09-01 01:54:14 +00:00
repoName := splitPkg[2]
2020-09-01 02:01:23 +00:00
basePath := splitPkg[len(splitPkg)-1]
2020-09-01 01:54:14 +00:00
if basePath == repoName {
depParts = []string{depRoot, repoName}
2020-09-01 01:54:14 +00:00
} else {
depParts = []string{depRoot, repoName, basePath}
2020-09-01 01:54:14 +00:00
}
2020-08-31 23:08:15 +00:00
return filepath.Join(depParts...)
}
depParts = append([]string{gopath, "src"}, splitPkg...)
return filepath.Join(depParts...)
}
// Fetchs the GOPATH
func GoPath() (string, error) {
gopath := os.Getenv("GOPATH")
if gopath == "" {
usr, userErr := user.Current()
if userErr != nil {
return "", userErr
}
gopath = filepath.Join(usr.HomeDir, "go")
}
return gopath, nil
}
// prepareGoProject runs setup necessary to get a Go project ready for `pulumi` commands.
func (pt *ProgramTester) prepareGoProject(projinfo *engine.Projinfo) error {
// Go programs are compiled, so we will compile the project first.
goBin, err := pt.getGoBin()
if err != nil {
return fmt.Errorf("locating `go` binary: %w", err)
}
depRoot := os.Getenv("PULUMI_GO_DEP_ROOT")
gopath, userError := GoPath()
if userError != nil {
return userError
}
cwd, _, err := projinfo.GetPwdMain()
if err != nil {
return err
}
// initialize a go.mod for dependency resolution if one doesn't exist
_, err = os.Stat(filepath.Join(cwd, "go.mod"))
if err != nil {
err = pt.runCommand("go-mod-init", []string{goBin, "mod", "init"}, cwd)
if err != nil {
return err
}
}
// install dev dependencies if requested
if pt.opts.InstallDevReleases {
// We're currently only installing pulumi/pulumi dependencies, which always have
// "master" as the default branch.
defaultBranch := "master"
err = pt.runCommand("go-get-dev-deps", []string{
goBin, "get", "-u", "github.com/pulumi/pulumi/sdk/v3@" + defaultBranch,
}, cwd)
if err != nil {
return err
}
}
// link local dependencies
for _, dep := range pt.opts.Dependencies {
editStr, err := getEditStr(dep, gopath, depRoot)
if err != nil {
return fmt.Errorf("error generating go mod replacement for dep %q: %w", dep, err)
}
err = pt.runCommand("go-mod-edit", []string{goBin, "mod", "edit", "-replace", editStr}, cwd)
if err != nil {
return err
}
}
2021-08-19 19:03:52 +00:00
// tidy to resolve all transitive dependencies including from local dependencies above.
err = pt.runCommand("go-mod-tidy", []string{goBin, "mod", "tidy"}, cwd)
if err != nil {
return err
}
if pt.opts.RunBuild {
outBin := filepath.Join(gopath, "bin", string(projinfo.Proj.Name))
if runtime.GOOS == windowsOS {
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
outBin = outBin + ".exe"
}
err = pt.runCommand("go-build", []string{goBin, "build", "-o", outBin, "."}, cwd)
if err != nil {
return err
}
_, err = os.Stat(outBin)
if err != nil {
return fmt.Errorf("error finding built application artifact: %w", err)
}
}
return nil
}
func getEditStr(dep string, gopath string, depRoot string) (string, error) {
checkModName := true
var err error
var replacedModName string
var targetModDir string
if strings.ContainsRune(dep, '=') {
parts := strings.Split(dep, "=")
replacedModName = parts[0]
targetModDir = parts[1]
} else if !modfile.IsDirectoryPath(dep) {
replacedModName = dep
targetModDir = getRewritePath(dep, gopath, depRoot)
} else {
targetModDir = dep
replacedModName, err = getModName(targetModDir)
if err != nil {
return "", err
}
// We've read the package name from the go.mod file, skip redundant check below.
checkModName = false
}
targetModDir, err = filepath.Abs(targetModDir)
if err != nil {
return "", err
}
if checkModName {
targetModName, err := getModName(targetModDir)
if err != nil {
return "", fmt.Errorf("no go.mod at directory, set the path to the module explicitly or place "+
"the dependency in the path specified by PULUMI_GO_DEP_ROOT or the default GOPATH: %w", err)
}
targetPrefix, _, ok := module.SplitPathVersion(targetModName)
if !ok {
return "", fmt.Errorf("invalid module path for target module %q", targetModName)
}
replacedPrefix, _, ok := module.SplitPathVersion(replacedModName)
if !ok {
return "", fmt.Errorf("invalid module path for replaced module %q", replacedModName)
}
if targetPrefix != replacedPrefix {
return "", fmt.Errorf("found module path with prefix %s, expected %s", targetPrefix, replacedPrefix)
}
}
editStr := fmt.Sprintf("%s=%s", replacedModName, targetModDir)
return editStr, nil
}
func getModName(dir string) (string, error) {
pkgModPath := filepath.Join(dir, "go.mod")
pkgModData, err := os.ReadFile(pkgModPath)
if err != nil {
return "", fmt.Errorf("error reading go.mod at %s: %w", dir, err)
}
pkgMod, err := modfile.Parse(pkgModPath, pkgModData, nil)
if err != nil {
return "", fmt.Errorf("error parsing go.mod at %s: %w", dir, err)
}
return pkgMod.Module.Mod.Path, nil
}
// prepareDotNetProject runs setup necessary to get a .NET project ready for `pulumi` commands.
func (pt *ProgramTester) prepareDotNetProject(projinfo *engine.Projinfo) error {
dotNetBin, err := pt.getDotNetBin()
if err != nil {
return fmt.Errorf("locating `dotnet` binary: %w", err)
}
cwd, _, err := projinfo.GetPwdMain()
if err != nil {
return err
}
localNuget := os.Getenv("PULUMI_LOCAL_NUGET")
if localNuget == "" {
home := os.Getenv("HOME")
localNuget = filepath.Join(home, ".pulumi-dev", "nuget")
}
if pt.opts.InstallDevReleases {
err = pt.runCommand("dotnet-add-package",
[]string{
dotNetBin, "add", "package", "Pulumi",
"--prerelease",
},
cwd)
if err != nil {
return err
}
}
for _, dep := range pt.opts.Dependencies {
// dotnet add package requires a specific version in case of a pre-release, so we have to look it up.
globPattern := filepath.Join(localNuget, dep+".?.*.nupkg")
matches, err := filepath.Glob(globPattern)
if err != nil {
return fmt.Errorf("failed to find a local Pulumi NuGet package: %w", err)
}
if len(matches) != 1 {
return fmt.Errorf("attempting to find a local NuGet package %s by searching %s yielded %d results: %v",
dep,
globPattern,
len(matches),
matches)
}
file := filepath.Base(matches[0])
r := strings.NewReplacer(dep+".", "", ".nupkg", "")
version := r.Replace(file)
// We don't restore because the program might depend on external
// packages which cannot be found in our local nuget source. A restore
// will happen automatically as part of the `pulumi up`.
err = pt.runCommand("dotnet-add-package",
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
[]string{
dotNetBin, "add", "package", dep,
"-v", version,
"-s", localNuget,
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
"--no-restore",
},
cwd)
if err != nil {
return fmt.Errorf("failed to add dependency on %s: %w", dep, err)
}
}
return nil
}
func (pt *ProgramTester) prepareYAMLProject(projinfo *engine.Projinfo) error {
// YAML doesn't need any system setup, and should auto-install required plugins
return nil
}
func (pt *ProgramTester) prepareJavaProject(projinfo *engine.Projinfo) error {
// Java doesn't need any system setup, and should auto-install required plugins
return nil
}
func (pt *ProgramTester) defaultPrepareProject(projinfo *engine.Projinfo) error {
// Based on the language, invoke the right routine to prepare the target directory.
switch rt := projinfo.Proj.Runtime.Name(); rt {
case NodeJSRuntime:
return pt.prepareNodeJSProject(projinfo)
case PythonRuntime:
return pt.preparePythonProject(projinfo)
case GoRuntime:
return pt.prepareGoProject(projinfo)
case DotNetRuntime:
return pt.prepareDotNetProject(projinfo)
case YAMLRuntime:
return pt.prepareYAMLProject(projinfo)
case JavaRuntime:
return pt.prepareJavaProject(projinfo)
default:
return fmt.Errorf("unrecognized project runtime: %s", rt)
}
}