// Copyright 2016-2022, 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 auto import ( "context" "errors" "fmt" "strings" ) // PREVIEW: NewRemoteStackGitSource creates a Stack backed by a RemoteWorkspace with source code from the specified // GitRepo. Pulumi operations on the stack (Preview, Update, Refresh, and Destroy) are performed remotely. func NewRemoteStackGitSource( ctx context.Context, stackName string, repo GitRepo, opts ...RemoteWorkspaceOption, ) (RemoteStack, error) { if !isFullyQualifiedStackName(stackName) { return RemoteStack{}, fmt.Errorf("stack name %q must be fully qualified", stackName) } localOpts, err := remoteToLocalOptions(repo, opts...) if err != nil { return RemoteStack{}, err } w, err := NewLocalWorkspace(ctx, localOpts...) if err != nil { return RemoteStack{}, fmt.Errorf("failed to create stack: %w", err) } s, err := NewStack(ctx, stackName, w) if err != nil { return RemoteStack{}, err } return RemoteStack{stack: s}, nil } // PREVIEW: UpsertRemoteStackGitSource creates a Stack backed by a RemoteWorkspace with source code from the // specified GitRepo. If the Stack already exists, it will not error and proceed with returning the Stack. // Pulumi operations on the stack (Preview, Update, Refresh, and Destroy) are performed remotely. func UpsertRemoteStackGitSource( ctx context.Context, stackName string, repo GitRepo, opts ...RemoteWorkspaceOption, ) (RemoteStack, error) { if !isFullyQualifiedStackName(stackName) { return RemoteStack{}, fmt.Errorf("stack name %q must be fully qualified", stackName) } localOpts, err := remoteToLocalOptions(repo, opts...) if err != nil { return RemoteStack{}, err } w, err := NewLocalWorkspace(ctx, localOpts...) if err != nil { return RemoteStack{}, fmt.Errorf("failed to create stack: %w", err) } s, err := UpsertStack(ctx, stackName, w) if err != nil { return RemoteStack{}, err } return RemoteStack{stack: s}, nil } // PREVIEW: SelectRemoteStackGitSource selects an existing Stack backed by a RemoteWorkspace with source code from the // specified GitRepo. Pulumi operations on the stack (Preview, Update, Refresh, and Destroy) are performed remotely. func SelectRemoteStackGitSource( ctx context.Context, stackName string, repo GitRepo, opts ...RemoteWorkspaceOption, ) (RemoteStack, error) { if !isFullyQualifiedStackName(stackName) { return RemoteStack{}, fmt.Errorf("stack name %q must be fully qualified", stackName) } localOpts, err := remoteToLocalOptions(repo, opts...) if err != nil { return RemoteStack{}, err } w, err := NewLocalWorkspace(ctx, localOpts...) if err != nil { return RemoteStack{}, fmt.Errorf("failed to select stack: %w", err) } s, err := SelectStack(ctx, stackName, w) if err != nil { return RemoteStack{}, err } return RemoteStack{stack: s}, nil } func remoteToLocalOptions(repo GitRepo, opts ...RemoteWorkspaceOption) ([]LocalWorkspaceOption, error) { if repo.Setup != nil { return nil, errors.New("repo.Setup cannot be used with remote workspaces") } if repo.URL == "" { return nil, errors.New("repo.URL is required") } if repo.Branch != "" && repo.CommitHash != "" { return nil, errors.New("repo.Branch and repo.CommitHash cannot both be specified") } if repo.Branch == "" && repo.CommitHash == "" { return nil, errors.New("either repo.Branch or repo.CommitHash is required") } if repo.Auth != nil { if repo.Auth.SSHPrivateKey != "" && repo.Auth.SSHPrivateKeyPath != "" { return nil, errors.New("repo.Auth.SSHPrivateKey and repo.Auth.SSHPrivateKeyPath cannot both be specified") } } remoteOpts := &remoteWorkspaceOptions{} for _, o := range opts { o.applyOption(remoteOpts) } for k, v := range remoteOpts.EnvVars { if k == "" { return nil, errors.New("envvar cannot be empty") } if v.Value == "" { return nil, fmt.Errorf("envvar %q cannot have an empty value", k) } } for index, command := range remoteOpts.PreRunCommands { if command == "" { return nil, fmt.Errorf("pre run command at index %v cannot be empty", index) } } localOpts := []LocalWorkspaceOption{ remote(true), remoteEnvVars(remoteOpts.EnvVars), preRunCommands(remoteOpts.PreRunCommands...), remoteSkipInstallDependencies(remoteOpts.SkipInstallDependencies), Repo(repo), } return localOpts, nil } type remoteWorkspaceOptions struct { // EnvVars is a map of environment values scoped to the workspace. // These values will be passed to all Workspace and Stack level commands. EnvVars map[string]EnvVarValue // PreRunCommands is an optional list of arbitrary commands to run before the remote Pulumi operation is invoked. PreRunCommands []string // SkipInstallDependencies sets whether to skip the default dependency installation step. Defaults to false. SkipInstallDependencies bool } // LocalWorkspaceOption is used to customize and configure a LocalWorkspace at initialization time. // See Workdir, Program, PulumiHome, Project, Stacks, and Repo for concrete options. type RemoteWorkspaceOption interface { applyOption(*remoteWorkspaceOptions) } type remoteWorkspaceOption func(*remoteWorkspaceOptions) func (o remoteWorkspaceOption) applyOption(opts *remoteWorkspaceOptions) { o(opts) } // RemoteEnvVars is a map of environment values scoped to the remote workspace. // These will be passed to remote operations. func RemoteEnvVars(envvars map[string]EnvVarValue) RemoteWorkspaceOption { return remoteWorkspaceOption(func(opts *remoteWorkspaceOptions) { opts.EnvVars = envvars }) } // RemotePreRunCommands is an optional list of arbitrary commands to run before the remote Pulumi operation is invoked. func RemotePreRunCommands(commands ...string) RemoteWorkspaceOption { return remoteWorkspaceOption(func(opts *remoteWorkspaceOptions) { opts.PreRunCommands = commands }) } // RemoteSkipInstallDependencies sets whether to skip the default dependency installation step. Defaults to false. func RemoteSkipInstallDependencies(skipInstallDependencies bool) RemoteWorkspaceOption { return remoteWorkspaceOption(func(opts *remoteWorkspaceOptions) { opts.SkipInstallDependencies = skipInstallDependencies }) } // isFullyQualifiedStackName returns true if the stack is fully qualified, // i.e. has owner, project, and stack components. func isFullyQualifiedStackName(stackName string) bool { split := strings.Split(stackName, "/") return len(split) == 3 && split[0] != "" && split[1] != "" && split[2] != "" }