Merge branch 'master' into features/dedeasync6
This commit is contained in:
commit
a8bdf28634
40
CHANGELOG.md
40
CHANGELOG.md
|
@ -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)
|
||||
|
|
|
@ -516,7 +516,7 @@ func promptAndCreateStack(prompt promptForValueFunc,
|
|||
}
|
||||
|
||||
for {
|
||||
stackName, err := prompt(yes, "stack name", "dev", false, workspace.ValidateStackName, opts)
|
||||
stackName, err := prompt(yes, "stack name", "dev", false, b.ValidateStackName, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -26,9 +26,11 @@ func newPolicyCmd() *cobra.Command {
|
|||
Args: cmdutil.NoArgs,
|
||||
}
|
||||
|
||||
cmd.AddCommand(newPolicyDisableCmd())
|
||||
cmd.AddCommand(newPolicyEnableCmd())
|
||||
cmd.AddCommand(newPolicyNewCmd())
|
||||
cmd.AddCommand(newPolicyPublishCmd())
|
||||
cmd.AddCommand(newPolicyApplyCmd())
|
||||
cmd.AddCommand(newPolicyRmCmd())
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
|
61
cmd/policy_disable.go
Normal file
61
cmd/policy_disable.go
Normal file
|
@ -0,0 +1,61 @@
|
|||
// Copyright 2016-2020, Pulumi Corporation.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/pulumi/pulumi/pkg/backend"
|
||||
"github.com/pulumi/pulumi/pkg/util/cmdutil"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type policyDisableArgs struct {
|
||||
policyGroup string
|
||||
}
|
||||
|
||||
func newPolicyDisableCmd() *cobra.Command {
|
||||
args := policyDisableArgs{}
|
||||
|
||||
var cmd = &cobra.Command{
|
||||
Use: "disable <org-name>/<policy-pack-name> <version>",
|
||||
Args: cmdutil.ExactArgs(2),
|
||||
Short: "Disable a Policy Pack for a Pulumi organization",
|
||||
Long: "Disable a Policy Pack for a Pulumi organization",
|
||||
Run: cmdutil.RunFunc(func(cmd *cobra.Command, cliArgs []string) error {
|
||||
// Obtain current PolicyPack, tied to the Pulumi service backend.
|
||||
policyPack, err := requirePolicyPack(cliArgs[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
version, err := strconv.Atoi(cliArgs[1])
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "Could not parse version (should be an integer)")
|
||||
}
|
||||
|
||||
// Attempt to disable the Policy Pack.
|
||||
return policyPack.Disable(commandContext(), args.policyGroup, backend.PolicyPackOperation{
|
||||
Version: version, Scopes: cancellationScopes})
|
||||
}),
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().StringVar(
|
||||
&args.policyGroup, "policy-group", "",
|
||||
"The Policy Group for which the Policy Pack will be disabled; if not specified, the default Policy Group is used")
|
||||
|
||||
return cmd
|
||||
}
|
61
cmd/policy_enable.go
Normal file
61
cmd/policy_enable.go
Normal file
|
@ -0,0 +1,61 @@
|
|||
// Copyright 2016-2020, Pulumi Corporation.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/pulumi/pulumi/pkg/backend"
|
||||
"github.com/pulumi/pulumi/pkg/util/cmdutil"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type policyEnableArgs struct {
|
||||
policyGroup string
|
||||
}
|
||||
|
||||
func newPolicyEnableCmd() *cobra.Command {
|
||||
args := policyEnableArgs{}
|
||||
|
||||
var cmd = &cobra.Command{
|
||||
Use: "enable <org-name>/<policy-pack-name> <version>",
|
||||
Args: cmdutil.ExactArgs(2),
|
||||
Short: "Enable a Policy Pack for a Pulumi organization",
|
||||
Long: "Enable a Policy Pack for a Pulumi organization",
|
||||
Run: cmdutil.RunFunc(func(cmd *cobra.Command, cliArgs []string) error {
|
||||
// Obtain current PolicyPack, tied to the Pulumi service backend.
|
||||
policyPack, err := requirePolicyPack(cliArgs[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
version, err := strconv.Atoi(cliArgs[1])
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "Could not parse version (should be an integer)")
|
||||
}
|
||||
|
||||
// Attempt to enable the PolicyPack.
|
||||
return policyPack.Apply(commandContext(), args.policyGroup, backend.PolicyPackOperation{
|
||||
Version: version, Scopes: cancellationScopes})
|
||||
}),
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().StringVar(
|
||||
&args.policyGroup, "policy-group", "",
|
||||
"The Policy Group for which the Policy Pack will be enabled; if not specified, the default Policy Group is used")
|
||||
|
||||
return cmd
|
||||
}
|
|
@ -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})
|
||||
}),
|
||||
}
|
|
@ -18,7 +18,6 @@ import (
|
|||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/pulumi/pulumi/pkg/workspace"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/pulumi/pulumi/pkg/backend/display"
|
||||
|
@ -87,7 +86,7 @@ func newStackInitCmd() *cobra.Command {
|
|||
"use the format <org-name>/<stack-name> (e.g. `acmecorp/dev`).\n")
|
||||
}
|
||||
|
||||
name, nameErr := promptForValue(false, "stack name", "dev", false, workspace.ValidateStackName, opts)
|
||||
name, nameErr := promptForValue(false, "stack name", "dev", false, b.ValidateStackName, opts)
|
||||
if nameErr != nil {
|
||||
return nameErr
|
||||
}
|
||||
|
@ -98,7 +97,7 @@ func newStackInitCmd() *cobra.Command {
|
|||
return errors.New("missing stack name")
|
||||
}
|
||||
|
||||
if err := workspace.ValidateStackName(stackName); err != nil {
|
||||
if err := b.ValidateStackName(stackName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ import (
|
|||
"github.com/pulumi/pulumi/pkg/backend/display"
|
||||
"github.com/pulumi/pulumi/pkg/backend/state"
|
||||
"github.com/pulumi/pulumi/pkg/util/cmdutil"
|
||||
"github.com/pulumi/pulumi/pkg/util/contract"
|
||||
)
|
||||
|
||||
// newStackSelectCmd handles both the "local" and "cloud" scenarios in its implementation.
|
||||
|
@ -55,7 +56,7 @@ func newStackSelectCmd() *cobra.Command {
|
|||
}
|
||||
|
||||
if stack != "" {
|
||||
// A stack was given, ask the backend about it
|
||||
// A stack was given, ask the backend about it.
|
||||
stackRef, stackErr := b.ParseStackReference(stack)
|
||||
if stackErr != nil {
|
||||
return stackErr
|
||||
|
@ -76,6 +77,8 @@ func newStackSelectCmd() *cobra.Command {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
contract.Assert(stack != nil)
|
||||
return state.SetCurrentStack(stack.Ref().String())
|
||||
|
||||
}),
|
||||
|
|
12
cmd/util.go
12
cmd/util.go
|
@ -229,6 +229,8 @@ func requireCurrentStack(offerNew bool, opts display.Options, setCurrent bool) (
|
|||
// true, then the option to create an entirely new stack is provided and will create one as desired.
|
||||
func chooseStack(
|
||||
b backend.Backend, offerNew bool, opts display.Options, setCurrent bool) (backend.Stack, error) {
|
||||
ctx := commandContext()
|
||||
|
||||
// Prepare our error in case we need to issue it. Bail early if we're not interactive.
|
||||
var chooseStackErr string
|
||||
if offerNew {
|
||||
|
@ -247,7 +249,7 @@ func chooseStack(
|
|||
|
||||
// List stacks as available options.
|
||||
project := string(proj.Name)
|
||||
summaries, err := b.ListStacks(commandContext(), backend.ListStacksFilter{Project: &project})
|
||||
summaries, err := b.ListStacks(ctx, backend.ListStacksFilter{Project: &project})
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "could not query backend for stacks")
|
||||
}
|
||||
|
@ -270,7 +272,7 @@ func chooseStack(
|
|||
|
||||
// If a stack is already selected, make that the default.
|
||||
var current string
|
||||
currStack, currErr := state.CurrentStack(commandContext(), b)
|
||||
currStack, currErr := state.CurrentStack(ctx, b)
|
||||
contract.IgnoreError(currErr)
|
||||
if currStack != nil {
|
||||
current = currStack.Ref().String()
|
||||
|
@ -321,10 +323,14 @@ func chooseStack(
|
|||
if err != nil {
|
||||
return nil, errors.Wrap(err, "parsing selected stack")
|
||||
}
|
||||
stack, err := b.GetStack(commandContext(), stackRef)
|
||||
// GetStack may return (nil, nil) if the stack isn't found.
|
||||
stack, err := b.GetStack(ctx, stackRef)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "getting selected stack")
|
||||
}
|
||||
if stack == nil {
|
||||
return nil, errors.Errorf("no stack named '%s' found", stackRef)
|
||||
}
|
||||
|
||||
// If setCurrent is true, we'll persist this choice so it'll be used for future CLI operations.
|
||||
if setCurrent {
|
||||
|
|
4
dist/actions/entrypoint.sh
vendored
4
dist/actions/entrypoint.sh
vendored
|
@ -90,6 +90,10 @@ if [ -e package.json ]; then
|
|||
if [ -f yarn.lock ] || [ ! -z $USE_YARN ]; then
|
||||
yarn install
|
||||
else
|
||||
# Set npm auth token if one is provided.
|
||||
if [ ! -z "$NPM_AUTH_TOKEN" ]; then
|
||||
echo "//registry.npmjs.org/:_authToken=$NPM_AUTH_TOKEN" > ~/.npmrc
|
||||
fi
|
||||
npm install
|
||||
fi
|
||||
fi
|
||||
|
|
|
@ -14,6 +14,9 @@
|
|||
|
||||
package apitype
|
||||
|
||||
// DefaultPolicyGroup is the name of the default Policy Group for organizations.
|
||||
const DefaultPolicyGroup = "default-policy-group"
|
||||
|
||||
// CreatePolicyPackRequest defines the request body for creating a new Policy
|
||||
// Pack for an organization. The request contains the metadata related to the
|
||||
// Policy Pack.
|
||||
|
@ -107,3 +110,27 @@ type GetStackPolicyPacksResponse struct {
|
|||
// RequiredPolicies is a list of required Policy Packs to run during the update.
|
||||
RequiredPolicies []RequiredPolicy `json:"requiredPolicies,omitempty"`
|
||||
}
|
||||
|
||||
// UpdatePolicyGroupRequest modifies a Policy Group.
|
||||
type UpdatePolicyGroupRequest struct {
|
||||
NewName *string `json:"newName,omitempty"`
|
||||
|
||||
AddStack *PulumiStackReference `json:"addStack,omitempty"`
|
||||
RemoveStack *PulumiStackReference `json:"removeStack,omitempty"`
|
||||
|
||||
AddPolicyPack *PolicyPackMetadata `json:"addPolicyPack,omitempty"`
|
||||
RemovePolicyPack *PolicyPackMetadata `json:"removePolicyPack,omitempty"`
|
||||
}
|
||||
|
||||
// PulumiStackReference contains the StackName and ProjectName of the stack.
|
||||
type PulumiStackReference struct {
|
||||
Name string `json:"name"`
|
||||
RoutingProject string `json:"routingProject"`
|
||||
}
|
||||
|
||||
// PolicyPackMetadata is the metadata of a Policy Pack.
|
||||
type PolicyPackMetadata struct {
|
||||
Name string `json:"name"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Version int `json:"version"`
|
||||
}
|
||||
|
|
|
@ -67,7 +67,7 @@ func (e OverStackLimitError) Error() string {
|
|||
// StackReference is an opaque type that refers to a stack managed by a backend. The CLI uses the ParseStackReference
|
||||
// method to turn a string like "my-great-stack" or "pulumi/my-great-stack" into a stack reference that can be used to
|
||||
// interact with the stack via the backend. Stack references are specific to a given backend and different back ends
|
||||
// may interpret the string passed to ParseStackReference differently
|
||||
// may interpret the string passed to ParseStackReference differently.
|
||||
type StackReference interface {
|
||||
// fmt.Stringer's String() method returns a string of the stack identity, suitable for display in the CLI
|
||||
fmt.Stringer
|
||||
|
@ -125,6 +125,9 @@ type Backend interface {
|
|||
// ParseStackReference takes a string representation and parses it to a reference which may be used for other
|
||||
// methods in this backend.
|
||||
ParseStackReference(s string) (StackReference, error)
|
||||
// ValidateStackName verifies that the string is a legal identifier for a (potentially qualified) stack.
|
||||
// Will check for any backend-specific naming restrictions.
|
||||
ValidateStackName(s string) error
|
||||
|
||||
// DoesProjectExist returns true if a project with the given name exists in this backend, or false otherwise.
|
||||
DoesProjectExist(ctx context.Context, projectName string) (bool, error)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -70,6 +70,12 @@ const (
|
|||
AccessTokenEnvVar = "PULUMI_ACCESS_TOKEN"
|
||||
)
|
||||
|
||||
// Name validation rules enforced by the Pulumi Service.
|
||||
var (
|
||||
stackOwnerRegexp = regexp.MustCompile("^[a-zA-Z0-9][a-zA-Z0-9-_]{1,38}[a-zA-Z0-9]$")
|
||||
stackNameAndProjectRegexp = regexp.MustCompile("^[A-Za-z0-9_.-]{1,100}$")
|
||||
)
|
||||
|
||||
// DefaultURL returns the default cloud URL. This may be overridden using the PULUMI_API environment
|
||||
// variable. If no override is found, and we are authenticated with a cloud, choose that. Otherwise,
|
||||
// we will default to the https://api.pulumi.com/ endpoint.
|
||||
|
@ -465,51 +471,141 @@ func (b *cloudBackend) SupportsOrganizations() bool {
|
|||
return true
|
||||
}
|
||||
|
||||
func (b *cloudBackend) ParseStackReference(s string) (backend.StackReference, error) {
|
||||
split := strings.Split(s, "/")
|
||||
var owner string
|
||||
var projectName string
|
||||
var stackName string
|
||||
// qualifiedStackReference describes a qualified stack on the Pulumi Service. The Owner or Project
|
||||
// may be "" if unspecified, e.g. "pulumi/production" specifies the Owner and Name, but not the
|
||||
// Project. We infer the missing data and try to make things work as best we can in ParseStackReference.
|
||||
type qualifiedStackReference struct {
|
||||
Owner string
|
||||
Project string
|
||||
Name string
|
||||
}
|
||||
|
||||
// parseStackName parses the stack name into a potentially qualifiedStackReference. Any omitted
|
||||
// portions will be left as "". For example:
|
||||
//
|
||||
// "alpha" - will just set the Name, but ignore Owner and Project.
|
||||
// "alpha/beta" - will set the Owner and Name, but not Project.
|
||||
// "alpha/beta/gamma" - will set Owner, Name, and Project.
|
||||
func (b *cloudBackend) parseStackName(s string) (qualifiedStackReference, error) {
|
||||
var q qualifiedStackReference
|
||||
|
||||
split := strings.Split(s, "/")
|
||||
switch len(split) {
|
||||
case 1:
|
||||
stackName = split[0]
|
||||
q.Name = split[0]
|
||||
case 2:
|
||||
owner = split[0]
|
||||
stackName = split[1]
|
||||
q.Owner = split[0]
|
||||
q.Name = split[1]
|
||||
case 3:
|
||||
owner = split[0]
|
||||
projectName = split[1]
|
||||
stackName = split[2]
|
||||
q.Owner = split[0]
|
||||
q.Project = split[1]
|
||||
q.Name = split[2]
|
||||
default:
|
||||
return nil, errors.Errorf("could not parse stack name '%s'", s)
|
||||
return qualifiedStackReference{}, errors.Errorf("could not parse stack name '%s'", s)
|
||||
}
|
||||
|
||||
if owner == "" {
|
||||
return q, nil
|
||||
}
|
||||
|
||||
func (b *cloudBackend) ParseStackReference(s string) (backend.StackReference, error) {
|
||||
// Parse the input as a qualified stack name.
|
||||
qualifiedName, err := b.parseStackName(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If the provided stack name didn't include the Owner or Project, infer them from the
|
||||
// local environment.
|
||||
if qualifiedName.Owner == "" {
|
||||
currentUser, userErr := b.CurrentUser()
|
||||
if userErr != nil {
|
||||
return nil, userErr
|
||||
}
|
||||
owner = currentUser
|
||||
qualifiedName.Owner = currentUser
|
||||
}
|
||||
|
||||
if projectName == "" {
|
||||
if qualifiedName.Project == "" {
|
||||
currentProject, projectErr := workspace.DetectProject()
|
||||
if projectErr != nil {
|
||||
return nil, projectErr
|
||||
}
|
||||
|
||||
projectName = currentProject.Name.String()
|
||||
qualifiedName.Project = currentProject.Name.String()
|
||||
}
|
||||
|
||||
return cloudBackendReference{
|
||||
owner: owner,
|
||||
project: projectName,
|
||||
name: tokens.QName(stackName),
|
||||
owner: qualifiedName.Owner,
|
||||
project: qualifiedName.Project,
|
||||
name: tokens.QName(qualifiedName.Name),
|
||||
b: b,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *cloudBackend) ValidateStackName(s string) error {
|
||||
qualifiedName, err := b.parseStackName(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// The Pulumi Service enforces specific naming restrictions for organizations,
|
||||
// projects, and stacks. Though ignore any values that need to be inferred later.
|
||||
if qualifiedName.Owner != "" {
|
||||
if err := validateOwnerName(qualifiedName.Owner); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if qualifiedName.Project != "" {
|
||||
if err := validateProjectName(qualifiedName.Project); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return validateStackName(qualifiedName.Name)
|
||||
}
|
||||
|
||||
// validateOwnerName checks if a stack owner name is valid. An "owner" is simply the namespace
|
||||
// a stack may exist within, which for the Pulumi Service is the user account or organization.
|
||||
func validateOwnerName(s string) error {
|
||||
if !stackOwnerRegexp.MatchString(s) {
|
||||
return errors.New("invalid stack owner")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateStackName checks if a stack name is valid, returning a user-suitable error if needed.
|
||||
func validateStackName(s string) error {
|
||||
if len(s) > 100 {
|
||||
return errors.New("stack names must be less than 100 characters")
|
||||
}
|
||||
if !stackNameAndProjectRegexp.MatchString(s) {
|
||||
return errors.New("stack names may only contain alphanumeric, hyphens, underscores, and periods")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateProjectName checks if a project name is valid, returning a user-suitable error if needed.
|
||||
//
|
||||
// NOTE: Be careful when requiring a project name be valid. The Pulumi.yaml file may contain
|
||||
// an invalid project name like "r@bid^W0MBAT!!", but we try to err on the side of flexibility by
|
||||
// implicitly "cleaning" the project name before we send it to the Pulumi Service. So when we go
|
||||
// to make HTTP requests, we use a more palitable name like "r_bid_W0MBAT__".
|
||||
//
|
||||
// The projects canonical name will be the sanitized "r_bid_W0MBAT__" form, but we do not require the
|
||||
// Pulumi.yaml file be updated.
|
||||
//
|
||||
// So we should only call validateProject name when creating _new_ stacks or creating _new_ projects.
|
||||
// We should not require that project names be valid when reading what is in the current workspace.
|
||||
func validateProjectName(s string) error {
|
||||
if len(s) > 100 {
|
||||
return errors.New("project names must be less than 100 characters")
|
||||
}
|
||||
if !stackNameAndProjectRegexp.MatchString(s) {
|
||||
return errors.New("project names may only contain alphanumeric, hyphens, underscores, and periods")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CloudConsoleURL returns a link to the cloud console with the given path elements. If a console link cannot be
|
||||
// created, we return the empty string instead (this can happen if the endpoint isn't a recognized pattern).
|
||||
func (b *cloudBackend) CloudConsoleURL(paths ...string) string {
|
||||
|
|
|
@ -103,13 +103,27 @@ func publishPolicyPackPath(orgName string) string {
|
|||
return fmt.Sprintf("/api/orgs/%s/policypacks", orgName)
|
||||
}
|
||||
|
||||
// appyPolicyPackPath returns the path for an API call to the Pulumi service to apply a PolicyPack
|
||||
// applyPolicyPackPath returns the path for an API call to the Pulumi service to apply a PolicyPack
|
||||
// to a Pulumi organization.
|
||||
func applyPolicyPackPath(orgName, policyPackName string, version int) string {
|
||||
return fmt.Sprintf(
|
||||
"/api/orgs/%s/policypacks/%s/versions/%d/apply", orgName, policyPackName, version)
|
||||
}
|
||||
|
||||
// updatePolicyGroupPath returns the path for an API call to the Pulumi service to update a PolicyGroup
|
||||
// for a Pulumi organization.
|
||||
func updatePolicyGroupPath(orgName, policyGroup string) string {
|
||||
return fmt.Sprintf(
|
||||
"/api/orgs/%s/policygroups/%s", orgName, policyGroup)
|
||||
}
|
||||
|
||||
// deletePolicyPackVersionPath returns the path for an API call to the Pulumi service to delete
|
||||
// a Policy Pack from a Pulumi organization.
|
||||
func deletePolicyPackVersionPath(orgName, policyPackName string, version int) string {
|
||||
return fmt.Sprintf(
|
||||
"/api/orgs/%s/policypacks/%s/versions/%d", orgName, policyPackName, version)
|
||||
}
|
||||
|
||||
// publishPolicyPackPublishComplete returns the path for an API call to signal to the Pulumi service
|
||||
// that a PolicyPack to a Pulumi organization.
|
||||
func publishPolicyPackPublishComplete(orgName, policyPackName string, version int) string {
|
||||
|
@ -503,7 +517,7 @@ func (pc *Client) PublishPolicyPack(ctx context.Context, orgName string,
|
|||
var resp apitype.CreatePolicyPackResponse
|
||||
err := pc.restCall(ctx, "POST", publishPolicyPackPath(orgName), nil, req, &resp)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "HTTP POST to publish policy pack failed")
|
||||
return errors.Wrapf(err, "Publish policy pack failed")
|
||||
}
|
||||
|
||||
fmt.Printf("Published as version %d\n", resp.Version)
|
||||
|
@ -530,24 +544,76 @@ func (pc *Client) PublishPolicyPack(ctx context.Context, orgName string,
|
|||
err = pc.restCall(ctx, "POST",
|
||||
publishPolicyPackPublishComplete(orgName, analyzerInfo.Name, resp.Version), nil, nil, nil)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "HTTP POST to signal completion of the publish operation failed")
|
||||
return errors.Wrapf(err, "Request to signal completion of the publish operation failed")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ApplyPolicyPack applies a `PolicyPack` to the Pulumi organization.
|
||||
func (pc *Client) ApplyPolicyPack(ctx context.Context, orgName string, policyPackName string,
|
||||
version int) error {
|
||||
// ApplyPolicyPack enables a `PolicyPack` to the Pulumi organization. If policyGroup is not empty,
|
||||
// it will enable the PolicyPack on the default PolicyGroup.
|
||||
func (pc *Client) ApplyPolicyPack(ctx context.Context, orgName string, policyGroup string,
|
||||
policyPackName string, version int) error {
|
||||
|
||||
req := apitype.ApplyPolicyPackRequest{Name: policyPackName, Version: version}
|
||||
if policyGroup == "" {
|
||||
req := apitype.ApplyPolicyPackRequest{Name: policyPackName, Version: version}
|
||||
|
||||
err := pc.restCall(
|
||||
ctx, "POST", applyPolicyPackPath(orgName, policyPackName, version), nil, req, nil)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "HTTP POST to apply policy pack failed")
|
||||
err := pc.restCall(
|
||||
ctx, "POST", applyPolicyPackPath(orgName, policyPackName, version), nil, req, nil)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "Enable policy pack failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// If a Policy Group was specified, enable it for the specific group only.
|
||||
req := apitype.UpdatePolicyGroupRequest{
|
||||
AddPolicyPack: &apitype.PolicyPackMetadata{
|
||||
Name: policyPackName,
|
||||
Version: version,
|
||||
},
|
||||
}
|
||||
|
||||
err := pc.restCall(ctx, http.MethodPatch, updatePolicyGroupPath(orgName, policyGroup), nil, req, nil)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "Enable policy pack failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DisablePolicyPack disables a `PolicyPack` to the Pulumi organization. If policyGroup is not empty,
|
||||
// it will disable the PolicyPack on the default PolicyGroup.
|
||||
func (pc *Client) DisablePolicyPack(ctx context.Context, orgName string, policyGroup string,
|
||||
policyPackName string, version int) error {
|
||||
|
||||
// If Policy Group was not specified, use the default Policy Group.
|
||||
if policyGroup == "" {
|
||||
policyGroup = apitype.DefaultPolicyGroup
|
||||
}
|
||||
|
||||
req := apitype.UpdatePolicyGroupRequest{
|
||||
RemovePolicyPack: &apitype.PolicyPackMetadata{
|
||||
Name: policyPackName,
|
||||
Version: version,
|
||||
},
|
||||
}
|
||||
|
||||
err := pc.restCall(ctx, http.MethodPatch, updatePolicyGroupPath(orgName, policyGroup), nil, req, nil)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "Request to disable policy pack failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemovePolicyPack removes a `PolicyPack` from the Pulumi organization.
|
||||
func (pc *Client) RemovePolicyPack(ctx context.Context, orgName string,
|
||||
policyPackName string, version int) error {
|
||||
|
||||
path := deletePolicyPackVersionPath(orgName, policyPackName, version)
|
||||
err := pc.restCall(ctx, http.MethodDelete, path, nil, nil, nil)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "Request to remove policy pack failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -166,8 +166,16 @@ func (pack *cloudPolicyPack) Publish(
|
|||
return nil
|
||||
}
|
||||
|
||||
func (pack *cloudPolicyPack) Apply(ctx context.Context, op backend.ApplyOperation) error {
|
||||
return pack.cl.ApplyPolicyPack(ctx, pack.ref.orgName, string(pack.ref.name), op.Version)
|
||||
func (pack *cloudPolicyPack) Apply(ctx context.Context, policyGroup string, op backend.PolicyPackOperation) error {
|
||||
return pack.cl.ApplyPolicyPack(ctx, pack.ref.orgName, policyGroup, string(pack.ref.name), op.Version)
|
||||
}
|
||||
|
||||
func (pack *cloudPolicyPack) Disable(ctx context.Context, policyGroup string, op backend.PolicyPackOperation) error {
|
||||
return pack.cl.DisablePolicyPack(ctx, pack.ref.orgName, policyGroup, string(pack.ref.name), op.Version)
|
||||
}
|
||||
|
||||
func (pack *cloudPolicyPack) Remove(ctx context.Context, op backend.PolicyPackOperation) error {
|
||||
return pack.cl.RemovePolicyPack(ctx, pack.ref.orgName, string(pack.ref.name), op.Version)
|
||||
}
|
||||
|
||||
const npmPackageDir = "package"
|
||||
|
|
|
@ -37,6 +37,7 @@ type MockBackend struct {
|
|||
GetPolicyPackF func(ctx context.Context, policyPack string, d diag.Sink) (PolicyPack, error)
|
||||
SupportsOrganizationsF func() bool
|
||||
ParseStackReferenceF func(s string) (StackReference, error)
|
||||
ValidateStackNameF func(s string) error
|
||||
DoesProjectExistF func(context.Context, string) (bool, error)
|
||||
GetStackF func(context.Context, StackReference) (Stack, error)
|
||||
CreateStackF func(context.Context, StackReference, interface{}) (Stack, error)
|
||||
|
@ -106,6 +107,13 @@ func (be *MockBackend) ParseStackReference(s string) (StackReference, error) {
|
|||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (be *MockBackend) ValidateStackName(s string) error {
|
||||
if be.ValidateStackNameF != nil {
|
||||
return be.ValidateStackNameF(s)
|
||||
}
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (be *MockBackend) DoesProjectExist(ctx context.Context, projectName string) (bool, error) {
|
||||
if be.DoesProjectExistF != nil {
|
||||
return be.DoesProjectExistF(ctx, projectName)
|
||||
|
|
|
@ -31,8 +31,8 @@ type PublishOperation struct {
|
|||
Scopes CancellationScopeSource
|
||||
}
|
||||
|
||||
// ApplyOperation publishes a PolicyPack to the backend.
|
||||
type ApplyOperation struct {
|
||||
// PolicyPackOperation is used to make various operations against a Policy Pack.
|
||||
type PolicyPackOperation struct {
|
||||
Version int
|
||||
Scopes CancellationScopeSource
|
||||
}
|
||||
|
@ -45,6 +45,15 @@ type PolicyPack interface {
|
|||
Backend() Backend
|
||||
// Publish the PolicyPack to the service.
|
||||
Publish(ctx context.Context, op PublishOperation) result.Result
|
||||
// Apply the PolicyPack to an organization.
|
||||
Apply(ctx context.Context, op ApplyOperation) error
|
||||
// Apply the PolicyPack to a Policy Group in an organization. If Policy Group is
|
||||
// empty, it enables it for the default Policy Group.
|
||||
Apply(ctx context.Context, policyGroup string, op PolicyPackOperation) error
|
||||
|
||||
// Disable the PolicyPack for a Policy Group in an organization. If Policy Group is
|
||||
// empty, it disables it for the default Policy Group.
|
||||
Disable(ctx context.Context, policyGroup string, op PolicyPackOperation) error
|
||||
|
||||
// Remove the PolicyPack from an organization. The Policy Pack must be removed from
|
||||
// all Policy Groups before it can be removed.
|
||||
Remove(ctx context.Context, op PolicyPackOperation) error
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -62,17 +62,6 @@ func TestGetValidDefaultProjectName(t *testing.T) {
|
|||
assert.Equal(t, "project", getValidProjectName("!@#$%^&*()"))
|
||||
}
|
||||
|
||||
func TestValidateStackName(t *testing.T) {
|
||||
assert.NoError(t, ValidateStackName("alpha-beta-gamma"))
|
||||
assert.NoError(t, ValidateStackName("owner-name/alpha-beta-gamma"))
|
||||
|
||||
err := ValidateStackName("alpha/beta/gamma")
|
||||
assert.Equal(t, err.Error(), "A stack name may not contain slashes")
|
||||
|
||||
err = ValidateStackName("mooo looo mi/alpha-beta-gamma")
|
||||
assert.Equal(t, err.Error(), "Invalid stack owner")
|
||||
}
|
||||
|
||||
func getValidProjectNamePrefixes() []string {
|
||||
var results []string
|
||||
for ch := 'A'; ch <= 'Z'; ch++ {
|
||||
|
|
|
@ -5,6 +5,7 @@ set -o errexit
|
|||
set -o pipefail
|
||||
|
||||
readonly SCRIPT_DIR="$( cd "$( dirname "${0}" )" && pwd )"
|
||||
readonly ROOT=${SCRIPT_DIR}/..
|
||||
|
||||
if [ -z "${1:-}" ]; then
|
||||
>&2 echo "error: missing version to publish"
|
||||
|
@ -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"
|
||||
|
|
113
sdk/dotnet/Pulumi.Tests/StackTests.cs
Normal file
113
sdk/dotnet/Pulumi.Tests/StackTests.cs
Normal file
|
@ -0,0 +1,113 @@
|
|||
// Copyright 2016-2019, Pulumi Corporation
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Moq;
|
||||
using Pulumi.Serialization;
|
||||
using Xunit;
|
||||
using Xunit.Sdk;
|
||||
|
||||
namespace Pulumi.Tests
|
||||
{
|
||||
public class StackTests
|
||||
{
|
||||
private class ValidStack : Stack
|
||||
{
|
||||
[Output("foo")]
|
||||
public Output<string> ExplicitName { get; }
|
||||
|
||||
[Output]
|
||||
public Output<string> ImplicitName { get; }
|
||||
|
||||
public ValidStack()
|
||||
{
|
||||
this.ExplicitName = Output.Create("bar");
|
||||
this.ImplicitName = Output.Create("buzz");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidStackInstantiationSucceeds()
|
||||
{
|
||||
var (stack, outputs) = await Run<ValidStack>();
|
||||
Assert.Equal(2, outputs.Count);
|
||||
Assert.Same(stack.ExplicitName, outputs["foo"]);
|
||||
Assert.Same(stack.ImplicitName, outputs["ImplicitName"]);
|
||||
}
|
||||
|
||||
private class NullOutputStack : Stack
|
||||
{
|
||||
[Output("foo")]
|
||||
public Output<string>? Foo { get; }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StackWithNullOutputsThrows()
|
||||
{
|
||||
try
|
||||
{
|
||||
var (stack, outputs) = await Run<NullOutputStack>();
|
||||
}
|
||||
catch (RunException ex)
|
||||
{
|
||||
Assert.Contains("foo", ex.Message);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new XunitException("Should not come here");
|
||||
}
|
||||
|
||||
private class InvalidOutputTypeStack : Stack
|
||||
{
|
||||
[Output("foo")]
|
||||
public string Foo { get; }
|
||||
|
||||
public InvalidOutputTypeStack()
|
||||
{
|
||||
this.Foo = "bar";
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StackWithInvalidOutputTypeThrows()
|
||||
{
|
||||
try
|
||||
{
|
||||
var (stack, outputs) = await Run<InvalidOutputTypeStack>();
|
||||
}
|
||||
catch (RunException ex)
|
||||
{
|
||||
Assert.Contains("foo", ex.Message);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new XunitException("Should not come here");
|
||||
}
|
||||
|
||||
private async Task<(T, IDictionary<string, object?>)> Run<T>() where T : Stack, new()
|
||||
{
|
||||
// Arrange
|
||||
Output<IDictionary<string, object?>>? outputs = null;
|
||||
|
||||
var mock = new Mock<IDeploymentInternal>(MockBehavior.Strict);
|
||||
mock.Setup(d => d.ProjectName).Returns("TestProject");
|
||||
mock.Setup(d => d.StackName).Returns("TestStack");
|
||||
mock.SetupSet(content => content.Stack = It.IsAny<Stack>());
|
||||
mock.Setup(d => d.ReadOrRegisterResource(It.IsAny<Stack>(), It.IsAny<ResourceArgs>(), It.IsAny<ResourceOptions>()));
|
||||
mock.Setup(d => d.RegisterResourceOutputs(It.IsAny<Stack>(), It.IsAny<Output<IDictionary<string, object?>>>()))
|
||||
.Callback((Resource _, Output<IDictionary<string, object?>> o) => outputs = o);
|
||||
|
||||
Deployment.Instance = mock.Object;
|
||||
|
||||
// Act
|
||||
var stack = new T();
|
||||
stack.RegisterPropertyOutputs();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(outputs);
|
||||
var values = await outputs!.DataTask;
|
||||
return (stack, values.Value);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Pulumi.Tests")]
|
||||
[assembly: InternalsVisibleTo("Pulumi.Tests")]
|
||||
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // Moq tests
|
||||
|
|
|
@ -15,6 +15,23 @@ namespace Pulumi
|
|||
public Runner(IDeploymentInternal deployment)
|
||||
=> _deployment = deployment;
|
||||
|
||||
public Task<int> RunAsync<TStack>() where TStack : Stack, new()
|
||||
{
|
||||
try
|
||||
{
|
||||
var stack = new TStack();
|
||||
// Stack doesn't call RegisterOutputs, so we register them on its behalf.
|
||||
stack.RegisterPropertyOutputs();
|
||||
RegisterTask("User program code.", stack.Outputs.DataTask);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return HandleExceptionAsync(ex);
|
||||
}
|
||||
|
||||
return WhileRunningAsync();
|
||||
}
|
||||
|
||||
public Task<int> RunAsync(Func<Task<IDictionary<string, object?>>> func)
|
||||
{
|
||||
var stack = new Stack(func);
|
||||
|
|
|
@ -9,6 +9,7 @@ namespace Pulumi
|
|||
public partial class Deployment
|
||||
{
|
||||
private Task<string>? _rootResource;
|
||||
private object _rootResourceLock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// Returns a root resource URN that will automatically become the default parent of all
|
||||
|
@ -23,21 +24,18 @@ namespace Pulumi
|
|||
if (type == Stack._rootPulumiStackTypeName)
|
||||
return null;
|
||||
|
||||
if (_rootResource == null)
|
||||
throw new InvalidOperationException($"Calling {nameof(GetRootResourceAsync)} before the root resource was registered!");
|
||||
lock (_rootResourceLock)
|
||||
{
|
||||
if (_rootResource == null)
|
||||
{
|
||||
var stack = InternalInstance.Stack ?? throw new InvalidOperationException($"Calling {nameof(GetRootResourceAsync)} before the stack was registered!");
|
||||
_rootResource = SetRootResourceWorkerAsync(stack);
|
||||
}
|
||||
}
|
||||
|
||||
return await _rootResource.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
Task IDeploymentInternal.SetRootResourceAsync(Stack stack)
|
||||
{
|
||||
if (_rootResource != null)
|
||||
throw new InvalidOperationException("Tried to set the root resource more than once!");
|
||||
|
||||
_rootResource = SetRootResourceWorkerAsync(stack);
|
||||
return _rootResource;
|
||||
}
|
||||
|
||||
private async Task<string> SetRootResourceWorkerAsync(Stack stack)
|
||||
{
|
||||
var resUrn = await stack.Urn.GetValueAsync().ConfigureAwait(false);
|
||||
|
|
|
@ -28,7 +28,7 @@ namespace Pulumi
|
|||
=> RunAsync(() => Task.FromResult(func()));
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="RunAsync(Func{Task{IDictionary{string, object}}})"/> is the
|
||||
/// <see cref="RunAsync(Func{Task{IDictionary{string, object}}})"/> is an
|
||||
/// entry-point to a Pulumi application. .NET applications should perform all startup logic
|
||||
/// they need in their <c>Main</c> method and then end with:
|
||||
/// <para>
|
||||
|
@ -36,7 +36,7 @@ namespace Pulumi
|
|||
/// static Task<int> Main(string[] args)
|
||||
/// {
|
||||
/// // program initialization code ...
|
||||
///
|
||||
///
|
||||
/// return Deployment.Run(async () =>
|
||||
/// {
|
||||
/// // Code that creates resources.
|
||||
|
@ -56,6 +56,38 @@ namespace Pulumi
|
|||
/// in this dictionary will become the outputs for the Pulumi Stack that is created.
|
||||
/// </summary>
|
||||
public static Task<int> RunAsync(Func<Task<IDictionary<string, object?>>> func)
|
||||
=> CreateRunner().RunAsync(func);
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="RunAsync{TStack}()"/> is an entry-point to a Pulumi
|
||||
/// application. .NET applications should perform all startup logic they
|
||||
/// need in their <c>Main</c> method and then end with:
|
||||
/// <para>
|
||||
/// <c>
|
||||
/// static Task<int> Main(string[] args) {// program
|
||||
/// initialization code ...
|
||||
///
|
||||
/// return Deployment.Run<MyStack>();}
|
||||
/// </c>
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Deployment will instantiate a new stack instance based on the type
|
||||
/// passed as TStack type parameter. Importantly, cloud resources cannot
|
||||
/// be created outside of the <see cref="Stack"/> component.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Because cloud Resource construction is inherently asynchronous, the
|
||||
/// result of this function is a <see cref="Task{T}"/> which should then
|
||||
/// be returned or awaited. This will ensure that any problems that are
|
||||
/// encountered during the running of the program are properly reported.
|
||||
/// Failure to do this may lead to the program ending early before all
|
||||
/// resources are properly registered.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static Task<int> RunAsync<TStack>() where TStack : Stack, new()
|
||||
=> CreateRunner().RunAsync<TStack>();
|
||||
|
||||
private static IRunner CreateRunner()
|
||||
{
|
||||
// Serilog.Log.Logger = new LoggerConfiguration().MinimumLevel.Debug().WriteTo.Console().CreateLogger();
|
||||
|
||||
|
@ -68,7 +100,7 @@ namespace Pulumi
|
|||
Serilog.Log.Debug("Creating new Deployment.");
|
||||
var deployment = new Deployment();
|
||||
Instance = deployment;
|
||||
return deployment._runner.RunAsync(func);
|
||||
return deployment._runner;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,8 +16,6 @@ namespace Pulumi
|
|||
ILogger Logger { get; }
|
||||
IRunner Runner { get; }
|
||||
|
||||
Task SetRootResourceAsync(Stack stack);
|
||||
|
||||
void ReadOrRegisterResource(Resource resource, ResourceArgs args, ResourceOptions opts);
|
||||
void RegisterResourceOutputs(Resource resource, Output<IDictionary<string, object?>> outputs);
|
||||
}
|
||||
|
|
|
@ -10,5 +10,6 @@ namespace Pulumi
|
|||
{
|
||||
void RegisterTask(string description, Task task);
|
||||
Task<int> RunAsync(Func<Task<IDictionary<string, object?>>> func);
|
||||
Task<int> RunAsync<TStack>() where TStack : Stack, new();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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...)
|
||||
|
|
|
@ -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") == "" {
|
||||
|
|
|
@ -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{
|
||||
|
|
43
tests/integration/policy/policy_test.go
Normal file
43
tests/integration/policy/policy_test.go
Normal 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")
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
name: aws-policy
|
||||
description: Example policies for AWS
|
||||
runtime: nodejs
|
30
tests/integration/policy/test_policy_pack/index.ts
Normal file
30
tests/integration/policy/test_policy_pack/index.ts
Normal 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");
|
||||
}
|
||||
}),
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
14
tests/integration/policy/test_policy_pack/package.json
Normal file
14
tests/integration/policy/test_policy_pack/package.json
Normal 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"
|
||||
}
|
||||
}
|
22
tests/integration/policy/test_policy_pack/tsconfig.json
Normal file
22
tests/integration/policy/test_policy_pack/tsconfig.json
Normal 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",
|
||||
]
|
||||
}
|
||||
|
5
tests/integration/stack_component/dotnet/.gitignore
vendored
Normal file
5
tests/integration/stack_component/dotnet/.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
/.pulumi/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
|
||||
|
30
tests/integration/stack_component/dotnet/Program.cs
Normal file
30
tests/integration/stack_component/dotnet/Program.cs
Normal 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>();
|
||||
}
|
3
tests/integration/stack_component/dotnet/Pulumi.yaml
Normal file
3
tests/integration/stack_component/dotnet/Pulumi.yaml
Normal file
|
@ -0,0 +1,3 @@
|
|||
name: stack_compoennt
|
||||
description: A program that is defined as a Stack component.
|
||||
runtime: dotnet
|
|
@ -0,0 +1,8 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp3.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
Loading…
Reference in a new issue