Merge branch 'master' into features/dedeasync

This commit is contained in:
Cyrus Najmabadi 2020-01-07 12:23:10 -08:00
commit 77ecc73622
145 changed files with 3106 additions and 630 deletions

BIN
.ionide/symbolCache.db Normal file

Binary file not shown.

View file

@ -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)

View file

@ -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
}

View file

@ -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
View 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
View 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
}

View file

@ -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)
}

View file

@ -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})
}),
}

View file

@ -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(
&parallel, "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")

View file

@ -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
}

View file

@ -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())
}),

View file

@ -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 {

View file

@ -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

View file

@ -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
View file

@ -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

View file

@ -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"`
}

View file

@ -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)

View file

@ -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)
}

View file

@ -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),

View file

@ -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)
}
}

View 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
}

View file

@ -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 {

View file

@ -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
}

View file

@ -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"

View file

@ -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)

View file

@ -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
}

View file

@ -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
}

View file

@ -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")
}

View file

@ -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()
}
}

View file

@ -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 {

View file

@ -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 {

View file

@ -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
}

View file

@ -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 {

View file

@ -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
}

View file

@ -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,

View file

@ -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
}

View file

@ -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")
}

View file

@ -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

View file

@ -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
}
}

View file

@ -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++ {

View file

@ -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

View file

@ -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>

View file

@ -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
{

View file

@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>

View 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");
}
}
}

View file

@ -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)
{

View 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);
}
}
}

View file

@ -1,3 +1,4 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Pulumi.Tests")]
[assembly: InternalsVisibleTo("Pulumi.Tests")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // Moq tests

View file

@ -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));
}
}

View file

@ -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);

View file

@ -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);

View file

@ -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&lt;int&gt; 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&lt;int&gt; Main(string[] args) {// program
/// initialization code ...
///
/// return Deployment.Run&lt;MyStack&gt;();}
/// </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;
}
}
}

View file

@ -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);
}

View file

@ -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();
}
}

View file

@ -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
{

View file

@ -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);
}

View file

@ -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);
}
}

View file

@ -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>>

View file

@ -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'">

View 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!;
}
}

View file

@ -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;
}

View file

@ -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)

View file

@ -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),
});

View file

@ -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);

View file

@ -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)
{

View file

@ -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)
{

View file

@ -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

View file

@ -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)

View file

@ -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 {

View file

@ -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}

View file

@ -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()

View file

@ -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));
}
};

View file

@ -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();
}

View file

@ -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));
}
};

View file

@ -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)) {

View file

@ -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.
*/

View file

@ -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> = {

View file

@ -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": {

View file

@ -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>>;

View file

@ -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

View file

@ -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)) {

View file

@ -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);
}

View file

@ -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!;
}
}

View file

@ -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!());
}
/**

View file

@ -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});

View file

@ -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);
});

View file

@ -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] });

View file

@ -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;
// }

View file

@ -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;

View 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"
}
}

View 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.

View 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;

View file

@ -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",

View 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",
]
}

View file

@ -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) } };

View file

@ -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...)

View file

@ -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]':

View file

@ -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