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:
parent
c282e7280a
commit
902be2b0b0
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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() != "" {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue