pulumi/pkg/backend/httpstate/client/client.go

788 lines
27 KiB
Go
Raw Normal View History

2018-05-22 21:43:36 +02:00
// Copyright 2016-2018, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package client
import (
"context"
"encoding/json"
"fmt"
2019-07-10 19:55:42 +02:00
"io"
"io/ioutil"
"net/http"
"path"
"time"
"github.com/pulumi/pulumi/pkg/resource/plugin"
"github.com/blang/semver"
"github.com/pkg/errors"
"github.com/pulumi/pulumi/pkg/apitype"
"github.com/pulumi/pulumi/pkg/diag"
"github.com/pulumi/pulumi/pkg/diag/colors"
"github.com/pulumi/pulumi/pkg/engine"
"github.com/pulumi/pulumi/pkg/resource/config"
"github.com/pulumi/pulumi/pkg/tokens"
"github.com/pulumi/pulumi/pkg/util/contract"
"github.com/pulumi/pulumi/pkg/util/validation"
"github.com/pulumi/pulumi/pkg/workspace"
)
// Client provides a slim wrapper around the Pulumi HTTP/REST API.
type Client struct {
apiURL string
apiToken apiAccessToken
apiUser string
diag diag.Sink
}
// NewClient creates a new Pulumi API client with the given URL and API token.
func NewClient(apiURL, apiToken string, d diag.Sink) *Client {
return &Client{
apiURL: apiURL,
apiToken: apiAccessToken(apiToken),
diag: d,
}
}
// URL returns the URL of the API endpoint this client interacts with
func (pc *Client) URL() string {
return pc.apiURL
}
// restCall makes a REST-style request to the Pulumi API using the given method, path, query object, and request
// object. If a response object is provided, the server's response is deserialized into that object.
func (pc *Client) restCall(ctx context.Context, method, path string, queryObj, reqObj, respObj interface{}) error {
return pulumiRESTCall(ctx, pc.diag, pc.apiURL, method, path, queryObj, reqObj, respObj, pc.apiToken, httpCallOptions{})
}
// restCall makes a REST-style request to the Pulumi API using the given method, path, query object, and request
// object. If a response object is provided, the server's response is deserialized into that object.
func (pc *Client) restCallWithOptions(ctx context.Context, method, path string, queryObj, reqObj,
respObj interface{}, opts httpCallOptions) error {
return pulumiRESTCall(ctx, pc.diag, pc.apiURL, method, path, queryObj, reqObj, respObj, pc.apiToken, opts)
}
// updateRESTCall makes a REST-style request to the Pulumi API using the given method, path, query object, and request
// object. The call is authorized with the indicated update token. If a response object is provided, the server's
// response is deserialized into that object.
func (pc *Client) updateRESTCall(ctx context.Context, method, path string, queryObj, reqObj, respObj interface{},
token updateAccessToken, httpOptions httpCallOptions) error {
return pulumiRESTCall(ctx, pc.diag, pc.apiURL, method, path, queryObj, reqObj, respObj, token, httpOptions)
}
// getProjectPath returns the API path for the given owner and the given project name joined with path separators
// and appended to the stack root.
func getProjectPath(owner string, projectName string) string {
return fmt.Sprintf("/api/stacks/%s/%s", owner, projectName)
}
// getStackPath returns the API path to for the given stack with the given components joined with path separators
// and appended to the stack root.
func getStackPath(stack StackIdentifier, components ...string) string {
prefix := fmt.Sprintf("/api/stacks/%s/%s/%s", stack.Owner, stack.Project, stack.Stack)
return path.Join(append([]string{prefix}, components...)...)
}
2020-01-16 21:04:51 +01:00
// listPolicyGroupsPath returns the path for an API call to the Pulumi service to list the Policy Groups
// in a Pulumi organization.
func listPolicyGroupsPath(orgName string) string {
return fmt.Sprintf("/api/orgs/%s/policygroups", orgName)
}
// listPolicyPacksPath returns the path for an API call to the Pulumi service to list the Policy Packs
// in a Pulumi organization.
func listPolicyPacksPath(orgName string) string {
return fmt.Sprintf("/api/orgs/%s/policypacks", orgName)
}
// publishPolicyPackPath returns the path for an API call to the Pulumi service to publish a new Policy Pack
// in a Pulumi organization.
func publishPolicyPackPath(orgName string) string {
return fmt.Sprintf("/api/orgs/%s/policypacks", orgName)
}
2020-01-03 23:16:39 +01:00
// applyPolicyPackPath returns the path for an API call to the Pulumi service to apply a PolicyPack
2019-06-28 19:07:49 +02:00
// 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)
2019-06-28 19:07:49 +02:00
}
2020-01-03 23:16:39 +01:00
// 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 {
return fmt.Sprintf(
"/api/orgs/%s/policypacks/%s/versions/%d/complete", orgName, policyPackName, version)
}
// getUpdatePath returns the API path to for the given stack with the given components joined with path separators
// and appended to the update root.
func getUpdatePath(update UpdateIdentifier, components ...string) string {
components = append([]string{string(apitype.UpdateUpdate), update.UpdateID}, components...)
return getStackPath(update.StackIdentifier, components...)
}
2018-04-18 20:25:16 +02:00
// GetPulumiAccountName returns the user implied by the API token associated with this client.
func (pc *Client) GetPulumiAccountName(ctx context.Context) (string, error) {
if pc.apiUser == "" {
resp := struct {
GitHubLogin string `json:"githubLogin"`
}{}
if err := pc.restCall(ctx, "GET", "/api/user", nil, nil, &resp); err != nil {
return "", err
}
2018-04-18 20:25:16 +02:00
if resp.GitHubLogin == "" {
return "", errors.New("unexpected response from server")
}
pc.apiUser = resp.GitHubLogin
}
return pc.apiUser, nil
}
// GetCLIVersionInfo asks the service for information about versions of the CLI (the newest version as well as the
// oldest version before the CLI should warn about an upgrade).
func (pc *Client) GetCLIVersionInfo(ctx context.Context) (semver.Version, semver.Version, error) {
var versionInfo apitype.CLIVersionResponse
if err := pc.restCall(ctx, "GET", "/api/cli/version", nil, nil, &versionInfo); err != nil {
return semver.Version{}, semver.Version{}, err
}
latestSem, err := semver.ParseTolerant(versionInfo.LatestVersion)
if err != nil {
return semver.Version{}, semver.Version{}, err
}
oldestSem, err := semver.ParseTolerant(versionInfo.OldestWithoutWarning)
if err != nil {
return semver.Version{}, semver.Version{}, err
}
return latestSem, oldestSem, nil
}
// ListStacksFilter describes optional filters when listing stacks.
type ListStacksFilter struct {
Project *string
Organization *string
TagName *string
TagValue *string
}
// ListStacks lists all stacks the current user has access to, optionally filtered by project.
func (pc *Client) ListStacks(
ctx context.Context, filter ListStacksFilter) ([]apitype.StackSummary, error) {
queryFilter := struct {
Project *string `url:"project,omitempty"`
Organization *string `url:"organization,omitempty"`
TagName *string `url:"tagName,omitempty"`
TagValue *string `url:"tagValue,omitempty"`
}{
Project: filter.Project,
Organization: filter.Organization,
TagName: filter.TagName,
TagValue: filter.TagValue,
}
var resp apitype.ListStacksResponse
if err := pc.restCall(ctx, "GET", "/api/user/stacks", queryFilter, nil, &resp); err != nil {
return nil, err
}
return resp.Stacks, nil
}
var (
// ErrNoPreviousDeployment is returned when there isn't a previous deployment.
ErrNoPreviousDeployment = errors.New("no previous deployment")
)
// GetLatestConfiguration returns the configuration for the latest deployment of a given stack.
func (pc *Client) GetLatestConfiguration(ctx context.Context, stackID StackIdentifier) (config.Map, error) {
latest := struct {
Info apitype.UpdateInfo `json:"info,omitempty"`
}{}
if err := pc.restCall(ctx, "GET", getStackPath(stackID, "updates", "latest"), nil, nil, &latest); err != nil {
if restErr, ok := err.(*apitype.ErrorResponse); ok {
if restErr.Code == http.StatusNotFound {
return nil, ErrNoPreviousDeployment
}
}
return nil, err
}
cfg := make(config.Map)
for k, v := range latest.Info.Config {
newKey, err := config.ParseKey(k)
if err != nil {
return nil, err
}
Support lists and maps in config (#3342) This change adds support for lists and maps in config. We now allow lists/maps (and nested structures) in `Pulumi.<stack>.yaml` (or `Pulumi.<stack>.json`; yes, we currently support that). For example: ```yaml config: proj:blah: - a - b - c proj:hello: world proj:outer: inner: value proj:servers: - port: 80 ``` While such structures could be specified in the `.yaml` file manually, we support setting values in maps/lists from the command line. As always, you can specify single values with: ```shell $ pulumi config set hello world ``` Which results in the following YAML: ```yaml proj:hello world ``` And single value secrets via: ```shell $ pulumi config set --secret token shhh ``` Which results in the following YAML: ```yaml proj:token: secure: v1:VZAhuroR69FkEPTk:isKafsoZVMWA9pQayGzbWNynww== ``` Values in a list can be set from the command line using the new `--path` flag, which indicates the config key contains a path to a property in a map or list: ```shell $ pulumi config set --path names[0] a $ pulumi config set --path names[1] b $ pulumi config set --path names[2] c ``` Which results in: ```yaml proj:names - a - b - c ``` Values can be obtained similarly: ```shell $ pulumi config get --path names[1] b ``` Or setting values in a map: ```shell $ pulumi config set --path outer.inner value ``` Which results in: ```yaml proj:outer: inner: value ``` Of course, setting values in nested structures is supported: ```shell $ pulumi config set --path servers[0].port 80 ``` Which results in: ```yaml proj:servers: - port: 80 ``` If you want to include a period in the name of a property, it can be specified as: ``` $ pulumi config set --path 'nested["foo.bar"]' baz ``` Which results in: ```yaml proj:nested: foo.bar: baz ``` Examples of valid paths: - root - root.nested - 'root["nested"]' - root.double.nest - 'root["double"].nest' - 'root["double"]["nest"]' - root.array[0] - root.array[100] - root.array[0].nested - root.array[0][1].nested - root.nested.array[0].double[1] - 'root["key with \"escaped\" quotes"]' - 'root["key with a ."]' - '["root key with \"escaped\" quotes"].nested' - '["root key with a ."][100]' Note: paths that contain quotes can be surrounded by single quotes. When setting values with `--path`, if the value is `"false"` or `"true"`, it will be saved as the boolean value, and if it is convertible to an integer, it will be saved as an integer. Secure values are supported in lists/maps as well: ```shell $ pulumi config set --path --secret tokens[0] shh ``` Will result in: ```yaml proj:tokens: - secure: v1:wpZRCe36sFg1RxwG:WzPeQrCn4n+m4Ks8ps15MxvFXg== ``` Note: maps of length 1 with a key of “secure” and string value are reserved for storing secret values. Attempting to create such a value manually will result in an error: ```shell $ pulumi config set --path parent.secure foo error: "secure" key in maps of length 1 are reserved ``` **Accessing config values from the command line with JSON** ```shell $ pulumi config --json ``` Will output: ```json { "proj:hello": { "value": "world", "secret": false, "object": false }, "proj:names": { "value": "[\"a\",\"b\",\"c\"]", "secret": false, "object": true, "objectValue": [ "a", "b", "c" ] }, "proj:nested": { "value": "{\"foo.bar\":\"baz\"}", "secret": false, "object": true, "objectValue": { "foo.bar": "baz" } }, "proj:outer": { "value": "{\"inner\":\"value\"}", "secret": false, "object": true, "objectValue": { "inner": "value" } }, "proj:servers": { "value": "[{\"port\":80}]", "secret": false, "object": true, "objectValue": [ { "port": 80 } ] }, "proj:token": { "secret": true, "object": false }, "proj:tokens": { "secret": true, "object": true } } ``` If the value is a map or list, `"object"` will be `true`. `"value"` will contain the object as serialized JSON and a new `"objectValue"` property will be available containing the value of the object. If the object contains any secret values, `"secret"` will be `true`, and just like with scalar values, the value will not be outputted unless `--show-secrets` is specified. **Accessing config values from Pulumi programs** Map/list values are available to Pulumi programs as serialized JSON, so the existing `getObject`/`requireObject`/`getSecretObject`/`requireSecretObject` functions can be used to retrieve such values, e.g.: ```typescript import * as pulumi from "@pulumi/pulumi"; interface Server { port: number; } const config = new pulumi.Config(); const names = config.requireObject<string[]>("names"); for (const n of names) { console.log(n); } const servers = config.requireObject<Server[]>("servers"); for (const s of servers) { console.log(s.port); } ```
2019-11-01 21:41:27 +01:00
if v.Object {
if v.Secret {
cfg[newKey] = config.NewSecureObjectValue(v.String)
} else {
cfg[newKey] = config.NewObjectValue(v.String)
}
} else {
Support lists and maps in config (#3342) This change adds support for lists and maps in config. We now allow lists/maps (and nested structures) in `Pulumi.<stack>.yaml` (or `Pulumi.<stack>.json`; yes, we currently support that). For example: ```yaml config: proj:blah: - a - b - c proj:hello: world proj:outer: inner: value proj:servers: - port: 80 ``` While such structures could be specified in the `.yaml` file manually, we support setting values in maps/lists from the command line. As always, you can specify single values with: ```shell $ pulumi config set hello world ``` Which results in the following YAML: ```yaml proj:hello world ``` And single value secrets via: ```shell $ pulumi config set --secret token shhh ``` Which results in the following YAML: ```yaml proj:token: secure: v1:VZAhuroR69FkEPTk:isKafsoZVMWA9pQayGzbWNynww== ``` Values in a list can be set from the command line using the new `--path` flag, which indicates the config key contains a path to a property in a map or list: ```shell $ pulumi config set --path names[0] a $ pulumi config set --path names[1] b $ pulumi config set --path names[2] c ``` Which results in: ```yaml proj:names - a - b - c ``` Values can be obtained similarly: ```shell $ pulumi config get --path names[1] b ``` Or setting values in a map: ```shell $ pulumi config set --path outer.inner value ``` Which results in: ```yaml proj:outer: inner: value ``` Of course, setting values in nested structures is supported: ```shell $ pulumi config set --path servers[0].port 80 ``` Which results in: ```yaml proj:servers: - port: 80 ``` If you want to include a period in the name of a property, it can be specified as: ``` $ pulumi config set --path 'nested["foo.bar"]' baz ``` Which results in: ```yaml proj:nested: foo.bar: baz ``` Examples of valid paths: - root - root.nested - 'root["nested"]' - root.double.nest - 'root["double"].nest' - 'root["double"]["nest"]' - root.array[0] - root.array[100] - root.array[0].nested - root.array[0][1].nested - root.nested.array[0].double[1] - 'root["key with \"escaped\" quotes"]' - 'root["key with a ."]' - '["root key with \"escaped\" quotes"].nested' - '["root key with a ."][100]' Note: paths that contain quotes can be surrounded by single quotes. When setting values with `--path`, if the value is `"false"` or `"true"`, it will be saved as the boolean value, and if it is convertible to an integer, it will be saved as an integer. Secure values are supported in lists/maps as well: ```shell $ pulumi config set --path --secret tokens[0] shh ``` Will result in: ```yaml proj:tokens: - secure: v1:wpZRCe36sFg1RxwG:WzPeQrCn4n+m4Ks8ps15MxvFXg== ``` Note: maps of length 1 with a key of “secure” and string value are reserved for storing secret values. Attempting to create such a value manually will result in an error: ```shell $ pulumi config set --path parent.secure foo error: "secure" key in maps of length 1 are reserved ``` **Accessing config values from the command line with JSON** ```shell $ pulumi config --json ``` Will output: ```json { "proj:hello": { "value": "world", "secret": false, "object": false }, "proj:names": { "value": "[\"a\",\"b\",\"c\"]", "secret": false, "object": true, "objectValue": [ "a", "b", "c" ] }, "proj:nested": { "value": "{\"foo.bar\":\"baz\"}", "secret": false, "object": true, "objectValue": { "foo.bar": "baz" } }, "proj:outer": { "value": "{\"inner\":\"value\"}", "secret": false, "object": true, "objectValue": { "inner": "value" } }, "proj:servers": { "value": "[{\"port\":80}]", "secret": false, "object": true, "objectValue": [ { "port": 80 } ] }, "proj:token": { "secret": true, "object": false }, "proj:tokens": { "secret": true, "object": true } } ``` If the value is a map or list, `"object"` will be `true`. `"value"` will contain the object as serialized JSON and a new `"objectValue"` property will be available containing the value of the object. If the object contains any secret values, `"secret"` will be `true`, and just like with scalar values, the value will not be outputted unless `--show-secrets` is specified. **Accessing config values from Pulumi programs** Map/list values are available to Pulumi programs as serialized JSON, so the existing `getObject`/`requireObject`/`getSecretObject`/`requireSecretObject` functions can be used to retrieve such values, e.g.: ```typescript import * as pulumi from "@pulumi/pulumi"; interface Server { port: number; } const config = new pulumi.Config(); const names = config.requireObject<string[]>("names"); for (const n of names) { console.log(n); } const servers = config.requireObject<Server[]>("servers"); for (const s of servers) { console.log(s.port); } ```
2019-11-01 21:41:27 +01:00
if v.Secret {
cfg[newKey] = config.NewSecureValue(v.String)
} else {
cfg[newKey] = config.NewValue(v.String)
}
}
}
return cfg, nil
}
// DoesProjectExist returns true if a project with the given name exists, or false otherwise.
func (pc *Client) DoesProjectExist(ctx context.Context, owner string, projectName string) (bool, error) {
if err := pc.restCall(ctx, "HEAD", getProjectPath(owner, projectName), nil, nil, nil); err != nil {
// If this was a 404, return false - project not found.
if errResp, ok := err.(*apitype.ErrorResponse); ok && errResp.Code == http.StatusNotFound {
return false, nil
}
return false, err
}
return true, nil
}
// GetStack retrieves the stack with the given name.
func (pc *Client) GetStack(ctx context.Context, stackID StackIdentifier) (apitype.Stack, error) {
var stack apitype.Stack
if err := pc.restCall(ctx, "GET", getStackPath(stackID), nil, nil, &stack); err != nil {
return apitype.Stack{}, err
}
return stack, nil
}
// CreateStack creates a stack with the given cloud and stack name in the scope of the indicated project.
func (pc *Client) CreateStack(
ctx context.Context, stackID StackIdentifier, tags map[apitype.StackTagName]string) (apitype.Stack, error) {
// Validate names and tags.
if err := validation.ValidateStackProperties(stackID.Stack, tags); err != nil {
return apitype.Stack{}, errors.Wrap(err, "validating stack properties")
}
stack := apitype.Stack{
StackName: tokens.QName(stackID.Stack),
ProjectName: stackID.Project,
OrgName: stackID.Owner,
Tags: tags,
}
createStackReq := apitype.CreateStackRequest{
StackName: stackID.Stack,
Tags: tags,
}
var createStackResp apitype.CreateStackResponse
endpoint := fmt.Sprintf("/api/stacks/%s/%s", stackID.Owner, stackID.Project)
if err := pc.restCall(
ctx, "POST", endpoint, nil, &createStackReq, &createStackResp); err != nil {
return apitype.Stack{}, err
}
return stack, nil
}
// DeleteStack deletes the indicated stack. If force is true, the stack is deleted even if it contains resources.
func (pc *Client) DeleteStack(ctx context.Context, stack StackIdentifier, force bool) (bool, error) {
path := getStackPath(stack)
queryObj := struct {
Force bool `url:"force"`
}{
Force: force,
}
err := pc.restCall(ctx, "DELETE", path, queryObj, nil, nil)
return isStackHasResourcesError(err), err
}
func isStackHasResourcesError(err error) bool {
if err == nil {
return false
}
errRsp, ok := err.(*apitype.ErrorResponse)
if !ok {
return false
}
return errRsp.Code == 400 && errRsp.Message == "Bad Request: Stack still contains resources."
}
// EncryptValue encrypts a plaintext value in the context of the indicated stack.
func (pc *Client) EncryptValue(ctx context.Context, stack StackIdentifier, plaintext []byte) ([]byte, error) {
req := apitype.EncryptValueRequest{Plaintext: plaintext}
var resp apitype.EncryptValueResponse
if err := pc.restCall(ctx, "POST", getStackPath(stack, "encrypt"), nil, &req, &resp); err != nil {
return nil, err
}
return resp.Ciphertext, nil
}
// DecryptValue decrypts a ciphertext value in the context of the indicated stack.
func (pc *Client) DecryptValue(ctx context.Context, stack StackIdentifier, ciphertext []byte) ([]byte, error) {
req := apitype.DecryptValueRequest{Ciphertext: ciphertext}
var resp apitype.DecryptValueResponse
if err := pc.restCall(ctx, "POST", getStackPath(stack, "decrypt"), nil, &req, &resp); err != nil {
return nil, err
}
return resp.Plaintext, nil
}
// GetStackUpdates returns all updates to the indicated stack.
func (pc *Client) GetStackUpdates(ctx context.Context, stack StackIdentifier) ([]apitype.UpdateInfo, error) {
var response apitype.GetHistoryResponse
if err := pc.restCall(ctx, "GET", getStackPath(stack, "updates"), nil, nil, &response); err != nil {
return nil, err
}
return response.Updates, nil
}
// ExportStackDeployment exports the indicated stack's deployment as a raw JSON message.
func (pc *Client) ExportStackDeployment(ctx context.Context,
stack StackIdentifier) (apitype.UntypedDeployment, error) {
var resp apitype.ExportStackResponse
if err := pc.restCall(ctx, "GET", getStackPath(stack, "export"), nil, nil, &resp); err != nil {
return apitype.UntypedDeployment{}, err
}
return apitype.UntypedDeployment(resp), nil
}
// ImportStackDeployment imports a new deployment into the indicated stack.
func (pc *Client) ImportStackDeployment(ctx context.Context, stack StackIdentifier,
deployment *apitype.UntypedDeployment) (UpdateIdentifier, error) {
var resp apitype.ImportStackResponse
if err := pc.restCall(ctx, "POST", getStackPath(stack, "import"), nil, deployment, &resp); err != nil {
return UpdateIdentifier{}, err
}
return UpdateIdentifier{
StackIdentifier: stack,
UpdateKind: apitype.UpdateUpdate,
UpdateID: resp.UpdateID,
}, nil
}
// CreateUpdate creates a new update for the indicated stack with the given kind and assorted options. If the update
// requires that the Pulumi program is uploaded, the provided getContents callback will be invoked to fetch the
// contents of the Pulumi program.
func (pc *Client) CreateUpdate(
ctx context.Context, kind apitype.UpdateKind, stack StackIdentifier, proj *workspace.Project,
cfg config.Map, m apitype.UpdateMetadata, opts engine.UpdateOptions,
dryRun bool) (UpdateIdentifier, []apitype.RequiredPolicy, error) {
// First create the update program request.
wireConfig := make(map[string]apitype.ConfigValue)
for k, cv := range cfg {
v, err := cv.Value(config.NopDecrypter)
contract.AssertNoError(err)
wireConfig[k.String()] = apitype.ConfigValue{
String: v,
Secret: cv.Secure(),
Support lists and maps in config (#3342) This change adds support for lists and maps in config. We now allow lists/maps (and nested structures) in `Pulumi.<stack>.yaml` (or `Pulumi.<stack>.json`; yes, we currently support that). For example: ```yaml config: proj:blah: - a - b - c proj:hello: world proj:outer: inner: value proj:servers: - port: 80 ``` While such structures could be specified in the `.yaml` file manually, we support setting values in maps/lists from the command line. As always, you can specify single values with: ```shell $ pulumi config set hello world ``` Which results in the following YAML: ```yaml proj:hello world ``` And single value secrets via: ```shell $ pulumi config set --secret token shhh ``` Which results in the following YAML: ```yaml proj:token: secure: v1:VZAhuroR69FkEPTk:isKafsoZVMWA9pQayGzbWNynww== ``` Values in a list can be set from the command line using the new `--path` flag, which indicates the config key contains a path to a property in a map or list: ```shell $ pulumi config set --path names[0] a $ pulumi config set --path names[1] b $ pulumi config set --path names[2] c ``` Which results in: ```yaml proj:names - a - b - c ``` Values can be obtained similarly: ```shell $ pulumi config get --path names[1] b ``` Or setting values in a map: ```shell $ pulumi config set --path outer.inner value ``` Which results in: ```yaml proj:outer: inner: value ``` Of course, setting values in nested structures is supported: ```shell $ pulumi config set --path servers[0].port 80 ``` Which results in: ```yaml proj:servers: - port: 80 ``` If you want to include a period in the name of a property, it can be specified as: ``` $ pulumi config set --path 'nested["foo.bar"]' baz ``` Which results in: ```yaml proj:nested: foo.bar: baz ``` Examples of valid paths: - root - root.nested - 'root["nested"]' - root.double.nest - 'root["double"].nest' - 'root["double"]["nest"]' - root.array[0] - root.array[100] - root.array[0].nested - root.array[0][1].nested - root.nested.array[0].double[1] - 'root["key with \"escaped\" quotes"]' - 'root["key with a ."]' - '["root key with \"escaped\" quotes"].nested' - '["root key with a ."][100]' Note: paths that contain quotes can be surrounded by single quotes. When setting values with `--path`, if the value is `"false"` or `"true"`, it will be saved as the boolean value, and if it is convertible to an integer, it will be saved as an integer. Secure values are supported in lists/maps as well: ```shell $ pulumi config set --path --secret tokens[0] shh ``` Will result in: ```yaml proj:tokens: - secure: v1:wpZRCe36sFg1RxwG:WzPeQrCn4n+m4Ks8ps15MxvFXg== ``` Note: maps of length 1 with a key of “secure” and string value are reserved for storing secret values. Attempting to create such a value manually will result in an error: ```shell $ pulumi config set --path parent.secure foo error: "secure" key in maps of length 1 are reserved ``` **Accessing config values from the command line with JSON** ```shell $ pulumi config --json ``` Will output: ```json { "proj:hello": { "value": "world", "secret": false, "object": false }, "proj:names": { "value": "[\"a\",\"b\",\"c\"]", "secret": false, "object": true, "objectValue": [ "a", "b", "c" ] }, "proj:nested": { "value": "{\"foo.bar\":\"baz\"}", "secret": false, "object": true, "objectValue": { "foo.bar": "baz" } }, "proj:outer": { "value": "{\"inner\":\"value\"}", "secret": false, "object": true, "objectValue": { "inner": "value" } }, "proj:servers": { "value": "[{\"port\":80}]", "secret": false, "object": true, "objectValue": [ { "port": 80 } ] }, "proj:token": { "secret": true, "object": false }, "proj:tokens": { "secret": true, "object": true } } ``` If the value is a map or list, `"object"` will be `true`. `"value"` will contain the object as serialized JSON and a new `"objectValue"` property will be available containing the value of the object. If the object contains any secret values, `"secret"` will be `true`, and just like with scalar values, the value will not be outputted unless `--show-secrets` is specified. **Accessing config values from Pulumi programs** Map/list values are available to Pulumi programs as serialized JSON, so the existing `getObject`/`requireObject`/`getSecretObject`/`requireSecretObject` functions can be used to retrieve such values, e.g.: ```typescript import * as pulumi from "@pulumi/pulumi"; interface Server { port: number; } const config = new pulumi.Config(); const names = config.requireObject<string[]>("names"); for (const n of names) { console.log(n); } const servers = config.requireObject<Server[]>("servers"); for (const s of servers) { console.log(s.port); } ```
2019-11-01 21:41:27 +01:00
Object: cv.Object(),
}
}
description := ""
if proj.Description != nil {
description = *proj.Description
}
updateRequest := apitype.UpdateProgramRequest{
Name: string(proj.Name),
Runtime: proj.Runtime.Name(),
Main: proj.Main,
Description: description,
Config: wireConfig,
Options: apitype.UpdateOptions{
LocalPolicyPackPaths: opts.LocalPolicyPackPaths,
Color: colors.Raw, // force raw colorization, we handle colorization in the CLI
DryRun: dryRun,
Parallel: opts.Parallel,
ShowConfig: false, // This is a legacy option now, the engine will always emit config information
ShowReplacementSteps: false, // This is a legacy option now, the engine will always emit this information
ShowSames: false, // This is a legacy option now, the engine will always emit this information
},
Metadata: m,
}
// Create the initial update object.
var endpoint string
switch kind {
case apitype.UpdateUpdate:
2018-08-03 05:13:12 +02:00
endpoint = "update"
case apitype.PreviewUpdate:
endpoint = "preview"
case apitype.RefreshUpdate:
endpoint = "refresh"
case apitype.DestroyUpdate:
endpoint = "destroy"
default:
contract.Failf("Unknown kind: %s", kind)
}
path := getStackPath(stack, endpoint)
var updateResponse apitype.UpdateProgramResponse
if err := pc.restCall(ctx, "POST", path, nil, &updateRequest, &updateResponse); err != nil {
return UpdateIdentifier{}, []apitype.RequiredPolicy{}, err
}
return UpdateIdentifier{
StackIdentifier: stack,
UpdateKind: kind,
UpdateID: updateResponse.UpdateID,
}, updateResponse.RequiredPolicies, nil
}
// RenameStack renames the provided stack to have the new identifier.
func (pc *Client) RenameStack(ctx context.Context, currentID, newID StackIdentifier) error {
req := apitype.StackRenameRequest{
NewName: newID.Stack,
NewProject: newID.Project,
}
var resp apitype.ImportStackResponse
return pc.restCall(ctx, "POST", getStackPath(currentID, "rename"), nil, &req, &resp)
}
// StartUpdate starts the indicated update. It returns the new version of the update's target stack and the token used
// to authenticate operations on the update if any. Replaces the stack's tags with the updated set.
func (pc *Client) StartUpdate(ctx context.Context, update UpdateIdentifier,
tags map[apitype.StackTagName]string) (int, string, error) {
// Validate names and tags.
if err := validation.ValidateStackProperties(update.StackIdentifier.Stack, tags); err != nil {
return 0, "", errors.Wrap(err, "validating stack properties")
}
req := apitype.StartUpdateRequest{
Tags: tags,
}
var resp apitype.StartUpdateResponse
if err := pc.restCall(ctx, "POST", getUpdatePath(update), nil, req, &resp); err != nil {
return 0, "", err
}
return resp.Version, resp.Token, nil
}
2020-01-16 21:04:51 +01:00
// ListPolicyGroups lists all `PolicyGroups` the organization has in the Pulumi service.
func (pc *Client) ListPolicyGroups(ctx context.Context, orgName string) (apitype.ListPolicyGroupsResponse, error) {
var resp apitype.ListPolicyGroupsResponse
err := pc.restCall(ctx, "GET", listPolicyGroupsPath(orgName), nil, nil, &resp)
if err != nil {
return resp, errors.Wrapf(err, "List Policy Groups failed")
}
return resp, nil
}
// ListPolicyPacks lists all `PolicyPack` the organization has in the Pulumi service.
func (pc *Client) ListPolicyPacks(ctx context.Context, orgName string) (apitype.ListPolicyPacksResponse, error) {
var resp apitype.ListPolicyPacksResponse
err := pc.restCall(ctx, "GET", listPolicyPacksPath(orgName), nil, nil, &resp)
if err != nil {
return resp, errors.Wrapf(err, "List Policy Packs failed")
}
return resp, nil
}
// PublishPolicyPack publishes a `PolicyPack` to the Pulumi service. If it's successful, it returns
// the version that was published.
func (pc *Client) PublishPolicyPack(ctx context.Context, orgName string,
analyzerInfo plugin.AnalyzerInfo, dirArchive io.Reader) (int, error) {
//
// Step 1: Send POST containing policy metadata to service. This begins process of creating
// publishing the PolicyPack.
//
req := apitype.CreatePolicyPackRequest{
Name: analyzerInfo.Name,
DisplayName: analyzerInfo.DisplayName,
Policies: analyzerInfo.Policies,
}
fmt.Printf("Publishing %s to %s\n", analyzerInfo.Name, orgName)
var resp apitype.CreatePolicyPackResponse
err := pc.restCall(ctx, "POST", publishPolicyPackPath(orgName), nil, req, &resp)
if err != nil {
return 0, errors.Wrapf(err, "Publish policy pack failed")
}
fmt.Printf("Published as version %d\n", resp.Version)
//
// Step 2: Upload the compressed PolicyPack directory to the presigned S3 URL. The PolicyPack is
// now published.
//
putS3Req, err := http.NewRequest(http.MethodPut, resp.UploadURI, dirArchive)
if err != nil {
return 0, errors.Wrapf(err, "Failed to upload compressed PolicyPack")
}
_, err = http.DefaultClient.Do(putS3Req)
if err != nil {
return 0, errors.Wrapf(err, "Failed to upload compressed PolicyPack")
}
//
// Step 3: Signal to the service that the PolicyPack publish operation is complete.
//
err = pc.restCall(ctx, "POST",
publishPolicyPackPublishComplete(orgName, analyzerInfo.Name, resp.Version), nil, nil, nil)
if err != nil {
return 0, errors.Wrapf(err, "Request to signal completion of the publish operation failed")
}
return resp.Version, nil
}
2020-01-03 23:16:39 +01:00
// 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 {
2019-06-28 19:07:49 +02:00
2020-01-03 23:16:39 +01:00
if policyGroup == "" {
req := apitype.ApplyPolicyPackRequest{Name: policyPackName, Version: version}
2019-06-28 19:07:49 +02:00
2020-01-03 23:16:39 +01:00
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)
2019-06-28 19:07:49 +02:00
if err != nil {
2020-01-03 23:16:39 +01:00
return errors.Wrapf(err, "Enable policy pack failed")
2019-06-28 19:07:49 +02:00
}
2020-01-03 23:16:39 +01:00
return nil
}
2019-06-28 19:07:49 +02:00
2020-01-03 23:16:39 +01:00
// 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")
}
2019-06-28 19:07:49 +02:00
return nil
}
// DownloadPolicyPack applies a `PolicyPack` to the Pulumi organization.
func (pc *Client) DownloadPolicyPack(ctx context.Context, url string) ([]byte, error) {
fmt.Println("Downloading policy pack")
getS3Req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, errors.Wrapf(err, "Failed to download compressed PolicyPack")
}
resp, err := http.DefaultClient.Do(getS3Req)
if err != nil {
return nil, errors.Wrapf(err, "Failed to download compressed PolicyPack")
}
defer resp.Body.Close()
tarball, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, errors.Wrapf(err, "Failed to download compressed PolicyPack")
}
return tarball, nil
}
// GetUpdateEvents returns all events, taking an optional continuation token from a previous call.
func (pc *Client) GetUpdateEvents(ctx context.Context, update UpdateIdentifier,
continuationToken *string) (apitype.UpdateResults, error) {
path := getUpdatePath(update)
if continuationToken != nil {
path += fmt.Sprintf("?continuationToken=%s", *continuationToken)
}
var results apitype.UpdateResults
if err := pc.restCall(ctx, "GET", path, nil, nil, &results); err != nil {
return apitype.UpdateResults{}, err
}
return results, nil
}
// RenewUpdateLease renews the indicated update lease for the given duration.
func (pc *Client) RenewUpdateLease(ctx context.Context, update UpdateIdentifier, token string,
duration time.Duration) (string, error) {
req := apitype.RenewUpdateLeaseRequest{
Duration: int(duration / time.Second),
}
var resp apitype.RenewUpdateLeaseResponse
// While renewing a lease uses POST, it is safe to send multiple requests (consider that we do this multiple times
// during a long running update). Since we would fail our update operation if we can't renew our lease, we'll retry
// these POST operations.
if err := pc.updateRESTCall(ctx, "POST", getUpdatePath(update, "renew_lease"), nil, req, &resp,
updateAccessToken(token), httpCallOptions{RetryAllMethods: true}); err != nil {
return "", err
}
return resp.Token, nil
}
// InvalidateUpdateCheckpoint invalidates the checkpoint for the indicated update.
func (pc *Client) InvalidateUpdateCheckpoint(ctx context.Context, update UpdateIdentifier, token string) error {
req := apitype.PatchUpdateCheckpointRequest{
IsInvalid: true,
}
// It is safe to retry this PATCH operation, because it is logically idempotent.
return pc.updateRESTCall(ctx, "PATCH", getUpdatePath(update, "checkpoint"), nil, req, nil,
updateAccessToken(token), httpCallOptions{RetryAllMethods: true})
}
// PatchUpdateCheckpoint patches the checkpoint for the indicated update with the given contents.
Implement more precise delete-before-replace semantics. (#2369) This implements the new algorithm for deciding which resources must be deleted due to a delete-before-replace operation. We need to compute the set of resources that may be replaced by a change to the resource under consideration. We do this by taking the complete set of transitive dependents on the resource under consideration and removing any resources that would not be replaced by changes to their dependencies. We determine whether or not a resource may be replaced by substituting unknowns for input properties that may change due to deletion of the resources their value depends on and calling the resource provider's Diff method. This is perhaps clearer when described by example. Consider the following dependency graph: A __|__ B C | _|_ D E F In this graph, all of B, C, D, E, and F transitively depend on A. It may be the case, however, that changes to the specific properties of any of those resources R that would occur if a resource on the path to A were deleted and recreated may not cause R to be replaced. For example, the edge from B to A may be a simple dependsOn edge such that a change to B does not actually influence any of B's input properties. In that case, neither B nor D would need to be deleted before A could be deleted. In order to make the above algorithm a reality, the resource monitor interface has been updated to include a map that associates an input property key with the list of resources that input property depends on. Older clients of the resource monitor will leave this map empty, in which case all input properties will be treated as depending on all dependencies of the resource. This is probably overly conservative, but it is less conservative than what we currently implement, and is certainly correct.
2019-01-28 18:46:30 +01:00
func (pc *Client) PatchUpdateCheckpoint(ctx context.Context, update UpdateIdentifier, deployment *apitype.DeploymentV3,
token string) error {
rawDeployment, err := json.Marshal(deployment)
if err != nil {
return err
}
req := apitype.PatchUpdateCheckpointRequest{
Implement more precise delete-before-replace semantics. (#2369) This implements the new algorithm for deciding which resources must be deleted due to a delete-before-replace operation. We need to compute the set of resources that may be replaced by a change to the resource under consideration. We do this by taking the complete set of transitive dependents on the resource under consideration and removing any resources that would not be replaced by changes to their dependencies. We determine whether or not a resource may be replaced by substituting unknowns for input properties that may change due to deletion of the resources their value depends on and calling the resource provider's Diff method. This is perhaps clearer when described by example. Consider the following dependency graph: A __|__ B C | _|_ D E F In this graph, all of B, C, D, E, and F transitively depend on A. It may be the case, however, that changes to the specific properties of any of those resources R that would occur if a resource on the path to A were deleted and recreated may not cause R to be replaced. For example, the edge from B to A may be a simple dependsOn edge such that a change to B does not actually influence any of B's input properties. In that case, neither B nor D would need to be deleted before A could be deleted. In order to make the above algorithm a reality, the resource monitor interface has been updated to include a map that associates an input property key with the list of resources that input property depends on. Older clients of the resource monitor will leave this map empty, in which case all input properties will be treated as depending on all dependencies of the resource. This is probably overly conservative, but it is less conservative than what we currently implement, and is certainly correct.
2019-01-28 18:46:30 +01:00
Version: 3,
Deployment: rawDeployment,
}
// It is safe to retry this PATCH operation, because it is logically idempotent, since we send the entire
// deployment instead of a set of changes to apply.
return pc.updateRESTCall(ctx, "PATCH", getUpdatePath(update, "checkpoint"), nil, req, nil,
updateAccessToken(token), httpCallOptions{RetryAllMethods: true, GzipCompress: true})
}
// CancelUpdate cancels the indicated update.
func (pc *Client) CancelUpdate(ctx context.Context, update UpdateIdentifier) error {
// It is safe to retry this PATCH operation, because it is logically idempotent.
return pc.restCallWithOptions(ctx, "POST", getUpdatePath(update, "cancel"), nil, nil, nil,
httpCallOptions{RetryAllMethods: true})
}
// CompleteUpdate completes the indicated update with the given status.
func (pc *Client) CompleteUpdate(ctx context.Context, update UpdateIdentifier, status apitype.UpdateStatus,
token string) error {
req := apitype.CompleteUpdateRequest{
Status: status,
}
// It is safe to retry this PATCH operation, because it is logically idempotent.
return pc.updateRESTCall(ctx, "POST", getUpdatePath(update, "complete"), nil, req, nil,
updateAccessToken(token), httpCallOptions{RetryAllMethods: true})
}
// RecordEngineEvents posts a batch of engine events to the Pulumi service.
func (pc *Client) RecordEngineEvents(
ctx context.Context, update UpdateIdentifier, batch apitype.EngineEventBatch, token string) error {
callOpts := httpCallOptions{
GzipCompress: true,
RetryAllMethods: true,
}
return pc.updateRESTCall(
ctx, "POST", getUpdatePath(update, "events/batch"),
nil, batch, nil,
updateAccessToken(token), callOpts)
}
// UpdateStackTags updates the stacks's tags, replacing all existing tags.
func (pc *Client) UpdateStackTags(
ctx context.Context, stack StackIdentifier, tags map[apitype.StackTagName]string) error {
// Validate stack tags.
if err := validation.ValidateStackTags(tags); err != nil {
return err
}
return pc.restCall(ctx, "PATCH", getStackPath(stack, "tags"), nil, tags, nil)
}