Merge branch 'master' into features/dedeasync
This commit is contained in:
commit
77ecc73622
BIN
.ionide/symbolCache.db
Normal file
BIN
.ionide/symbolCache.db
Normal file
Binary file not shown.
69
CHANGELOG.md
69
CHANGELOG.md
|
@ -1,14 +1,77 @@
|
|||
CHANGELOG
|
||||
=========
|
||||
|
||||
## HEAD (Unreleased)
|
||||
|
||||
- Improvements to `pulumi policy` functionality. Add ability to remove & disable Policy Packs.
|
||||
|
||||
- Breaking change for Policy which is in Public Preview: Change `pulumi policy apply` to `pulumi policy enable`, and allow users to specify the Policy Group.
|
||||
|
||||
## 1.8.1 (2019-12-20)
|
||||
|
||||
- Fix a panic in `pulumi stack select`. [#3687](https://github.com/pulumi/pulumi/pull/3687)
|
||||
|
||||
## 1.8.0 (2019-12-19)
|
||||
|
||||
- Update version of TypeScript used by Pulumi to `3.7.3`. [#3627](https://github.com/pulumi/pulumi/pull/3627)
|
||||
|
||||
- Add support for GOOGLE_CREDENTIALS when using Google Cloud Storage backend. [#2906](https://github.com/pulumi/pulumi/pull/2906)
|
||||
|
||||
```sh
|
||||
export GOOGLE_CREDENTIALS="$(cat ~/service-account-credentials.json)"
|
||||
pulumi login gs://my-bucket
|
||||
```
|
||||
|
||||
- Support for using `Config`, `getProject()`, `getStack()`, and `isDryRun()` from Policy Packs.
|
||||
[#3612](https://github.com/pulumi/pulumi/pull/3612)
|
||||
- Top-level Stack component in the .NET SDK.
|
||||
[#3618](https://github.com/pulumi/pulumi/pull/3618)
|
||||
|
||||
- Add the .NET Core 3.0 runtime to the `pulumi/pulumi` container. [#3616](https://github.com/pulumi/pulumi/pull/3616)
|
||||
|
||||
- Add `pulumi preview` support for `--refresh`, `--target`, `--replace`, `--target-replace` and
|
||||
`--target-dependents` to align with `pulumi up`.
|
||||
[#3675](https://github.com/pulumi/pulumi/pull/3675)
|
||||
|
||||
- `ComponentResource`s now have built-in support for asynchronously constructing their children. [#3676](https://github.com/pulumi/pulumi/pull/3676)
|
||||
|
||||
- `Output.apply` (for the JS, Python and .Net sdks) has updated semantics, and will lift dependencies from inner Outputs to the returned Output.
|
||||
[#3663](https://github.com/pulumi/pulumi/pull/3663)
|
||||
|
||||
- Fix bug in determining PRNumber and BuildURL for an Azure Pipelines CI environment. [#3677](https://github.com/pulumi/pulumi/pull/3677)
|
||||
|
||||
- Improvements to `pulumi policy` functionality. Add ability to remove & disable Policy Packs.
|
||||
|
||||
- Breaking change for Policy which is in Public Preview: Change `pulumi policy apply` to `pulumi policy enable`, and allow users to specify the Policy Group.
|
||||
|
||||
## 1.7.1 (2019-12-13)
|
||||
|
||||
- Fix [SxS issue](https://github.com/pulumi/pulumi/issues/3652) introduced in 1.7.0 when assigning
|
||||
`Output`s across different versions of the `@pulumi/pulumi` SDK. [#3658](https://github.com/pulumi/pulumi/pull/3658)
|
||||
|
||||
## 1.7.0 (2019-12-11)
|
||||
|
||||
- A Pulumi JavaScript/TypeScript program can now consist of a single exported top level function. This
|
||||
allows for an easy approach to create a Pulumi program that needs to perform `async`/`await`
|
||||
operations at the top-level. [#3321](https://github.com/pulumi/pulumi/pull/3321)
|
||||
|
||||
```ts
|
||||
// JavaScript
|
||||
module.exports = async () => {
|
||||
}
|
||||
|
||||
//TypeScript
|
||||
export = async () => {
|
||||
}
|
||||
```
|
||||
|
||||
## 1.6.1 (2019-11-26)
|
||||
|
||||
- Support passing a parent and providers for `ReadResource`, `RegisterResource`, and `Invoke` in the go SDK. [#3563](https://github.com/pulumi/pulumi/pull/3563)
|
||||
|
||||
- Fix go SDK ReadResource [#3581](https://github.com/pulumi/pulumi/pull/3581)
|
||||
- Fix go SDK ReadResource. [#3581](https://github.com/pulumi/pulumi/pull/3581)
|
||||
|
||||
- Fix go SDK DeleteBeforeReplace [#3572](https://github.com/pulumi/pulumi/pull/3572)
|
||||
- Fix go SDK DeleteBeforeReplace. [#3572](https://github.com/pulumi/pulumi/pull/3572)
|
||||
|
||||
- Support for setting the `PULUMI_PREFER_YARN` environment variable to opt-in to using `yarn` instead of `npm` for
|
||||
installing Node.js dependencies. [#3556](https://github.com/pulumi/pulumi/pull/3556)
|
||||
|
@ -36,7 +99,7 @@ export default async () => {
|
|||
|
||||
- Support for config.GetObject and related variants for Golang. [#3526](https://github.com/pulumi/pulumi/pull/3526)
|
||||
|
||||
- Add support for IgnoreChanges in the go SDK [#3514](https://github.com/pulumi/pulumi/pull/3514)
|
||||
- Add support for IgnoreChanges in the go SDK. [#3514](https://github.com/pulumi/pulumi/pull/3514)
|
||||
|
||||
- Support for a `go run` style workflow. Building or installing a pulumi program written in go is
|
||||
now optional. [#3503](https://github.com/pulumi/pulumi/pull/3503)
|
||||
|
|
|
@ -152,7 +152,7 @@ func runNew(args newArgs) error {
|
|||
|
||||
// Do a dry run, if we're not forcing files to be overwritten.
|
||||
if !args.force {
|
||||
if err = workspace.CopyTemplateFilesDryRun(template.Dir, cwd); err != nil {
|
||||
if err = workspace.CopyTemplateFilesDryRun(template.Dir, cwd, args.name); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return errors.Wrapf(err, "template '%s' not found", args.templateNameOrURL)
|
||||
}
|
||||
|
@ -516,7 +516,7 @@ func promptAndCreateStack(prompt promptForValueFunc,
|
|||
}
|
||||
|
||||
for {
|
||||
stackName, err := prompt(yes, "stack name", "dev", false, workspace.ValidateStackName, opts)
|
||||
stackName, err := prompt(yes, "stack name", "dev", false, b.ValidateStackName, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -26,9 +26,11 @@ func newPolicyCmd() *cobra.Command {
|
|||
Args: cmdutil.NoArgs,
|
||||
}
|
||||
|
||||
cmd.AddCommand(newPolicyDisableCmd())
|
||||
cmd.AddCommand(newPolicyEnableCmd())
|
||||
cmd.AddCommand(newPolicyNewCmd())
|
||||
cmd.AddCommand(newPolicyPublishCmd())
|
||||
cmd.AddCommand(newPolicyApplyCmd())
|
||||
cmd.AddCommand(newPolicyRmCmd())
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
61
cmd/policy_disable.go
Normal file
61
cmd/policy_disable.go
Normal file
|
@ -0,0 +1,61 @@
|
|||
// Copyright 2016-2020, Pulumi Corporation.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/pulumi/pulumi/pkg/backend"
|
||||
"github.com/pulumi/pulumi/pkg/util/cmdutil"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type policyDisableArgs struct {
|
||||
policyGroup string
|
||||
}
|
||||
|
||||
func newPolicyDisableCmd() *cobra.Command {
|
||||
args := policyDisableArgs{}
|
||||
|
||||
var cmd = &cobra.Command{
|
||||
Use: "disable <org-name>/<policy-pack-name> <version>",
|
||||
Args: cmdutil.ExactArgs(2),
|
||||
Short: "Disable a Policy Pack for a Pulumi organization",
|
||||
Long: "Disable a Policy Pack for a Pulumi organization",
|
||||
Run: cmdutil.RunFunc(func(cmd *cobra.Command, cliArgs []string) error {
|
||||
// Obtain current PolicyPack, tied to the Pulumi service backend.
|
||||
policyPack, err := requirePolicyPack(cliArgs[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
version, err := strconv.Atoi(cliArgs[1])
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "Could not parse version (should be an integer)")
|
||||
}
|
||||
|
||||
// Attempt to disable the Policy Pack.
|
||||
return policyPack.Disable(commandContext(), args.policyGroup, backend.PolicyPackOperation{
|
||||
Version: version, Scopes: cancellationScopes})
|
||||
}),
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().StringVar(
|
||||
&args.policyGroup, "policy-group", "",
|
||||
"The Policy Group for which the Policy Pack will be disabled; if not specified, the default Policy Group is used")
|
||||
|
||||
return cmd
|
||||
}
|
61
cmd/policy_enable.go
Normal file
61
cmd/policy_enable.go
Normal file
|
@ -0,0 +1,61 @@
|
|||
// Copyright 2016-2020, Pulumi Corporation.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/pulumi/pulumi/pkg/backend"
|
||||
"github.com/pulumi/pulumi/pkg/util/cmdutil"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type policyEnableArgs struct {
|
||||
policyGroup string
|
||||
}
|
||||
|
||||
func newPolicyEnableCmd() *cobra.Command {
|
||||
args := policyEnableArgs{}
|
||||
|
||||
var cmd = &cobra.Command{
|
||||
Use: "enable <org-name>/<policy-pack-name> <version>",
|
||||
Args: cmdutil.ExactArgs(2),
|
||||
Short: "Enable a Policy Pack for a Pulumi organization",
|
||||
Long: "Enable a Policy Pack for a Pulumi organization",
|
||||
Run: cmdutil.RunFunc(func(cmd *cobra.Command, cliArgs []string) error {
|
||||
// Obtain current PolicyPack, tied to the Pulumi service backend.
|
||||
policyPack, err := requirePolicyPack(cliArgs[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
version, err := strconv.Atoi(cliArgs[1])
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "Could not parse version (should be an integer)")
|
||||
}
|
||||
|
||||
// Attempt to enable the PolicyPack.
|
||||
return policyPack.Apply(commandContext(), args.policyGroup, backend.PolicyPackOperation{
|
||||
Version: version, Scopes: cancellationScopes})
|
||||
}),
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().StringVar(
|
||||
&args.policyGroup, "policy-group", "",
|
||||
"The Policy Group for which the Policy Pack will be enabled; if not specified, the default Policy Group is used")
|
||||
|
||||
return cmd
|
||||
}
|
|
@ -143,7 +143,7 @@ func runNewPolicyPack(args newPolicyArgs) error {
|
|||
|
||||
// Do a dry run, if we're not forcing files to be overwritten.
|
||||
if !args.force {
|
||||
if err = workspace.CopyTemplateFilesDryRun(template.Dir, cwd); err != nil {
|
||||
if err = workspace.CopyTemplateFilesDryRun(template.Dir, cwd, ""); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return errors.Wrapf(err, "template '%s' not found", args.templateNameOrURL)
|
||||
}
|
||||
|
|
|
@ -23,32 +23,27 @@ import (
|
|||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func newPolicyApplyCmd() *cobra.Command {
|
||||
func newPolicyRmCmd() *cobra.Command {
|
||||
var cmd = &cobra.Command{
|
||||
Use: "apply <org-name>/<policy-pack-name> <version>",
|
||||
Use: "rm <org-name>/<policy-pack-name> <version>",
|
||||
Args: cmdutil.ExactArgs(2),
|
||||
Short: "Apply a Policy Pack to a Pulumi organization",
|
||||
Long: "Apply a Policy Pack to a Pulumi organization",
|
||||
Run: cmdutil.RunFunc(func(cmd *cobra.Command, args []string) error {
|
||||
//
|
||||
Short: "Removes a Policy Pack from a Pulumi organization",
|
||||
Long: "Removes a Policy Pack from a Pulumi organization. " +
|
||||
"The Policy Pack must be disabled from all Policy Groups before it can be removed.",
|
||||
Run: cmdutil.RunFunc(func(cmd *cobra.Command, cliArgs []string) error {
|
||||
// Obtain current PolicyPack, tied to the Pulumi service backend.
|
||||
//
|
||||
|
||||
policyPack, err := requirePolicyPack(args[0])
|
||||
policyPack, err := requirePolicyPack(cliArgs[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
version, err := strconv.Atoi(args[1])
|
||||
version, err := strconv.Atoi(cliArgs[1])
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "Could not parse version (should be an integer)")
|
||||
}
|
||||
|
||||
//
|
||||
// Attempt to publish the PolicyPack.
|
||||
//
|
||||
|
||||
return policyPack.Apply(commandContext(), backend.ApplyOperation{
|
||||
// Attempt to remove the Policy Pack.
|
||||
return policyPack.Remove(commandContext(), backend.PolicyPackOperation{
|
||||
Version: version, Scopes: cancellationScopes})
|
||||
}),
|
||||
}
|
|
@ -21,6 +21,7 @@ import (
|
|||
"github.com/pulumi/pulumi/pkg/backend"
|
||||
"github.com/pulumi/pulumi/pkg/backend/display"
|
||||
"github.com/pulumi/pulumi/pkg/engine"
|
||||
"github.com/pulumi/pulumi/pkg/resource"
|
||||
"github.com/pulumi/pulumi/pkg/util/cmdutil"
|
||||
"github.com/pulumi/pulumi/pkg/util/result"
|
||||
)
|
||||
|
@ -34,16 +35,21 @@ func newPreviewCmd() *cobra.Command {
|
|||
var configPath bool
|
||||
|
||||
// Flags for engine.UpdateOptions.
|
||||
var jsonDisplay bool
|
||||
var policyPackPaths []string
|
||||
var diffDisplay bool
|
||||
var eventLogPath string
|
||||
var jsonDisplay bool
|
||||
var parallel int
|
||||
var refresh bool
|
||||
var showConfig bool
|
||||
var showReplacementSteps bool
|
||||
var showSames bool
|
||||
var showReads bool
|
||||
var suppressOutputs bool
|
||||
var targets []string
|
||||
var replaces []string
|
||||
var targetReplaces []string
|
||||
var targetDependents bool
|
||||
|
||||
var cmd = &cobra.Command{
|
||||
Use: "preview",
|
||||
|
@ -68,29 +74,21 @@ func newPreviewCmd() *cobra.Command {
|
|||
displayType = display.DisplayDiff
|
||||
}
|
||||
|
||||
opts := backend.UpdateOptions{
|
||||
Engine: engine.UpdateOptions{
|
||||
LocalPolicyPackPaths: policyPackPaths,
|
||||
Parallel: parallel,
|
||||
Debug: debug,
|
||||
UseLegacyDiff: useLegacyDiff(),
|
||||
},
|
||||
Display: display.Options{
|
||||
Color: cmdutil.GetGlobalColorization(),
|
||||
ShowConfig: showConfig,
|
||||
ShowReplacementSteps: showReplacementSteps,
|
||||
ShowSameResources: showSames,
|
||||
ShowReads: showReads,
|
||||
SuppressOutputs: suppressOutputs,
|
||||
IsInteractive: cmdutil.Interactive(),
|
||||
Type: displayType,
|
||||
JSONDisplay: jsonDisplay,
|
||||
EventLogPath: eventLogPath,
|
||||
Debug: debug,
|
||||
},
|
||||
displayOpts := display.Options{
|
||||
Color: cmdutil.GetGlobalColorization(),
|
||||
ShowConfig: showConfig,
|
||||
ShowReplacementSteps: showReplacementSteps,
|
||||
ShowSameResources: showSames,
|
||||
ShowReads: showReads,
|
||||
SuppressOutputs: suppressOutputs,
|
||||
IsInteractive: cmdutil.Interactive(),
|
||||
Type: displayType,
|
||||
JSONDisplay: jsonDisplay,
|
||||
EventLogPath: eventLogPath,
|
||||
Debug: debug,
|
||||
}
|
||||
|
||||
s, err := requireStack(stack, true, opts.Display, true /*setCurrent*/)
|
||||
s, err := requireStack(stack, true, displayOpts, true /*setCurrent*/)
|
||||
if err != nil {
|
||||
return result.FromError(err)
|
||||
}
|
||||
|
@ -105,7 +103,7 @@ func newPreviewCmd() *cobra.Command {
|
|||
return result.FromError(err)
|
||||
}
|
||||
|
||||
m, err := getUpdateMetadata("", root)
|
||||
m, err := getUpdateMetadata(message, root)
|
||||
if err != nil {
|
||||
return result.FromError(errors.Wrap(err, "gathering environment metadata"))
|
||||
}
|
||||
|
@ -120,6 +118,35 @@ func newPreviewCmd() *cobra.Command {
|
|||
return result.FromError(errors.Wrap(err, "getting stack configuration"))
|
||||
}
|
||||
|
||||
targetURNs := []resource.URN{}
|
||||
for _, t := range targets {
|
||||
targetURNs = append(targetURNs, resource.URN(t))
|
||||
}
|
||||
|
||||
replaceURNs := []resource.URN{}
|
||||
for _, r := range replaces {
|
||||
replaceURNs = append(replaceURNs, resource.URN(r))
|
||||
}
|
||||
|
||||
for _, tr := range targetReplaces {
|
||||
targetURNs = append(targetURNs, resource.URN(tr))
|
||||
replaceURNs = append(replaceURNs, resource.URN(tr))
|
||||
}
|
||||
|
||||
opts := backend.UpdateOptions{
|
||||
Engine: engine.UpdateOptions{
|
||||
LocalPolicyPackPaths: policyPackPaths,
|
||||
Parallel: parallel,
|
||||
Debug: debug,
|
||||
Refresh: refresh,
|
||||
ReplaceTargets: replaceURNs,
|
||||
UseLegacyDiff: useLegacyDiff(),
|
||||
UpdateTargets: targetURNs,
|
||||
TargetDependents: targetDependents,
|
||||
},
|
||||
Display: displayOpts,
|
||||
}
|
||||
|
||||
changes, res := s.Preview(commandContext(), backend.UpdateOperation{
|
||||
Proj: proj,
|
||||
Root: root,
|
||||
|
@ -164,6 +191,21 @@ func newPreviewCmd() *cobra.Command {
|
|||
&message, "message", "m", "",
|
||||
"Optional message to associate with the preview operation")
|
||||
|
||||
cmd.PersistentFlags().StringArrayVarP(
|
||||
&targets, "target", "t", []string{},
|
||||
"Specify a single resource URN to update. Other resources will not be updated."+
|
||||
" Multiple resources can be specified using --target urn1 --target urn2")
|
||||
cmd.PersistentFlags().StringArrayVar(
|
||||
&replaces, "replace", []string{},
|
||||
"Specify resources to replace. Multiple resources can be specified using --replace run1 --replace urn2")
|
||||
cmd.PersistentFlags().StringArrayVar(
|
||||
&targetReplaces, "target-replace", []string{},
|
||||
"Specify a single resource URN to replace. Other resources will not be updated."+
|
||||
" Shorthand for --target urn --replace urn.")
|
||||
cmd.PersistentFlags().BoolVar(
|
||||
&targetDependents, "target-dependents", false,
|
||||
"Allows updating of dependent targets discovered but not specified in --target list")
|
||||
|
||||
// Flags for engine.UpdateOptions.
|
||||
if hasDebugCommands() || hasExperimentalCommands() {
|
||||
cmd.PersistentFlags().StringSliceVar(
|
||||
|
@ -179,6 +221,9 @@ func newPreviewCmd() *cobra.Command {
|
|||
cmd.PersistentFlags().IntVarP(
|
||||
¶llel, "parallel", "p", defaultParallel,
|
||||
"Allow P resource operations to run in parallel at once (1 for no parallelism). Defaults to unbounded.")
|
||||
cmd.PersistentFlags().BoolVarP(
|
||||
&refresh, "refresh", "r", false,
|
||||
"Refresh the state of the stack's resources before this update")
|
||||
cmd.PersistentFlags().BoolVar(
|
||||
&showConfig, "show-config", false,
|
||||
"Show configuration keys and variables")
|
||||
|
|
|
@ -18,7 +18,6 @@ import (
|
|||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/pulumi/pulumi/pkg/workspace"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/pulumi/pulumi/pkg/backend/display"
|
||||
|
@ -87,7 +86,7 @@ func newStackInitCmd() *cobra.Command {
|
|||
"use the format <org-name>/<stack-name> (e.g. `acmecorp/dev`).\n")
|
||||
}
|
||||
|
||||
name, nameErr := promptForValue(false, "stack name", "dev", false, workspace.ValidateStackName, opts)
|
||||
name, nameErr := promptForValue(false, "stack name", "dev", false, b.ValidateStackName, opts)
|
||||
if nameErr != nil {
|
||||
return nameErr
|
||||
}
|
||||
|
@ -98,7 +97,7 @@ func newStackInitCmd() *cobra.Command {
|
|||
return errors.New("missing stack name")
|
||||
}
|
||||
|
||||
if err := workspace.ValidateStackName(stackName); err != nil {
|
||||
if err := b.ValidateStackName(stackName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ import (
|
|||
"github.com/pulumi/pulumi/pkg/backend/display"
|
||||
"github.com/pulumi/pulumi/pkg/backend/state"
|
||||
"github.com/pulumi/pulumi/pkg/util/cmdutil"
|
||||
"github.com/pulumi/pulumi/pkg/util/contract"
|
||||
)
|
||||
|
||||
// newStackSelectCmd handles both the "local" and "cloud" scenarios in its implementation.
|
||||
|
@ -55,7 +56,7 @@ func newStackSelectCmd() *cobra.Command {
|
|||
}
|
||||
|
||||
if stack != "" {
|
||||
// A stack was given, ask the backend about it
|
||||
// A stack was given, ask the backend about it.
|
||||
stackRef, stackErr := b.ParseStackReference(stack)
|
||||
if stackErr != nil {
|
||||
return stackErr
|
||||
|
@ -76,6 +77,8 @@ func newStackSelectCmd() *cobra.Command {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
contract.Assert(stack != nil)
|
||||
return state.SetCurrentStack(stack.Ref().String())
|
||||
|
||||
}),
|
||||
|
|
12
cmd/util.go
12
cmd/util.go
|
@ -229,6 +229,8 @@ func requireCurrentStack(offerNew bool, opts display.Options, setCurrent bool) (
|
|||
// true, then the option to create an entirely new stack is provided and will create one as desired.
|
||||
func chooseStack(
|
||||
b backend.Backend, offerNew bool, opts display.Options, setCurrent bool) (backend.Stack, error) {
|
||||
ctx := commandContext()
|
||||
|
||||
// Prepare our error in case we need to issue it. Bail early if we're not interactive.
|
||||
var chooseStackErr string
|
||||
if offerNew {
|
||||
|
@ -247,7 +249,7 @@ func chooseStack(
|
|||
|
||||
// List stacks as available options.
|
||||
project := string(proj.Name)
|
||||
summaries, err := b.ListStacks(commandContext(), backend.ListStacksFilter{Project: &project})
|
||||
summaries, err := b.ListStacks(ctx, backend.ListStacksFilter{Project: &project})
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not query backend for stacks")
|
||||
}
|
||||
|
@ -270,7 +272,7 @@ func chooseStack(
|
|||
|
||||
// If a stack is already selected, make that the default.
|
||||
var current string
|
||||
currStack, currErr := state.CurrentStack(commandContext(), b)
|
||||
currStack, currErr := state.CurrentStack(ctx, b)
|
||||
contract.IgnoreError(currErr)
|
||||
if currStack != nil {
|
||||
current = currStack.Ref().String()
|
||||
|
@ -321,10 +323,14 @@ func chooseStack(
|
|||
if err != nil {
|
||||
return nil, errors.Wrap(err, "parsing selected stack")
|
||||
}
|
||||
stack, err := b.GetStack(commandContext(), stackRef)
|
||||
// GetStack may return (nil, nil) if the stack isn't found.
|
||||
stack, err := b.GetStack(ctx, stackRef)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "getting selected stack")
|
||||
}
|
||||
if stack == nil {
|
||||
return nil, errors.Errorf("no stack named '%s' found", stackRef)
|
||||
}
|
||||
|
||||
// If setCurrent is true, we'll persist this choice so it'll be used for future CLI operations.
|
||||
if setCurrent {
|
||||
|
|
4
dist/actions/entrypoint.sh
vendored
4
dist/actions/entrypoint.sh
vendored
|
@ -90,6 +90,10 @@ if [ -e package.json ]; then
|
|||
if [ -f yarn.lock ] || [ ! -z $USE_YARN ]; then
|
||||
yarn install
|
||||
else
|
||||
# Set npm auth token if one is provided.
|
||||
if [ ! -z "$NPM_AUTH_TOKEN" ]; then
|
||||
echo "//registry.npmjs.org/:_authToken=$NPM_AUTH_TOKEN" > ~/.npmrc
|
||||
fi
|
||||
npm install
|
||||
fi
|
||||
fi
|
||||
|
|
40
dist/docker/Dockerfile → dist/pulumi/Dockerfile
vendored
40
dist/docker/Dockerfile → dist/pulumi/Dockerfile
vendored
|
@ -1,5 +1,4 @@
|
|||
FROM python:3.7-slim-stretch
|
||||
# TODO[pulumi/pulumi#1986]: consider switching to, or supporting, Alpine Linux for smaller image sizes.
|
||||
|
||||
LABEL "repository"="https://github.com/pulumi/pulumi"
|
||||
LABEL "homepage"="https://pulumi.com/"
|
||||
|
@ -15,37 +14,46 @@ RUN apt-get update -y && \
|
|||
git \
|
||||
gnupg \
|
||||
software-properties-common \
|
||||
&& \
|
||||
# Get all of the signatures we need all at once
|
||||
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add - && \
|
||||
curl -fsSL https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
|
||||
curl -fsSL https://download.docker.com/linux/debian/gpg | apt-key add && \
|
||||
wget && \
|
||||
# Get all of the signatures we need all at once.
|
||||
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add - && \
|
||||
curl -fsSL https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
|
||||
curl -fsSL https://download.docker.com/linux/debian/gpg | apt-key add - && \
|
||||
curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add - && \
|
||||
curl -fsSL https://packages.microsoft.com/keys/microsoft.asc | apt-key add - && \
|
||||
curl -fsSL https://packages.microsoft.com/keys/microsoft.asc | apt-key add - && \
|
||||
# IAM Authenticator for EKS
|
||||
curl -fsSLo /usr/bin/aws-iam-authenticator https://amazon-eks.s3-us-west-2.amazonaws.com/1.10.3/2018-07-26/bin/linux/amd64/aws-iam-authenticator && \
|
||||
chmod +x /usr/bin/aws-iam-authenticator && \
|
||||
# Add additional apt repos all at once
|
||||
echo "deb https://deb.nodesource.com/node_11.x $(lsb_release -cs) main" | tee /etc/apt/sources.list.d/node.list && \
|
||||
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \
|
||||
echo "deb [arch=amd64] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list && \
|
||||
echo "deb http://packages.cloud.google.com/apt cloud-sdk-$(lsb_release -cs) main" | tee /etc/apt/sources.list.d/google-cloud-sdk.list && \
|
||||
echo "deb http://apt.kubernetes.io/ kubernetes-xenial main" | tee /etc/apt/sources.list.d/kubernetes.list &&\
|
||||
echo "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ $(lsb_release -cs) main" | tee /etc/apt/sources.list.d/azure.list && \
|
||||
echo "deb https://deb.nodesource.com/node_11.x $(lsb_release -cs) main" | tee /etc/apt/sources.list.d/node.list && \
|
||||
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \
|
||||
echo "deb [arch=amd64] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list && \
|
||||
echo "deb http://packages.cloud.google.com/apt cloud-sdk-$(lsb_release -cs) main" | tee /etc/apt/sources.list.d/google-cloud-sdk.list && \
|
||||
echo "deb http://apt.kubernetes.io/ kubernetes-xenial main" | tee /etc/apt/sources.list.d/kubernetes.list && \
|
||||
echo "deb [arch=amd64] https://packages.microsoft.com/repos/azure-cli/ $(lsb_release -cs) main" | tee /etc/apt/sources.list.d/azure.list && \
|
||||
# Install second wave of dependencies
|
||||
apt-get update -y && \
|
||||
apt-get install -y \
|
||||
apt-get install -y \
|
||||
azure-cli \
|
||||
docker-ce \
|
||||
google-cloud-sdk \
|
||||
kubectl \
|
||||
nodejs \
|
||||
yarn \
|
||||
&& \
|
||||
yarn && \
|
||||
pip install awscli --upgrade && \
|
||||
# Clean up the lists work
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install .NET Core SDK
|
||||
RUN wget -q https://packages.microsoft.com/config/ubuntu/19.04/packages-microsoft-prod.deb \
|
||||
-O packages-microsoft-prod.deb && \
|
||||
dpkg -i packages-microsoft-prod.deb && \
|
||||
rm packages-microsoft-prod.deb && \
|
||||
apt-get update -y && \
|
||||
apt-get install -y \
|
||||
apt-transport-https \
|
||||
dotnet-sdk-3.1
|
||||
|
||||
# Install Helm
|
||||
RUN curl -L https://git.io/get_helm.sh | bash
|
||||
|
1
go.mod
1
go.mod
|
@ -57,6 +57,7 @@ require (
|
|||
gocloud.dev/secrets/hashivault v0.18.0
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5
|
||||
golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58
|
||||
golang.org/x/sys v0.0.0-20190620070143-6f217b454f45
|
||||
google.golang.org/api v0.6.0
|
||||
|
|
|
@ -14,6 +14,9 @@
|
|||
|
||||
package apitype
|
||||
|
||||
// DefaultPolicyGroup is the name of the default Policy Group for organizations.
|
||||
const DefaultPolicyGroup = "default-policy-group"
|
||||
|
||||
// CreatePolicyPackRequest defines the request body for creating a new Policy
|
||||
// Pack for an organization. The request contains the metadata related to the
|
||||
// Policy Pack.
|
||||
|
@ -107,3 +110,27 @@ type GetStackPolicyPacksResponse struct {
|
|||
// RequiredPolicies is a list of required Policy Packs to run during the update.
|
||||
RequiredPolicies []RequiredPolicy `json:"requiredPolicies,omitempty"`
|
||||
}
|
||||
|
||||
// UpdatePolicyGroupRequest modifies a Policy Group.
|
||||
type UpdatePolicyGroupRequest struct {
|
||||
NewName *string `json:"newName,omitempty"`
|
||||
|
||||
AddStack *PulumiStackReference `json:"addStack,omitempty"`
|
||||
RemoveStack *PulumiStackReference `json:"removeStack,omitempty"`
|
||||
|
||||
AddPolicyPack *PolicyPackMetadata `json:"addPolicyPack,omitempty"`
|
||||
RemovePolicyPack *PolicyPackMetadata `json:"removePolicyPack,omitempty"`
|
||||
}
|
||||
|
||||
// PulumiStackReference contains the StackName and ProjectName of the stack.
|
||||
type PulumiStackReference struct {
|
||||
Name string `json:"name"`
|
||||
RoutingProject string `json:"routingProject"`
|
||||
}
|
||||
|
||||
// PolicyPackMetadata is the metadata of a Policy Pack.
|
||||
type PolicyPackMetadata struct {
|
||||
Name string `json:"name"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Version int `json:"version"`
|
||||
}
|
||||
|
|
|
@ -67,7 +67,7 @@ func (e OverStackLimitError) Error() string {
|
|||
// StackReference is an opaque type that refers to a stack managed by a backend. The CLI uses the ParseStackReference
|
||||
// method to turn a string like "my-great-stack" or "pulumi/my-great-stack" into a stack reference that can be used to
|
||||
// interact with the stack via the backend. Stack references are specific to a given backend and different back ends
|
||||
// may interpret the string passed to ParseStackReference differently
|
||||
// may interpret the string passed to ParseStackReference differently.
|
||||
type StackReference interface {
|
||||
// fmt.Stringer's String() method returns a string of the stack identity, suitable for display in the CLI
|
||||
fmt.Stringer
|
||||
|
@ -125,6 +125,9 @@ type Backend interface {
|
|||
// ParseStackReference takes a string representation and parses it to a reference which may be used for other
|
||||
// methods in this backend.
|
||||
ParseStackReference(s string) (StackReference, error)
|
||||
// ValidateStackName verifies that the string is a legal identifier for a (potentially qualified) stack.
|
||||
// Will check for any backend-specific naming restrictions.
|
||||
ValidateStackName(s string) error
|
||||
|
||||
// DoesProjectExist returns true if a project with the given name exists in this backend, or false otherwise.
|
||||
DoesProjectExist(ctx context.Context, projectName string) (bool, error)
|
||||
|
|
|
@ -19,6 +19,7 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/pulumi/pulumi/pkg/apitype"
|
||||
"github.com/pulumi/pulumi/pkg/engine"
|
||||
|
@ -77,12 +78,15 @@ func startEventLogger(events <-chan engine.Event, done chan<- bool, path string)
|
|||
contract.IgnoreError(logFile.Close())
|
||||
}()
|
||||
|
||||
sequence := 0
|
||||
encoder := json.NewEncoder(logFile)
|
||||
logEvent := func(e engine.Event) error {
|
||||
apiEvent, err := ConvertEngineEvent(e)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
apiEvent.Sequence, sequence = sequence, sequence+1
|
||||
apiEvent.Timestamp = int(time.Now().Unix())
|
||||
return encoder.Encode(apiEvent)
|
||||
}
|
||||
|
||||
|
|
|
@ -4,13 +4,18 @@ import (
|
|||
"github.com/pkg/errors"
|
||||
"github.com/pulumi/pulumi/pkg/apitype"
|
||||
"github.com/pulumi/pulumi/pkg/engine"
|
||||
"github.com/pulumi/pulumi/pkg/resource/config"
|
||||
"github.com/pulumi/pulumi/pkg/resource/plugin"
|
||||
"github.com/pulumi/pulumi/pkg/resource/stack"
|
||||
"github.com/pulumi/pulumi/pkg/util/contract"
|
||||
)
|
||||
|
||||
// convertEngineEvent converts a raw engine.Event into an apitype.EngineEvent used in the Pulumi
|
||||
// ConvertEngineEvent converts a raw engine.Event into an apitype.EngineEvent used in the Pulumi
|
||||
// REST API. Returns an error if the engine event is unknown or not in an expected format.
|
||||
// EngineEvent.{ Sequence, Timestamp } are expected to be set by the caller.
|
||||
//
|
||||
// IMPORTANT: Any resource secret data stored in the engine event will be encrypted using the
|
||||
// blinding encrypter, and unrecoverable. So this operation is inherently lossy.
|
||||
func ConvertEngineEvent(e engine.Event) (apitype.EngineEvent, error) {
|
||||
var apiEvent apitype.EngineEvent
|
||||
|
||||
|
@ -182,19 +187,22 @@ func convertStepEventMetadata(md engine.StepEventMetadata) apitype.StepEventMeta
|
|||
}
|
||||
}
|
||||
|
||||
// convertStepEventStateMetadata converts the internal StepEventStateMetadata to the API type
|
||||
// we send over the wire.
|
||||
//
|
||||
// IMPORTANT: Any secret values are encrypted using the blinding encrypter. So any secret data
|
||||
// in the resource state will be lost and unrecoverable.
|
||||
func convertStepEventStateMetadata(md *engine.StepEventStateMetadata) *apitype.StepEventStateMetadata {
|
||||
if md == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
inputs := make(map[string]interface{})
|
||||
for k, v := range md.Inputs {
|
||||
inputs[string(k)] = v
|
||||
}
|
||||
outputs := make(map[string]interface{})
|
||||
for k, v := range md.Outputs {
|
||||
outputs[string(k)] = v
|
||||
}
|
||||
encrypter := config.BlindingCrypter
|
||||
inputs, err := stack.SerializeProperties(md.Inputs, encrypter)
|
||||
contract.IgnoreError(err)
|
||||
|
||||
outputs, err := stack.SerializeProperties(md.Outputs, encrypter)
|
||||
contract.IgnoreError(err)
|
||||
|
||||
return &apitype.StepEventStateMetadata{
|
||||
Type: string(md.Type),
|
||||
|
|
|
@ -23,6 +23,7 @@ import (
|
|||
"os/user"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -30,7 +31,7 @@ import (
|
|||
"gocloud.dev/blob"
|
||||
_ "gocloud.dev/blob/azureblob" // driver for azblob://
|
||||
_ "gocloud.dev/blob/fileblob" // driver for file://
|
||||
_ "gocloud.dev/blob/gcsblob" // driver for gs://
|
||||
"gocloud.dev/blob/gcsblob" // driver for gs://
|
||||
_ "gocloud.dev/blob/s3blob" // driver for s3://
|
||||
"gocloud.dev/gcerrors"
|
||||
|
||||
|
@ -47,6 +48,7 @@ import (
|
|||
"github.com/pulumi/pulumi/pkg/resource/edit"
|
||||
"github.com/pulumi/pulumi/pkg/resource/stack"
|
||||
"github.com/pulumi/pulumi/pkg/tokens"
|
||||
"github.com/pulumi/pulumi/pkg/util/cmdutil"
|
||||
"github.com/pulumi/pulumi/pkg/util/contract"
|
||||
"github.com/pulumi/pulumi/pkg/util/logging"
|
||||
"github.com/pulumi/pulumi/pkg/util/result"
|
||||
|
@ -106,16 +108,28 @@ func New(d diag.Sink, originalURL string) (Backend, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
bucket, err := blob.OpenBucket(context.TODO(), u)
|
||||
p, err := url.Parse(u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
blobmux := blob.DefaultURLMux()
|
||||
|
||||
// for gcp we want to support additional credentials
|
||||
// schemes on top of go-cloud's default credentials mux.
|
||||
if p.Scheme == gcsblob.Scheme {
|
||||
blobmux, err = GoogleCredentialsMux(context.TODO())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
bucket, err := blobmux.OpenBucket(context.TODO(), u)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "unable to open bucket %s", u)
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(u, FilePathPrefix) {
|
||||
p, err := url.Parse(u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bucketSubDir := strings.TrimLeft(p.Path, "/")
|
||||
if bucketSubDir != "" {
|
||||
if !strings.HasSuffix(bucketSubDir, "/") {
|
||||
|
@ -221,6 +235,21 @@ func (b *localBackend) ParseStackReference(stackRefName string) (backend.StackRe
|
|||
return localBackendReference{name: tokens.QName(stackRefName)}, nil
|
||||
}
|
||||
|
||||
// ValidateStackName verifies the stack name is valid for the local backend. We use the same rules as the
|
||||
// httpstate backend.
|
||||
func (b *localBackend) ValidateStackName(stackName string) error {
|
||||
if strings.Contains(stackName, "/") {
|
||||
return errors.New("stack names may not contain slashes")
|
||||
}
|
||||
|
||||
validNameRegex := regexp.MustCompile("^[A-Za-z0-9_.-]{1,100}$")
|
||||
if !validNameRegex.MatchString(stackName) {
|
||||
return errors.New("stack names may only contain alphanumeric, hyphens, underscores, or periods")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *localBackend) DoesProjectExist(ctx context.Context, projectName string) (bool, error) {
|
||||
// Local backends don't really have multiple projects, so just return false here.
|
||||
return false, nil
|
||||
|
@ -541,7 +570,12 @@ func (b *localBackend) apply(
|
|||
} else {
|
||||
link, err = b.bucket.SignedURL(context.TODO(), b.stackPath(stackName), nil)
|
||||
if err != nil {
|
||||
return changes, result.FromError(errors.Wrap(err, "Could not get signed url for stack location"))
|
||||
// we log a warning here rather then returning an error to avoid exiting
|
||||
// pulumi with an error code.
|
||||
// printing a statefile perma link happens after all the providers have finished
|
||||
// deploying the infrastructure, failing the pulumi update because there was a
|
||||
// problem printing a statefile perma link can be missleading in automated CI environments.
|
||||
cmdutil.Diag().Warningf(diag.Message("", "Could not get signed url for stack location: %v"), err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
85
pkg/backend/filestate/gcpauth.go
Normal file
85
pkg/backend/filestate/gcpauth.go
Normal file
|
@ -0,0 +1,85 @@
|
|||
package filestate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
|
||||
"github.com/pulumi/pulumi/pkg/diag"
|
||||
"github.com/pulumi/pulumi/pkg/util/cmdutil"
|
||||
|
||||
"golang.org/x/oauth2/google"
|
||||
|
||||
"gocloud.dev/blob/gcsblob"
|
||||
|
||||
"cloud.google.com/go/storage"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gocloud.dev/blob"
|
||||
"gocloud.dev/gcp"
|
||||
)
|
||||
|
||||
type GoogleCredentials struct {
|
||||
PrivateKeyID string `json:"private_key_id"`
|
||||
PrivateKey string `json:"private_key"`
|
||||
ClientEmail string `json:"client_email"`
|
||||
ClientID string `json:"client_id"`
|
||||
}
|
||||
|
||||
func googleCredentials(ctx context.Context) (*google.Credentials, error) {
|
||||
// GOOGLE_CREDENTIALS aren't part of the gcloud standard authorization variables
|
||||
// but the GCP terraform provider uses this variable to allow users to authenticate
|
||||
// with the contents of a credentials.json file instead of just a file path.
|
||||
// https://www.terraform.io/docs/backends/types/gcs.html
|
||||
if creds := os.Getenv("GOOGLE_CREDENTIALS"); creds != "" {
|
||||
// We try $GOOGLE_CREDENTIALS before gcp.DefaultCredentials
|
||||
// so that users can override the default creds
|
||||
credentials, err := google.CredentialsFromJSON(ctx, []byte(creds), storage.ScopeReadWrite)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to parse credentials from $GOOGLE_CREDENTIALS")
|
||||
}
|
||||
return credentials, nil
|
||||
}
|
||||
|
||||
// DefaultCredentials will attempt to load creds in the following order:
|
||||
// 1. a file located at $GOOGLE_APPLICATION_CREDENTIALS
|
||||
// 2. application_default_credentials.json file in ~/.config/gcloud or $APPDATA\gcloud
|
||||
credentials, err := gcp.DefaultCredentials(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to find gcp credentials")
|
||||
}
|
||||
return credentials, nil
|
||||
}
|
||||
|
||||
func GoogleCredentialsMux(ctx context.Context) (*blob.URLMux, error) {
|
||||
credentials, err := googleCredentials(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.New("missing google credentials")
|
||||
}
|
||||
|
||||
client, err := gcp.NewHTTPClient(gcp.DefaultTransport(), credentials.TokenSource)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
options := gcsblob.Options{}
|
||||
account := GoogleCredentials{}
|
||||
err = json.Unmarshal(credentials.JSON, &account)
|
||||
if err == nil && account.ClientEmail != "" && account.PrivateKey != "" {
|
||||
options.GoogleAccessID = account.ClientEmail
|
||||
options.PrivateKey = []byte(account.PrivateKey)
|
||||
} else {
|
||||
cmdutil.Diag().Warningf(diag.Message("",
|
||||
"Pulumi will not be able to print a statefile permalink using these credentials. "+
|
||||
"Neither a GoogleAccessID or PrivateKey are available. "+
|
||||
"Try using a GCP Service Account."))
|
||||
}
|
||||
|
||||
blobmux := &blob.URLMux{}
|
||||
blobmux.RegisterBucket(gcsblob.Scheme, &gcsblob.URLOpener{
|
||||
Client: client,
|
||||
Options: options,
|
||||
})
|
||||
|
||||
return blobmux, nil
|
||||
}
|
|
@ -70,6 +70,12 @@ const (
|
|||
AccessTokenEnvVar = "PULUMI_ACCESS_TOKEN"
|
||||
)
|
||||
|
||||
// Name validation rules enforced by the Pulumi Service.
|
||||
var (
|
||||
stackOwnerRegexp = regexp.MustCompile("^[a-zA-Z0-9][a-zA-Z0-9-_]{1,38}[a-zA-Z0-9]$")
|
||||
stackNameAndProjectRegexp = regexp.MustCompile("^[A-Za-z0-9_.-]{1,100}$")
|
||||
)
|
||||
|
||||
// DefaultURL returns the default cloud URL. This may be overridden using the PULUMI_API environment
|
||||
// variable. If no override is found, and we are authenticated with a cloud, choose that. Otherwise,
|
||||
// we will default to the https://api.pulumi.com/ endpoint.
|
||||
|
@ -465,51 +471,141 @@ func (b *cloudBackend) SupportsOrganizations() bool {
|
|||
return true
|
||||
}
|
||||
|
||||
func (b *cloudBackend) ParseStackReference(s string) (backend.StackReference, error) {
|
||||
split := strings.Split(s, "/")
|
||||
var owner string
|
||||
var projectName string
|
||||
var stackName string
|
||||
// qualifiedStackReference describes a qualified stack on the Pulumi Service. The Owner or Project
|
||||
// may be "" if unspecified, e.g. "pulumi/production" specifies the Owner and Name, but not the
|
||||
// Project. We infer the missing data and try to make things work as best we can in ParseStackReference.
|
||||
type qualifiedStackReference struct {
|
||||
Owner string
|
||||
Project string
|
||||
Name string
|
||||
}
|
||||
|
||||
// parseStackName parses the stack name into a potentially qualifiedStackReference. Any omitted
|
||||
// portions will be left as "". For example:
|
||||
//
|
||||
// "alpha" - will just set the Name, but ignore Owner and Project.
|
||||
// "alpha/beta" - will set the Owner and Name, but not Project.
|
||||
// "alpha/beta/gamma" - will set Owner, Name, and Project.
|
||||
func (b *cloudBackend) parseStackName(s string) (qualifiedStackReference, error) {
|
||||
var q qualifiedStackReference
|
||||
|
||||
split := strings.Split(s, "/")
|
||||
switch len(split) {
|
||||
case 1:
|
||||
stackName = split[0]
|
||||
q.Name = split[0]
|
||||
case 2:
|
||||
owner = split[0]
|
||||
stackName = split[1]
|
||||
q.Owner = split[0]
|
||||
q.Name = split[1]
|
||||
case 3:
|
||||
owner = split[0]
|
||||
projectName = split[1]
|
||||
stackName = split[2]
|
||||
q.Owner = split[0]
|
||||
q.Project = split[1]
|
||||
q.Name = split[2]
|
||||
default:
|
||||
return nil, errors.Errorf("could not parse stack name '%s'", s)
|
||||
return qualifiedStackReference{}, errors.Errorf("could not parse stack name '%s'", s)
|
||||
}
|
||||
|
||||
if owner == "" {
|
||||
return q, nil
|
||||
}
|
||||
|
||||
func (b *cloudBackend) ParseStackReference(s string) (backend.StackReference, error) {
|
||||
// Parse the input as a qualified stack name.
|
||||
qualifiedName, err := b.parseStackName(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If the provided stack name didn't include the Owner or Project, infer them from the
|
||||
// local environment.
|
||||
if qualifiedName.Owner == "" {
|
||||
currentUser, userErr := b.CurrentUser()
|
||||
if userErr != nil {
|
||||
return nil, userErr
|
||||
}
|
||||
owner = currentUser
|
||||
qualifiedName.Owner = currentUser
|
||||
}
|
||||
|
||||
if projectName == "" {
|
||||
if qualifiedName.Project == "" {
|
||||
currentProject, projectErr := workspace.DetectProject()
|
||||
if projectErr != nil {
|
||||
return nil, projectErr
|
||||
}
|
||||
|
||||
projectName = currentProject.Name.String()
|
||||
qualifiedName.Project = currentProject.Name.String()
|
||||
}
|
||||
|
||||
return cloudBackendReference{
|
||||
owner: owner,
|
||||
project: projectName,
|
||||
name: tokens.QName(stackName),
|
||||
owner: qualifiedName.Owner,
|
||||
project: qualifiedName.Project,
|
||||
name: tokens.QName(qualifiedName.Name),
|
||||
b: b,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *cloudBackend) ValidateStackName(s string) error {
|
||||
qualifiedName, err := b.parseStackName(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// The Pulumi Service enforces specific naming restrictions for organizations,
|
||||
// projects, and stacks. Though ignore any values that need to be inferred later.
|
||||
if qualifiedName.Owner != "" {
|
||||
if err := validateOwnerName(qualifiedName.Owner); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if qualifiedName.Project != "" {
|
||||
if err := validateProjectName(qualifiedName.Project); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return validateStackName(qualifiedName.Name)
|
||||
}
|
||||
|
||||
// validateOwnerName checks if a stack owner name is valid. An "owner" is simply the namespace
|
||||
// a stack may exist within, which for the Pulumi Service is the user account or organization.
|
||||
func validateOwnerName(s string) error {
|
||||
if !stackOwnerRegexp.MatchString(s) {
|
||||
return errors.New("invalid stack owner")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateStackName checks if a stack name is valid, returning a user-suitable error if needed.
|
||||
func validateStackName(s string) error {
|
||||
if len(s) > 100 {
|
||||
return errors.New("stack names must be less than 100 characters")
|
||||
}
|
||||
if !stackNameAndProjectRegexp.MatchString(s) {
|
||||
return errors.New("stack names may only contain alphanumeric, hyphens, underscores, and periods")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateProjectName checks if a project name is valid, returning a user-suitable error if needed.
|
||||
//
|
||||
// NOTE: Be careful when requiring a project name be valid. The Pulumi.yaml file may contain
|
||||
// an invalid project name like "r@bid^W0MBAT!!", but we try to err on the side of flexibility by
|
||||
// implicitly "cleaning" the project name before we send it to the Pulumi Service. So when we go
|
||||
// to make HTTP requests, we use a more palitable name like "r_bid_W0MBAT__".
|
||||
//
|
||||
// The projects canonical name will be the sanitized "r_bid_W0MBAT__" form, but we do not require the
|
||||
// Pulumi.yaml file be updated.
|
||||
//
|
||||
// So we should only call validateProject name when creating _new_ stacks or creating _new_ projects.
|
||||
// We should not require that project names be valid when reading what is in the current workspace.
|
||||
func validateProjectName(s string) error {
|
||||
if len(s) > 100 {
|
||||
return errors.New("project names must be less than 100 characters")
|
||||
}
|
||||
if !stackNameAndProjectRegexp.MatchString(s) {
|
||||
return errors.New("project names may only contain alphanumeric, hyphens, underscores, and periods")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CloudConsoleURL returns a link to the cloud console with the given path elements. If a console link cannot be
|
||||
// created, we return the empty string instead (this can happen if the endpoint isn't a recognized pattern).
|
||||
func (b *cloudBackend) CloudConsoleURL(paths ...string) string {
|
||||
|
|
|
@ -103,13 +103,27 @@ func publishPolicyPackPath(orgName string) string {
|
|||
return fmt.Sprintf("/api/orgs/%s/policypacks", orgName)
|
||||
}
|
||||
|
||||
// appyPolicyPackPath returns the path for an API call to the Pulumi service to apply a PolicyPack
|
||||
// applyPolicyPackPath returns the path for an API call to the Pulumi service to apply a PolicyPack
|
||||
// to a Pulumi organization.
|
||||
func applyPolicyPackPath(orgName, policyPackName string, version int) string {
|
||||
return fmt.Sprintf(
|
||||
"/api/orgs/%s/policypacks/%s/versions/%d/apply", orgName, policyPackName, version)
|
||||
}
|
||||
|
||||
// updatePolicyGroupPath returns the path for an API call to the Pulumi service to update a PolicyGroup
|
||||
// for a Pulumi organization.
|
||||
func updatePolicyGroupPath(orgName, policyGroup string) string {
|
||||
return fmt.Sprintf(
|
||||
"/api/orgs/%s/policygroups/%s", orgName, policyGroup)
|
||||
}
|
||||
|
||||
// deletePolicyPackVersionPath returns the path for an API call to the Pulumi service to delete
|
||||
// a Policy Pack from a Pulumi organization.
|
||||
func deletePolicyPackVersionPath(orgName, policyPackName string, version int) string {
|
||||
return fmt.Sprintf(
|
||||
"/api/orgs/%s/policypacks/%s/versions/%d", orgName, policyPackName, version)
|
||||
}
|
||||
|
||||
// publishPolicyPackPublishComplete returns the path for an API call to signal to the Pulumi service
|
||||
// that a PolicyPack to a Pulumi organization.
|
||||
func publishPolicyPackPublishComplete(orgName, policyPackName string, version int) string {
|
||||
|
@ -503,7 +517,7 @@ func (pc *Client) PublishPolicyPack(ctx context.Context, orgName string,
|
|||
var resp apitype.CreatePolicyPackResponse
|
||||
err := pc.restCall(ctx, "POST", publishPolicyPackPath(orgName), nil, req, &resp)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "HTTP POST to publish policy pack failed")
|
||||
return errors.Wrapf(err, "Publish policy pack failed")
|
||||
}
|
||||
|
||||
fmt.Printf("Published as version %d\n", resp.Version)
|
||||
|
@ -530,24 +544,76 @@ func (pc *Client) PublishPolicyPack(ctx context.Context, orgName string,
|
|||
err = pc.restCall(ctx, "POST",
|
||||
publishPolicyPackPublishComplete(orgName, analyzerInfo.Name, resp.Version), nil, nil, nil)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "HTTP POST to signal completion of the publish operation failed")
|
||||
return errors.Wrapf(err, "Request to signal completion of the publish operation failed")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ApplyPolicyPack applies a `PolicyPack` to the Pulumi organization.
|
||||
func (pc *Client) ApplyPolicyPack(ctx context.Context, orgName string, policyPackName string,
|
||||
version int) error {
|
||||
// ApplyPolicyPack enables a `PolicyPack` to the Pulumi organization. If policyGroup is not empty,
|
||||
// it will enable the PolicyPack on the default PolicyGroup.
|
||||
func (pc *Client) ApplyPolicyPack(ctx context.Context, orgName string, policyGroup string,
|
||||
policyPackName string, version int) error {
|
||||
|
||||
req := apitype.ApplyPolicyPackRequest{Name: policyPackName, Version: version}
|
||||
if policyGroup == "" {
|
||||
req := apitype.ApplyPolicyPackRequest{Name: policyPackName, Version: version}
|
||||
|
||||
err := pc.restCall(
|
||||
ctx, "POST", applyPolicyPackPath(orgName, policyPackName, version), nil, req, nil)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "HTTP POST to apply policy pack failed")
|
||||
err := pc.restCall(
|
||||
ctx, "POST", applyPolicyPackPath(orgName, policyPackName, version), nil, req, nil)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "Enable policy pack failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// If a Policy Group was specified, enable it for the specific group only.
|
||||
req := apitype.UpdatePolicyGroupRequest{
|
||||
AddPolicyPack: &apitype.PolicyPackMetadata{
|
||||
Name: policyPackName,
|
||||
Version: version,
|
||||
},
|
||||
}
|
||||
|
||||
err := pc.restCall(ctx, http.MethodPatch, updatePolicyGroupPath(orgName, policyGroup), nil, req, nil)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "Enable policy pack failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DisablePolicyPack disables a `PolicyPack` to the Pulumi organization. If policyGroup is not empty,
|
||||
// it will disable the PolicyPack on the default PolicyGroup.
|
||||
func (pc *Client) DisablePolicyPack(ctx context.Context, orgName string, policyGroup string,
|
||||
policyPackName string, version int) error {
|
||||
|
||||
// If Policy Group was not specified, use the default Policy Group.
|
||||
if policyGroup == "" {
|
||||
policyGroup = apitype.DefaultPolicyGroup
|
||||
}
|
||||
|
||||
req := apitype.UpdatePolicyGroupRequest{
|
||||
RemovePolicyPack: &apitype.PolicyPackMetadata{
|
||||
Name: policyPackName,
|
||||
Version: version,
|
||||
},
|
||||
}
|
||||
|
||||
err := pc.restCall(ctx, http.MethodPatch, updatePolicyGroupPath(orgName, policyGroup), nil, req, nil)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "Request to disable policy pack failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemovePolicyPack removes a `PolicyPack` from the Pulumi organization.
|
||||
func (pc *Client) RemovePolicyPack(ctx context.Context, orgName string,
|
||||
policyPackName string, version int) error {
|
||||
|
||||
path := deletePolicyPackVersionPath(orgName, policyPackName, version)
|
||||
err := pc.restCall(ctx, http.MethodDelete, path, nil, nil, nil)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "Request to remove policy pack failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -124,7 +124,7 @@ func (pack *cloudPolicyPack) Publish(
|
|||
return result.FromError(err)
|
||||
}
|
||||
|
||||
analyzer, err := op.PlugCtx.Host.PolicyAnalyzer(tokens.QName(abs), op.PlugCtx.Pwd)
|
||||
analyzer, err := op.PlugCtx.Host.PolicyAnalyzer(tokens.QName(abs), op.PlugCtx.Pwd, nil /*opts*/)
|
||||
if err != nil {
|
||||
return result.FromError(err)
|
||||
}
|
||||
|
@ -166,8 +166,16 @@ func (pack *cloudPolicyPack) Publish(
|
|||
return nil
|
||||
}
|
||||
|
||||
func (pack *cloudPolicyPack) Apply(ctx context.Context, op backend.ApplyOperation) error {
|
||||
return pack.cl.ApplyPolicyPack(ctx, pack.ref.orgName, string(pack.ref.name), op.Version)
|
||||
func (pack *cloudPolicyPack) Apply(ctx context.Context, policyGroup string, op backend.PolicyPackOperation) error {
|
||||
return pack.cl.ApplyPolicyPack(ctx, pack.ref.orgName, policyGroup, string(pack.ref.name), op.Version)
|
||||
}
|
||||
|
||||
func (pack *cloudPolicyPack) Disable(ctx context.Context, policyGroup string, op backend.PolicyPackOperation) error {
|
||||
return pack.cl.DisablePolicyPack(ctx, pack.ref.orgName, policyGroup, string(pack.ref.name), op.Version)
|
||||
}
|
||||
|
||||
func (pack *cloudPolicyPack) Remove(ctx context.Context, op backend.PolicyPackOperation) error {
|
||||
return pack.cl.RemovePolicyPack(ctx, pack.ref.orgName, string(pack.ref.name), op.Version)
|
||||
}
|
||||
|
||||
const npmPackageDir = "package"
|
||||
|
|
|
@ -37,6 +37,7 @@ type MockBackend struct {
|
|||
GetPolicyPackF func(ctx context.Context, policyPack string, d diag.Sink) (PolicyPack, error)
|
||||
SupportsOrganizationsF func() bool
|
||||
ParseStackReferenceF func(s string) (StackReference, error)
|
||||
ValidateStackNameF func(s string) error
|
||||
DoesProjectExistF func(context.Context, string) (bool, error)
|
||||
GetStackF func(context.Context, StackReference) (Stack, error)
|
||||
CreateStackF func(context.Context, StackReference, interface{}) (Stack, error)
|
||||
|
@ -106,6 +107,13 @@ func (be *MockBackend) ParseStackReference(s string) (StackReference, error) {
|
|||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (be *MockBackend) ValidateStackName(s string) error {
|
||||
if be.ValidateStackNameF != nil {
|
||||
return be.ValidateStackNameF(s)
|
||||
}
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (be *MockBackend) DoesProjectExist(ctx context.Context, projectName string) (bool, error) {
|
||||
if be.DoesProjectExistF != nil {
|
||||
return be.DoesProjectExistF(ctx, projectName)
|
||||
|
|
|
@ -31,8 +31,8 @@ type PublishOperation struct {
|
|||
Scopes CancellationScopeSource
|
||||
}
|
||||
|
||||
// ApplyOperation publishes a PolicyPack to the backend.
|
||||
type ApplyOperation struct {
|
||||
// PolicyPackOperation is used to make various operations against a Policy Pack.
|
||||
type PolicyPackOperation struct {
|
||||
Version int
|
||||
Scopes CancellationScopeSource
|
||||
}
|
||||
|
@ -45,6 +45,15 @@ type PolicyPack interface {
|
|||
Backend() Backend
|
||||
// Publish the PolicyPack to the service.
|
||||
Publish(ctx context.Context, op PublishOperation) result.Result
|
||||
// Apply the PolicyPack to an organization.
|
||||
Apply(ctx context.Context, op ApplyOperation) error
|
||||
// Apply the PolicyPack to a Policy Group in an organization. If Policy Group is
|
||||
// empty, it enables it for the default Policy Group.
|
||||
Apply(ctx context.Context, policyGroup string, op PolicyPackOperation) error
|
||||
|
||||
// Disable the PolicyPack for a Policy Group in an organization. If Policy Group is
|
||||
// empty, it disables it for the default Policy Group.
|
||||
Disable(ctx context.Context, policyGroup string, op PolicyPackOperation) error
|
||||
|
||||
// Remove the PolicyPack from an organization. The Policy Pack must be removed from
|
||||
// all Policy Groups before it can be removed.
|
||||
Remove(ctx context.Context, op PolicyPackOperation) error
|
||||
}
|
||||
|
|
|
@ -16,10 +16,12 @@ package engine
|
|||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/blang/semver"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/pulumi/pulumi/pkg/diag"
|
||||
"github.com/pulumi/pulumi/pkg/resource"
|
||||
"github.com/pulumi/pulumi/pkg/resource/deploy"
|
||||
|
@ -184,19 +186,37 @@ func installPlugins(
|
|||
return allPlugins, defaultProviderVersions, nil
|
||||
}
|
||||
|
||||
func installAndLoadPolicyPlugins(plugctx *plugin.Context, policies []RequiredPolicy) error {
|
||||
func installAndLoadPolicyPlugins(plugctx *plugin.Context, policies []RequiredPolicy, localPolicyPackPaths []string,
|
||||
opts *plugin.PolicyAnalyzerOptions) error {
|
||||
|
||||
// Install and load required policy packs.
|
||||
for _, policy := range policies {
|
||||
policyPath, err := policy.Install(context.Background())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = plugctx.Host.PolicyAnalyzer(tokens.QName(policy.Name()), policyPath)
|
||||
_, err = plugctx.Host.PolicyAnalyzer(tokens.QName(policy.Name()), policyPath, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Load local policy packs.
|
||||
for _, path := range localPolicyPackPaths {
|
||||
abs, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
analyzer, err := plugctx.Host.PolicyAnalyzer(tokens.QName(abs), path, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if analyzer == nil {
|
||||
return errors.Errorf("analyzer could not be loaded from path %q", path)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -226,7 +246,19 @@ func newUpdateSource(
|
|||
// Step 2: Install and load policy plugins.
|
||||
//
|
||||
|
||||
if err := installAndLoadPolicyPlugins(plugctx, opts.RequiredPolicies); err != nil {
|
||||
// Decrypt the configuration.
|
||||
config, err := target.Config.Decrypt(target.Decrypter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
analyzerOpts := plugin.PolicyAnalyzerOptions{
|
||||
Project: proj.Name.String(),
|
||||
Stack: target.Name.String(),
|
||||
Config: config,
|
||||
DryRun: dryRun,
|
||||
}
|
||||
if err := installAndLoadPolicyPlugins(plugctx, opts.RequiredPolicies, opts.LocalPolicyPackPaths,
|
||||
&analyzerOpts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
|
|
@ -180,7 +180,8 @@ func (host *pluginHost) GetRequiredPlugins(info plugin.ProgInfo,
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
func (host *pluginHost) PolicyAnalyzer(name tokens.QName, path string) (plugin.Analyzer, error) {
|
||||
func (host *pluginHost) PolicyAnalyzer(name tokens.QName, path string,
|
||||
opts *plugin.PolicyAnalyzerOptions) (plugin.Analyzer, error) {
|
||||
return nil, errors.New("unsupported")
|
||||
}
|
||||
|
||||
|
|
|
@ -269,7 +269,11 @@ func (pe *planExecutor) Execute(callerCtx context.Context, opts Options, preview
|
|||
if res == nil && !pe.stepExec.Errored() {
|
||||
res := pe.stepGen.AnalyzeResources()
|
||||
if res != nil {
|
||||
return res
|
||||
if resErr := res.Error(); resErr != nil {
|
||||
logging.V(4).Infof("planExecutor.Execute(...): error analyzing resources: %v", resErr)
|
||||
pe.reportError("", resErr)
|
||||
}
|
||||
return result.Bail()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -54,7 +54,8 @@ func (host *testPluginHost) LogStatus(sev diag.Severity, urn resource.URN, msg s
|
|||
func (host *testPluginHost) Analyzer(nm tokens.QName) (plugin.Analyzer, error) {
|
||||
return nil, errors.New("unsupported")
|
||||
}
|
||||
func (host *testPluginHost) PolicyAnalyzer(name tokens.QName, path string) (plugin.Analyzer, error) {
|
||||
func (host *testPluginHost) PolicyAnalyzer(name tokens.QName, path string,
|
||||
opts *plugin.PolicyAnalyzerOptions) (plugin.Analyzer, error) {
|
||||
return nil, errors.New("unsupported")
|
||||
}
|
||||
func (host *testPluginHost) ListAnalyzers() []plugin.Analyzer {
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
package deploy
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
@ -314,22 +313,6 @@ func (sg *stepGenerator) generateSteps(event RegisterResourceEvent) ([]Step, res
|
|||
new.Inputs = inputs
|
||||
}
|
||||
|
||||
// Load all policy packs into the plugin host.
|
||||
for _, path := range sg.plan.localPolicyPackPaths {
|
||||
abs, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
return nil, result.FromError(err)
|
||||
}
|
||||
|
||||
var analyzer plugin.Analyzer
|
||||
analyzer, err = sg.plan.ctx.Host.PolicyAnalyzer(tokens.QName(abs), path)
|
||||
if err != nil {
|
||||
return nil, result.FromError(err)
|
||||
} else if analyzer == nil {
|
||||
return nil, result.Errorf("analyzer could not be loaded from path %q", path)
|
||||
}
|
||||
}
|
||||
|
||||
// Send the resource off to any Analyzers before being operated on.
|
||||
analyzers := sg.plan.ctx.Host.ListAnalyzers()
|
||||
for _, analyzer := range analyzers {
|
||||
|
|
|
@ -15,7 +15,9 @@
|
|||
package plugin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/blang/semver"
|
||||
|
@ -59,7 +61,7 @@ func NewAnalyzer(host Host, ctx *Context, name tokens.QName) (Analyzer, error) {
|
|||
}
|
||||
|
||||
plug, err := newPlugin(ctx, ctx.Pwd, path, fmt.Sprintf("%v (analyzer)", name),
|
||||
[]string{host.ServerAddr(), ctx.Pwd})
|
||||
[]string{host.ServerAddr(), ctx.Pwd}, nil /*env*/)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -77,7 +79,7 @@ const policyAnalyzerName = "policy"
|
|||
|
||||
// NewPolicyAnalyzer boots the nodejs analyzer plugin located at `policyPackpath`
|
||||
func NewPolicyAnalyzer(
|
||||
host Host, ctx *Context, name tokens.QName, policyPackPath string) (Analyzer, error) {
|
||||
host Host, ctx *Context, name tokens.QName, policyPackPath string, opts *PolicyAnalyzerOptions) (Analyzer, error) {
|
||||
|
||||
// Load the policy-booting analyzer plugin (i.e., `pulumi-analyzer-${policyAnalyzerName}`).
|
||||
_, pluginPath, err := workspace.GetPluginPath(
|
||||
|
@ -91,6 +93,12 @@ func NewPolicyAnalyzer(
|
|||
"does not support resource policies", string(name))
|
||||
}
|
||||
|
||||
// Create the environment variables from the options.
|
||||
env, err := constructEnv(opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// The `pulumi-analyzer-policy` plugin is a script that looks for the '@pulumi/pulumi/cmd/run-policy-pack'
|
||||
// node module and runs it with node. To allow non-node Pulumi programs (e.g. Python, .NET, Go, etc.) to
|
||||
// run node policy packs, we must set the plugin's pwd to the policy pack directory instead of the Pulumi
|
||||
|
@ -98,7 +106,7 @@ func NewPolicyAnalyzer(
|
|||
// node_modules is used.
|
||||
pwd := policyPackPath
|
||||
plug, err := newPlugin(ctx, pwd, pluginPath, fmt.Sprintf("%v (analyzer)", name),
|
||||
[]string{host.ServerAddr(), "."})
|
||||
[]string{host.ServerAddr(), "."}, env)
|
||||
if err != nil {
|
||||
if err == errRunPolicyModuleNotFound {
|
||||
return nil, fmt.Errorf("it looks like the policy pack's dependencies are not installed; "+
|
||||
|
@ -305,3 +313,49 @@ func convertDiagnostics(protoDiagnostics []*pulumirpc.AnalyzeDiagnostic) ([]Anal
|
|||
|
||||
return diagnostics, nil
|
||||
}
|
||||
|
||||
// constructEnv creates a slice of key/value pairs to be used as the environment for the policy pack process. Each entry
|
||||
// is of the form "key=value". Config is passed as an environment variable (including unecrypted secrets), similar to
|
||||
// how config is passed to each language runtime plugin.
|
||||
func constructEnv(opts *PolicyAnalyzerOptions) ([]string, error) {
|
||||
env := os.Environ()
|
||||
|
||||
maybeAppendEnv := func(k, v string) {
|
||||
if v != "" {
|
||||
env = append(env, k+"="+v)
|
||||
}
|
||||
}
|
||||
|
||||
config, err := constructConfig(opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
maybeAppendEnv("PULUMI_CONFIG", config)
|
||||
|
||||
if opts != nil {
|
||||
maybeAppendEnv("PULUMI_NODEJS_PROJECT", opts.Project)
|
||||
maybeAppendEnv("PULUMI_NODEJS_STACK", opts.Stack)
|
||||
maybeAppendEnv("PULUMI_NODEJS_DRY_RUN", fmt.Sprintf("%v", opts.DryRun))
|
||||
}
|
||||
|
||||
return env, nil
|
||||
}
|
||||
|
||||
// constructConfig JSON-serializes the configuration data.
|
||||
func constructConfig(opts *PolicyAnalyzerOptions) (string, error) {
|
||||
if opts == nil || opts.Config == nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
config := make(map[string]string)
|
||||
for k, v := range opts.Config {
|
||||
config[k.String()] = v
|
||||
}
|
||||
|
||||
configJSON, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(configJSON), nil
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ import (
|
|||
|
||||
"github.com/pulumi/pulumi/pkg/diag"
|
||||
"github.com/pulumi/pulumi/pkg/resource"
|
||||
"github.com/pulumi/pulumi/pkg/resource/config"
|
||||
"github.com/pulumi/pulumi/pkg/tokens"
|
||||
"github.com/pulumi/pulumi/pkg/util/cmdutil"
|
||||
"github.com/pulumi/pulumi/pkg/util/contract"
|
||||
|
@ -53,7 +54,7 @@ type Host interface {
|
|||
// because policy analyzers generally do not need to be "discovered" -- the engine is given a
|
||||
// set of policies that are required to be run during an update, so they tend to be in a
|
||||
// well-known place.
|
||||
PolicyAnalyzer(name tokens.QName, path string) (Analyzer, error)
|
||||
PolicyAnalyzer(name tokens.QName, path string, opts *PolicyAnalyzerOptions) (Analyzer, error)
|
||||
|
||||
// ListAnalyzers returns a list of all analyzer plugins known to the plugin host.
|
||||
ListAnalyzers() []Analyzer
|
||||
|
@ -117,6 +118,14 @@ func NewDefaultHost(ctx *Context, config ConfigSource, runtimeOptions map[string
|
|||
return host, nil
|
||||
}
|
||||
|
||||
// PolicyAnalyzerOptions includes a bag of options to pass along to a policy analyzer.
|
||||
type PolicyAnalyzerOptions struct {
|
||||
Project string
|
||||
Stack string
|
||||
Config map[config.Key]string
|
||||
DryRun bool
|
||||
}
|
||||
|
||||
type pluginLoadRequest struct {
|
||||
load func() error
|
||||
result chan<- error
|
||||
|
@ -209,7 +218,7 @@ func (host *defaultHost) Analyzer(name tokens.QName) (Analyzer, error) {
|
|||
return plugin.(Analyzer), nil
|
||||
}
|
||||
|
||||
func (host *defaultHost) PolicyAnalyzer(name tokens.QName, path string) (Analyzer, error) {
|
||||
func (host *defaultHost) PolicyAnalyzer(name tokens.QName, path string, opts *PolicyAnalyzerOptions) (Analyzer, error) {
|
||||
plugin, err := host.loadPlugin(func() (interface{}, error) {
|
||||
// First see if we already loaded this plugin.
|
||||
if plug, has := host.analyzerPlugins[name]; has {
|
||||
|
@ -218,7 +227,7 @@ func (host *defaultHost) PolicyAnalyzer(name tokens.QName, path string) (Analyze
|
|||
}
|
||||
|
||||
// If not, try to load and bind to a plugin.
|
||||
plug, err := NewPolicyAnalyzer(host, host.ctx, name, path)
|
||||
plug, err := NewPolicyAnalyzer(host, host.ctx, name, path, opts)
|
||||
if err == nil && plug != nil {
|
||||
info, infoerr := plug.GetPluginInfo()
|
||||
if infoerr != nil {
|
||||
|
|
|
@ -61,7 +61,7 @@ func NewLanguageRuntime(host Host, ctx *Context, runtime string,
|
|||
}
|
||||
args = append(args, host.ServerAddr())
|
||||
|
||||
plug, err := newPlugin(ctx, ctx.Pwd, path, runtime, args)
|
||||
plug, err := newPlugin(ctx, ctx.Pwd, path, runtime, args, nil /*env*/)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -43,8 +43,11 @@ type plugin struct {
|
|||
stdoutDone <-chan bool
|
||||
stderrDone <-chan bool
|
||||
|
||||
Bin string
|
||||
Args []string
|
||||
Bin string
|
||||
Args []string
|
||||
// Env specifies the environment of the plugin in the same format as go's os/exec.Cmd.Env
|
||||
// https://golang.org/pkg/os/exec/#Cmd (each entry is of the form "key=value").
|
||||
Env []string
|
||||
Conn *grpc.ClientConn
|
||||
Proc *os.Process
|
||||
Stdin io.WriteCloser
|
||||
|
@ -67,7 +70,7 @@ var nextStreamID int32
|
|||
// the stack's Pulumi SDK did not have the required modules. i.e. is too old.
|
||||
var errRunPolicyModuleNotFound = errors.New("pulumi SDK does not support policy as code")
|
||||
|
||||
func newPlugin(ctx *Context, pwd, bin, prefix string, args []string) (*plugin, error) {
|
||||
func newPlugin(ctx *Context, pwd, bin, prefix string, args, env []string) (*plugin, error) {
|
||||
if logging.V(9) {
|
||||
var argstr string
|
||||
for i, arg := range args {
|
||||
|
@ -80,7 +83,7 @@ func newPlugin(ctx *Context, pwd, bin, prefix string, args []string) (*plugin, e
|
|||
}
|
||||
|
||||
// Try to execute the binary.
|
||||
plug, err := execPlugin(bin, args, pwd)
|
||||
plug, err := execPlugin(bin, args, pwd, env)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to load plugin %s", bin)
|
||||
}
|
||||
|
@ -231,7 +234,7 @@ func newPlugin(ctx *Context, pwd, bin, prefix string, args []string) (*plugin, e
|
|||
return plug, nil
|
||||
}
|
||||
|
||||
func execPlugin(bin string, pluginArgs []string, pwd string) (*plugin, error) {
|
||||
func execPlugin(bin string, pluginArgs []string, pwd string, env []string) (*plugin, error) {
|
||||
var args []string
|
||||
// Flow the logging information if set.
|
||||
if logging.LogFlow {
|
||||
|
@ -251,6 +254,9 @@ func execPlugin(bin string, pluginArgs []string, pwd string) (*plugin, error) {
|
|||
cmd := exec.Command(bin, args...)
|
||||
cmdutil.RegisterProcessGroup(cmd)
|
||||
cmd.Dir = pwd
|
||||
if len(env) > 0 {
|
||||
cmd.Env = env
|
||||
}
|
||||
in, _ := cmd.StdinPipe()
|
||||
out, _ := cmd.StdoutPipe()
|
||||
err, _ := cmd.StderrPipe()
|
||||
|
@ -261,6 +267,7 @@ func execPlugin(bin string, pluginArgs []string, pwd string) (*plugin, error) {
|
|||
return &plugin{
|
||||
Bin: bin,
|
||||
Args: args,
|
||||
Env: env,
|
||||
Proc: cmd.Process,
|
||||
Stdin: in,
|
||||
Stdout: out,
|
||||
|
|
|
@ -77,7 +77,8 @@ func NewProvider(host Host, ctx *Context, pkg tokens.Package, version *semver.Ve
|
|||
})
|
||||
}
|
||||
|
||||
plug, err := newPlugin(ctx, ctx.Pwd, path, fmt.Sprintf("%v (resource)", pkg), []string{host.ServerAddr()})
|
||||
plug, err := newPlugin(ctx, ctx.Pwd, path, fmt.Sprintf("%v (resource)", pkg),
|
||||
[]string{host.ServerAddr()}, nil /*env*/)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -1668,7 +1668,7 @@ func (pt *programTester) prepareDotNetProject(projinfo *engine.Projinfo) error {
|
|||
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.
|
||||
matches, err := filepath.Glob(filepath.Join(localNuget, dep+".?.?.*.nupkg"))
|
||||
matches, err := filepath.Glob(filepath.Join(localNuget, dep+".?.*.nupkg"))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to find a local Pulumi NuGet package")
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ package ciutil
|
|||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// azurePipelinesCI represents the Azure Pipelines CI/CD system
|
||||
|
@ -33,23 +34,39 @@ func (az azurePipelinesCI) DetectVars() Vars {
|
|||
v.BuildID = os.Getenv("BUILD_BUILDID")
|
||||
v.BuildType = os.Getenv("BUILD_REASON")
|
||||
v.SHA = os.Getenv("BUILD_SOURCEVERSION")
|
||||
v.BranchName = os.Getenv("BUILD_SOURCEBRANCHNAME")
|
||||
v.CommitMessage = os.Getenv("BUILD_SOURCEVERSIONMESSAGE")
|
||||
|
||||
orgURI := os.Getenv("SYSTEM_TEAMFOUNDATIONCOLLECTIONURI")
|
||||
orgURI = strings.TrimSuffix(orgURI, "/")
|
||||
projectName := os.Getenv("SYSTEM_TEAMPROJECT")
|
||||
v.BuildURL = fmt.Sprintf("%v/%v/_build/results?buildId=%v", orgURI, projectName, v.BuildID)
|
||||
|
||||
// Azure Pipelines can be connected to external repos.
|
||||
// So we check if the provider is GitHub, then we use
|
||||
// `SYSTEM_PULLREQUEST_PULLREQUESTNUMBER` instead of `SYSTEM_PULLREQUEST_PULLREQUESTID`.
|
||||
// The PR ID/number only applies to Git repos.
|
||||
// If the repo provider is GitHub, then we need to use
|
||||
// `SYSTEM_PULLREQUEST_PULLREQUESTNUMBER` instead of
|
||||
// `SYSTEM_PULLREQUEST_PULLREQUESTID`. For other Git repos,
|
||||
// `SYSTEM_PULLREQUEST_PULLREQUESTID` may be the only variable
|
||||
// that is set if the build is running for a PR build.
|
||||
//
|
||||
// Note that the PR ID/number only applies to Git repos.
|
||||
vcsProvider := os.Getenv("BUILD_REPOSITORY_PROVIDER")
|
||||
switch vcsProvider {
|
||||
case "TfsGit":
|
||||
orgURI := os.Getenv("SYSTEM_PULLREQUEST_TEAMFOUNDATIONCOLLECTIONURI")
|
||||
projectName := os.Getenv("SYSTEM_TEAMPROJECT")
|
||||
v.BuildURL = fmt.Sprintf("%v/%v/_build/results?buildId=%v", orgURI, projectName, v.BuildID)
|
||||
// TfsGit is a git repo hosted on Azure DevOps.
|
||||
v.PRNumber = os.Getenv("SYSTEM_PULLREQUEST_PULLREQUESTID")
|
||||
case "GitHub":
|
||||
// GitHub is a git repo hosted on GitHub.
|
||||
v.PRNumber = os.Getenv("SYSTEM_PULLREQUEST_PULLREQUESTNUMBER")
|
||||
default:
|
||||
v.PRNumber = os.Getenv("SYSTEM_PULLREQUEST_PULLREQUESTID")
|
||||
}
|
||||
|
||||
// Build.SourceBranchName is the last part of the head.
|
||||
// If the build is running because of a PR, we should use the
|
||||
// PR source branch name, instead of Build.SourceBranchName.
|
||||
// That's because Build.SourceBranchName will always be `merge` --
|
||||
// the last part of `refs/pull/1/merge`.
|
||||
if v.PRNumber != "" {
|
||||
v.BranchName = os.Getenv("SYSTEM_PULLREQUEST_SOURCEBRANCH")
|
||||
} else {
|
||||
v.BranchName = os.Getenv("BUILD_SOURCEBRANCHNAME")
|
||||
}
|
||||
|
||||
return v
|
||||
|
|
|
@ -404,14 +404,15 @@ func LoadTemplate(path string) (Template, error) {
|
|||
|
||||
// CopyTemplateFilesDryRun does a dry run of copying a template to a destination directory,
|
||||
// to ensure it won't overwrite any files.
|
||||
func CopyTemplateFilesDryRun(sourceDir, destDir string) error {
|
||||
func CopyTemplateFilesDryRun(sourceDir, destDir, projectName string) error {
|
||||
var existing []string
|
||||
if err := walkFiles(sourceDir, destDir, func(info os.FileInfo, source string, dest string) error {
|
||||
if destInfo, statErr := os.Stat(dest); statErr == nil && !destInfo.IsDir() {
|
||||
existing = append(existing, filepath.Base(dest))
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
if err := walkFiles(sourceDir, destDir, projectName,
|
||||
func(info os.FileInfo, source string, dest string) error {
|
||||
if destInfo, statErr := os.Stat(dest); statErr == nil && !destInfo.IsDir() {
|
||||
existing = append(existing, filepath.Base(dest))
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -425,35 +426,36 @@ func CopyTemplateFilesDryRun(sourceDir, destDir string) error {
|
|||
func CopyTemplateFiles(
|
||||
sourceDir, destDir string, force bool, projectName string, projectDescription string) error {
|
||||
|
||||
return walkFiles(sourceDir, destDir, func(info os.FileInfo, source string, dest string) error {
|
||||
if info.IsDir() {
|
||||
// Create the destination directory.
|
||||
return os.Mkdir(dest, 0700)
|
||||
}
|
||||
|
||||
// Read the source file.
|
||||
b, err := ioutil.ReadFile(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Transform only if it isn't a binary file.
|
||||
result := b
|
||||
if !isBinary(b) {
|
||||
transformed := transform(string(b), projectName, projectDescription)
|
||||
result = []byte(transformed)
|
||||
}
|
||||
|
||||
// Write to the destination file.
|
||||
err = writeAllBytes(dest, result, force)
|
||||
if err != nil {
|
||||
// An existing file has shown up in between the dry run and the actual copy operation.
|
||||
if os.IsExist(err) {
|
||||
return newExistingFilesError([]string{filepath.Base(dest)})
|
||||
return walkFiles(sourceDir, destDir, projectName,
|
||||
func(info os.FileInfo, source string, dest string) error {
|
||||
if info.IsDir() {
|
||||
// Create the destination directory.
|
||||
return os.Mkdir(dest, 0700)
|
||||
}
|
||||
}
|
||||
return err
|
||||
})
|
||||
|
||||
// Read the source file.
|
||||
b, err := ioutil.ReadFile(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Transform only if it isn't a binary file.
|
||||
result := b
|
||||
if !isBinary(b) {
|
||||
transformed := transform(string(b), projectName, projectDescription)
|
||||
result = []byte(transformed)
|
||||
}
|
||||
|
||||
// Write to the destination file.
|
||||
err = writeAllBytes(dest, result, force)
|
||||
if err != nil {
|
||||
// An existing file has shown up in between the dry run and the actual copy operation.
|
||||
if os.IsExist(err) {
|
||||
return newExistingFilesError([]string{filepath.Base(dest)})
|
||||
}
|
||||
}
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
// LoadPolicyPackTemplate returns a Policy Pack template from a path.
|
||||
|
@ -502,13 +504,11 @@ func GetTemplateDir(templateKind TemplateKind) (string, error) {
|
|||
return GetPulumiPath(TemplateDir)
|
||||
}
|
||||
|
||||
// We are moving towards a world where these restrictions will be enforced by all our backends. When we get there,
|
||||
// we can consider removing this code in favor of exported functions in the backend package. For now, these are more
|
||||
// restrictive that what the backend enforces, but we want to "stop the bleeding" for new projects created via
|
||||
// `pulumi new`.
|
||||
// Naming rules are backend-specific. However, we provide baseline sanitization for project names
|
||||
// in this file. Though the backend may enforce stronger restrictions for a project name or description
|
||||
// further down the line.
|
||||
var (
|
||||
stackOwnerRegexp = regexp.MustCompile("^[a-zA-Z0-9][a-zA-Z0-9-_]{1,38}[a-zA-Z0-9]$")
|
||||
stackNameAndProjectRegexp = regexp.MustCompile("^[A-Za-z0-9_.-]{1,100}$")
|
||||
validProjectNameRegexp = regexp.MustCompile("^[A-Za-z0-9_.-]{1,100}$")
|
||||
)
|
||||
|
||||
// ValidateProjectName ensures a project name is valid, if it is not it returns an error with a message suitable
|
||||
|
@ -518,7 +518,7 @@ func ValidateProjectName(s string) error {
|
|||
return errors.New("A project name must be 100 characters or less")
|
||||
}
|
||||
|
||||
if !stackNameAndProjectRegexp.MatchString(s) {
|
||||
if !validProjectNameRegexp.MatchString(s) {
|
||||
return errors.New("A project name may only contain alphanumeric, hyphens, underscores, and periods")
|
||||
}
|
||||
|
||||
|
@ -537,51 +537,6 @@ func ValidateProjectDescription(s string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// ValidateStackName ensures a -- potentially qualified -- stack name is valid, if it is not it
|
||||
// returns an error with a message suitable for display to an end user.
|
||||
func ValidateStackName(s string) error {
|
||||
// First, see if the stack name is qualified or not. It may be of the form "owner/name", when
|
||||
// you have access to multiple organizations.
|
||||
parts := strings.Split(s, "/")
|
||||
switch len(parts) {
|
||||
case 1:
|
||||
return validateStackName(parts[0])
|
||||
case 2:
|
||||
if err := validateStackOwner(parts[0]); err != nil {
|
||||
return err
|
||||
}
|
||||
return validateStackName(parts[1])
|
||||
default:
|
||||
return errors.New("A stack name may not contain slashes")
|
||||
}
|
||||
}
|
||||
|
||||
// validateStackOwner checks if a stack owner name is valid. An "owner" is simply the namespace
|
||||
// a stack may exist within, which for the Pulumi Service is the user account or organization.
|
||||
func validateStackOwner(s string) error {
|
||||
// The error message takes a different from here, since stack names are created via the CLI,
|
||||
// Pulumi organizations are created on the Pulumi Service. And so
|
||||
if !stackOwnerRegexp.MatchString(s) {
|
||||
return errors.New("Invalid stack owner")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateStackName checks if a stack name is valid, returning a user-suitable error if needed. May
|
||||
// need to be paired with validateOwnerName when checking a qualified stack reference.
|
||||
func validateStackName(s string) error {
|
||||
if len(s) > 100 {
|
||||
return errors.New("A stack name must be 100 characters or less")
|
||||
}
|
||||
|
||||
if !stackNameAndProjectRegexp.MatchString(s) {
|
||||
return errors.New("A stack name may only contain alphanumeric, hyphens, underscores, and periods")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValueOrSanitizedDefaultProjectName returns the value or a sanitized valid project name
|
||||
// based on defaultNameToSanitize.
|
||||
func ValueOrSanitizedDefaultProjectName(name string, projectName string, defaultNameToSanitize string) string {
|
||||
|
@ -643,7 +598,7 @@ func getValidProjectName(name string) string {
|
|||
|
||||
// walkFiles is a helper that walks the directories/files in a source directory
|
||||
// and performs an action for each item.
|
||||
func walkFiles(sourceDir string, destDir string,
|
||||
func walkFiles(sourceDir string, destDir string, projectName string,
|
||||
actionFn func(info os.FileInfo, source string, dest string) error) error {
|
||||
|
||||
contract.Require(sourceDir != "", "sourceDir")
|
||||
|
@ -669,7 +624,7 @@ func walkFiles(sourceDir string, destDir string,
|
|||
return err
|
||||
}
|
||||
|
||||
if err := walkFiles(source, dest, actionFn); err != nil {
|
||||
if err := walkFiles(source, dest, projectName, actionFn); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
|
@ -678,7 +633,10 @@ func walkFiles(sourceDir string, destDir string,
|
|||
continue
|
||||
}
|
||||
|
||||
if err := actionFn(info, source, dest); err != nil {
|
||||
// The file name may contain a placeholder for project name: replace it with the actual value.
|
||||
newDest := transform(dest, projectName, "")
|
||||
|
||||
if err := actionFn(info, source, newDest); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
|
@ -62,17 +62,6 @@ func TestGetValidDefaultProjectName(t *testing.T) {
|
|||
assert.Equal(t, "project", getValidProjectName("!@#$%^&*()"))
|
||||
}
|
||||
|
||||
func TestValidateStackName(t *testing.T) {
|
||||
assert.NoError(t, ValidateStackName("alpha-beta-gamma"))
|
||||
assert.NoError(t, ValidateStackName("owner-name/alpha-beta-gamma"))
|
||||
|
||||
err := ValidateStackName("alpha/beta/gamma")
|
||||
assert.Equal(t, err.Error(), "A stack name may not contain slashes")
|
||||
|
||||
err = ValidateStackName("mooo looo mi/alpha-beta-gamma")
|
||||
assert.Equal(t, err.Error(), "Invalid stack owner")
|
||||
}
|
||||
|
||||
func getValidProjectNamePrefixes() []string {
|
||||
var results []string
|
||||
for ch := 'A'; ch <= 'Z'; ch++ {
|
||||
|
|
|
@ -5,6 +5,7 @@ set -o errexit
|
|||
set -o pipefail
|
||||
|
||||
readonly SCRIPT_DIR="$( cd "$( dirname "${0}" )" && pwd )"
|
||||
readonly ROOT=${SCRIPT_DIR}/..
|
||||
|
||||
if [ -z "${1:-}" ]; then
|
||||
>&2 echo "error: missing version to publish"
|
||||
|
@ -32,21 +33,32 @@ fi
|
|||
|
||||
docker login -u "${DOCKER_HUB_USER}" -p "${DOCKER_HUB_PASSWORD}"
|
||||
|
||||
echo "Building and publishing pulumi/pulumi:${CLI_VERSION}"
|
||||
docker build --build-arg PULUMI_VERSION="${CLI_VERSION}" \
|
||||
-t "pulumi/pulumi:${CLI_VERSION}" \
|
||||
-t "pulumi/pulumi:latest" \
|
||||
"${SCRIPT_DIR}/../dist/docker"
|
||||
docker push "pulumi/pulumi:${CLI_VERSION}"
|
||||
docker push "pulumi/pulumi:latest"
|
||||
echo "Building containers..."
|
||||
for container in pulumi actions; do
|
||||
echo "- pulumi/${container}"
|
||||
docker build --build-arg PULUMI_VERSION="${CLI_VERSION}" \
|
||||
-t "pulumi/${container}:${CLI_VERSION}" \
|
||||
-t "pulumi/${container}:latest" \
|
||||
"${SCRIPT_DIR}/../dist/${container}"
|
||||
done
|
||||
|
||||
# Pulumi container optimized for GitHub Actions.
|
||||
echo "Building and publishing pulumi/actions:${CLI_VERSION}"
|
||||
docker build --build-arg PULUMI_VERSION="${CLI_VERSION}" \
|
||||
-t "pulumi/actions:${CLI_VERSION}" \
|
||||
-t "pulumi/actions:latest" \
|
||||
"${SCRIPT_DIR}/../dist/actions"
|
||||
docker push "pulumi/actions:${CLI_VERSION}"
|
||||
docker push "pulumi/actions:latest"
|
||||
echo "Running container runtime tests..."
|
||||
GOOS=linux go test -c -o /tmp/pulumi-test-containers ${ROOT}/tests/containers/...
|
||||
docker run -e RUN_CONTAINER_TESTS=true \
|
||||
-e PULUMI_ACCESS_TOKEN=${PULUMI_ACCESS_TOKEN} \
|
||||
--volume /tmp:/src \
|
||||
--entrypoint /bin/bash \
|
||||
pulumi/pulumi:latest \
|
||||
-c "pip install pipenv && /src/pulumi-test-containers -test.parallel=1 -test.v -test.run TestPulumiDockerImage"
|
||||
|
||||
echo "Running container entrypoint tests..."
|
||||
RUN_CONTAINER_TESTS=true go test ${ROOT}/tests/containers/... -test.run TestPulumiActionsImage -test.v
|
||||
|
||||
echo "Publishing containers..."
|
||||
for container in pulumi actions; do
|
||||
echo "- pulumi/${container}"
|
||||
docker push "pulumi/${container}:${CLI_VERSION}"
|
||||
docker push "pulumi/${container}:latest"
|
||||
done
|
||||
|
||||
docker logout
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
<RepositoryUrl>https://github.com/pulumi/pulumi</RepositoryUrl>
|
||||
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
|
||||
<PackageIcon>pulumi_logo_64x64.png</PackageIcon>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
|
|
|
@ -10,8 +10,8 @@ namespace Pulumi.Tests.Core
|
|||
public partial class OutputTests : PulumiTest
|
||||
{
|
||||
private static Output<T> CreateOutput<T>(T value, bool isKnown, bool isSecret = false)
|
||||
=> new Output<T>(ImmutableHashSet<Resource>.Empty,
|
||||
Task.FromResult(OutputData.Create(value, isKnown, isSecret)));
|
||||
=> new Output<T>(Task.FromResult(OutputData.Create(
|
||||
ImmutableHashSet<Resource>.Empty, value, isKnown, isSecret)));
|
||||
|
||||
public class PreviewTests
|
||||
{
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
58
sdk/dotnet/Pulumi.Tests/Serialization/ArgsConverterTests.cs
Normal file
58
sdk/dotnet/Pulumi.Tests/Serialization/ArgsConverterTests.cs
Normal file
|
@ -0,0 +1,58 @@
|
|||
// Copyright 2016-2019, Pulumi Corporation
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Pulumi.Serialization;
|
||||
using Xunit;
|
||||
|
||||
namespace Pulumi.Tests.Serialization
|
||||
{
|
||||
public class ArgsConverterTests : ConverterTests
|
||||
{
|
||||
public class SimpleInvokeArgs1 : InvokeArgs
|
||||
{
|
||||
[Input("s")]
|
||||
public string? S { get; set; }
|
||||
}
|
||||
|
||||
public class ComplexInvokeArgs1 : InvokeArgs
|
||||
{
|
||||
[Input("v")]
|
||||
public SimpleInvokeArgs1? V { get; set; }
|
||||
}
|
||||
|
||||
public class SimpleResourceArgs1 : ResourceArgs
|
||||
{
|
||||
[Input("s")]
|
||||
public Input<string>? S { get; set; }
|
||||
}
|
||||
|
||||
public class ComplexResourceArgs1 : ResourceArgs
|
||||
{
|
||||
[Input("v")]
|
||||
public Input<SimpleResourceArgs1>? V { get; set; }
|
||||
}
|
||||
|
||||
private async Task Test(object args, string expected)
|
||||
{
|
||||
var serialized = await SerializeToValueAsync(args);
|
||||
var converted = Converter.ConvertValue<JsonElement>("", serialized);
|
||||
var value = converted.Value.GetProperty("v").GetProperty("s").GetString();
|
||||
Assert.Equal(expected, value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task InvokeArgs()
|
||||
{
|
||||
var args = new ComplexInvokeArgs1 { V = new SimpleInvokeArgs1 { S = "value1" } };
|
||||
await Test(args, "value1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ResourceArgs()
|
||||
{
|
||||
var args = new ComplexResourceArgs1 { V = new SimpleResourceArgs1 { S = "value2" } };
|
||||
await Test(args, "value2");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -25,7 +25,8 @@ namespace Pulumi.Tests.Serialization
|
|||
};
|
||||
|
||||
protected Output<T> CreateUnknownOutput<T>(T value)
|
||||
=> new Output<T>(ImmutableHashSet<Resource>.Empty, Task.FromResult(new OutputData<T>(value, isKnown: false, isSecret: false)));
|
||||
=> new Output<T>(Task.FromResult(new OutputData<T>(
|
||||
ImmutableHashSet<Resource>.Empty, value, isKnown: false, isSecret: false)));
|
||||
|
||||
protected async Task<Value> SerializeToValueAsync(object? value)
|
||||
{
|
||||
|
|
113
sdk/dotnet/Pulumi.Tests/StackTests.cs
Normal file
113
sdk/dotnet/Pulumi.Tests/StackTests.cs
Normal file
|
@ -0,0 +1,113 @@
|
|||
// Copyright 2016-2019, Pulumi Corporation
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Moq;
|
||||
using Pulumi.Serialization;
|
||||
using Xunit;
|
||||
using Xunit.Sdk;
|
||||
|
||||
namespace Pulumi.Tests
|
||||
{
|
||||
public class StackTests
|
||||
{
|
||||
private class ValidStack : Stack
|
||||
{
|
||||
[Output("foo")]
|
||||
public Output<string> ExplicitName { get; }
|
||||
|
||||
[Output]
|
||||
public Output<string> ImplicitName { get; }
|
||||
|
||||
public ValidStack()
|
||||
{
|
||||
this.ExplicitName = Output.Create("bar");
|
||||
this.ImplicitName = Output.Create("buzz");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidStackInstantiationSucceeds()
|
||||
{
|
||||
var (stack, outputs) = await Run<ValidStack>();
|
||||
Assert.Equal(2, outputs.Count);
|
||||
Assert.Same(stack.ExplicitName, outputs["foo"]);
|
||||
Assert.Same(stack.ImplicitName, outputs["ImplicitName"]);
|
||||
}
|
||||
|
||||
private class NullOutputStack : Stack
|
||||
{
|
||||
[Output("foo")]
|
||||
public Output<string>? Foo { get; }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StackWithNullOutputsThrows()
|
||||
{
|
||||
try
|
||||
{
|
||||
var (stack, outputs) = await Run<NullOutputStack>();
|
||||
}
|
||||
catch (RunException ex)
|
||||
{
|
||||
Assert.Contains("foo", ex.Message);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new XunitException("Should not come here");
|
||||
}
|
||||
|
||||
private class InvalidOutputTypeStack : Stack
|
||||
{
|
||||
[Output("foo")]
|
||||
public string Foo { get; }
|
||||
|
||||
public InvalidOutputTypeStack()
|
||||
{
|
||||
this.Foo = "bar";
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StackWithInvalidOutputTypeThrows()
|
||||
{
|
||||
try
|
||||
{
|
||||
var (stack, outputs) = await Run<InvalidOutputTypeStack>();
|
||||
}
|
||||
catch (RunException ex)
|
||||
{
|
||||
Assert.Contains("foo", ex.Message);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new XunitException("Should not come here");
|
||||
}
|
||||
|
||||
private async Task<(T, IDictionary<string, object?>)> Run<T>() where T : Stack, new()
|
||||
{
|
||||
// Arrange
|
||||
Output<IDictionary<string, object?>>? outputs = null;
|
||||
|
||||
var mock = new Mock<IDeploymentInternal>(MockBehavior.Strict);
|
||||
mock.Setup(d => d.ProjectName).Returns("TestProject");
|
||||
mock.Setup(d => d.StackName).Returns("TestStack");
|
||||
mock.SetupSet(content => content.Stack = It.IsAny<Stack>());
|
||||
mock.Setup(d => d.ReadOrRegisterResource(It.IsAny<Stack>(), It.IsAny<ResourceArgs>(), It.IsAny<ResourceOptions>()));
|
||||
mock.Setup(d => d.RegisterResourceOutputs(It.IsAny<Stack>(), It.IsAny<Output<IDictionary<string, object?>>>()))
|
||||
.Callback((Resource _, Output<IDictionary<string, object?>> o) => outputs = o);
|
||||
|
||||
Deployment.Instance = mock.Object;
|
||||
|
||||
// Act
|
||||
var stack = new T();
|
||||
stack.RegisterPropertyOutputs();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(outputs);
|
||||
var values = await outputs!.DataTask;
|
||||
return (stack, values.Value);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Pulumi.Tests")]
|
||||
[assembly: InternalsVisibleTo("Pulumi.Tests")]
|
||||
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // Moq tests
|
||||
|
|
|
@ -80,7 +80,7 @@ namespace Pulumi
|
|||
/// </summary>
|
||||
internal interface IOutput
|
||||
{
|
||||
ImmutableHashSet<Resource> Resources { get; }
|
||||
Task<ImmutableHashSet<Resource>> GetResourcesAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Returns an <see cref="Output{T}"/> equivalent to this, except with our
|
||||
|
@ -105,14 +105,10 @@ namespace Pulumi
|
|||
/// </summary>
|
||||
public sealed class Output<T> : IOutput
|
||||
{
|
||||
internal ImmutableHashSet<Resource> Resources { get; private set; }
|
||||
internal Task<OutputData<T>> DataTask { get; private set; }
|
||||
|
||||
internal Output(ImmutableHashSet<Resource> resources, Task<OutputData<T>> dataTask)
|
||||
{
|
||||
Resources = resources;
|
||||
DataTask = dataTask;
|
||||
}
|
||||
internal Output(Task<OutputData<T>> dataTask)
|
||||
=> DataTask = dataTask;
|
||||
|
||||
internal async Task<T> GetValueAsync()
|
||||
{
|
||||
|
@ -120,7 +116,11 @@ namespace Pulumi
|
|||
return data.Value;
|
||||
}
|
||||
|
||||
ImmutableHashSet<Resource> IOutput.Resources => this.Resources;
|
||||
async Task<ImmutableHashSet<Resource>> IOutput.GetResourcesAsync()
|
||||
{
|
||||
var data = await DataTask.ConfigureAwait(false);
|
||||
return data.Resources;
|
||||
}
|
||||
|
||||
async Task<OutputData<object?>> IOutput.GetDataAsync()
|
||||
=> await DataTask.ConfigureAwait(false);
|
||||
|
@ -131,6 +131,17 @@ namespace Pulumi
|
|||
internal static Output<T> CreateSecret(Task<T> value)
|
||||
=> Create(value, isSecret: true);
|
||||
|
||||
internal Output<T> WithIsSecret(Task<bool> isSecret)
|
||||
{
|
||||
async Task<OutputData<T>> GetData()
|
||||
{
|
||||
var data = await this.DataTask.ConfigureAwait(false);
|
||||
return new OutputData<T>(data.Resources, data.Value, data.IsKnown, await isSecret.ConfigureAwait(false));
|
||||
}
|
||||
|
||||
return new Output<T>(GetData());
|
||||
}
|
||||
|
||||
private static Output<T> Create(Task<T> value, bool isSecret)
|
||||
{
|
||||
if (value == null)
|
||||
|
@ -139,8 +150,8 @@ namespace Pulumi
|
|||
}
|
||||
|
||||
var tcs = new TaskCompletionSource<OutputData<T>>();
|
||||
value.Assign(tcs, t => OutputData.Create(t, isKnown: true, isSecret: isSecret));
|
||||
return new Output<T>(ImmutableHashSet<Resource>.Empty, tcs.Task);
|
||||
value.Assign(tcs, t => OutputData.Create(ImmutableHashSet<Resource>.Empty, t, isKnown: true, isSecret: isSecret));
|
||||
return new Output<T>(tcs.Task);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -191,37 +202,39 @@ namespace Pulumi
|
|||
/// then).
|
||||
/// </summary>
|
||||
public Output<U> Apply<U>(Func<T, Output<U>?> func)
|
||||
=> new Output<U>(Resources, ApplyHelperAsync(DataTask, func));
|
||||
=> new Output<U>(ApplyHelperAsync(DataTask, func));
|
||||
|
||||
private static async Task<OutputData<U>> ApplyHelperAsync<U>(
|
||||
Task<OutputData<T>> dataTask, Func<T, Output<U>?> func)
|
||||
{
|
||||
var data = await dataTask.ConfigureAwait(false);
|
||||
|
||||
var resources = data.Resources;
|
||||
// During previews only perform the apply if the engine was able to
|
||||
// give us an actual value for this Output.
|
||||
if (!data.IsKnown && Deployment.Instance.IsDryRun)
|
||||
{
|
||||
return new OutputData<U>(default!, isKnown: false, data.IsSecret);
|
||||
return new OutputData<U>(resources, default!, isKnown: false, data.IsSecret);
|
||||
}
|
||||
|
||||
var inner = func(data.Value);
|
||||
if (inner == null)
|
||||
{
|
||||
return OutputData.Create(default(U)!, data.IsKnown, data.IsSecret);
|
||||
return OutputData.Create(resources, default(U)!, data.IsKnown, data.IsSecret);
|
||||
}
|
||||
|
||||
var innerData = await inner.DataTask.ConfigureAwait(false);
|
||||
|
||||
return OutputData.Create(
|
||||
innerData.Value, data.IsKnown && innerData.IsKnown, data.IsSecret || innerData.IsSecret);
|
||||
data.Resources.Union(innerData.Resources), innerData.Value,
|
||||
data.IsKnown && innerData.IsKnown, data.IsSecret || innerData.IsSecret);
|
||||
}
|
||||
|
||||
internal static Output<ImmutableArray<T>> All(ImmutableArray<Input<T>> inputs)
|
||||
=> new Output<ImmutableArray<T>>(GetAllResources(inputs), AllHelperAsync(inputs));
|
||||
=> new Output<ImmutableArray<T>>(AllHelperAsync(inputs));
|
||||
|
||||
private static async Task<OutputData<ImmutableArray<T>>> AllHelperAsync(ImmutableArray<Input<T>> inputs)
|
||||
{
|
||||
var resources = ImmutableHashSet.CreateBuilder<Resource>();
|
||||
var values = ImmutableArray.CreateBuilder<T>(inputs.Length);
|
||||
var isKnown = true;
|
||||
var isSecret = false;
|
||||
|
@ -231,23 +244,24 @@ namespace Pulumi
|
|||
var data = await output.DataTask.ConfigureAwait(false);
|
||||
|
||||
values.Add(data.Value);
|
||||
resources.UnionWith(data.Resources);
|
||||
(isKnown, isSecret) = OutputData.Combine(data, isKnown, isSecret);
|
||||
}
|
||||
|
||||
return OutputData.Create(values.MoveToImmutable(), isKnown, isSecret);
|
||||
return OutputData.Create(resources.ToImmutable(), values.MoveToImmutable(), isKnown, isSecret);
|
||||
}
|
||||
|
||||
internal static Output<(T1, T2, T3, T4, T5, T6, T7, T8)> Tuple<T1, T2, T3, T4, T5, T6, T7, T8>(
|
||||
Input<T1> item1, Input<T2> item2, Input<T3> item3, Input<T4> item4,
|
||||
Input<T5> item5, Input<T6> item6, Input<T7> item7, Input<T8> item8)
|
||||
=> new Output<(T1, T2, T3, T4, T5, T6, T7, T8)>(
|
||||
GetAllResources(new IInput[] { item1, item2, item3, item4, item5, item6, item7, item8 }),
|
||||
TupleHelperAsync(item1, item2, item3, item4, item5, item6, item7, item8));
|
||||
|
||||
private static async Task<OutputData<(T1, T2, T3, T4, T5, T6, T7, T8)>> TupleHelperAsync<T1, T2, T3, T4, T5, T6, T7, T8>(
|
||||
Input<T1> item1, Input<T2> item2, Input<T3> item3, Input<T4> item4,
|
||||
Input<T5> item5, Input<T6> item6, Input<T7> item7, Input<T8> item8)
|
||||
{
|
||||
var resources = ImmutableHashSet.CreateBuilder<Resource>();
|
||||
(T1, T2, T3, T4, T5, T6, T7, T8) tuple = default;
|
||||
var isKnown = true;
|
||||
var isSecret = false;
|
||||
|
@ -261,7 +275,7 @@ namespace Pulumi
|
|||
Update(await GetData(item7).ConfigureAwait(false), ref tuple.Item7);
|
||||
Update(await GetData(item8).ConfigureAwait(false), ref tuple.Item8);
|
||||
|
||||
return OutputData.Create(tuple, isKnown, isSecret);
|
||||
return OutputData.Create(resources.ToImmutable(), tuple, isKnown, isSecret);
|
||||
|
||||
static async Task<OutputData<X>> GetData<X>(Input<X> input)
|
||||
{
|
||||
|
@ -271,12 +285,10 @@ namespace Pulumi
|
|||
|
||||
void Update<X>(OutputData<X> data, ref X location)
|
||||
{
|
||||
resources.UnionWith(data.Resources);
|
||||
location = data.Value;
|
||||
(isKnown, isSecret) = OutputData.Combine(data, isKnown, isSecret);
|
||||
}
|
||||
}
|
||||
|
||||
private static ImmutableHashSet<Resource> GetAllResources(IEnumerable<IInput> inputs)
|
||||
=> ImmutableHashSet.CreateRange(inputs.SelectMany(i => i.ToOutput().Resources));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,23 @@ namespace Pulumi
|
|||
public Runner(IDeploymentInternal deployment)
|
||||
=> _deployment = deployment;
|
||||
|
||||
public Task<int> RunAsync<TStack>() where TStack : Stack, new()
|
||||
{
|
||||
try
|
||||
{
|
||||
var stack = new TStack();
|
||||
// Stack doesn't call RegisterOutputs, so we register them on its behalf.
|
||||
stack.RegisterPropertyOutputs();
|
||||
RegisterTask("User program code.", stack.Outputs.DataTask);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return HandleExceptionAsync(ex);
|
||||
}
|
||||
|
||||
return WhileRunningAsync();
|
||||
}
|
||||
|
||||
public Task<int> RunAsync(Func<Task<IDictionary<string, object?>>> func)
|
||||
{
|
||||
var stack = new Stack(func);
|
||||
|
|
|
@ -9,6 +9,7 @@ namespace Pulumi
|
|||
public partial class Deployment
|
||||
{
|
||||
private Task<string>? _rootResource;
|
||||
private object _rootResourceLock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// Returns a root resource URN that will automatically become the default parent of all
|
||||
|
@ -23,21 +24,18 @@ namespace Pulumi
|
|||
if (type == Stack._rootPulumiStackTypeName)
|
||||
return null;
|
||||
|
||||
if (_rootResource == null)
|
||||
throw new InvalidOperationException($"Calling {nameof(GetRootResourceAsync)} before the root resource was registered!");
|
||||
lock (_rootResourceLock)
|
||||
{
|
||||
if (_rootResource == null)
|
||||
{
|
||||
var stack = InternalInstance.Stack ?? throw new InvalidOperationException($"Calling {nameof(GetRootResourceAsync)} before the stack was registered!");
|
||||
_rootResource = SetRootResourceWorkerAsync(stack);
|
||||
}
|
||||
}
|
||||
|
||||
return await _rootResource.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
Task IDeploymentInternal.SetRootResourceAsync(Stack stack)
|
||||
{
|
||||
if (_rootResource != null)
|
||||
throw new InvalidOperationException("Tried to set the root resource more than once!");
|
||||
|
||||
_rootResource = SetRootResourceWorkerAsync(stack);
|
||||
return _rootResource;
|
||||
}
|
||||
|
||||
private async Task<string> SetRootResourceWorkerAsync(Stack stack)
|
||||
{
|
||||
var resUrn = await stack.Urn.GetValueAsync().ConfigureAwait(false);
|
||||
|
|
|
@ -28,7 +28,7 @@ namespace Pulumi
|
|||
=> RunAsync(() => Task.FromResult(func()));
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="RunAsync(Func{Task{IDictionary{string, object}}})"/> is the
|
||||
/// <see cref="RunAsync(Func{Task{IDictionary{string, object}}})"/> is an
|
||||
/// entry-point to a Pulumi application. .NET applications should perform all startup logic
|
||||
/// they need in their <c>Main</c> method and then end with:
|
||||
/// <para>
|
||||
|
@ -36,7 +36,7 @@ namespace Pulumi
|
|||
/// static Task<int> Main(string[] args)
|
||||
/// {
|
||||
/// // program initialization code ...
|
||||
///
|
||||
///
|
||||
/// return Deployment.Run(async () =>
|
||||
/// {
|
||||
/// // Code that creates resources.
|
||||
|
@ -56,6 +56,38 @@ namespace Pulumi
|
|||
/// in this dictionary will become the outputs for the Pulumi Stack that is created.
|
||||
/// </summary>
|
||||
public static Task<int> RunAsync(Func<Task<IDictionary<string, object?>>> func)
|
||||
=> CreateRunner().RunAsync(func);
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="RunAsync{TStack}()"/> is an entry-point to a Pulumi
|
||||
/// application. .NET applications should perform all startup logic they
|
||||
/// need in their <c>Main</c> method and then end with:
|
||||
/// <para>
|
||||
/// <c>
|
||||
/// static Task<int> Main(string[] args) {// program
|
||||
/// initialization code ...
|
||||
///
|
||||
/// return Deployment.Run<MyStack>();}
|
||||
/// </c>
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Deployment will instantiate a new stack instance based on the type
|
||||
/// passed as TStack type parameter. Importantly, cloud resources cannot
|
||||
/// be created outside of the <see cref="Stack"/> component.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Because cloud Resource construction is inherently asynchronous, the
|
||||
/// result of this function is a <see cref="Task{T}"/> which should then
|
||||
/// be returned or awaited. This will ensure that any problems that are
|
||||
/// encountered during the running of the program are properly reported.
|
||||
/// Failure to do this may lead to the program ending early before all
|
||||
/// resources are properly registered.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static Task<int> RunAsync<TStack>() where TStack : Stack, new()
|
||||
=> CreateRunner().RunAsync<TStack>();
|
||||
|
||||
private static IRunner CreateRunner()
|
||||
{
|
||||
// Serilog.Log.Logger = new LoggerConfiguration().MinimumLevel.Debug().WriteTo.Console().CreateLogger();
|
||||
|
||||
|
@ -68,7 +100,7 @@ namespace Pulumi
|
|||
Serilog.Log.Debug("Creating new Deployment.");
|
||||
var deployment = new Deployment();
|
||||
Instance = deployment;
|
||||
return deployment._runner.RunAsync(func);
|
||||
return deployment._runner;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,8 +16,6 @@ namespace Pulumi
|
|||
ILogger Logger { get; }
|
||||
IRunner Runner { get; }
|
||||
|
||||
Task SetRootResourceAsync(Stack stack);
|
||||
|
||||
void ReadOrRegisterResource(Resource resource, ResourceArgs args, ResourceOptions opts);
|
||||
void RegisterResourceOutputs(Resource resource, Output<IDictionary<string, object?>> outputs);
|
||||
}
|
||||
|
|
|
@ -10,5 +10,6 @@ namespace Pulumi
|
|||
{
|
||||
void RegisterTask(string description, Task task);
|
||||
Task<int> RunAsync(Func<Task<IDictionary<string, object?>>> func);
|
||||
Task<int> RunAsync<TStack>() where TStack : Stack, new();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ namespace Pulumi
|
|||
{
|
||||
/// <summary>
|
||||
/// Options to help control the behavior of <see cref="IDeployment.InvokeAsync{T}(string,
|
||||
/// ResourceArgs, InvokeOptions)"/>.
|
||||
/// InvokeArgs, InvokeOptions)"/>.
|
||||
/// </summary>
|
||||
public class InvokeOptions
|
||||
{
|
||||
|
|
|
@ -31,7 +31,7 @@ namespace Pulumi
|
|||
{
|
||||
var output = obj is IInput input ? input.ToOutput() : obj as IOutput;
|
||||
return output != null
|
||||
? new Output<object?>(output.Resources, output.GetDataAsync())
|
||||
? new Output<object?>(output.GetDataAsync())
|
||||
: Output.Create(obj);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
// Copyright 2016-2019, Pulumi Corporation
|
||||
|
||||
using System;
|
||||
|
||||
namespace Pulumi
|
||||
{
|
||||
/// <summary>
|
||||
|
@ -23,16 +25,23 @@ namespace Pulumi
|
|||
=> Deployment.InternalInstance.Logger.InfoAsync(message, resource, streamId, ephemeral);
|
||||
|
||||
/// <summary>
|
||||
/// Warn logs a warning to indicate that something went wrong, but not catastrophically so.
|
||||
/// Logs a warning to indicate that something went wrong, but not catastrophically so.
|
||||
/// </summary>
|
||||
public static void Warn(string message, Resource? resource = null, int? streamId = null, bool? ephemeral = null)
|
||||
=> Deployment.InternalInstance.Logger.WarnAsync(message, resource, streamId, ephemeral);
|
||||
|
||||
/// <summary>
|
||||
/// Error logs a fatal error to indicate that the tool should stop processing resource
|
||||
/// Logs a fatal error to indicate that the tool should stop processing resource
|
||||
/// operations immediately.
|
||||
/// </summary>
|
||||
public static void Error(string message, Resource? resource = null, int? streamId = null, bool? ephemeral = null)
|
||||
=> Deployment.InternalInstance.Logger.ErrorAsync(message, resource, streamId, ephemeral);
|
||||
|
||||
/// <summary>
|
||||
/// Logs a fatal exception to indicate that the tool should stop processing resource
|
||||
/// operations immediately.
|
||||
/// </summary>
|
||||
public static void Exception(Exception exception, Resource? resource = null, int? streamId = null, bool? ephemeral = null)
|
||||
=> Error(exception.ToString(), resource, streamId, ephemeral);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -165,11 +165,26 @@ Pulumi.Serialization.InputAttribute
|
|||
Pulumi.Serialization.InputAttribute.InputAttribute(string name, bool required = false, bool json = false) -> void
|
||||
Pulumi.Serialization.OutputAttribute
|
||||
Pulumi.Serialization.OutputAttribute.Name.get -> string
|
||||
Pulumi.Serialization.OutputAttribute.OutputAttribute(string name) -> void
|
||||
Pulumi.Serialization.OutputAttribute.OutputAttribute(string name = null) -> void
|
||||
Pulumi.Serialization.OutputConstructorAttribute
|
||||
Pulumi.Serialization.OutputConstructorAttribute.OutputConstructorAttribute() -> void
|
||||
Pulumi.Serialization.OutputTypeAttribute
|
||||
Pulumi.Serialization.OutputTypeAttribute.OutputTypeAttribute() -> void
|
||||
Pulumi.Stack
|
||||
Pulumi.Stack.Stack() -> void
|
||||
Pulumi.StackReference
|
||||
Pulumi.StackReference.GetOutput(Pulumi.Input<string> name) -> Pulumi.Output<object>
|
||||
Pulumi.StackReference.GetValueAsync(Pulumi.Input<string> name) -> System.Threading.Tasks.Task<object>
|
||||
Pulumi.StackReference.Name.get -> Pulumi.Output<string>
|
||||
Pulumi.StackReference.Outputs.get -> Pulumi.Output<System.Collections.Immutable.ImmutableDictionary<string, object>>
|
||||
Pulumi.StackReference.RequireOutput(Pulumi.Input<string> name) -> Pulumi.Output<object>
|
||||
Pulumi.StackReference.RequireValueAsync(Pulumi.Input<string> name) -> System.Threading.Tasks.Task<object>
|
||||
Pulumi.StackReference.SecretOutputNames.get -> Pulumi.Output<System.Collections.Immutable.ImmutableArray<string>>
|
||||
Pulumi.StackReference.StackReference(string name, Pulumi.StackReferenceArgs args = null, Pulumi.ResourceOptions options = null) -> void
|
||||
Pulumi.StackReferenceArgs
|
||||
Pulumi.StackReferenceArgs.Name.get -> Pulumi.Input<string>
|
||||
Pulumi.StackReferenceArgs.Name.set -> void
|
||||
Pulumi.StackReferenceArgs.StackReferenceArgs() -> void
|
||||
Pulumi.StringAsset
|
||||
Pulumi.StringAsset.StringAsset(string text) -> void
|
||||
Pulumi.Union<T0, T1>
|
||||
|
@ -195,6 +210,7 @@ static Pulumi.Deployment.Instance.get -> Pulumi.IDeployment
|
|||
static Pulumi.Deployment.RunAsync(System.Action action) -> System.Threading.Tasks.Task<int>
|
||||
static Pulumi.Deployment.RunAsync(System.Func<System.Collections.Generic.IDictionary<string, object>> func) -> System.Threading.Tasks.Task<int>
|
||||
static Pulumi.Deployment.RunAsync(System.Func<System.Threading.Tasks.Task<System.Collections.Generic.IDictionary<string, object>>> func) -> System.Threading.Tasks.Task<int>
|
||||
static Pulumi.Deployment.RunAsync<TStack>() -> System.Threading.Tasks.Task<int>
|
||||
static Pulumi.Input<T>.implicit operator Pulumi.Input<T>(Pulumi.Output<T> value) -> Pulumi.Input<T>
|
||||
static Pulumi.Input<T>.implicit operator Pulumi.Input<T>(T value) -> Pulumi.Input<T>
|
||||
static Pulumi.Input<T>.implicit operator Pulumi.Output<T>(Pulumi.Input<T> input) -> Pulumi.Output<T>
|
||||
|
@ -237,6 +253,7 @@ static Pulumi.InputUnion<T0, T1>.implicit operator Pulumi.InputUnion<T0, T1>(T0
|
|||
static Pulumi.InputUnion<T0, T1>.implicit operator Pulumi.InputUnion<T0, T1>(T1 value) -> Pulumi.InputUnion<T0, T1>
|
||||
static Pulumi.Log.Debug(string message, Pulumi.Resource resource = null, int? streamId = null, bool? ephemeral = null) -> void
|
||||
static Pulumi.Log.Error(string message, Pulumi.Resource resource = null, int? streamId = null, bool? ephemeral = null) -> void
|
||||
static Pulumi.Log.Exception(System.Exception exception, Pulumi.Resource resource = null, int? streamId = null, bool? ephemeral = null) -> void
|
||||
static Pulumi.Log.Info(string message, Pulumi.Resource resource = null, int? streamId = null, bool? ephemeral = null) -> void
|
||||
static Pulumi.Log.Warn(string message, Pulumi.Resource resource = null, int? streamId = null, bool? ephemeral = null) -> void
|
||||
static Pulumi.Output.All<T>(System.Collections.Immutable.ImmutableArray<Pulumi.Input<T>> inputs) -> Pulumi.Output<System.Collections.Immutable.ImmutableArray<T>>
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<Authors>Pulumi</Authors>
|
||||
<Company>Pulumi Corp.</Company>
|
||||
|
@ -9,9 +11,7 @@
|
|||
<RepositoryUrl>https://github.com/pulumi/pulumi</RepositoryUrl>
|
||||
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
|
||||
<PackageIcon>pulumi_logo_64x64.png</PackageIcon>
|
||||
|
||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
|
|
156
sdk/dotnet/Pulumi/Resources/StackReference.cs
Normal file
156
sdk/dotnet/Pulumi/Resources/StackReference.cs
Normal file
|
@ -0,0 +1,156 @@
|
|||
// Copyright 2016-2019, Pulumi Corporation
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Threading.Tasks;
|
||||
using Pulumi.Serialization;
|
||||
|
||||
namespace Pulumi
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages a reference to a Pulumi stack and provides access to the referenced stack's outputs.
|
||||
/// </summary>
|
||||
public class StackReference : CustomResource
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the referenced stack.
|
||||
/// </summary>
|
||||
[Output("name")]
|
||||
public Output<string> Name { get; private set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The outputs of the referenced stack.
|
||||
/// </summary>
|
||||
[Output("outputs")]
|
||||
public Output<ImmutableDictionary<string, object>> Outputs { get; private set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The names of any stack outputs which contain secrets.
|
||||
/// </summary>
|
||||
[Output("secretOutputNames")]
|
||||
public Output<ImmutableArray<string>> SecretOutputNames { get; private set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Create a <see cref="StackReference"/> resource with the given unique name, arguments, and options.
|
||||
/// <para />
|
||||
/// If args is not specified, the name of the referenced stack will be the name of the StackReference resource.
|
||||
/// </summary>
|
||||
/// <param name="name">The unique name of the stack reference.</param>
|
||||
/// <param name="args">The arguments to use to populate this resource's properties.</param>
|
||||
/// <param name="options">A bag of options that control this resource's behavior.</param>
|
||||
public StackReference(string name, StackReferenceArgs? args = null, ResourceOptions? options = null)
|
||||
: base("pulumi:pulumi:StackReference",
|
||||
name,
|
||||
new StackReferenceArgs { Name = args?.Name ?? name },
|
||||
CustomResourceOptions.Merge(options, new CustomResourceOptions { Id = args?.Name ?? name }))
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches the value of the named stack output, or null if the stack output was not found.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the stack output to fetch.</param>
|
||||
/// <returns>An <see cref="Output{T}"/> containing the requested value.</returns>
|
||||
public Output<object?> GetOutput(Input<string> name)
|
||||
{
|
||||
// Note that this is subltly different from "Apply" here. A default "Apply" will set the secret bit if any
|
||||
// of the inputs are a secret, and this.Outputs is always a secret if it contains any secrets. We do this dance
|
||||
// so we can ensure that the Output we return is not needlessly tainted as a secret.
|
||||
var inputs = (Input<ImmutableDictionary<string, object>>)this.Outputs;
|
||||
var value = Output.Tuple(name, inputs).Apply(v =>
|
||||
v.Item2.TryGetValue(v.Item1, out var result) ? result : null);
|
||||
|
||||
return value.WithIsSecret(IsSecretOutputName(name));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches the value of the named stack output, or throws an error if the output was not found.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the stack output to fetch.</param>
|
||||
/// <returns>An <see cref="Output{T}"/> containing the requested value.</returns>
|
||||
public Output<object> RequireOutput(Input<string> name)
|
||||
{
|
||||
var inputs = (Input<ImmutableDictionary<string, object>>)this.Outputs;
|
||||
var value = Output.Tuple(name, inputs).Apply(v =>
|
||||
v.Item2.TryGetValue(v.Item1, out var result)
|
||||
? result
|
||||
: throw new KeyNotFoundException(
|
||||
$"Required output '{name}' does not exist on stack '{Deployment.Instance.StackName}'."));
|
||||
|
||||
return value.WithIsSecret(IsSecretOutputName(name));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches the value of the named stack output. May return null if the value is
|
||||
/// not known for some reason.
|
||||
/// <para />
|
||||
/// This operation is not supported (and will throw) for secret outputs.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the stack output to fetch.</param>
|
||||
/// <returns>The value of the referenced stack output.</returns>
|
||||
public async Task<object?> GetValueAsync(Input<string> name)
|
||||
{
|
||||
var output = this.GetOutput(name);
|
||||
var data = await output.DataTask.ConfigureAwait(false);
|
||||
if (data.IsSecret)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Cannot call 'GetOutputValueAsync' if the referenced stack has secret outputs. Use 'GetOutput' instead.");
|
||||
}
|
||||
|
||||
return data.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches the value promptly of the named stack output. Throws an error if the stack output is
|
||||
/// not found.
|
||||
/// <para />
|
||||
/// This operation is not supported (and will throw) for secret outputs.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the stack output to fetch.</param>
|
||||
/// <returns>The value of the referenced stack output.</returns>
|
||||
public async Task<object> RequireValueAsync(Input<string> name)
|
||||
{
|
||||
var output = this.RequireOutput(name);
|
||||
var data = await output.DataTask.ConfigureAwait(false);
|
||||
if (data.IsSecret)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Cannot call 'RequireOutputValueAsync' if the referenced stack has secret outputs. Use 'RequireOutput' instead.");
|
||||
}
|
||||
|
||||
return data.Value;
|
||||
}
|
||||
|
||||
private async Task<bool> IsSecretOutputName(Input<string> name)
|
||||
{
|
||||
var nameOutput = await name.ToOutput().DataTask.ConfigureAwait(false);
|
||||
var secretOutputNamesData = await this.SecretOutputNames.DataTask.ConfigureAwait(false);
|
||||
|
||||
// If either the name or set of secret outputs is unknown, we can't do anything smart, so we just copy the
|
||||
// secretness from the entire outputs value.
|
||||
if (!(nameOutput.IsKnown && secretOutputNamesData.IsKnown))
|
||||
{
|
||||
return (await this.Outputs.DataTask.ConfigureAwait(false)).IsSecret;
|
||||
}
|
||||
|
||||
// Otherwise, if we have a list of outputs we know are secret, we can use that list to determine if this
|
||||
// output should be secret.
|
||||
var names = secretOutputNamesData.Value;
|
||||
return names.Contains(nameOutput.Value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The set of arguments for constructing a StackReference resource.
|
||||
/// </summary>
|
||||
public class StackReferenceArgs : ResourceArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the stack to reference.
|
||||
/// </summary>
|
||||
[Input("name", required: true)]
|
||||
public Input<string>? Name { get; set; } = null!;
|
||||
}
|
||||
}
|
|
@ -11,9 +11,9 @@ namespace Pulumi.Serialization
|
|||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public sealed class OutputAttribute : Attribute
|
||||
{
|
||||
public string Name { get; }
|
||||
public string? Name { get; }
|
||||
|
||||
public OutputAttribute(string name)
|
||||
public OutputAttribute(string? name = null)
|
||||
{
|
||||
Name = name;
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ namespace Pulumi.Serialization
|
|||
public static OutputData<T> ConvertValue<T>(string context, Value value)
|
||||
{
|
||||
var (data, isKnown, isSecret) = ConvertValue(context, value, typeof(T));
|
||||
return new OutputData<T>((T)data!, isKnown, isSecret);
|
||||
return new OutputData<T>(ImmutableHashSet<Resource>.Empty, (T)data!, isKnown, isSecret);
|
||||
}
|
||||
|
||||
public static OutputData<object?> ConvertValue(string context, Value value, System.Type targetType)
|
||||
|
@ -26,7 +26,8 @@ namespace Pulumi.Serialization
|
|||
var (deserialized, isKnown, isSecret) = Deserializer.Deserialize(value);
|
||||
var converted = ConvertObject(context, deserialized, targetType);
|
||||
|
||||
return new OutputData<object?>(converted, isKnown, isSecret);
|
||||
return new OutputData<object?>(
|
||||
ImmutableHashSet<Resource>.Empty, converted, isKnown, isSecret);
|
||||
}
|
||||
|
||||
private static object? ConvertObject(string context, object? val, System.Type targetType)
|
||||
|
|
|
@ -19,16 +19,16 @@ namespace Pulumi.Serialization
|
|||
value.StringValue == Constants.UnknownValue)
|
||||
{
|
||||
// always deserialize unknown as the null value.
|
||||
return new OutputData<T>(default!, isKnown: false, isSecret);
|
||||
return new OutputData<T>(ImmutableHashSet<Resource>.Empty, default!, isKnown: false, isSecret);
|
||||
}
|
||||
|
||||
if (TryDeserializeAssetOrArchive(value, out var assetOrArchive))
|
||||
{
|
||||
return new OutputData<T>((T)(object)assetOrArchive, isKnown: true, isSecret);
|
||||
return new OutputData<T>(ImmutableHashSet<Resource>.Empty, (T)(object)assetOrArchive, isKnown: true, isSecret);
|
||||
}
|
||||
|
||||
var innerData = func(value);
|
||||
return OutputData.Create(innerData.Value, innerData.IsKnown, isSecret || innerData.IsSecret);
|
||||
return OutputData.Create(innerData.Resources, innerData.Value, innerData.IsKnown, isSecret || innerData.IsSecret);
|
||||
}
|
||||
|
||||
private static OutputData<T> DeserializeOneOf<T>(Value value, Value.KindOneofCase kind, Func<Value, OutputData<T>> func)
|
||||
|
@ -36,7 +36,8 @@ namespace Pulumi.Serialization
|
|||
v.KindCase == kind ? func(v) : throw new InvalidOperationException($"Trying to deserialize {v.KindCase} as a {kind}"));
|
||||
|
||||
private static OutputData<T> DeserializePrimitive<T>(Value value, Value.KindOneofCase kind, Func<Value, T> func)
|
||||
=> DeserializeOneOf(value, kind, v => OutputData.Create(func(v), isKnown: true, isSecret: false));
|
||||
=> DeserializeOneOf(value, kind, v => OutputData.Create(
|
||||
ImmutableHashSet<Resource>.Empty, func(v), isKnown: true, isSecret: false));
|
||||
|
||||
private static OutputData<bool> DeserializeBoolean(Value value)
|
||||
=> DeserializePrimitive(value, Value.KindOneofCase.BoolValue, v => v.BoolValue);
|
||||
|
@ -51,6 +52,7 @@ namespace Pulumi.Serialization
|
|||
=> DeserializeOneOf(value, Value.KindOneofCase.ListValue,
|
||||
v =>
|
||||
{
|
||||
var resources = ImmutableHashSet.CreateBuilder<Resource>();
|
||||
var result = ImmutableArray.CreateBuilder<object?>();
|
||||
var isKnown = true;
|
||||
var isSecret = false;
|
||||
|
@ -59,16 +61,18 @@ namespace Pulumi.Serialization
|
|||
{
|
||||
var elementData = Deserialize(element);
|
||||
(isKnown, isSecret) = OutputData.Combine(elementData, isKnown, isSecret);
|
||||
resources.UnionWith(elementData.Resources);
|
||||
result.Add(elementData.Value);
|
||||
}
|
||||
|
||||
return OutputData.Create(result.ToImmutable(), isKnown, isSecret);
|
||||
return OutputData.Create(resources.ToImmutable(), result.ToImmutable(), isKnown, isSecret);
|
||||
});
|
||||
|
||||
private static OutputData<ImmutableDictionary<string, object?>> DeserializeStruct(Value value)
|
||||
=> DeserializeOneOf(value, Value.KindOneofCase.StructValue,
|
||||
v =>
|
||||
{
|
||||
var resources = ImmutableHashSet.CreateBuilder<Resource>();
|
||||
var result = ImmutableDictionary.CreateBuilder<string, object?>();
|
||||
var isKnown = true;
|
||||
var isSecret = false;
|
||||
|
@ -85,9 +89,10 @@ namespace Pulumi.Serialization
|
|||
var elementData = Deserialize(element);
|
||||
(isKnown, isSecret) = OutputData.Combine(elementData, isKnown, isSecret);
|
||||
result.Add(key, elementData.Value);
|
||||
resources.UnionWith(elementData.Resources);
|
||||
}
|
||||
|
||||
return OutputData.Create(result.ToImmutable(), isKnown, isSecret);
|
||||
return OutputData.Create(resources.ToImmutable(), result.ToImmutable(), isKnown, isSecret);
|
||||
});
|
||||
|
||||
public static OutputData<object?> Deserialize(Value value)
|
||||
|
@ -99,7 +104,7 @@ namespace Pulumi.Serialization
|
|||
Value.KindOneofCase.BoolValue => DeserializeBoolean(v),
|
||||
Value.KindOneofCase.StructValue => DeserializeStruct(v),
|
||||
Value.KindOneofCase.ListValue => DeserializeList(v),
|
||||
Value.KindOneofCase.NullValue => new OutputData<object?>(null, isKnown: true, isSecret: false),
|
||||
Value.KindOneofCase.NullValue => new OutputData<object?>(ImmutableHashSet<Resource>.Empty, null, isKnown: true, isSecret: false),
|
||||
Value.KindOneofCase.None => throw new InvalidOperationException("Should never get 'None' type when deserializing protobuf"),
|
||||
_ => throw new InvalidOperationException("Unknown type when deserializing protobuf: " + v.KindCase),
|
||||
});
|
||||
|
|
|
@ -24,15 +24,15 @@ namespace Pulumi.Serialization
|
|||
|
||||
internal class OutputCompletionSource<T> : IOutputCompletionSource
|
||||
{
|
||||
private readonly ImmutableHashSet<Resource> _resources;
|
||||
private readonly TaskCompletionSource<OutputData<T>> _taskCompletionSource;
|
||||
public readonly Output<T> Output;
|
||||
|
||||
public OutputCompletionSource(Resource? resource)
|
||||
{
|
||||
_resources = resource == null ? ImmutableHashSet<Resource>.Empty : ImmutableHashSet.Create(resource);
|
||||
_taskCompletionSource = new TaskCompletionSource<OutputData<T>>();
|
||||
Output = new Output<T>(
|
||||
resource == null ? ImmutableHashSet<Resource>.Empty : ImmutableHashSet.Create(resource),
|
||||
_taskCompletionSource.Task);
|
||||
Output = new Output<T>(_taskCompletionSource.Task);
|
||||
}
|
||||
|
||||
public System.Type TargetType => typeof(T);
|
||||
|
@ -40,13 +40,16 @@ namespace Pulumi.Serialization
|
|||
IOutput IOutputCompletionSource.Output => Output;
|
||||
|
||||
public void SetStringValue(string value, bool isKnown)
|
||||
=> _taskCompletionSource.SetResult(new OutputData<T>((T)(object)value, isKnown, isSecret: false));
|
||||
=> _taskCompletionSource.SetResult(new OutputData<T>(
|
||||
_resources, (T)(object)value, isKnown, isSecret: false));
|
||||
|
||||
public void SetValue(OutputData<object?> data)
|
||||
=> _taskCompletionSource.SetResult(new OutputData<T>((T)data.Value!, data.IsKnown, data.IsSecret));
|
||||
=> _taskCompletionSource.SetResult(new OutputData<T>(
|
||||
_resources, (T)data.Value!, data.IsKnown, data.IsSecret));
|
||||
|
||||
public void TrySetDefaultResult(bool isKnown)
|
||||
=> _taskCompletionSource.TrySetResult(new OutputData<T>(default!, isKnown, isSecret: false));
|
||||
=> _taskCompletionSource.TrySetResult(new OutputData<T>(
|
||||
_resources, default!, isKnown, isSecret: false));
|
||||
|
||||
public void TrySetException(Exception exception)
|
||||
=> _taskCompletionSource.TrySetException(exception);
|
||||
|
@ -89,7 +92,9 @@ namespace Pulumi.Serialization
|
|||
var completionSource = (IOutputCompletionSource)ocsContructor.Invoke(new[] { resource });
|
||||
|
||||
setMethod.Invoke(resource, new[] { completionSource.Output });
|
||||
result.Add(attr.Name, completionSource);
|
||||
|
||||
var outputName = attr.Name ?? prop.Name;
|
||||
result.Add(outputName, completionSource);
|
||||
}
|
||||
|
||||
Log.Debug("Fields to assign: " + JsonSerializer.Serialize(result.Keys), resource);
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
// Copyright 2016-2019, Pulumi Corporation
|
||||
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace Pulumi.Serialization
|
||||
{
|
||||
internal static class OutputData
|
||||
{
|
||||
public static OutputData<X> Create<X>(X value, bool isKnown, bool isSecret)
|
||||
=> new OutputData<X>(value, isKnown, isSecret);
|
||||
public static OutputData<X> Create<X>(ImmutableHashSet<Resource> resources, X value, bool isKnown, bool isSecret)
|
||||
=> new OutputData<X>(resources, value, isKnown, isSecret);
|
||||
|
||||
public static (bool isKnown, bool isSecret) Combine<X>(OutputData<X> data, bool isKnown, bool isSecret)
|
||||
=> (isKnown && data.IsKnown, isSecret || data.IsSecret);
|
||||
|
@ -13,19 +15,21 @@ namespace Pulumi.Serialization
|
|||
|
||||
internal struct OutputData<X>
|
||||
{
|
||||
public readonly ImmutableHashSet<Resource> Resources;
|
||||
public readonly X Value;
|
||||
public readonly bool IsKnown;
|
||||
public readonly bool IsSecret;
|
||||
|
||||
public OutputData(X value, bool isKnown, bool isSecret)
|
||||
public OutputData(ImmutableHashSet<Resource> resources, X value, bool isKnown, bool isSecret)
|
||||
{
|
||||
Resources = resources;
|
||||
Value = value;
|
||||
IsKnown = isKnown;
|
||||
IsSecret = isSecret;
|
||||
}
|
||||
|
||||
public static implicit operator OutputData<object?>(OutputData<X> data)
|
||||
=> new OutputData<object?>(data.Value, data.IsKnown, data.IsSecret);
|
||||
=> new OutputData<object?>(data.Resources, data.Value, data.IsKnown, data.IsSecret);
|
||||
|
||||
public void Deconstruct(out X value, out bool isKnown, out bool isSecret)
|
||||
{
|
||||
|
|
|
@ -84,8 +84,8 @@ namespace Pulumi.Serialization
|
|||
return prop;
|
||||
}
|
||||
|
||||
if (prop is ResourceArgs args)
|
||||
return await SerializeResourceArgsAsync(ctx, args).ConfigureAwait(false);
|
||||
if (prop is InputArgs args)
|
||||
return await SerializeInputArgsAsync(ctx, args).ConfigureAwait(false);
|
||||
|
||||
if (prop is AssetOrArchive assetOrArchive)
|
||||
return await SerializeAssetOrArchiveAsync(ctx, assetOrArchive).ConfigureAwait(false);
|
||||
|
@ -133,8 +133,8 @@ $"Tasks are not allowed inside ResourceArgs. Please wrap your Task in an Output:
|
|||
Log.Debug($"Serialize property[{ctx}]: Recursing into Output");
|
||||
}
|
||||
|
||||
this.DependentResources.AddRange(output.Resources);
|
||||
var data = await output.GetDataAsync().ConfigureAwait(false);
|
||||
this.DependentResources.AddRange(data.Resources);
|
||||
|
||||
// When serializing an Output, we will either serialize it as its resolved value or the "unknown value"
|
||||
// sentinel. We will do the former for all outputs created directly by user code (such outputs always
|
||||
|
@ -262,7 +262,7 @@ $"Tasks are not allowed inside ResourceArgs. Please wrap your Task in an Output:
|
|||
return builder.ToImmutable();
|
||||
}
|
||||
|
||||
private async Task<ImmutableDictionary<string, object>> SerializeResourceArgsAsync(string ctx, ResourceArgs args)
|
||||
private async Task<ImmutableDictionary<string, object>> SerializeInputArgsAsync(string ctx, InputArgs args)
|
||||
{
|
||||
if (_excessiveDebugOutput)
|
||||
{
|
||||
|
|
|
@ -3,18 +3,18 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using Pulumi.Serialization;
|
||||
|
||||
namespace Pulumi
|
||||
{
|
||||
/// <summary>
|
||||
/// Stack is the root resource for a Pulumi stack. Before invoking the <c>init</c> callback, it
|
||||
/// registers itself as the root resource with the Pulumi engine.
|
||||
///
|
||||
/// An instance of this will be automatically created when any <see
|
||||
/// cref="Deployment.RunAsync(Action)"/> overload is called.
|
||||
/// Stack is the root resource for a Pulumi stack. Derive from this class to create your
|
||||
/// stack definitions.
|
||||
/// </summary>
|
||||
internal sealed class Stack : ComponentResource
|
||||
public class Stack : ComponentResource
|
||||
{
|
||||
/// <summary>
|
||||
/// Constant to represent the 'root stack' resource for a Pulumi application. The purpose
|
||||
|
@ -31,7 +31,7 @@ namespace Pulumi
|
|||
/// may look a bit confusing and may incorrectly look like something that could be removed
|
||||
/// without changing semantics.
|
||||
/// </summary>
|
||||
public static readonly Resource? Root = null;
|
||||
internal static readonly Resource? Root = null;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="_rootPulumiStackTypeName"/> is the type name that should be used to construct
|
||||
|
@ -44,14 +44,25 @@ namespace Pulumi
|
|||
/// <summary>
|
||||
/// The outputs of this stack, if the <c>init</c> callback exited normally.
|
||||
/// </summary>
|
||||
public readonly Output<IDictionary<string, object?>> Outputs =
|
||||
internal Output<IDictionary<string, object?>> Outputs =
|
||||
Output.Create<IDictionary<string, object?>>(ImmutableDictionary<string, object?>.Empty);
|
||||
|
||||
internal Stack(Func<Task<IDictionary<string, object?>>> init)
|
||||
/// <summary>
|
||||
/// Create a Stack with stack resources defined in derived class constructor.
|
||||
/// </summary>
|
||||
public Stack()
|
||||
: base(_rootPulumiStackTypeName, $"{Deployment.Instance.ProjectName}-{Deployment.Instance.StackName}")
|
||||
{
|
||||
Deployment.InternalInstance.Stack = this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a Stack with stack resources created by the <c>init</c> callback.
|
||||
/// An instance of this will be automatically created when any <see
|
||||
/// cref="Deployment.RunAsync(Action)"/> overload is called.
|
||||
/// </summary>
|
||||
internal Stack(Func<Task<IDictionary<string, object?>>> init) : this()
|
||||
{
|
||||
try
|
||||
{
|
||||
this.Outputs = Output.Create(RunInitAsync(init));
|
||||
|
@ -62,12 +73,46 @@ namespace Pulumi
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inspect all public properties of the stack to find outputs. Validate the values and register them as stack outputs.
|
||||
/// </summary>
|
||||
internal void RegisterPropertyOutputs()
|
||||
{
|
||||
var outputs = (from property in this.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)
|
||||
let attr = property.GetCustomAttribute<OutputAttribute>()
|
||||
where attr != null
|
||||
let name = attr.Name ?? property.Name
|
||||
select new KeyValuePair<string, object?>(name, property.GetValue(this))).ToList();
|
||||
|
||||
// Check that none of the values are null: catch unassigned outputs
|
||||
var nulls = (from kv in outputs
|
||||
where kv.Value == null
|
||||
select kv.Key).ToList();
|
||||
if (nulls.Any())
|
||||
{
|
||||
var message = $"Output(s) '{string.Join(", ", nulls)}' have no value assigned. [Output] attributed properties must be assigned inside Stack constructor.";
|
||||
throw new RunException(message);
|
||||
}
|
||||
|
||||
// Check that all the values are Output<T>
|
||||
var wrongTypes = (from kv in outputs
|
||||
let type = kv.Value.GetType()
|
||||
let isOutput = type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Output<>)
|
||||
where !isOutput
|
||||
select kv.Key).ToList();
|
||||
if (wrongTypes.Any())
|
||||
{
|
||||
var message = $"Output(s) '{string.Join(", ", wrongTypes)}' have incorrect type. [Output] attributed properties must be instances of Output<T>.";
|
||||
throw new RunException(message);
|
||||
}
|
||||
|
||||
IDictionary<string, object?> dict = new Dictionary<string, object?>(outputs);
|
||||
this.Outputs = Output.Create(dict);
|
||||
this.RegisterOutputs(this.Outputs);
|
||||
}
|
||||
|
||||
private async Task<IDictionary<string, object?>> RunInitAsync(Func<Task<IDictionary<string, object?>>> init)
|
||||
{
|
||||
// Ensure we are known as the root resource. This is needed before we execute any user
|
||||
// code as many codepaths will request the root resource.
|
||||
await Deployment.InternalInstance.SetRootResourceAsync(this).ConfigureAwait(false);
|
||||
|
||||
var dictionary = await init().ConfigureAwait(false);
|
||||
return dictionary == null
|
||||
? ImmutableDictionary<string, object?>.Empty
|
||||
|
|
|
@ -384,7 +384,7 @@ func (host *dotnetLanguageHost) RunDotnetCommand(
|
|||
}
|
||||
|
||||
// Now simply spawn a process to execute the requested program, wiring up stdout/stderr directly.
|
||||
cmd := exec.Command(host.exec, args...) // nolint: gas, intentionally running dynamic program name.
|
||||
cmd := exec.Command(host.exec, args...) // nolint: gas // intentionally running dynamic program name.
|
||||
|
||||
cmd.Stdout = infoWriter
|
||||
cmd.Stderr = errorWriter
|
||||
|
@ -477,7 +477,7 @@ func (host *dotnetLanguageHost) Run(ctx context.Context, req *pulumirpc.RunReque
|
|||
|
||||
// Now simply spawn a process to execute the requested program, wiring up stdout/stderr directly.
|
||||
var errResult string
|
||||
cmd := exec.Command(host.exec, args...) // nolint: gas, intentionally running dynamic program name.
|
||||
cmd := exec.Command(host.exec, args...) // nolint: gas // intentionally running dynamic program name.
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Env = host.constructEnv(req, config)
|
||||
|
|
|
@ -232,7 +232,7 @@ func (host *goLanguageHost) constructEnv(req *pulumirpc.RunRequest) ([]string, e
|
|||
return env, nil
|
||||
}
|
||||
|
||||
// constructConfig json-serializes the configuration data given as part of a RunRequest.
|
||||
// constructConfig JSON-serializes the configuration data given as part of a RunRequest.
|
||||
func (host *goLanguageHost) constructConfig(req *pulumirpc.RunRequest) (string, error) {
|
||||
configMap := req.GetConfig()
|
||||
if configMap == nil {
|
||||
|
|
|
@ -47,9 +47,8 @@ istanbul_tests::
|
|||
./node_modules/.bin/istanbul report text
|
||||
|
||||
sxs_tests::
|
||||
# Intentionally disabled as PR https://github.com/pulumi/pulumi/pull/2609 breaks SxS
|
||||
# will be renabled once that goes in.
|
||||
# pushd tests/sxs && yarn && tsc && popd
|
||||
pushd tests/sxs_ts_3.6 && yarn && tsc && popd
|
||||
pushd tests/sxs_ts_latest && yarn && tsc && popd
|
||||
|
||||
test_fast:: sxs_tests istanbul_tests
|
||||
$(GO_TEST_FAST) ${PROJECT_PKGS}
|
||||
|
|
|
@ -571,7 +571,7 @@ func (host *nodeLanguageHost) constructArguments(req *pulumirpc.RunRequest, addr
|
|||
return args
|
||||
}
|
||||
|
||||
// constructConfig json-serializes the configuration data given as part of
|
||||
// constructConfig JSON-serializes the configuration data given as part of
|
||||
// a RunRequest.
|
||||
func (host *nodeLanguageHost) constructConfig(req *pulumirpc.RunRequest) (string, error) {
|
||||
configMap := req.GetConfig()
|
||||
|
|
|
@ -33,7 +33,7 @@ let programRunning = false;
|
|||
const uncaughtHandler = (err: Error) => {
|
||||
uncaughtErrors.add(err);
|
||||
if (!programRunning) {
|
||||
console.error(err.stack || err.message);
|
||||
console.error(err.stack || err.message || ("" + err));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -18,8 +18,7 @@ import * as path from "path";
|
|||
import * as tsnode from "ts-node";
|
||||
import { ResourceError, RunError } from "../../errors";
|
||||
import * as log from "../../log";
|
||||
import { disconnectSync } from "../../runtime/settings";
|
||||
import { runInPulumiStack } from "../../runtime/stack";
|
||||
import * as runtime from "../../runtime";
|
||||
|
||||
// Keep track if we already logged the information about an unhandled error to the user.. If
|
||||
// so, we end with a different exit code. The language host recognizes this and will not print
|
||||
|
@ -202,7 +201,10 @@ export function run(opts: RunOpts): Promise<Record<string, any> | undefined> | P
|
|||
|
||||
// Default message should be to include the full stack (which includes the message), or
|
||||
// fallback to just the message if we can't get the stack.
|
||||
const defaultMessage = err.stack || err.message;
|
||||
//
|
||||
// If both the stack and message are empty, then just stringify the err object itself. This
|
||||
// is also necessary as users can throw arbitrary things in JS (including non-Errors).
|
||||
const defaultMessage = err.stack || err.message || ("" + err);
|
||||
|
||||
// First, log the error.
|
||||
if (RunError.isInstance(err)) {
|
||||
|
@ -226,7 +228,7 @@ export function run(opts: RunOpts): Promise<Record<string, any> | undefined> | P
|
|||
// @ts-ignore 'unhandledRejection' will almost always invoke uncaughtHandler with an Error. so
|
||||
// just suppress the TS strictness here.
|
||||
process.on("unhandledRejection", uncaughtHandler);
|
||||
process.on("exit", disconnectSync);
|
||||
process.on("exit", runtime.disconnectSync);
|
||||
|
||||
opts.programStarted();
|
||||
|
||||
|
@ -270,5 +272,5 @@ export function run(opts: RunOpts): Promise<Record<string, any> | undefined> | P
|
|||
}
|
||||
};
|
||||
|
||||
return opts.runInStack ? runInPulumiStack(runProgram) : runProgram();
|
||||
return opts.runInStack ? runtime.runInPulumiStack(runProgram) : runProgram();
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ let programRunning = false;
|
|||
const uncaughtHandler = (err: Error) => {
|
||||
uncaughtErrors.add(err);
|
||||
if (!programRunning) {
|
||||
console.error(err.stack || err.message);
|
||||
console.error(err.stack || err.message || ("" + err));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -188,7 +188,10 @@ export function run(argv: minimist.ParsedArgs,
|
|||
|
||||
// Default message should be to include the full stack (which includes the message), or
|
||||
// fallback to just the message if we can't get the stack.
|
||||
const defaultMessage = err.stack || err.message;
|
||||
//
|
||||
// If both the stack and message are empty, then just stringify the err object itself. This
|
||||
// is also necessary as users can throw arbitrary things in JS (including non-Errors).
|
||||
const defaultMessage = err.stack || err.message || ("" + err);
|
||||
|
||||
// First, log the error.
|
||||
if (RunError.isInstance(err)) {
|
||||
|
|
|
@ -19,7 +19,10 @@ import { Output } from "./output";
|
|||
import { getConfig } from "./runtime";
|
||||
|
||||
function makeSecret<T>(value: T): Output<T> {
|
||||
return new Output([], Promise.resolve(value), Promise.resolve(true), Promise.resolve(true));
|
||||
return new Output(
|
||||
[], Promise.resolve(value),
|
||||
/*isKnown:*/ Promise.resolve(true), /*isSecret:*/ Promise.resolve(true),
|
||||
Promise.resolve([]));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -325,7 +328,7 @@ export class Config {
|
|||
/**
|
||||
* StringConfigOptions may be used to constrain the set of legal values a string config value may contain.
|
||||
*/
|
||||
interface StringConfigOptions<K extends string = string> {
|
||||
export interface StringConfigOptions<K extends string = string> {
|
||||
/**
|
||||
* The legal enum values. If it does not match, a ConfigEnumError is thrown.
|
||||
*/
|
||||
|
@ -347,7 +350,7 @@ interface StringConfigOptions<K extends string = string> {
|
|||
/**
|
||||
* NumberConfigOptions may be used to constrain the set of legal values a number config value may contain.
|
||||
*/
|
||||
interface NumberConfigOptions {
|
||||
export interface NumberConfigOptions {
|
||||
/**
|
||||
* The minimum number value, inclusive. If the number is less than this, a ConfigRangeError is thrown.
|
||||
*/
|
||||
|
|
|
@ -66,12 +66,33 @@ class OutputImpl<T> implements OutputInstance<T> {
|
|||
|
||||
/**
|
||||
* @internal
|
||||
* The list of resource that this output value depends on.
|
||||
* The list of resources that this output value depends on.
|
||||
*
|
||||
* Only callable on the outside.
|
||||
*
|
||||
* This only returns the set of dependent resources that were known at Output construction time.
|
||||
* It represents the `@pulumi/pulumi` api prior to the addition of 'async resource'
|
||||
* dependencies. Code inside @pulumi/pulumi should use `.allResources` instead.
|
||||
*/
|
||||
public readonly resources: () => Set<Resource>;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* The entire list of resources that this output depends on.
|
||||
*
|
||||
* This includes both the dependent resources that were known when the Output was explicitly
|
||||
* instantiated, along with any dependent resources produced asynchronously and returned from
|
||||
* the function passed to `Output.apply`.
|
||||
*
|
||||
* This should be used whenever available inside this package. However, code that uses this
|
||||
* should be resilient to it being absent and should fall back to using `.resources()` instead.
|
||||
*
|
||||
* Note: it is fine to use this property if it is guaranteed that it is on an output produced by
|
||||
* this SDK (and not another sxs version).
|
||||
*/
|
||||
// Marked as optional for sxs scenarios.
|
||||
public readonly allResources?: () => Promise<Set<Resource>>;
|
||||
|
||||
/**
|
||||
* [toString] on an [Output<T>] is not supported. This is because the value an [Output] points
|
||||
* to is asynchronously computed (and thus, this is akin to calling [toString] on a [Promise]).
|
||||
|
@ -140,28 +161,27 @@ class OutputImpl<T> implements OutputInstance<T> {
|
|||
resources: Set<Resource> | Resource[] | Resource,
|
||||
promise: Promise<T>,
|
||||
isKnown: Promise<boolean>,
|
||||
isSecret: Promise<boolean>) {
|
||||
isSecret: Promise<boolean>,
|
||||
allResources: Promise<Set<Resource> | Resource[] | Resource> | undefined) {
|
||||
|
||||
// We are only known if we are not explicitly unknown and the resolved value of the output
|
||||
// contains no distinguished unknown values.
|
||||
this.isKnown = Promise.all([isKnown, promise]).then(([known, val]) => known && !containsUnknowns(val));
|
||||
this.isSecret = isSecret;
|
||||
|
||||
let resourcesArray: Resource[];
|
||||
|
||||
// Always create a copy so that no one accidentally modifies our Resource list.
|
||||
if (Array.isArray(resources)) {
|
||||
resourcesArray = resources;
|
||||
} else if (resources instanceof Set) {
|
||||
resourcesArray = [...resources];
|
||||
} else {
|
||||
resourcesArray = [resources];
|
||||
}
|
||||
const resourcesCopy = copyResources(resources);
|
||||
|
||||
// Create a copy of the async resources. Populate this with the sync-resources if that's
|
||||
// all we have. That way this is always ensured to be a superset of the list of sync resources.
|
||||
allResources = allResources || Promise.resolve([]);
|
||||
const allResourcesCopy = allResources.then(r => utils.union(copyResources(r), resourcesCopy));
|
||||
|
||||
this.resources = () => resourcesCopy;
|
||||
this.allResources = () => allResourcesCopy;
|
||||
|
||||
this.resources = () => new Set<Resource>(resourcesArray);
|
||||
this.promise = (withUnknowns?: boolean) => OutputImpl.getPromisedValue(promise, withUnknowns);
|
||||
|
||||
const firstResource = resourcesArray[0];
|
||||
this.toString = () => {
|
||||
const message =
|
||||
`Calling [toString] on an [Output<T>] is not supported.
|
||||
|
@ -269,17 +289,39 @@ To manipulate the value of this Output, use '.apply' instead.`);
|
|||
// advantage of this to allow proxied property accesses to return known values even if other properties of
|
||||
// the containing object are unknown.
|
||||
public apply<U>(func: (t: T) => Input<U>, runWithUnknowns?: boolean): Output<U> {
|
||||
const applied = Promise.all([this.promise(/*withUnknowns*/ true), this.isKnown, this.isSecret])
|
||||
.then(([value, isKnown, isSecret]) => applyHelperAsync<T, U>(value, isKnown, isSecret, func, !!runWithUnknowns));
|
||||
// we're inside the modern `output` code, so it's safe to call `.allResources!` here.
|
||||
|
||||
const applied = Promise.all([this.allResources!(), this.promise(/*withUnknowns*/ true), this.isKnown, this.isSecret])
|
||||
.then(([allResources, value, isKnown, isSecret]) => applyHelperAsync<T, U>(allResources, value, isKnown, isSecret, func, !!runWithUnknowns));
|
||||
|
||||
const result = new OutputImpl<U>(
|
||||
this.resources(), applied.then(a => a.value), applied.then(a => a.isKnown), applied.then(a => a.isSecret));
|
||||
this.resources(),
|
||||
applied.then(a => a.value),
|
||||
applied.then(a => a.isKnown),
|
||||
applied.then(a => a.isSecret),
|
||||
applied.then(a => a.allResources));
|
||||
return <Output<U>><any>result;
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function getAllResources<T>(op: OutputInstance<T>): Promise<Set<Resource>> {
|
||||
return op.allResources instanceof Function
|
||||
? op.allResources()
|
||||
: Promise.resolve(op.resources());
|
||||
}
|
||||
|
||||
function copyResources(resources: Set<Resource> | Resource[] | Resource) {
|
||||
const copy = Array.isArray(resources) ? new Set(resources) :
|
||||
resources instanceof Set ? new Set(resources) :
|
||||
new Set([resources]);
|
||||
return copy;
|
||||
}
|
||||
|
||||
// tslint:disable:max-line-length
|
||||
async function applyHelperAsync<T, U>(value: T, isKnown: boolean, isSecret: boolean, func: (t: T) => Input<U>, runWithUnknowns: boolean) {
|
||||
async function applyHelperAsync<T, U>(
|
||||
allResources: Set<Resource>, value: T, isKnown: boolean, isSecret: boolean,
|
||||
func: (t: T) => Input<U>, runWithUnknowns: boolean) {
|
||||
if (runtime.isDryRun()) {
|
||||
// During previews only perform the apply if the engine was able to give us an actual value
|
||||
// for this Output.
|
||||
|
@ -288,6 +330,7 @@ async function applyHelperAsync<T, U>(value: T, isKnown: boolean, isSecret: bool
|
|||
if (!applyDuringPreview) {
|
||||
// We didn't actually run the function, our new Output is definitely **not** known.
|
||||
return {
|
||||
allResources,
|
||||
value: <U><any>undefined,
|
||||
isKnown: false,
|
||||
isSecret,
|
||||
|
@ -305,18 +348,22 @@ async function applyHelperAsync<T, U>(value: T, isKnown: boolean, isSecret: bool
|
|||
const transformed = await func(value);
|
||||
if (Output.isInstance(transformed)) {
|
||||
// Note: if the func returned a Output, we unwrap that to get the inner value returned by
|
||||
// that Output. Note that we are *not* capturing the Resources of this inner Output.
|
||||
// That's intentional. As the Output returned is only supposed to be related this *this*
|
||||
// Output object, those resources should already be in our transitively reachable resource
|
||||
// graph.
|
||||
// that Output. Note that we *are* capturing the Resources of this inner Output and lifting
|
||||
// them up to the outer Output as well.
|
||||
|
||||
// Note: we intentionally await all the promises of the transformed value. This way we
|
||||
// properly propogate any rejections of any of them through ourselves as well.
|
||||
// properly propagate any rejections of any of them through ourselves as well.
|
||||
const innerValue = await transformed.promise(/*withUnknowns*/ true);
|
||||
const innerIsKnown = await transformed.isKnown;
|
||||
const innerIsSecret = await (transformed.isSecret || Promise.resolve(false));
|
||||
|
||||
// If we're working with a new-style output, grab all its resources and merge into ours.
|
||||
// otherwise, if this is an old-style output, just grab the resources it was known to have
|
||||
// at construction time.
|
||||
const innerResources = await getAllResources(transformed);
|
||||
const totalResources = utils.union(allResources, innerResources);
|
||||
return {
|
||||
allResources: totalResources,
|
||||
value: innerValue,
|
||||
isKnown: innerIsKnown,
|
||||
isSecret: isSecret || innerIsSecret,
|
||||
|
@ -326,6 +373,7 @@ async function applyHelperAsync<T, U>(value: T, isKnown: boolean, isSecret: bool
|
|||
// We successfully ran the inner function. Our new Output should be considered known. We
|
||||
// preserve secretness from our original Output to the new one we're creating.
|
||||
return {
|
||||
allResources,
|
||||
value: transformed,
|
||||
isKnown: true,
|
||||
isSecret,
|
||||
|
@ -373,21 +421,31 @@ export function output<T>(val: Input<T | undefined>): Output<Unwrap<T | undefine
|
|||
}
|
||||
else if (isUnknown(val)) {
|
||||
// Turn unknowns into unknown outputs.
|
||||
return <any>new Output(new Set(), Promise.resolve(<any>val), /*isKnown*/ Promise.resolve(false), /*isSecret*/ Promise.resolve(false));
|
||||
return <any>new Output(
|
||||
new Set(), Promise.resolve(<any>val), /*isKnown*/ Promise.resolve(false), /*isSecret*/ Promise.resolve(false), Promise.resolve(new Set()));
|
||||
}
|
||||
else if (val instanceof Promise) {
|
||||
// For a promise, we can just treat the same as an output that points to that resource. So
|
||||
// we just create an Output around the Promise, and immediately apply the unwrap function on
|
||||
// it to transform the value it points at.
|
||||
const newOutput = new Output(new Set(), val, /*isKnown*/ Promise.resolve(true), /*isSecret*/ Promise.resolve(false));
|
||||
const newOutput = new Output(
|
||||
new Set(), val, /*isKnown*/ Promise.resolve(true), /*isSecret*/ Promise.resolve(false), Promise.resolve(new Set()));
|
||||
return <any>(<any>newOutput).apply(output, /*runWithUnknowns*/ true);
|
||||
}
|
||||
else if (Output.isInstance(val)) {
|
||||
// We create a new output here from the raw pieces of the original output in order to accommodate outputs from
|
||||
// downlevel SxS SDKs. This ensures that first-class unknowns are properly represented in the system: if this
|
||||
// was a downlevel output where val.isKnown resolves to false, this guarantees that the returned output's
|
||||
// promise resolves to unknown.
|
||||
const newOutput = new Output(val.resources(), val.promise(/*withUnknowns*/ true), val.isKnown, val.isSecret);
|
||||
// We create a new output here from the raw pieces of the original output in order to
|
||||
// accommodate outputs from downlevel SxS SDKs. This ensures that within this package it is
|
||||
// safe to assume the implementation of any Output returned by the `output` function.
|
||||
//
|
||||
// This includes:
|
||||
// 1. that first-class unknowns are properly represented in the system: if this was a
|
||||
// downlevel output where val.isKnown resolves to false, this guarantees that the
|
||||
// returned output's promise resolves to unknown.
|
||||
// 2. That the `isSecret` property is available.
|
||||
// 3. That the `.allResources` is available.
|
||||
const allResources = getAllResources(val);
|
||||
const newOutput = new Output(
|
||||
val.resources(), val.promise(/*withUnknowns*/ true), val.isKnown, val.isSecret, allResources);
|
||||
return <any>(<any>newOutput).apply(output, /*runWithUnknowns*/ true);
|
||||
}
|
||||
else if (val instanceof Array) {
|
||||
|
@ -410,7 +468,11 @@ export function secret<T>(val: Input<T>): Output<Unwrap<T>>;
|
|||
export function secret<T>(val: Input<T> | undefined): Output<Unwrap<T | undefined>>;
|
||||
export function secret<T>(val: Input<T | undefined>): Output<Unwrap<T | undefined>> {
|
||||
const o = output(val);
|
||||
return new Output(o.resources(), o.promise(/*withUnknowns*/ true), o.isKnown, Promise.resolve(true));
|
||||
|
||||
// we called `output` right above this, so it's safe to call `.allResources` on the result.
|
||||
return new Output(
|
||||
o.resources(), o.promise(/*withUnknowns*/ true),
|
||||
o.isKnown, Promise.resolve(true), o.allResources!());
|
||||
}
|
||||
|
||||
function createSimpleOutput(val: any) {
|
||||
|
@ -418,7 +480,8 @@ function createSimpleOutput(val: any) {
|
|||
new Set(),
|
||||
Promise.resolve(val),
|
||||
/*isKnown*/ Promise.resolve(true),
|
||||
/*isSecret */ Promise.resolve(false));
|
||||
/*isSecret */ Promise.resolve(false),
|
||||
Promise.resolve(new Set()));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -451,18 +514,18 @@ export function all<T>(val: Input<T>[] | Record<string, Input<T>>): Output<any>
|
|||
if (val instanceof Array) {
|
||||
const allOutputs = val.map(v => output(v));
|
||||
|
||||
const [resources, isKnown, isSecret] = getResourcesAndDetails(allOutputs);
|
||||
const [syncResources, isKnown, isSecret, allResources] = getResourcesAndDetails(allOutputs);
|
||||
const promisedArray = Promise.all(allOutputs.map(o => o.promise(/*withUnknowns*/ true)));
|
||||
|
||||
return new Output<Unwrap<T>[]>(new Set<Resource>(resources), promisedArray, isKnown, isSecret);
|
||||
return new Output<Unwrap<T>[]>(syncResources, promisedArray, isKnown, isSecret, allResources);
|
||||
} else {
|
||||
const keysAndOutputs = Object.keys(val).map(key => ({ key, value: output(val[key]) }));
|
||||
const allOutputs = keysAndOutputs.map(kvp => kvp.value);
|
||||
|
||||
const [resources, isKnown, isSecret] = getResourcesAndDetails(allOutputs);
|
||||
const [syncResources, isKnown, isSecret, allResources] = getResourcesAndDetails(allOutputs);
|
||||
const promisedObject = getPromisedObject(keysAndOutputs);
|
||||
|
||||
return new Output<Record<string, Unwrap<T>>>(new Set<Resource>(resources), promisedObject, isKnown, isSecret);
|
||||
return new Output<Record<string, Unwrap<T>>>(syncResources, promisedObject, isKnown, isSecret, allResources);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -476,16 +539,35 @@ async function getPromisedObject<T>(
|
|||
return result;
|
||||
}
|
||||
|
||||
function getResourcesAndDetails<T>(allOutputs: Output<Unwrap<T>>[]): [Resource[], Promise<boolean>, Promise<boolean>] {
|
||||
const allResources = allOutputs.reduce<Resource[]>((arr, o) => (arr.push(...o.resources()), arr), []);
|
||||
function getResourcesAndDetails<T>(allOutputs: Output<Unwrap<T>>[]): [Set<Resource>, Promise<boolean>, Promise<boolean>, Promise<Set<Resource>>] {
|
||||
const syncResources = new Set<Resource>();
|
||||
for (const op of allOutputs) {
|
||||
for (const res of op.resources()) {
|
||||
syncResources.add(res);
|
||||
}
|
||||
}
|
||||
|
||||
// All the outputs were generated in `function all` using `output(v)`. So it's safe
|
||||
// to call `.allResources!` here.
|
||||
const allResources = Promise.all(allOutputs.map(o => o.allResources!())).then(arr => {
|
||||
const result = new Set<Resource>();
|
||||
|
||||
for (const set of arr) {
|
||||
for (const res of set) {
|
||||
result.add(res);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
// A merged output is known if all of its inputs are known.
|
||||
const isKnown = Promise.all(allOutputs.map(o => o.isKnown)).then(ps => ps.every(b => b));
|
||||
|
||||
// A merged output is secret if any of its inputs are secret.
|
||||
const isSecret = Promise.all(allOutputs.map(o => isSecretOutput(o))).then(ps => ps.find(b => b) !== undefined);
|
||||
const isSecret = Promise.all(allOutputs.map(o => isSecretOutput(o))).then(ps => ps.some(b => b));
|
||||
|
||||
return [allResources, isKnown, isSecret];
|
||||
return [syncResources, isKnown, isSecret, allResources];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -631,6 +713,8 @@ export type UnwrappedObject<T> = {
|
|||
* for working with the underlying value of an [Output<T>].
|
||||
*/
|
||||
export interface OutputInstance<T> {
|
||||
/** @internal */ allResources?: () => Promise<Set<Resource>>;
|
||||
|
||||
/** @internal */ readonly isKnown: Promise<boolean>;
|
||||
/** @internal */ readonly isSecret: Promise<boolean>;
|
||||
/** @internal */ promise(withUnknowns?: boolean): Promise<T>;
|
||||
|
@ -651,7 +735,7 @@ export interface OutputInstance<T> {
|
|||
* ```
|
||||
*
|
||||
* In this example, taking a dependency on d2 means a resource will depend on all the resources
|
||||
* of d1. It will *not* depend on the resources of v.x.y.OtherDep.
|
||||
* of d1. It will *also* depend on the resources of v.x.y.OtherDep.
|
||||
*
|
||||
* Importantly, the Resources that d2 feels like it will depend on are the same resources as d1.
|
||||
* If you need have multiple Outputs and a single Output is needed that combines both
|
||||
|
@ -692,7 +776,8 @@ export interface OutputConstructor {
|
|||
resources: Set<Resource> | Resource[] | Resource,
|
||||
promise: Promise<T>,
|
||||
isKnown: Promise<boolean>,
|
||||
isSecret: Promise<boolean>): Output<T>;
|
||||
isSecret: Promise<boolean>,
|
||||
allResources: Promise<Set<Resource> | Resource[] | Resource>): Output<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -775,13 +860,15 @@ export const Output: OutputConstructor = <any>OutputImpl;
|
|||
* ```
|
||||
*/
|
||||
export type Lifted<T> =
|
||||
// Output<T> is an intersection type with 'Lifted<T>'. So, when we don't want to add any
|
||||
// members to Output<T>, we just return `{}` which will leave it untouched.
|
||||
//
|
||||
// Specially handle 'string' since TS doesn't map the 'String.Length' property to it.
|
||||
T extends string ? LiftedObject<String, NonFunctionPropertyNames<String>> :
|
||||
T extends Array<infer U> ? LiftedArray<U> :
|
||||
LiftedObject<T, NonFunctionPropertyNames<T>>;
|
||||
T extends object ? LiftedObject<T, NonFunctionPropertyNames<T>> :
|
||||
// fallback to lifting no properties. Note that `Lifted` is used in
|
||||
// Output<T> = OutputInstance<T> & Lifted<T>
|
||||
// so returning an empty object just means that we're adding nothing to Output<T>.
|
||||
// This is needed for cases like `Output<any>`.
|
||||
{};
|
||||
|
||||
// The set of property names in T that are *not* functions.
|
||||
type NonFunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? never : K }[keyof T];
|
||||
|
@ -789,7 +876,8 @@ type NonFunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? nev
|
|||
// Lift up all the non-function properties. If it was optional before, keep it optional after.
|
||||
// If it's require before, keep it required afterwards.
|
||||
export type LiftedObject<T, K extends keyof T> = {
|
||||
[P in K]: Output<T[P]>
|
||||
[P in K]: T[P] extends OutputInstance<infer T1> ? Output<T1> :
|
||||
T[P] extends Promise<infer T2> ? Output<T2> : Output<T[P]>
|
||||
};
|
||||
|
||||
export type LiftedArray<T> = {
|
||||
|
|
|
@ -20,8 +20,8 @@
|
|||
"require-from-string": "^2.0.1",
|
||||
"semver": "^6.1.0",
|
||||
"source-map-support": "^0.4.16",
|
||||
"ts-node": "^7.0.0",
|
||||
"typescript": "=3.6.3",
|
||||
"ts-node": "^8.5.4",
|
||||
"typescript": "~3.7.3",
|
||||
"upath": "^1.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -20,7 +20,7 @@ import { Resource } from "../resource";
|
|||
* is useful primarily when we're querying over resource outputs (e.g., using
|
||||
* `pulumi.runtime.listResourceOutputs`), and we expect all values to be present and fully-resolved.
|
||||
*/
|
||||
export type ResolvedResource<T extends Resource> = PulumiOmit<Resolved<T>, "urn" | "getProvider">;
|
||||
export type ResolvedResource<T extends Resource> = Omit<Resolved<T>, "urn" | "getProvider">;
|
||||
|
||||
export type Resolved<T> = T extends Promise<infer U1>
|
||||
? ResolvedSimple<U1>
|
||||
|
@ -49,6 +49,3 @@ type OptionalKeys<T> = { [P in keyof T]: undefined extends T[P] ? P : never }[ke
|
|||
|
||||
type ModifyOptionalProperties<T> = { [P in RequiredKeys<T>]: T[P] } &
|
||||
{ [P in OptionalKeys<T>]?: T[P] };
|
||||
|
||||
type PulumiExclude<T, U> = T extends U ? never : T;
|
||||
type PulumiOmit<T, K extends keyof any> = Pick<T, PulumiExclude<keyof T, K>>;
|
||||
|
|
|
@ -743,13 +743,21 @@ export abstract class ProviderResource extends CustomResource {
|
|||
* level abstraction. The component resource itself is a resource, but does not require custom CRUD
|
||||
* operations for provisioning.
|
||||
*/
|
||||
export class ComponentResource extends Resource {
|
||||
export class ComponentResource<TData = any> extends Resource {
|
||||
/**
|
||||
* @internal
|
||||
* A private field to help with RTTI that works in SxS scenarios.
|
||||
*/
|
||||
// tslint:disable-next-line:variable-name
|
||||
public readonly __pulumiComponentResource: boolean;
|
||||
public readonly __pulumiComponentResource = true;
|
||||
|
||||
/** @internal */
|
||||
// tslint:disable-next-line:variable-name
|
||||
public readonly __data: Promise<TData>;
|
||||
|
||||
/** @internal */
|
||||
// tslint:disable-next-line:variable-name
|
||||
private __registered = false;
|
||||
|
||||
/**
|
||||
* Returns true if the given object is an instance of CustomResource. This is designed to work even when
|
||||
|
@ -768,11 +776,10 @@ export class ComponentResource extends Resource {
|
|||
*
|
||||
* @param t The type of the resource.
|
||||
* @param name The _unique_ name of the resource.
|
||||
* @param unused [Deprecated]. Component resources do not communicate or store their properties
|
||||
* with the Pulumi engine.
|
||||
* @param args Information passed to [initialize] method.
|
||||
* @param opts A bag of options that control this resource's behavior.
|
||||
*/
|
||||
constructor(type: string, name: string, unused?: Inputs, opts: ComponentResourceOptions = {}) {
|
||||
constructor(type: string, name: string, args: Inputs = {}, opts: ComponentResourceOptions = {}) {
|
||||
// Explicitly ignore the props passed in. We allow them for back compat reasons. However,
|
||||
// we explicitly do not want to pass them along to the engine. The ComponentResource acts
|
||||
// only as a container for other resources. Another way to think about this is that a normal
|
||||
|
@ -782,29 +789,61 @@ export class ComponentResource extends Resource {
|
|||
// not correspond to a real piece of cloud infrastructure. As such, changes to it *itself*
|
||||
// do not have any effect on the cloud side of things at all.
|
||||
super(type, name, /*custom:*/ false, /*props:*/ {}, opts);
|
||||
this.__pulumiComponentResource = true;
|
||||
this.__data = this.initializeAndRegisterOutputs(args);
|
||||
}
|
||||
|
||||
// registerOutputs registers synthetic outputs that a component has initialized, usually by
|
||||
// allocating other child sub-resources and propagating their resulting property values.
|
||||
// ComponentResources should always call this at the end of their constructor to indicate that
|
||||
// they are done creating child resources. While not strictly necessary, this helps the
|
||||
// experience by ensuring the UI transitions the ComponentResource to the 'complete' state as
|
||||
// quickly as possible (instead of waiting until the entire application completes).
|
||||
/** @internal */
|
||||
private async initializeAndRegisterOutputs(args: Inputs) {
|
||||
const data = await this.initialize(args);
|
||||
this.registerOutputs();
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be overridden by a subclass to asynchronously initialize data for this Component
|
||||
* automatically when constructed. The data will be available immediately for subclass
|
||||
* constructors to use. To access the data use `.getData`.
|
||||
*/
|
||||
protected async initialize(args: Inputs): Promise<TData> {
|
||||
return <TData>undefined!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the data produces by [initialize]. The data is immediately available in a
|
||||
* derived class's constructor after the `super(...)` call to `ComponentResource`.
|
||||
*/
|
||||
protected getData(): Promise<TData> {
|
||||
return this.__data;
|
||||
}
|
||||
|
||||
/**
|
||||
* registerOutputs registers synthetic outputs that a component has initialized, usually by
|
||||
* allocating other child sub-resources and propagating their resulting property values.
|
||||
*
|
||||
* ComponentResources can call this at the end of their constructor to indicate that they are
|
||||
* done creating child resources. This is not strictly necessary as this will automatically be
|
||||
* called after the `initialize` method completes.
|
||||
*/
|
||||
protected registerOutputs(outputs?: Inputs | Promise<Inputs> | Output<Inputs>): void {
|
||||
if (this.__registered) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.__registered = true;
|
||||
registerResourceOutputs(this, outputs || {});
|
||||
}
|
||||
}
|
||||
|
||||
(<any>ComponentResource).doNotCapture = true;
|
||||
(<any>ComponentResource.prototype).registerOutputs.doNotCapture = true;
|
||||
(<any>ComponentResource.prototype).initialize.doNotCapture = true;
|
||||
(<any>ComponentResource.prototype).initializeAndRegisterOutputs.doNotCapture = true;
|
||||
|
||||
/** @internal */
|
||||
export const testingOptions = {
|
||||
isDryRun: false,
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* [mergeOptions] takes two ResourceOptions values and produces a new ResourceOptions with the
|
||||
* respective properties of `opts2` merged over the same properties in `opts1`. The original
|
||||
|
|
|
@ -17,7 +17,7 @@ import * as grpc from "grpc";
|
|||
import * as log from "../log";
|
||||
import * as utils from "../utils";
|
||||
|
||||
import { Input, Inputs, Output, output, unknown } from "../output";
|
||||
import { getAllResources, Input, Inputs, Output, output } from "../output";
|
||||
import { ResolvedResource } from "../queryable";
|
||||
import {
|
||||
ComponentResource,
|
||||
|
@ -277,7 +277,8 @@ async function prepareResource(label: string, res: Resource, custom: boolean,
|
|||
new Promise<URN>(resolve => resolveURN = resolve),
|
||||
`resolveURN(${label})`),
|
||||
/*isKnown:*/ Promise.resolve(true),
|
||||
/*isSecret:*/ Promise.resolve(false));
|
||||
/*isSecret:*/ Promise.resolve(false),
|
||||
Promise.resolve(res));
|
||||
|
||||
// If a custom resource, make room for the ID property.
|
||||
let resolveID: ((v: any, performApply: boolean) => void) | undefined;
|
||||
|
@ -289,7 +290,8 @@ async function prepareResource(label: string, res: Resource, custom: boolean,
|
|||
debuggablePromise(new Promise<ID>(resolve => resolveValue = resolve), `resolveID(${label})`),
|
||||
debuggablePromise(new Promise<boolean>(
|
||||
resolve => resolveIsKnown = resolve), `resolveIDIsKnown(${label})`),
|
||||
Promise.resolve(false));
|
||||
Promise.resolve(false),
|
||||
Promise.resolve(res));
|
||||
|
||||
resolveID = (v, isKnown) => {
|
||||
resolveValue(v);
|
||||
|
@ -397,7 +399,7 @@ async function getAllTransitivelyReferencedCustomResourceURNs(resources: Set<Res
|
|||
// To do this, first we just get the transitively reachable set of resources (not diving
|
||||
// into custom resources). In the above picture, if we start with 'Comp1', this will be
|
||||
// [Comp1, Cust1, Comp2, Cust2, Cust3]
|
||||
const transitivelyReachableResources = getTransitivelyReferencedChildResourcesOfComponentResources(resources);
|
||||
const transitivelyReachableResources = await getTransitivelyReferencedChildResourcesOfComponentResources(resources);
|
||||
|
||||
const transitivelyReachableCustomResources = [...transitivelyReachableResources].filter(r => CustomResource.isInstance(r));
|
||||
const promises = transitivelyReachableCustomResources.map(r => r.urn.promise());
|
||||
|
@ -409,20 +411,24 @@ async function getAllTransitivelyReferencedCustomResourceURNs(resources: Set<Res
|
|||
* Recursively walk the resources passed in, returning them and all resources reachable from
|
||||
* [Resource.__childResources] through any **Component** resources we encounter.
|
||||
*/
|
||||
function getTransitivelyReferencedChildResourcesOfComponentResources(resources: Set<Resource>) {
|
||||
async function getTransitivelyReferencedChildResourcesOfComponentResources(resources: Set<Resource>) {
|
||||
// Recursively walk the dependent resources through their children, adding them to the result set.
|
||||
const result = new Set<Resource>();
|
||||
addTransitivelyReferencedChildResourcesOfComponentResources(resources, result);
|
||||
await addTransitivelyReferencedChildResourcesOfComponentResources(resources, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
function addTransitivelyReferencedChildResourcesOfComponentResources(resources: Set<Resource> | undefined, result: Set<Resource>) {
|
||||
async function addTransitivelyReferencedChildResourcesOfComponentResources(resources: Set<Resource> | undefined, result: Set<Resource>) {
|
||||
if (resources) {
|
||||
for (const resource of resources) {
|
||||
if (!result.has(resource)) {
|
||||
result.add(resource);
|
||||
|
||||
if (ComponentResource.isInstance(resource)) {
|
||||
// This await is safe even if __isConstructed is undefined. Ensure that the
|
||||
// resource has completely finished construction. That way all parent/child
|
||||
// relationships will have been setup.
|
||||
await resource.__data;
|
||||
addTransitivelyReferencedChildResourcesOfComponentResources(resource.__childResources, result);
|
||||
}
|
||||
}
|
||||
|
@ -449,7 +455,8 @@ async function gatherExplicitDependencies(
|
|||
// Recursively gather dependencies, await the promise, and append the output's dependencies.
|
||||
const dos = (dependsOn as Output<Input<Resource>[] | Input<Resource>>).apply(v => gatherExplicitDependencies(v));
|
||||
const urns = await dos.promise();
|
||||
const implicits = await gatherExplicitDependencies([...dos.resources()]);
|
||||
const dosResources = await getAllResources(dos);
|
||||
const implicits = await gatherExplicitDependencies([...dosResources]);
|
||||
return urns.concat(implicits);
|
||||
} else {
|
||||
if (!Resource.isInstance(dependsOn)) {
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
import * as asset from "../asset";
|
||||
import * as log from "../log";
|
||||
import { Input, Inputs, isUnknown, Output, unknown } from "../output";
|
||||
import { getAllResources, Input, Inputs, isUnknown, Output, unknown } from "../output";
|
||||
import { ComponentResource, CustomResource, Resource } from "../resource";
|
||||
import { debuggablePromise, errorString } from "./debuggable";
|
||||
import { excessiveDebugOutput, isDryRun, monitorSupportsSecrets } from "./settings";
|
||||
|
@ -69,7 +69,8 @@ export function transferProperties(onto: Resource, label: string, props: Inputs)
|
|||
`transferIsStable(${label}, ${k}, ${propString})`),
|
||||
debuggablePromise(
|
||||
new Promise<boolean>(resolve => resolveIsSecret = resolve),
|
||||
`transferIsSecret(${label}, ${k}, ${props[k]})`));
|
||||
`transferIsSecret(${label}, ${k}, ${props[k]})`),
|
||||
Promise.resolve(onto));
|
||||
}
|
||||
|
||||
return resolvers;
|
||||
|
@ -269,7 +270,11 @@ export async function serializeProperty(ctx: string, prop: Input<any>, dependent
|
|||
log.debug(`Serialize property [${ctx}]: Output<T>`);
|
||||
}
|
||||
|
||||
for (const resource of prop.resources()) {
|
||||
// handle serializing both old-style outputs (with sync resources) and new-style outputs
|
||||
// (with async resources).
|
||||
|
||||
const propResources = await getAllResources(prop);
|
||||
for (const resource of propResources) {
|
||||
dependentResources.add(resource);
|
||||
}
|
||||
|
||||
|
|
|
@ -49,15 +49,16 @@ export function runInPulumiStack(init: () => Promise<any>): Promise<Inputs | und
|
|||
* Stack is the root resource for a Pulumi stack. Before invoking the `init` callback, it registers itself as the root
|
||||
* resource with the Pulumi engine.
|
||||
*/
|
||||
class Stack extends ComponentResource {
|
||||
class Stack extends ComponentResource<Inputs> {
|
||||
/**
|
||||
* The outputs of this stack, if the `init` callback exited normally.
|
||||
*/
|
||||
public readonly outputs: Output<Inputs | undefined>;
|
||||
public readonly outputs: Output<Inputs>;
|
||||
|
||||
constructor(init: () => Promise<Inputs>) {
|
||||
super(rootPulumiStackTypeName, `${getProject()}-${getStack()}`);
|
||||
this.outputs = output(this.runInit(init));
|
||||
super(rootPulumiStackTypeName, `${getProject()}-${getStack()}`, { init });
|
||||
const data = this.getData();
|
||||
this.outputs = output(data);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -66,7 +67,7 @@ class Stack extends ComponentResource {
|
|||
*
|
||||
* @param init The callback to run in the context of this Pulumi stack
|
||||
*/
|
||||
private async runInit(init: () => Promise<Inputs>): Promise<Inputs | undefined> {
|
||||
async initialize(args: { init: () => Promise<Inputs> }): Promise<Inputs> {
|
||||
const parent = await getRootResource();
|
||||
if (parent) {
|
||||
throw new Error("Only one root Pulumi Stack may be active at once");
|
||||
|
@ -78,7 +79,7 @@ class Stack extends ComponentResource {
|
|||
|
||||
let outputs: Inputs | undefined;
|
||||
try {
|
||||
const inputs = await init();
|
||||
const inputs = await args.init();
|
||||
outputs = await massage(inputs, []);
|
||||
} finally {
|
||||
// We want to expose stack outputs as simple pojo objects (including Resources). This
|
||||
|
@ -88,7 +89,7 @@ class Stack extends ComponentResource {
|
|||
super.registerOutputs(outputs);
|
||||
}
|
||||
|
||||
return outputs;
|
||||
return outputs!;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -67,7 +67,13 @@ export class StackReference extends CustomResource {
|
|||
// of the inputs are a secret, and this.outputs is always a secret if it contains any secrets. We do this dance
|
||||
// so we can ensure that the Output we return is not needlessly tainted as a secret.
|
||||
const value = all([output(name), this.outputs]).apply(([n, os]) => os[n]);
|
||||
return new Output(value.resources(), value.promise(), value.isKnown, isSecretOutputName(this, output(name)));
|
||||
|
||||
// 'value' is an Output produced by our own `.apply` implementation. So it's safe to
|
||||
// `.allResources!` on it.
|
||||
return new Output(
|
||||
value.resources(), value.promise(),
|
||||
value.isKnown, isSecretOutputName(this, output(name)),
|
||||
value.allResources!());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -82,7 +88,10 @@ export class StackReference extends CustomResource {
|
|||
}
|
||||
return os[n];
|
||||
});
|
||||
return new Output(value.resources(), value.promise(), value.isKnown, isSecretOutputName(this, output(name)));
|
||||
return new Output(
|
||||
value.resources(), value.promise(),
|
||||
value.isKnown, isSecretOutputName(this, output(name)),
|
||||
value.allResources!());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
// tslint:disable
|
||||
|
||||
import * as assert from "assert";
|
||||
import { Output, OutputInstance, all, concat, interpolate, output, unknown } from "../output";
|
||||
import { Output, all, concat, interpolate, output, unknown } from "../output";
|
||||
import { Resource } from "../resource";
|
||||
import * as runtime from "../runtime";
|
||||
import { asyncTest } from "./util";
|
||||
|
@ -67,8 +67,8 @@ describe("output", () => {
|
|||
it("propagates true isKnown bit from inner Output", asyncTest(async () => {
|
||||
runtime._setIsDryRun(true);
|
||||
|
||||
const output1 = new Output(new Set(), Promise.resolve("outer"), Promise.resolve(true), Promise.resolve(false));
|
||||
const output2 = output1.apply(v => new Output(new Set(), Promise.resolve("inner"), Promise.resolve(true), Promise.resolve(false)));
|
||||
const output1 = new Output(new Set(), Promise.resolve("outer"), Promise.resolve(true), Promise.resolve(false), Promise.resolve(new Set()));
|
||||
const output2 = output1.apply(v => new Output(new Set(), Promise.resolve("inner"), Promise.resolve(true), Promise.resolve(false), Promise.resolve(new Set())));
|
||||
|
||||
const isKnown = await output2.isKnown;
|
||||
assert.equal(isKnown, true);
|
||||
|
@ -80,8 +80,8 @@ describe("output", () => {
|
|||
it("propagates false isKnown bit from inner Output", asyncTest(async () => {
|
||||
runtime._setIsDryRun(true);
|
||||
|
||||
const output1 = new Output(new Set(), Promise.resolve("outer"), Promise.resolve(true), Promise.resolve(false));
|
||||
const output2 = output1.apply(v => new Output(new Set(), Promise.resolve("inner"), Promise.resolve(false), Promise.resolve(false)));
|
||||
const output1 = new Output(new Set(), Promise.resolve("outer"), Promise.resolve(true), Promise.resolve(false), Promise.resolve(new Set()));
|
||||
const output2 = output1.apply(v => new Output(new Set(), Promise.resolve("inner"), Promise.resolve(false), Promise.resolve(false), Promise.resolve(new Set())));
|
||||
|
||||
const isKnown = await output2.isKnown;
|
||||
assert.equal(isKnown, false);
|
||||
|
@ -93,8 +93,8 @@ describe("output", () => {
|
|||
it("can not await if isKnown is a rejected promise.", asyncTest(async () => {
|
||||
runtime._setIsDryRun(true);
|
||||
|
||||
const output1 = new Output(new Set(), Promise.resolve("outer"), Promise.resolve(true), Promise.resolve(false));
|
||||
const output2 = output1.apply(v => new Output(new Set(), Promise.resolve("inner"), Promise.reject(new Error("foo")), Promise.resolve(false)));
|
||||
const output1 = new Output(new Set(), Promise.resolve("outer"), Promise.resolve(true), Promise.resolve(false), Promise.resolve(new Set()));
|
||||
const output2 = output1.apply(v => new Output(new Set(), Promise.resolve("inner"), Promise.reject(new Error("foo")), Promise.resolve(false), Promise.resolve(new Set())));
|
||||
|
||||
try {
|
||||
const isKnown = await output2.isKnown;
|
||||
|
@ -114,8 +114,8 @@ describe("output", () => {
|
|||
it("propagates true isSecret bit from inner Output", asyncTest(async () => {
|
||||
runtime._setIsDryRun(true);
|
||||
|
||||
const output1 = new Output(new Set(), Promise.resolve("outer"), Promise.resolve(true), Promise.resolve(false));
|
||||
const output2 = output1.apply(v => new Output(new Set(), Promise.resolve("inner"), Promise.resolve(true), Promise.resolve(true)));
|
||||
const output1 = new Output(new Set(), Promise.resolve("outer"), Promise.resolve(true), Promise.resolve(false), Promise.resolve(new Set()));
|
||||
const output2 = output1.apply(v => new Output(new Set(), Promise.resolve("inner"), Promise.resolve(true), Promise.resolve(true), Promise.resolve(new Set())));
|
||||
|
||||
const isSecret = await output2.isSecret;
|
||||
assert.equal(isSecret, true);
|
||||
|
@ -127,8 +127,8 @@ describe("output", () => {
|
|||
it("retains true isSecret bit from outer Output", asyncTest(async () => {
|
||||
runtime._setIsDryRun(true);
|
||||
|
||||
const output1 = new Output(new Set(), Promise.resolve("outer"), Promise.resolve(true), Promise.resolve(true));
|
||||
const output2 = output1.apply(v => new Output(new Set(), Promise.resolve("inner"), Promise.resolve(true), Promise.resolve(false)));
|
||||
const output1 = new Output(new Set(), Promise.resolve("outer"), Promise.resolve(true), Promise.resolve(true), Promise.resolve(new Set()));
|
||||
const output2 = output1.apply(v => new Output(new Set(), Promise.resolve("inner"), Promise.resolve(true), Promise.resolve(false), Promise.resolve(new Set())));
|
||||
|
||||
const isSecret = await output2.isSecret;
|
||||
assert.equal(isSecret, true);
|
||||
|
@ -138,8 +138,7 @@ describe("output", () => {
|
|||
}));
|
||||
|
||||
describe("isKnown", () => {
|
||||
function or<T>(output1: Output<T>, output2: Output<T>): Output<T>;
|
||||
function or<T>(output1: any, output2: any): any {
|
||||
function or<T>(output1: Output<T>, output2: Output<T>): Output<T> {
|
||||
const val1 = output1.promise();
|
||||
const val2 = output2.promise();
|
||||
return new Output<T>(
|
||||
|
@ -149,14 +148,16 @@ describe("output", () => {
|
|||
Promise.all([val1, output1.isKnown, output2.isKnown])
|
||||
.then(([val1, isKnown1, isKnown2]) => val1 ? isKnown1 : isKnown2),
|
||||
Promise.all([val1, output1.isSecret, output2.isSecret])
|
||||
.then(([val1, isSecret1, isSecret2]) => val1 ? isSecret1 : isSecret2));
|
||||
.then(([val1, isSecret1, isSecret2]) => val1 ? isSecret1 : isSecret2),
|
||||
Promise.all([output1.allResources!(), output2.allResources!()])
|
||||
.then(([r1, r2]) => new Set([...r1, ...r2])));
|
||||
}
|
||||
|
||||
it("choose between known and known output, non-secret", asyncTest(async () => {
|
||||
runtime._setIsDryRun(true);
|
||||
|
||||
const o1 = new Output(new Set(), Promise.resolve("foo"), Promise.resolve(true), Promise.resolve(false));
|
||||
const o2 = new Output(new Set(), Promise.resolve("bar"), Promise.resolve(true), Promise.resolve(false));
|
||||
const o1 = new Output(new Set(), Promise.resolve("foo"), Promise.resolve(true), Promise.resolve(false), Promise.resolve(new Set()));
|
||||
const o2 = new Output(new Set(), Promise.resolve("bar"), Promise.resolve(true), Promise.resolve(false), Promise.resolve(new Set()));
|
||||
|
||||
const result = or(o1, o2);
|
||||
|
||||
|
@ -173,8 +174,8 @@ describe("output", () => {
|
|||
it("choose between known and known output, secret", asyncTest(async () => {
|
||||
runtime._setIsDryRun(true);
|
||||
|
||||
const o1 = new Output(new Set(), Promise.resolve("foo"), Promise.resolve(true), Promise.resolve(true));
|
||||
const o2 = new Output(new Set(), Promise.resolve("bar"), Promise.resolve(true), Promise.resolve(false));
|
||||
const o1 = new Output(new Set(), Promise.resolve("foo"), Promise.resolve(true), Promise.resolve(true), Promise.resolve(new Set()));
|
||||
const o2 = new Output(new Set(), Promise.resolve("bar"), Promise.resolve(true), Promise.resolve(false), Promise.resolve(new Set()));
|
||||
|
||||
const result = or(o1, o2);
|
||||
|
||||
|
@ -191,8 +192,8 @@ describe("output", () => {
|
|||
it("choose between known and unknown output, non-secret", asyncTest(async () => {
|
||||
runtime._setIsDryRun(true);
|
||||
|
||||
const o1 = new Output(new Set(), Promise.resolve("foo"), Promise.resolve(true), Promise.resolve(false));
|
||||
const o2 = new Output(new Set(), Promise.resolve(undefined), Promise.resolve(false), Promise.resolve(false));
|
||||
const o1 = new Output(new Set(), Promise.resolve("foo"), Promise.resolve(true), Promise.resolve(false), Promise.resolve(new Set()));
|
||||
const o2 = new Output(new Set(), Promise.resolve(undefined), Promise.resolve(false), Promise.resolve(false), Promise.resolve(new Set()));
|
||||
|
||||
const result = or(o1, o2);
|
||||
|
||||
|
@ -209,8 +210,8 @@ describe("output", () => {
|
|||
it("choose between known and unknown output, secret", asyncTest(async () => {
|
||||
runtime._setIsDryRun(true);
|
||||
|
||||
const o1 = new Output(new Set(), Promise.resolve("foo"), Promise.resolve(true), Promise.resolve(true));
|
||||
const o2 = new Output(new Set(), Promise.resolve(undefined), Promise.resolve(false), Promise.resolve(false));
|
||||
const o1 = new Output(new Set(), Promise.resolve("foo"), Promise.resolve(true), Promise.resolve(true), Promise.resolve(new Set()));
|
||||
const o2 = new Output(new Set(), Promise.resolve(undefined), Promise.resolve(false), Promise.resolve(false), Promise.resolve(new Set()));
|
||||
|
||||
const result = or(o1, o2);
|
||||
|
||||
|
@ -227,8 +228,8 @@ describe("output", () => {
|
|||
it("choose between unknown and known output, non-secret", asyncTest(async () => {
|
||||
runtime._setIsDryRun(true);
|
||||
|
||||
const o1 = new Output(new Set(), Promise.resolve(undefined), Promise.resolve(false), Promise.resolve(false));
|
||||
const o2 = new Output(new Set(), Promise.resolve("bar"), Promise.resolve(true), Promise.resolve(false));
|
||||
const o1 = new Output(new Set(), Promise.resolve(undefined), Promise.resolve(false), Promise.resolve(false), Promise.resolve(new Set()));
|
||||
const o2 = new Output(new Set(), Promise.resolve("bar"), Promise.resolve(true), Promise.resolve(false), Promise.resolve(new Set()));
|
||||
|
||||
const result = or(o1, o2);
|
||||
|
||||
|
@ -245,8 +246,8 @@ describe("output", () => {
|
|||
it("choose between unknown and known output, secret", asyncTest(async () => {
|
||||
runtime._setIsDryRun(true);
|
||||
|
||||
const o1 = new Output(new Set(), Promise.resolve(undefined), Promise.resolve(false), Promise.resolve(false));
|
||||
const o2 = new Output(new Set(), Promise.resolve("bar"), Promise.resolve(true), Promise.resolve(true));
|
||||
const o1 = new Output(new Set(), Promise.resolve(undefined), Promise.resolve(false), Promise.resolve(false), Promise.resolve(new Set()));
|
||||
const o2 = new Output(new Set(), Promise.resolve("bar"), Promise.resolve(true), Promise.resolve(true), Promise.resolve(new Set()));
|
||||
|
||||
const result = or(o1, o2);
|
||||
|
||||
|
@ -263,8 +264,8 @@ describe("output", () => {
|
|||
it("choose between unknown and unknown output, non-secret", asyncTest(async () => {
|
||||
runtime._setIsDryRun(true);
|
||||
|
||||
const o1 = new Output(new Set(), Promise.resolve(undefined), Promise.resolve(false), Promise.resolve(false));
|
||||
const o2 = new Output(new Set(), Promise.resolve(undefined), Promise.resolve(false), Promise.resolve(false));
|
||||
const o1 = new Output(new Set(), Promise.resolve(undefined), Promise.resolve(false), Promise.resolve(false), Promise.resolve(new Set()));
|
||||
const o2 = new Output(new Set(), Promise.resolve(undefined), Promise.resolve(false), Promise.resolve(false), Promise.resolve(new Set()));
|
||||
|
||||
const result = or(o1, o2);
|
||||
|
||||
|
@ -281,8 +282,8 @@ describe("output", () => {
|
|||
it("choose between unknown and unknown output, secret1", asyncTest(async () => {
|
||||
runtime._setIsDryRun(true);
|
||||
|
||||
const o1 = new Output(new Set(), Promise.resolve(undefined), Promise.resolve(false), Promise.resolve(true));
|
||||
const o2 = new Output(new Set(), Promise.resolve(undefined), Promise.resolve(false), Promise.resolve(false));
|
||||
const o1 = new Output(new Set(), Promise.resolve(undefined), Promise.resolve(false), Promise.resolve(true), Promise.resolve(new Set()));
|
||||
const o2 = new Output(new Set(), Promise.resolve(undefined), Promise.resolve(false), Promise.resolve(false), Promise.resolve(new Set()));
|
||||
|
||||
const result = or(o1, o2);
|
||||
|
||||
|
@ -299,8 +300,8 @@ describe("output", () => {
|
|||
it("choose between unknown and unknown output, secret2", asyncTest(async () => {
|
||||
runtime._setIsDryRun(true);
|
||||
|
||||
const o1 = new Output(new Set(), Promise.resolve(undefined), Promise.resolve(false), Promise.resolve(false));
|
||||
const o2 = new Output(new Set(), Promise.resolve(undefined), Promise.resolve(false), Promise.resolve(true));
|
||||
const o1 = new Output(new Set(), Promise.resolve(undefined), Promise.resolve(false), Promise.resolve(false), Promise.resolve(new Set()));
|
||||
const o2 = new Output(new Set(), Promise.resolve(undefined), Promise.resolve(false), Promise.resolve(true), Promise.resolve(new Set()));
|
||||
|
||||
const result = or(o1, o2);
|
||||
|
||||
|
@ -317,8 +318,8 @@ describe("output", () => {
|
|||
it("choose between unknown and unknown output, secret3", asyncTest(async () => {
|
||||
runtime._setIsDryRun(true);
|
||||
|
||||
const o1 = new Output(new Set(), Promise.resolve(undefined), Promise.resolve(false), Promise.resolve(true));
|
||||
const o2 = new Output(new Set(), Promise.resolve(undefined), Promise.resolve(false), Promise.resolve(true));
|
||||
const o1 = new Output(new Set(), Promise.resolve(undefined), Promise.resolve(false), Promise.resolve(true), Promise.resolve(new Set()));
|
||||
const o2 = new Output(new Set(), Promise.resolve(undefined), Promise.resolve(false), Promise.resolve(true), Promise.resolve(new Set()));
|
||||
|
||||
const result = or(o1, o2);
|
||||
|
||||
|
@ -335,9 +336,9 @@ describe("output", () => {
|
|||
it("is unknown if the value is or contains unknowns", asyncTest(async () => {
|
||||
runtime._setIsDryRun(true);
|
||||
|
||||
const o1 = new Output(new Set(), Promise.resolve(unknown), Promise.resolve(true), Promise.resolve(false));
|
||||
const o2 = new Output(new Set(), Promise.resolve(["foo", unknown]), Promise.resolve(true), Promise.resolve(false));
|
||||
const o3 = new Output(new Set(), Promise.resolve({"foo": "foo", unknown}), Promise.resolve(true), Promise.resolve(false));
|
||||
const o1 = new Output(new Set(), Promise.resolve(unknown), Promise.resolve(true), Promise.resolve(false), Promise.resolve(new Set()));
|
||||
const o2 = new Output(new Set(), Promise.resolve(["foo", unknown]), Promise.resolve(true), Promise.resolve(false), Promise.resolve(new Set()));
|
||||
const o3 = new Output(new Set(), Promise.resolve({"foo": "foo", unknown}), Promise.resolve(true), Promise.resolve(false), Promise.resolve(new Set()));
|
||||
|
||||
assert.equal(await o1.isKnown, false);
|
||||
assert.equal(await o2.isKnown, false);
|
||||
|
@ -347,7 +348,7 @@ describe("output", () => {
|
|||
it("is unknown if the result after apply is unknown or contains unknowns", asyncTest(async () => {
|
||||
runtime._setIsDryRun(true);
|
||||
|
||||
const o1 = new Output(new Set(), Promise.resolve("foo"), Promise.resolve(true), Promise.resolve(false));
|
||||
const o1 = new Output(new Set(), Promise.resolve("foo"), Promise.resolve(true), Promise.resolve(false), Promise.resolve(new Set()));
|
||||
const r1 = o1.apply(v => unknown);
|
||||
const r2 = o1.apply(v => [v, unknown]);
|
||||
const r3 = o1.apply(v => <any>{v, unknown});
|
||||
|
|
|
@ -54,3 +54,24 @@ res.outprop3.apply(prop => {
|
|||
assert.equal(prop, undefined);
|
||||
});
|
||||
|
||||
let resOutput = pulumi.output(res);
|
||||
resOutput.urn.apply(urn => {
|
||||
console.log(`URN: ${urn}`);
|
||||
assert.equal(urn, "test:index:MyResource::testResource1");
|
||||
});
|
||||
resOutput.id.apply(id => {
|
||||
console.log(`ID: ${id}`);
|
||||
assert.equal(id, "testResource1");
|
||||
});
|
||||
resOutput.outprop1.apply(prop => {
|
||||
console.log(`OutProp1: ${prop}`);
|
||||
assert.equal(prop, "output properties ftw");
|
||||
});
|
||||
resOutput.outprop2.apply(prop => {
|
||||
console.log(`OutProp2: ${prop}`);
|
||||
assert.equal(prop, 998.6);
|
||||
});
|
||||
resOutput.outprop3.apply(prop => {
|
||||
console.log(`OutProp3: ${prop}`);
|
||||
assert.equal(prop, undefined);
|
||||
});
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
// Test the ability to invoke provider functions via RPC.
|
||||
|
||||
let assert = require("assert");
|
||||
let pulumi = require("../../../../../");
|
||||
|
||||
class CustResource extends pulumi.CustomResource {
|
||||
constructor(name, opts) {
|
||||
super("test:index:CustResource", name, {}, opts)
|
||||
}
|
||||
}
|
||||
|
||||
class CompResource extends pulumi.ComponentResource {
|
||||
constructor(name, opts) {
|
||||
super("test:index:CompResource", name, {}, opts)
|
||||
const data = this.getData();
|
||||
this.a = pulumi.output(data.then(d => d.a));
|
||||
this.b = pulumi.output(data.then(d => d.b));
|
||||
this.cust1 = pulumi.output(data.then(d => d.cust1));
|
||||
this.cust2 = pulumi.output(data.then(d => d.cust2));
|
||||
}
|
||||
|
||||
/** @override */
|
||||
async initialize() {
|
||||
const cust1 = new CustResource("a", { parent: this });
|
||||
const cust2 = new CustResource("b", { parent: this });
|
||||
return { a: 1, b: 2, cust1, cust2 }
|
||||
}
|
||||
}
|
||||
|
||||
const comp = new CompResource("comp", {});
|
||||
comp.a.apply(v => {
|
||||
assert.equal(v, 1);
|
||||
});
|
||||
comp.b.apply(v => {
|
||||
assert.equal(v, 2);
|
||||
});
|
||||
|
||||
// Have a custom resource depend on the async component. We should still pick up 'a' and 'b' as
|
||||
// dependencies.
|
||||
const c = new CustResource("c", { dependsOn: comp });
|
||||
|
||||
// Have another depend on the child resources that are exposed through Output wrappers of async
|
||||
// computation. We should still pick up 'a' and 'b' as dependencies.
|
||||
const d = new CustResource("d", { dependsOn: [comp.cust1, comp.cust2] });
|
|
@ -1118,10 +1118,27 @@ describe("rpc", () => {
|
|||
return { failures: undefined, ret: args };
|
||||
},
|
||||
},
|
||||
"async_components": {
|
||||
program: path.join(base, "064.async_components"),
|
||||
expectResourceCount: 5,
|
||||
registerResource: (ctx: any, dryrun: boolean, t: string, name: string, res: any, dependencies?: string[],
|
||||
custom?: boolean, protect?: boolean, parent?: string, provider?: string) => {
|
||||
|
||||
if (name === "c" || name === "d") {
|
||||
dependencies = dependencies || [];
|
||||
dependencies.sort();
|
||||
// resources 'c' and 'd' should see resources 'a' and 'b' as dependencies (even
|
||||
// though they are async constructed by the component)
|
||||
assert.deepEqual(dependencies, ["test:index:CustResource::a", "test:index:CustResource::b"]);
|
||||
}
|
||||
|
||||
return { urn: makeUrn(t, name), id: undefined, props: undefined };
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
for (const casename of Object.keys(cases)) {
|
||||
// if (casename.indexOf("provider_in_parent_invokes") < 0) {
|
||||
// if (casename.indexOf("async_components") < 0) {
|
||||
// continue;
|
||||
// }
|
||||
|
||||
|
|
|
@ -55,3 +55,9 @@ declare let localUnshippedDerivedComponentResource: LocalUnshippedResourceExampl
|
|||
|
||||
latestShippedResource = localUnshippedDerivedComponentResource;
|
||||
localUnshippedResource = latestShippedDerivedComponentResource;
|
||||
|
||||
declare let latestOutput: latestShipped.Output<any>;
|
||||
declare let localOutput: localUnshipped.Output<any>;
|
||||
|
||||
latestOutput = localOutput;
|
||||
localOutput = latestOutput;
|
12
sdk/nodejs/tests/sxs_ts_3.6/package.json
Normal file
12
sdk/nodejs/tests/sxs_ts_3.6/package.json
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"name": "sxs",
|
||||
"version": "${VERSION}",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@pulumi/pulumi": "=1.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^8.0.0",
|
||||
"typescript": "=3.6"
|
||||
}
|
||||
}
|
13
sdk/nodejs/tests/sxs_ts_latest/README.md
Normal file
13
sdk/nodejs/tests/sxs_ts_latest/README.md
Normal file
|
@ -0,0 +1,13 @@
|
|||
This test validates that changes we're making in @pulumi/pulumi will be side-by-side compatible with the 'latest' version of `@pulumi/pulumi` that has already shipped.
|
||||
|
||||
If a change is made that is not compatible, then the process should be:
|
||||
|
||||
1. Ensure that the change is absolutely what we want to make.
|
||||
2. Disable running this test.
|
||||
3. Commit the change and update the minor version of `@pulumi/pulumi` (i.e. from 0.17.x to 0.18.0).
|
||||
4. Flow this change downstream, rev'ing the minor version of all downstream packages.
|
||||
5. Re-enable the test. Because there is now a new 'latest' `@pulumi/pulumi`, this test should pass.
|
||||
|
||||
Step '3' indicates that we've made a breaking change, and that if 0.18 is pulled in from any package, that it must be pulled in from all packages.
|
||||
|
||||
Step '4' is necessary so that people can pick a set of packages that all agree on using this new `@pulumi/pulumi` version. While not necessary to rev the minor version of these packages, we still do so to make it clear that there is a significant change here, and that one should not move to it as readily as they would a patch update.
|
63
sdk/nodejs/tests/sxs_ts_latest/index.ts
Normal file
63
sdk/nodejs/tests/sxs_ts_latest/index.ts
Normal file
|
@ -0,0 +1,63 @@
|
|||
// tslint:disable:file-header
|
||||
|
||||
// See README.md for information on what to do if this test fails.
|
||||
|
||||
import * as latestShipped from "@pulumi/pulumi";
|
||||
|
||||
// Note: we reference 'bin' as we want to see the typescript types with all internal information
|
||||
// stripped.
|
||||
import * as localUnshipped from "../../bin";
|
||||
|
||||
declare let latestShippedResource: latestShipped.Resource;
|
||||
declare let localUnshippedResource: localUnshipped.Resource;
|
||||
|
||||
declare let latestShippedComponentResourceOptions: latestShipped.ComponentResourceOptions;
|
||||
declare let localUnshippedComponentResourceOptions: localUnshipped.ComponentResourceOptions;
|
||||
|
||||
declare let latestShippedCustomResourceOptions: latestShipped.CustomResourceOptions;
|
||||
declare let localUnshippedCustomResourceOptions: localUnshipped.CustomResourceOptions;
|
||||
|
||||
latestShippedResource = localUnshippedResource;
|
||||
localUnshippedResource = latestShippedResource;
|
||||
|
||||
latestShippedComponentResourceOptions = localUnshippedComponentResourceOptions;
|
||||
localUnshippedComponentResourceOptions = latestShippedComponentResourceOptions;
|
||||
|
||||
latestShippedCustomResourceOptions = localUnshippedCustomResourceOptions;
|
||||
localUnshippedCustomResourceOptions = latestShippedCustomResourceOptions;
|
||||
|
||||
// simulate a resource similar to AWSX where there are instance methods that take
|
||||
// other resources and options.
|
||||
|
||||
class LatestShippedResourceExample1 extends latestShipped.ComponentResource {
|
||||
constructor(name: string, props: any, opts: latestShipped.ComponentResourceOptions) {
|
||||
super("", name, undefined, opts);
|
||||
}
|
||||
|
||||
public createInstance(name: string, opts: latestShipped.ComponentResourceOptions): LatestShippedResourceExample1 {
|
||||
throw new Error();
|
||||
}
|
||||
}
|
||||
|
||||
class LocalUnshippedResourceExample1 extends localUnshipped.ComponentResource {
|
||||
constructor(name: string, props: any, opts: localUnshipped.ComponentResourceOptions) {
|
||||
super("", name, undefined, opts);
|
||||
}
|
||||
|
||||
public createInstance(name: string, opts: localUnshipped.ComponentResourceOptions): LocalUnshippedResourceExample1 {
|
||||
throw new Error();
|
||||
}
|
||||
}
|
||||
|
||||
// make sure we can at least assign these to the Resource types from different versions.
|
||||
declare let latestShippedDerivedComponentResource: LatestShippedResourceExample1;
|
||||
declare let localUnshippedDerivedComponentResource: LocalUnshippedResourceExample1;
|
||||
|
||||
latestShippedResource = localUnshippedDerivedComponentResource;
|
||||
localUnshippedResource = latestShippedDerivedComponentResource;
|
||||
|
||||
declare let latestOutput: latestShipped.Output<any>;
|
||||
declare let localOutput: localUnshipped.Output<any>;
|
||||
|
||||
latestOutput = localOutput;
|
||||
localOutput = latestOutput;
|
|
@ -3,7 +3,7 @@
|
|||
"version": "${VERSION}",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@pulumi/pulumi": "latest"
|
||||
"@pulumi/pulumi": "=1.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^8.0.0",
|
21
sdk/nodejs/tests/sxs_ts_latest/tsconfig.json
Normal file
21
sdk/nodejs/tests/sxs_ts_latest/tsconfig.json
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"outDir": "bin",
|
||||
"target": "es2016",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"declaration": true,
|
||||
"sourceMap": false,
|
||||
"stripInternal": true,
|
||||
"experimentalDecorators": true,
|
||||
"pretty": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitReturns": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"files": [
|
||||
"index.ts",
|
||||
]
|
||||
}
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
// tslint:disable
|
||||
|
||||
import * as assert from "assert";
|
||||
import { output, Output, Resource } from "../index";
|
||||
import { all, output, Output, unknown } from "../index";
|
||||
import { asyncTest } from "./util";
|
||||
|
||||
function test(val: any, expected: any) {
|
||||
|
@ -38,21 +38,23 @@ function testOutput(val: any) {
|
|||
return test(output(val), val);
|
||||
}
|
||||
|
||||
function testResources(val: any, expected: any, resources: TestResource[]) {
|
||||
function testResources(val: any, expected: any, resources: TestResource[], allResources: TestResource[], withUnknowns?: boolean) {
|
||||
return asyncTest(async () => {
|
||||
const unwrapped = output(val);
|
||||
const actual = await unwrapped.promise();
|
||||
const actual = await unwrapped.promise(withUnknowns);
|
||||
|
||||
const syncResources = unwrapped.resources();
|
||||
const asyncResources = await unwrapped.allResources!()
|
||||
|
||||
assert.deepStrictEqual(actual, expected);
|
||||
assert.deepStrictEqual(unwrapped.resources(), new Set(resources));
|
||||
assert.deepStrictEqual(syncResources, new Set(resources));
|
||||
assert.deepStrictEqual(asyncResources, new Set(allResources));
|
||||
|
||||
const unwrappedResources: TestResource[] = <any>[...unwrapped.resources()];
|
||||
unwrappedResources.sort((r1, r2) => r1.name.localeCompare(r2.name));
|
||||
|
||||
resources.sort((r1, r2) => r1.name.localeCompare(r2.name));
|
||||
assert.equal(
|
||||
JSON.stringify(unwrappedResources),
|
||||
JSON.stringify(resources));
|
||||
for (const res of syncResources) {
|
||||
if (!asyncResources.has(res)) {
|
||||
assert.fail(`async resources did not contain: ${(<TestResource><any>res).name}`)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -136,7 +138,7 @@ describe("unwrap", () => {
|
|||
function createOutput<T>(cv: T, ...resources: TestResource[]): Output<T> {
|
||||
return Output.isInstance<T>(cv)
|
||||
? cv
|
||||
: new Output(<any>new Set(resources), Promise.resolve(cv), Promise.resolve(true), Promise.resolve(false))
|
||||
: new Output(<any>new Set(resources), Promise.resolve(cv), Promise.resolve(true), Promise.resolve(false), Promise.resolve(<any>new Set(resources)))
|
||||
}
|
||||
|
||||
describe("preserves resources", () => {
|
||||
|
@ -152,101 +154,129 @@ describe("unwrap", () => {
|
|||
it("with single output", testResources(
|
||||
createOutput(3, r1, r2),
|
||||
3,
|
||||
[r1, r2],
|
||||
[r1, r2]));
|
||||
|
||||
it("inside array", testResources(
|
||||
[createOutput(3, r1, r2)],
|
||||
[3],
|
||||
[r1, r2],
|
||||
[r1, r2]));
|
||||
|
||||
it("inside multi array", testResources(
|
||||
[createOutput(1, r1, r2),createOutput(2, r2, r3)],
|
||||
[1, 2],
|
||||
[r1, r2, r3],
|
||||
[r1, r2, r3]));
|
||||
|
||||
it("inside nested array", testResources(
|
||||
[createOutput(1, r1, r2), createOutput(2, r2, r3), [createOutput(3, r5)]],
|
||||
[1, 2, [3]],
|
||||
[r1, r2, r3, r5],
|
||||
[r1, r2, r3, r5]));
|
||||
|
||||
it("inside object", testResources(
|
||||
{ a: createOutput(3, r1, r2) },
|
||||
{ a: 3 },
|
||||
[r1, r2],
|
||||
[r1, r2]));
|
||||
|
||||
it("inside multi object", testResources(
|
||||
{ a: createOutput(1, r1, r2), b: createOutput(2, r2, r3) },
|
||||
{ a: 1, b: 2 },
|
||||
[r1, r2, r3],
|
||||
[r1, r2, r3]));
|
||||
|
||||
it("inside nested object", testResources(
|
||||
{ a: createOutput(1, r1, r2), b: createOutput(2, r2, r3), c: { d: createOutput(3, r5) } },
|
||||
{ a: 1, b: 2, c: { d: 3 } },
|
||||
[r1, r2, r3, r5],
|
||||
[r1, r2, r3, r5]));
|
||||
|
||||
it("across inner promise", testResources(
|
||||
createOutput(Promise.resolve(3), r1, r2),
|
||||
3,
|
||||
[r1, r2],
|
||||
[r1, r2]));
|
||||
|
||||
describe("with unknowns", () => {
|
||||
it("across 'all' without unknowns", testResources(
|
||||
all([Promise.resolve({ a: createOutput(unknown, r1, r2)}), Promise.resolve({ b: createOutput(unknown, r3, r4)})]),
|
||||
undefined,
|
||||
[],
|
||||
[r1, r2, r3, r4]));
|
||||
|
||||
it("across 'all' with unknowns", testResources(
|
||||
all([Promise.resolve({ a: createOutput(unknown, r1, r2)}), Promise.resolve({ b: createOutput(unknown, r3, r4)})]),
|
||||
[{a: unknown}, {b: unknown}],
|
||||
[],
|
||||
[r1, r2, r3, r4],
|
||||
/*withUnknowns:*/ true));
|
||||
});
|
||||
|
||||
describe("across promise boundaries", () => {
|
||||
it("inside and outside of array", testResources(
|
||||
createOutput([createOutput(3, r1, r2)], r2, r3),
|
||||
[3],
|
||||
[r2, r3],
|
||||
[r1, r2, r3]));
|
||||
|
||||
it("inside and outside of object", testResources(
|
||||
createOutput({ a: createOutput(3, r1, r2) }, r2, r3),
|
||||
{ a: 3 },
|
||||
[r2, r3],
|
||||
[r1, r2, r3]));
|
||||
|
||||
it("inside nested object and array", testResources(
|
||||
{ a: createOutput(1, r1, r2), b: createOutput(2, r2, r3), c: { d: createOutput([createOutput(3, r5)], r6) } },
|
||||
{ a: 1, b: 2, c: { d: [3] } },
|
||||
[r1, r2, r3, r6],
|
||||
[r1, r2, r3, r5, r6]));
|
||||
|
||||
it("inside nested array and object", testResources(
|
||||
{ a: createOutput(1, r1, r2), b: createOutput(2, r2, r3), c: createOutput([{ d: createOutput(3, r5) }], r6) },
|
||||
{ a: 1, b: 2, c: [{ d: 3 }] },
|
||||
[r1, r2, r3, r6],
|
||||
[r1, r2, r3, r5, r6]));
|
||||
|
||||
it("across outer promise", testResources(
|
||||
Promise.resolve(createOutput(3, r1, r2)),
|
||||
3,
|
||||
[],
|
||||
[r1, r2]));
|
||||
|
||||
it("across inner and outer promise", testResources(
|
||||
Promise.resolve(createOutput(Promise.resolve(3), r1, r2)),
|
||||
3,
|
||||
[],
|
||||
[r1, r2]));
|
||||
|
||||
it("across promise and inner object", testResources(
|
||||
Promise.resolve(createOutput(Promise.resolve({ a: createOutput(1, r4, r5)}), r1, r2)),
|
||||
{ a: 1 },
|
||||
[],
|
||||
[r1, r2, r4, r5]));
|
||||
|
||||
it("across promise and inner array and object", testResources(
|
||||
Promise.resolve(createOutput([Promise.resolve({ a: createOutput(1, r4, r5)})], r1, r2)),
|
||||
[{ a: 1 }],
|
||||
[],
|
||||
[r1, r2, r4, r5]));
|
||||
|
||||
it("across inner object", testResources(
|
||||
createOutput(Promise.resolve({ a: createOutput(1, r4, r5)}), r1, r2),
|
||||
{ a: 1 },
|
||||
[r1, r2],
|
||||
[r1, r2, r4, r5]));
|
||||
|
||||
it("across 'all'", testResources(
|
||||
all([Promise.resolve({ a: createOutput(1, r1, r2)}), Promise.resolve({ b: createOutput(2, r3, r4)})]),
|
||||
[{ a: 1 }, { b: 2 }],
|
||||
[],
|
||||
[r1, r2, r3, r4]));
|
||||
});
|
||||
});
|
||||
|
||||
describe("does not preserve all resources", () => {
|
||||
const r1 = new TestResource("r1");
|
||||
const r2 = new TestResource("r2");
|
||||
const r3 = new TestResource("r3");
|
||||
const r4 = new TestResource("r4");
|
||||
const r5 = new TestResource("r5");
|
||||
const r6 = new TestResource("r6");
|
||||
|
||||
// in these tests, not all resources are preserved as they may cross promise boundaries.
|
||||
|
||||
it("inside and outside of array", testResources(
|
||||
createOutput([createOutput(3, r1, r2)], r2, r3),
|
||||
[3],
|
||||
[r2, r3]));
|
||||
|
||||
it("inside and outside of object", testResources(
|
||||
createOutput({ a: createOutput(3, r1, r2) }, r2, r3),
|
||||
{ a: 3 },
|
||||
[r2, r3]));
|
||||
|
||||
it("inside nested object and array", testResources(
|
||||
{ a: createOutput(1, r1, r2), b: createOutput(2, r2, r3), c: { d: createOutput([createOutput(3, r5)], r6) } },
|
||||
{ a: 1, b: 2, c: { d: [3] } },
|
||||
[r1, r2, r3, r6]));
|
||||
|
||||
it("inside nested array and object", testResources(
|
||||
{ a: createOutput(1, r1, r2), b: createOutput(2, r2, r3), c: createOutput([{ d: createOutput(3, r5) }], r6) },
|
||||
{ a: 1, b: 2, c: [{ d: 3 }] },
|
||||
[r1, r2, r3, r6]));
|
||||
|
||||
it("across outer promise", testResources(
|
||||
Promise.resolve(createOutput(3, r1, r2)),
|
||||
3,
|
||||
[]));
|
||||
|
||||
it("across inner and outer promise", testResources(
|
||||
Promise.resolve(createOutput(Promise.resolve(3), r1, r2)),
|
||||
3,
|
||||
[]));
|
||||
|
||||
it("across promise and inner object", testResources(
|
||||
Promise.resolve(createOutput(Promise.resolve({ a: createOutput(1, r4, r5)}), r1, r2)),
|
||||
{ a: 1 },
|
||||
[]));
|
||||
|
||||
it("across promise and inner array and object", testResources(
|
||||
Promise.resolve(createOutput([Promise.resolve({ a: createOutput(1, r4, r5)})], r1, r2)),
|
||||
[{ a: 1 }],
|
||||
[]));
|
||||
|
||||
it("across inner object", testResources(
|
||||
createOutput(Promise.resolve({ a: createOutput(1, r4, r5)}), r1, r2),
|
||||
{ a: 1 },
|
||||
[r1, r2]));
|
||||
});
|
||||
|
||||
|
||||
describe("type system", () => {
|
||||
it ("across promises", asyncTest(async () => {
|
||||
var v = { a: 1, b: Promise.resolve(""), c: { d: true, e: Promise.resolve(4) } };
|
||||
|
|
|
@ -160,18 +160,29 @@ func (host *pythonLanguageHost) Run(ctx context.Context, req *pulumirpc.RunReque
|
|||
|
||||
// Now simply spawn a process to execute the requested program, wiring up stdout/stderr directly.
|
||||
var errResult string
|
||||
pythonCmd := os.Getenv("PULUMI_PYTHON_CMD")
|
||||
if pythonCmd == "" {
|
||||
// Look for "python3" by default. "python" usually refers to Python 2.7 on most distros.
|
||||
pythonCmd = "python3"
|
||||
var pythonCmds []string
|
||||
var pythonPath string
|
||||
|
||||
if pythonCmd := os.Getenv("PULUMI_PYTHON_CMD"); pythonCmd != "" {
|
||||
pythonCmds = []string{pythonCmd}
|
||||
} else {
|
||||
// Look for "python3" by default, but fallback to `python` if not found as some Python 3
|
||||
// distributions (in particular the default python.org Windows installation) do not include
|
||||
// a `python3` binary.
|
||||
pythonCmds = []string{"python3", "python"}
|
||||
}
|
||||
|
||||
// Look for the Python we intend to launch and emit an error if we can't find it. This is intended
|
||||
// to catch people that don't have Python 3 installed.
|
||||
pythonPath, err := exec.LookPath(pythonCmd)
|
||||
for _, pythonCmd := range pythonCmds {
|
||||
pythonPath, err = exec.LookPath(pythonCmd)
|
||||
// Break on the first cmd we find on the path (if any)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"Failed to locate '%s' on your PATH. Have you installed Python 3.6 or greater?", pythonCmd)
|
||||
"Failed to locate any of %q on your PATH. Have you installed Python 3.6 or greater?",
|
||||
pythonCmds)
|
||||
}
|
||||
|
||||
cmd := exec.Command(pythonPath, args...)
|
||||
|
|
|
@ -74,20 +74,26 @@ class Output(Generic[T]):
|
|||
Future that actually produces the concrete value of this output.
|
||||
"""
|
||||
|
||||
_resources: Set['Resource']
|
||||
_resources: Awaitable[Set['Resource']]
|
||||
"""
|
||||
The list of resources that this output value depends on.
|
||||
"""
|
||||
|
||||
def __init__(self, resources: Set['Resource'], future: Awaitable[T],
|
||||
is_known: Awaitable[bool], is_secret: Optional[Awaitable[bool]] = None) -> None:
|
||||
def __init__(self, resources: Union[Awaitable[Set['Resource']], Set['Resource']],
|
||||
future: Awaitable[T], is_known: Awaitable[bool],
|
||||
is_secret: Optional[Awaitable[bool]] = None) -> None:
|
||||
is_known = asyncio.ensure_future(is_known)
|
||||
future = asyncio.ensure_future(future)
|
||||
|
||||
async def is_value_known() -> bool:
|
||||
return await is_known and not contains_unknowns(await future)
|
||||
|
||||
self._resources = resources
|
||||
if isinstance(resources, set):
|
||||
self._resources = asyncio.Future()
|
||||
self._resources.set_result(resources)
|
||||
else:
|
||||
self._resources = asyncio.ensure_future(resources)
|
||||
|
||||
self._future = future
|
||||
self._is_known = asyncio.ensure_future(is_value_known())
|
||||
|
||||
|
@ -98,7 +104,7 @@ class Output(Generic[T]):
|
|||
self._is_secret.set_result(False)
|
||||
|
||||
# Private implementation details - do not document.
|
||||
def resources(self) -> Set['Resource']:
|
||||
def resources(self) -> Awaitable[Set['Resource']]:
|
||||
return self._resources
|
||||
|
||||
def future(self, with_unknowns: Optional[bool] = None) -> Awaitable[T]:
|
||||
|
@ -135,6 +141,7 @@ class Output(Generic[T]):
|
|||
:return: A transformed Output obtained from running the transformation function on this Output's value.
|
||||
:rtype: Output[U]
|
||||
"""
|
||||
result_resources: asyncio.Future = asyncio.Future()
|
||||
result_is_known: asyncio.Future = asyncio.Future()
|
||||
result_is_secret: asyncio.Future = asyncio.Future()
|
||||
|
||||
|
@ -142,6 +149,7 @@ class Output(Generic[T]):
|
|||
async def run() -> U:
|
||||
try:
|
||||
# Await this output's details.
|
||||
resources = await self._resources
|
||||
is_known = await self._is_known
|
||||
is_secret = await self._is_secret
|
||||
value = await self._future
|
||||
|
@ -154,6 +162,7 @@ class Output(Generic[T]):
|
|||
if not apply_during_preview:
|
||||
# We didn't actually run the function, our new Output is definitely
|
||||
# **not** known and **not** secret
|
||||
result_resources.set_result(resources)
|
||||
result_is_known.set_result(False)
|
||||
result_is_secret.set_result(False)
|
||||
return cast(U, None)
|
||||
|
@ -169,7 +178,9 @@ class Output(Generic[T]):
|
|||
# 1. transformed is an Output[U]
|
||||
if isinstance(transformed, Output):
|
||||
transformed_as_output = cast(Output[U], transformed)
|
||||
# Forward along the inner output's _is_known and _is_secret values.
|
||||
# Forward along the inner output's _resources, _is_known and _is_secret values.
|
||||
transformed_resources = await transformed_as_output._resources
|
||||
result_resources.set_result(resources | transformed_resources)
|
||||
result_is_known.set_result(await transformed_as_output._is_known)
|
||||
result_is_secret.set_result(await transformed_as_output._is_secret or is_secret)
|
||||
return await transformed.future(with_unknowns=True)
|
||||
|
@ -177,11 +188,13 @@ class Output(Generic[T]):
|
|||
# 2. transformed is an Awaitable[U]
|
||||
if isawaitable(transformed):
|
||||
# Since transformed is not an Output, it is both known and not a secret.
|
||||
result_resources.set_result(resources)
|
||||
result_is_known.set_result(True)
|
||||
result_is_secret.set_result(False)
|
||||
return await cast(Awaitable[U], transformed)
|
||||
|
||||
# 3. transformed is U. It is trivially known.
|
||||
result_resources.set_result(resources)
|
||||
result_is_known.set_result(True)
|
||||
result_is_secret.set_result(False)
|
||||
return cast(U, transformed)
|
||||
|
@ -191,13 +204,14 @@ class Output(Generic[T]):
|
|||
# Try and set the result. This might fail if we're shutting down,
|
||||
# so swallow that error if that occurs.
|
||||
try:
|
||||
result_resources.set_result(resources)
|
||||
result_is_known.set_result(False)
|
||||
result_is_secret.set_result(False)
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
run_fut = asyncio.ensure_future(run())
|
||||
return Output(self._resources, run_fut, result_is_known, result_is_secret)
|
||||
return Output(result_resources, run_fut, result_is_known, result_is_secret)
|
||||
|
||||
def __getattr__(self, item: str) -> 'Output[Any]':
|
||||
"""
|
||||
|
@ -309,6 +323,11 @@ class Output(Generic[T]):
|
|||
each_is_secret = await asyncio.gather(*is_secret_futures)
|
||||
return any(each_is_secret)
|
||||
|
||||
async def get_resources(outputs):
|
||||
resources_futures = list(map(lambda o: o._resources, outputs))
|
||||
resources_agg = await asyncio.gather(*resources_futures)
|
||||
# Merge the list of resource dependencies across all inputs.
|
||||
return reduce(lambda acc, r: acc.union(r), resources_agg, set())
|
||||
|
||||
# gather_futures, which aggregates the list of futures in each input to a future of a list.
|
||||
async def gather_futures(outputs):
|
||||
|
@ -318,16 +337,14 @@ class Output(Generic[T]):
|
|||
# First, map all inputs to outputs using `from_input`.
|
||||
all_outputs = list(map(Output.from_input, args))
|
||||
|
||||
# Merge the list of resource dependencies across all inputs.
|
||||
resources = reduce(lambda acc, r: acc.union(r.resources()), all_outputs, set())
|
||||
|
||||
# Aggregate the list of futures into a future of lists.
|
||||
value_futures = asyncio.ensure_future(gather_futures(all_outputs))
|
||||
|
||||
# Aggregate whether or not this output is known.
|
||||
resources_futures = asyncio.ensure_future(get_resources(all_outputs))
|
||||
known_futures = asyncio.ensure_future(is_known(all_outputs))
|
||||
secret_futures = asyncio.ensure_future(is_secret(all_outputs))
|
||||
return Output(resources, value_futures, known_futures, secret_futures)
|
||||
return Output(resources_futures, value_futures, known_futures, secret_futures)
|
||||
|
||||
@staticmethod
|
||||
def concat(*args: List[Input[str]]) -> 'Output[str]':
|
||||
|
|
|
@ -151,7 +151,8 @@ async def serialize_property(value: 'Input[Any]',
|
|||
return await serialize_property(future_return, deps, input_transformer)
|
||||
|
||||
if known_types.is_output(value):
|
||||
deps.extend(value.resources())
|
||||
value_resources = await value.resources()
|
||||
deps.extend(value_resources)
|
||||
|
||||
# When serializing an Output, we will either serialize it as its resolved value or the
|
||||
# "unknown value" sentinel. We will do the former for all outputs created directly by user
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue