Use project name as part of stack identity with cloud backend

This change starts to use a stack's project name as part of it's
identity when talking to the cloud backend, which the Pulumi Service
now supports.

When displaying or parsing stack names for the cloud backend, we now
support the following schemes:

`<stack-name>`
`<owner-name>/<stack-name>`
`<owner-name>/<project-name>/<stack-name>`

When the owner is not specificed, we assume the currently logged in
user (as we did before). When the project name is not specificed, we
use the current project (and fail if we can't find a `Pulumi.yaml`)

Fixes #2039
This commit is contained in:
Matt Ellis 2019-01-18 14:37:05 -08:00
parent c282e7280a
commit 902be2b0b0
5 changed files with 78 additions and 25 deletions

View file

@ -1,5 +1,7 @@
## 0.16.12 (Unreleased)
- Stack names are now scoped within the context of a project, so you may duplicate stack names across different projects.
### Improvements
- Add `--json` to `pulumi config`, `pulumi config get`, `pulumi history` and `pulumi plugin ls` to request the output be in JSON.

View file

@ -26,6 +26,7 @@ import (
"net/url"
"os"
"path"
"regexp"
"runtime"
"strconv"
"strings"
@ -143,6 +144,7 @@ type cloudBackend struct {
url string
stackConfigFile string
client *client.Client
currentProject *workspace.Project
}
// New creates a new Pulumi backend for the given cloud API URL and token.
@ -153,11 +155,18 @@ func New(d diag.Sink, cloudURL, stackConfigFile string) (Backend, error) {
return nil, errors.Wrap(err, "getting stored credentials")
}
// When stringifying backend references, we take the current project (if present) into account.
currentProject, err := workspace.DetectProject()
if err != nil {
currentProject = nil
}
return &cloudBackend{
d: d,
url: cloudURL,
stackConfigFile: stackConfigFile,
client: client.NewClient(cloudURL, apiToken, d),
currentProject: currentProject,
}, nil
}
@ -378,6 +387,7 @@ func (b *cloudBackend) CloudURL() string { return b.url }
func (b *cloudBackend) ParseStackReference(s string) (backend.StackReference, error) {
split := strings.Split(s, "/")
var owner string
var projectName string
var stackName string
if len(split) == 1 {
@ -385,6 +395,10 @@ func (b *cloudBackend) ParseStackReference(s string) (backend.StackReference, er
} else if len(split) == 2 {
owner = split[0]
stackName = split[1]
} else if len(split) == 3 {
owner = split[0]
projectName = split[1]
stackName = split[2]
} else {
return nil, errors.Errorf("could not parse stack name '%s'", s)
}
@ -397,10 +411,20 @@ func (b *cloudBackend) ParseStackReference(s string) (backend.StackReference, er
owner = currentUser
}
if projectName == "" {
currentProject, projectErr := workspace.DetectProject()
if projectErr != nil {
return nil, projectErr
}
projectName = currentProject.Name.String()
}
return cloudBackendReference{
owner: owner,
name: tokens.QName(stackName),
b: b,
owner: owner,
project: projectName,
name: tokens.QName(stackName),
b: b,
}, nil
}
@ -433,7 +457,7 @@ func serveBrowserLoginServer(l net.Listener, expectedNonce string, destinationUR
// CloudConsoleStackPath returns the stack path components for getting to a stack in the cloud console. This path
// must, of course, be combined with the actual console base URL by way of the CloudConsoleURL function above.
func (b *cloudBackend) cloudConsoleStackPath(stackID client.StackIdentifier) string {
return path.Join(stackID.Owner, stackID.Stack)
return path.Join(stackID.Owner, stackID.Project, stackID.Stack)
}
// Logout logs out of the target cloud URL.
@ -979,6 +1003,17 @@ func (b *cloudBackend) ImportDeployment(ctx context.Context, stackRef backend.St
return nil
}
var (
projectNameCleanRegexp = regexp.MustCompile("[^a-zA-Z0-9-_.]")
)
// cleanProjectName replaces undesirable characters in project names with hypens. At some point, these restrictions
// will be further enfornced by the service, but for now we need to ensure that if we are making a rest call, we
// do this cleaning on our end.
func cleanProjectName(projectName string) string {
return projectNameCleanRegexp.ReplaceAllString(projectName, "-")
}
// getCloudStackIdentifier converts a backend.StackReference to a client.StackIdentifier for the same logical stack
func (b *cloudBackend) getCloudStackIdentifier(stackRef backend.StackReference) (client.StackIdentifier, error) {
cloudBackendStackRef, ok := stackRef.(cloudBackendReference)
@ -987,8 +1022,9 @@ func (b *cloudBackend) getCloudStackIdentifier(stackRef backend.StackReference)
}
return client.StackIdentifier{
Owner: cloudBackendStackRef.owner,
Stack: string(cloudBackendStackRef.name),
Owner: cloudBackendStackRef.owner,
Project: cleanProjectName(cloudBackendStackRef.project),
Stack: string(cloudBackendStackRef.name),
}, nil
}

View file

@ -42,8 +42,9 @@ import (
// StackIdentifier is the set of data needed to identify a Pulumi Cloud stack.
type StackIdentifier struct {
Owner string
Stack string
Owner string
Project string
Stack string
}
// UpdateIdentifier is the set of data needed to identify an update to a Pulumi Cloud stack.
@ -158,7 +159,7 @@ func pulumiAPICall(ctx context.Context, d diag.Sink, cloudAPI, method, path stri
userAgent := fmt.Sprintf("pulumi-cli/1 (%s; %s)", version.Version, runtime.GOOS)
req.Header.Set("User-Agent", userAgent)
// Specify the specific API version we accept.
req.Header.Set("Accept", "application/vnd.pulumi+2")
req.Header.Set("Accept", "application/vnd.pulumi+3")
// Apply credentials if provided.
if tok.String() != "" {

View file

@ -85,7 +85,8 @@ func (pc *Client) updateRESTCall(ctx context.Context, method, path string, query
// 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 {
return path.Join(append([]string{fmt.Sprintf("/api/stacks/%s/%s", stack.Owner, stack.Stack)}, components...)...)
prefix := fmt.Sprintf("/api/stacks/%s/%s/%s", stack.Owner, stack.Project, stack.Stack)
return path.Join(append([]string{prefix}, components...)...)
}
// getUpdatePath returns the API path to for the given stack with the given components joined with path separators
@ -218,9 +219,10 @@ func (pc *Client) CreateStack(
}
stack := apitype.Stack{
StackName: tokens.QName(stackID.Stack),
OrgName: stackID.Owner,
Tags: tags,
StackName: tokens.QName(stackID.Stack),
ProjectName: stackID.Project,
OrgName: stackID.Owner,
Tags: tags,
}
createStackReq := apitype.CreateStackRequest{
StackName: stackID.Stack,
@ -228,8 +230,10 @@ func (pc *Client) CreateStack(
}
var createStackResp apitype.CreateStackResponse
endpoint := fmt.Sprintf("/api/stacks/%s/%s", stackID.Owner, stackID.Project)
if err := pc.restCall(
ctx, "POST", fmt.Sprintf("/api/stacks/%s", stackID.Owner), nil, &createStackReq, &createStackResp); err != nil {
ctx, "POST", endpoint, nil, &createStackReq, &createStackResp); err != nil {
return apitype.Stack{}, err
}

View file

@ -26,6 +26,7 @@ import (
"github.com/pulumi/pulumi/pkg/resource/config"
"github.com/pulumi/pulumi/pkg/resource/deploy"
"github.com/pulumi/pulumi/pkg/tokens"
"github.com/pulumi/pulumi/pkg/util/contract"
)
// Stack is a cloud stack. This simply adds some cloud-specific properties atop the standard backend stack interface.
@ -38,9 +39,10 @@ type Stack interface {
}
type cloudBackendReference struct {
name tokens.QName
owner string
b *cloudBackend
name tokens.QName
project string
owner string
b *cloudBackend
}
func (c cloudBackendReference) String() string {
@ -49,11 +51,15 @@ func (c cloudBackendReference) String() string {
curUser = ""
}
if c.owner == curUser {
if c.owner == curUser && c.b.currentProject != nil && c.project == string(c.b.currentProject.Name) {
return string(c.name)
}
return fmt.Sprintf("%s/%s", c.owner, c.name)
if c.b.currentProject != nil && c.project == string(c.b.currentProject.Name) {
return fmt.Sprintf("%s/%s", c.owner, c.name)
}
return fmt.Sprintf("%s/%s/%s", c.owner, c.project, c.name)
}
func (c cloudBackendReference) Name() tokens.QName {
@ -82,9 +88,10 @@ func newStack(apistack apitype.Stack, b *cloudBackend) Stack {
// Now assemble all the pieces into a stack structure.
return &cloudStack{
ref: cloudBackendReference{
owner: apistack.OrgName,
name: apistack.StackName,
b: b,
owner: apistack.OrgName,
project: apistack.ProjectName,
name: apistack.StackName,
b: b,
},
cloudURL: b.CloudURL(),
orgName: apistack.OrgName,
@ -160,10 +167,13 @@ type cloudStackSummary struct {
}
func (css cloudStackSummary) Name() backend.StackReference {
contract.Assert(css.summary.ProjectName != "")
return cloudBackendReference{
owner: css.summary.OrgName,
name: tokens.QName(css.summary.StackName),
b: css.b,
owner: css.summary.OrgName,
project: css.summary.ProjectName,
name: tokens.QName(css.summary.StackName),
b: css.b,
}
}