Merge branch 'master' into features/dedeasync6

This commit is contained in:
Cyrus Najmabadi 2020-01-07 12:18:30 -08:00
commit a8bdf28634
46 changed files with 998 additions and 197 deletions

View file

@ -3,10 +3,21 @@ CHANGELOG
## HEAD (Unreleased)
- Update version of TypeScript used by Pulumi to 3.7.3.
- Add support for GOOGLE_CREDENTIALS when using Google Cloud Storage backend. [#2906](https://github.com/pulumi/pulumi/pull/2906) (Fixes [#2790](https://github.com/pulumi/pulumi/issues/2790), [#2791](https://github.com/pulumi/pulumi/issues/2791))
- 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)"
@ -15,18 +26,26 @@ CHANGELOG
- 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 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. See https://github.com/pulumi/pulumi/pull/3676 for more details.
- `ComponentResource`s now have built-in support for asynchronously constructing their children. See [#3676](https://github.com/pulumi/pulumi/pull/3676) for more details.
- `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
@ -34,7 +53,9 @@ CHANGELOG
## 1.7.0 (2019-12-11)
- A Pulumi JavaScript/TypeScript program can now consist of a single exported top level function. i.e.:
- 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
@ -46,16 +67,13 @@ CHANGELOG
}
```
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)
## 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)
@ -67,7 +85,7 @@ CHANGELOG
- 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

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

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

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

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

@ -23,6 +23,7 @@ import (
"os/user"
"path"
"path/filepath"
"regexp"
"strings"
"time"
@ -234,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

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

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

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

@ -504,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
@ -520,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")
}
@ -539,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 {

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"
@ -33,19 +34,28 @@ fi
docker login -u "${DOCKER_HUB_USER}" -p "${DOCKER_HUB_PASSWORD}"
echo "Building containers..."
for container in [pulumi actions]; do
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/docker"
"${SCRIPT_DIR}/../dist/${container}"
done
echo "Running container runtime tests..."
RUN_CONTAINER_TESTS=true go test tests/containers/...
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
for container in pulumi actions; do
echo "- pulumi/${container}"
docker push "pulumi/${container}:${CLI_VERSION}"
docker push "pulumi/${container}:latest"

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

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

@ -165,11 +165,13 @@ 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>
@ -208,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>

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

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

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

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

@ -22,11 +22,59 @@ import (
"github.com/stretchr/testify/assert"
ptesting "github.com/pulumi/pulumi/pkg/testing"
"github.com/pulumi/pulumi/pkg/testing/integration"
)
// TestPulumiContainerImages simulates building and running Pulumi programs
// on all of the supported container images.
func TestPulumiContainerImages(t *testing.T) {
// TestPulumiDockerImage simulates building and running Pulumi programs on the pulumi/pulumi Docker image.
//
// NOTE: This test is intended to be run inside the aforementioned container, unlike the actions test below.
func TestPulumiDockerImage(t *testing.T) {
const stackOwner = "moolumi"
if os.Getenv("RUN_CONTAINER_TESTS") == "" {
t.Skip("Skipping container runtime tests because RUN_CONTAINER_TESTS not set.")
}
// Confirm we have credentials.
if os.Getenv("PULUMI_ACCESS_TOKEN") == "" {
t.Fatal("PULUMI_ACCESS_TOKEN not found, aborting tests.")
}
base := integration.ProgramTestOptions{
Tracing: "https://tracing.pulumi-engineering.com/collector/api/v1/spans",
ExpectRefreshChanges: true,
Quick: true,
SkipRefresh: true,
NoParallel: true, // we mark tests as Parallel manually when instantiating
}
for _, template := range []string{"csharp", "python", "typescript"} {
t.Run(template, func(t *testing.T) {
t.Parallel()
e := ptesting.NewEnvironment(t)
defer func() {
e.RunCommand("pulumi", "stack", "rm", "--force", "--yes")
e.DeleteEnvironment()
}()
stackName := fmt.Sprintf("%s/container-%s-%x", stackOwner, template, time.Now().UnixNano())
e.RunCommand("pulumi", "new", template, "-f", "-s", stackName)
example := base.With(integration.ProgramTestOptions{
Dir: e.RootPath,
})
integration.ProgramTest(t, &example)
})
}
}
// TestPulumiActionsImage simulates building and running Pulumi programs on the pulumi/actions image.
//
// The main codepath being tested is the entrypoint script of the container, which contains logic for
// downloading dependencies, honoring various environment variables, etc.
func TestPulumiActionsImage(t *testing.T) {
const pulumiContainerToTest = "pulumi/actions:latest"
if os.Getenv("RUN_CONTAINER_TESTS") == "" {

View file

@ -228,7 +228,7 @@ func TestStackTagValidation(t *testing.T) {
stdout, stderr := e.RunCommandExpectError("pulumi", "stack", "init", "invalid name (spaces, parens, etc.)")
assert.Equal(t, "", stdout)
assert.Contains(t, stderr, "stack name may only contain alphanumeric, hyphens, underscores, and periods")
assert.Contains(t, stderr, "stack names may only contain alphanumeric, hyphens, underscores, or periods")
})
t.Run("Error_DescriptionLength", func(t *testing.T) {
@ -505,6 +505,29 @@ func TestStackDependencyGraph(t *testing.T) {
})
}
// TestStackComponentDotNet tests the programming model of defining a stack as an explicit top-level component.
func TestStackComponentDotNet(t *testing.T) {
integration.ProgramTest(t, &integration.ProgramTestOptions{
Dir: filepath.Join("stack_component", "dotnet"),
Dependencies: []string{"Pulumi"},
Quick: true,
ExtraRuntimeValidation: func(t *testing.T, stackInfo integration.RuntimeValidationStackInfo) {
// Ensure the checkpoint contains a single resource, the Stack, with two outputs.
fmt.Printf("Deployment: %v", stackInfo.Deployment)
assert.NotNil(t, stackInfo.Deployment)
if assert.Equal(t, 1, len(stackInfo.Deployment.Resources)) {
stackRes := stackInfo.Deployment.Resources[0]
assert.NotNil(t, stackRes)
assert.Equal(t, resource.RootStackType, stackRes.URN.Type())
assert.Equal(t, 0, len(stackRes.Inputs))
assert.Equal(t, 2, len(stackRes.Outputs))
assert.Equal(t, "ABC", stackRes.Outputs["abc"])
assert.Equal(t, float64(42), stackRes.Outputs["Foo"])
}
},
})
}
// TestConfigSave ensures that config commands in the Pulumi CLI work as expected.
func TestConfigSave(t *testing.T) {
e := ptesting.NewEnvironment(t)
@ -1149,8 +1172,8 @@ func TestPython3NotInstalled(t *testing.T) {
stderr := &bytes.Buffer{}
badPython := "python3000"
expectedError := fmt.Sprintf(
"error: Failed to locate '%s' on your PATH. Have you installed Python 3.6 or greater?",
badPython)
"error: Failed to locate any of %q on your PATH. Have you installed Python 3.6 or greater?",
[]string{badPython})
integration.ProgramTest(t, &integration.ProgramTestOptions{
Dir: path.Join("empty", "python"),
Dependencies: []string{

View file

@ -0,0 +1,43 @@
// Copyright 2016-2020, Pulumi Corporation. All rights reserved.
package ints
import (
"fmt"
"os"
"strings"
"testing"
"time"
ptesting "github.com/pulumi/pulumi/pkg/testing"
)
// TestPolicy tests policy related commands work.
func TestPolicy(t *testing.T) {
e := ptesting.NewEnvironment(t)
defer func() {
if !t.Failed() {
e.DeleteEnvironment()
}
}()
// Confirm we have credentials.
if os.Getenv("PULUMI_ACCESS_TOKEN") == "" {
t.Fatal("PULUMI_ACCESS_TOKEN not found, aborting tests.")
}
name, _ := e.RunCommand("pulumi", "whoami")
orgName := strings.TrimSpace(name)
// Pack and push a Policy Pack for the organization.
policyPackName := fmt.Sprintf("%s-%x", "test-policy-pack", time.Now().UnixNano())
e.ImportDirectory("test_policy_pack")
e.RunCommand("yarn", "install")
os.Setenv("TEST_POLICY_PACK", policyPackName)
e.RunCommand("pulumi", "policy", "publish", orgName)
// Enable, Disable and then Delete the Policy Pack.
e.RunCommand("pulumi", "policy", "enable", fmt.Sprintf("%s/%s", orgName, policyPackName), "1")
e.RunCommand("pulumi", "policy", "disable", fmt.Sprintf("%s/%s", orgName, policyPackName), "1")
e.RunCommand("pulumi", "policy", "rm", fmt.Sprintf("%s/%s", orgName, policyPackName), "1")
}

View file

@ -0,0 +1,3 @@
name: aws-policy
description: Example policies for AWS
runtime: nodejs

View file

@ -0,0 +1,30 @@
// Copyright 2016-2020, Pulumi Corporation. All rights reserved.
import * as aws from "@pulumi/aws";
import * as policy from "@pulumi/policy";
const packName = process.env.TEST_POLICY_PACK;
if (!packName) {
console.log("no policy name provided");
process.exit(-1);
} else {
const policies = new policy.PolicyPack(packName, {
policies: [
{
name: "s3-no-public-read",
description: "Prohibits setting the publicRead or publicReadWrite permission on AWS S3 buckets.",
enforcementLevel: "mandatory",
validateResource: policy.validateTypedResource(aws.s3.Bucket, (bucket, args, reportViolation) => {
if (bucket.acl === "public-read" || bucket.acl === "public-read-write") {
reportViolation(
"You cannot set public-read or public-read-write on an S3 bucket. " +
"Read more about ACLs here: " +
"https://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html");
}
}),
},
],
});
}

View file

@ -0,0 +1,14 @@
{
"name": "aws-policy",
"version": "0.0.1",
"dependencies": {
"@pulumi/pulumi": "^0.17.28",
"@pulumi/aws": "0.18.16",
"@pulumi/policy": "^0.2.0"
},
"devDependencies": {
"@types/mocha": "^2.2.42",
"@types/node": "^10.12.7",
"tslint": "^5.11.0"
}
}

View file

@ -0,0 +1,22 @@
{
"compilerOptions": {
"outDir": "bin",
"target": "es6",
"module": "commonjs",
"moduleResolution": "node",
"declaration": true,
"sourceMap": false,
"stripInternal": true,
"experimentalDecorators": true,
"pretty": true,
"noFallthroughCasesInSwitch": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"forceConsistentCasingInFileNames": true,
"strictNullChecks": true,
},
"files": [
"index.ts",
]
}

View file

@ -0,0 +1,5 @@
/.pulumi/
[Bb]in/
[Oo]bj/

View file

@ -0,0 +1,30 @@
// Copyright 2016-2019, Pulumi Corporation. All rights reserved.
using System.Collections.Generic;
using System.Threading.Tasks;
using Pulumi;
using Pulumi.Serialization;
class MyStack : Stack
{
[Output("abc")]
public Output<string> Abc { get; private set; }
[Output]
public Output<int> Foo { get; private set; }
// This should NOT be exported as stack output due to the missing attribute
public Output<string> Bar { get; private set; }
public MyStack()
{
this.Abc = Output.Create("ABC");
this.Foo = Output.Create(42);
this.Bar = Output.Create("this should not come to output");
}
}
class Program
{
static Task<int> Main(string[] args) => Deployment.RunAsync<MyStack>();
}

View file

@ -0,0 +1,3 @@
name: stack_compoennt
description: A program that is defined as a Stack component.
runtime: dotnet

View file

@ -0,0 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.0</TargetFramework>
</PropertyGroup>
</Project>