Skip to content

Commit

Permalink
[auto/python] Support for remote operations
Browse files Browse the repository at this point in the history
  • Loading branch information
justinvp committed Oct 27, 2022
1 parent fd3cb15 commit 322227c
Show file tree
Hide file tree
Showing 7 changed files with 585 additions and 15 deletions.
@@ -0,0 +1,4 @@
changes:
- type: feat
scope: auto/python
description: Support for remote operations
22 changes: 21 additions & 1 deletion sdk/python/lib/pulumi/automation/__init__.py
@@ -1,4 +1,4 @@
# Copyright 2016-2021, Pulumi Corporation.
# 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.
Expand Down Expand Up @@ -88,6 +88,16 @@ def pulumi_program():
"""

from pulumi.automation._remote_workspace import (
RemoteWorkspaceOptions,
RemoteGitAuth,
create_remote_stack_git_source,
create_or_select_remote_stack_git_source,
select_remote_stack_git_source,
)

from pulumi.automation._remote_stack import RemoteStack

from ._cmd import CommandResult, OnOutput

from ._config import ConfigMap, ConfigValue
Expand Down Expand Up @@ -126,6 +136,7 @@ def pulumi_program():
from ._local_workspace import (
LocalWorkspace,
LocalWorkspaceOptions,
Secret,
create_stack,
select_stack,
create_or_select_stack,
Expand Down Expand Up @@ -197,6 +208,7 @@ def pulumi_program():
# _local_workspace
"LocalWorkspace",
"LocalWorkspaceOptions",
"Secret",
"create_stack",
"select_stack",
"create_or_select_stack",
Expand Down Expand Up @@ -225,6 +237,14 @@ def pulumi_program():
"RefreshResult",
"DestroyResult",
"fully_qualified_stack_name",
# _remote_workspace
"RemoteWorkspaceOptions",
"RemoteGitAuth",
"create_remote_stack_git_source",
"create_or_select_remote_stack_git_source",
"select_remote_stack_git_source",
# _remote_stack
"RemoteStack",
# sub-modules
"errors",
"events",
Expand Down
98 changes: 93 additions & 5 deletions sdk/python/lib/pulumi/automation/_local_workspace.py
@@ -1,4 +1,4 @@
# Copyright 2016-2021, Pulumi Corporation.
# 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.
Expand All @@ -12,11 +12,13 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from __future__ import annotations

import os
import tempfile
import json
from datetime import datetime
from typing import Optional, List, Mapping, Callable
from typing import Optional, List, Mapping, Callable, Union, TYPE_CHECKING
from semver import VersionInfo
import yaml

Expand All @@ -37,11 +39,20 @@
from ._minimum_version import _MINIMUM_VERSION
from .errors import InvalidVersionError

if TYPE_CHECKING:
from pulumi.automation._remote_workspace import RemoteGitAuth

_setting_extensions = [".yaml", ".yml", ".json"]

_SKIP_VERSION_CHECK_VAR = "PULUMI_AUTOMATION_API_SKIP_VERSION_CHECK"


class Secret(str):
"""
Represents a secret value.
"""


class LocalWorkspaceOptions:
work_dir: Optional[str] = None
pulumi_home: Optional[str] = None
Expand Down Expand Up @@ -83,6 +94,15 @@ class LocalWorkspace(Workspace):
This is identical to the behavior of Pulumi CLI driven workspaces.
"""

_remote: bool = False
_remote_env_vars: Optional[Mapping[str, Union[str, Secret]]]
_remote_pre_run_commands: Optional[List[str]]
_remote_git_url: str
_remote_git_project_path: Optional[str]
_remote_git_branch: Optional[str]
_remote_git_commit_hash: Optional[str]
_remote_git_auth: Optional[RemoteGitAuth]

def __init__(
self,
work_dir: Optional[str] = None,
Expand All @@ -102,9 +122,7 @@ def __init__(
)

pulumi_version = self._get_pulumi_version()
opt_out = os.getenv(_SKIP_VERSION_CHECK_VAR) is not None
if env_vars:
opt_out = opt_out or env_vars.get(_SKIP_VERSION_CHECK_VAR) is not None
opt_out = self._version_check_opt_out()
version = _parse_and_validate_pulumi_version(
_MINIMUM_VERSION, pulumi_version, opt_out
)
Expand Down Expand Up @@ -269,6 +287,8 @@ def create_stack(self, stack_name: str) -> None:
args = ["stack", "init", stack_name]
if self.secrets_provider:
args.extend(["--secrets-provider", self.secrets_provider])
if self._remote:
args.append("--no-select")
self._run_pulumi_cmd_sync(args)

def select_stack(self, stack_name: str) -> None:
Expand Down Expand Up @@ -373,20 +393,88 @@ def stack_outputs(self, stack_name: str) -> OutputMap:
outputs[key] = OutputValue(value=plaintext_outputs[key], secret=secret)
return outputs

def _version_check_opt_out(self) -> bool:
return (
os.getenv(_SKIP_VERSION_CHECK_VAR) is not None
or self.env_vars.get(_SKIP_VERSION_CHECK_VAR) is not None
)

def _get_pulumi_version(self) -> str:
result = self._run_pulumi_cmd_sync(["version"])
version_string = result.stdout.strip()
if version_string[0] == "v":
version_string = version_string[1:]
return version_string

def _remote_supported(self) -> bool:
# See if `--remote` is present in `pulumi preview --help`'s output.
result = self._run_pulumi_cmd_sync(["preview", "--help"])
help_string = result.stdout.strip()
return "--remote" in help_string

def _run_pulumi_cmd_sync(
self, args: List[str], on_output: Optional[OnOutput] = None
) -> CommandResult:
envs = {"PULUMI_HOME": self.pulumi_home} if self.pulumi_home else {}
if self._remote:
envs["PULUMI_EXPERIMENTAL"] = "true"
envs = {**envs, **self.env_vars}
return _run_pulumi_cmd(args, self.work_dir, envs, on_output)

def _remote_args(self) -> List[str]:
args: List[str] = []
if not self._remote:
return args

args.append("--remote")
if self._remote_git_url:
args.append(self._remote_git_url)
if self._remote_git_project_path:
args.append("--remote-git-repo-dir")
args.append(self._remote_git_project_path)
if self._remote_git_branch:
args.append("--remote-git-branch")
args.append(self._remote_git_branch)
if self._remote_git_commit_hash:
args.append("--remote-git-commit")
args.append(self._remote_git_commit_hash)
auth = self._remote_git_auth
if auth is not None:
if auth.personal_access_token:
args.append("--remote-git-auth-access-token")
args.append(auth.personal_access_token)
if auth.ssh_private_key:
args.append("--remote-git-auth-ssh-private-key")
args.append(auth.ssh_private_key)
if auth.ssh_private_key_path:
args.append("--remote-git-auth-ssh-private-key-path")
args.append(auth.ssh_private_key_path)
if auth.password:
args.append("--remote-git-auth-password")
args.append(auth.password)
if auth.username:
args.append("--remote-git-auth-username")
args.append(auth.username)

if self._remote_env_vars is not None:
for k in self._remote_env_vars:
v = self._remote_env_vars[k]
if isinstance(v, Secret):
args.append("--remote-env-secret")
args.append(f"{k}={v}")
elif isinstance(v, str):
args.append("--remote-env")
args.append(f"{k}={v}")
else:
raise AssertionError(f"unexpected env value {v} for key '{k}'")

if self._remote_pre_run_commands is not None:
for command in self._remote_pre_run_commands:
args.append("--remote-pre-run-command")
args.append(command)

return args


def _is_inline_program(**kwargs) -> bool:
for key in ["program", "project_name"]:
Expand Down
156 changes: 156 additions & 0 deletions sdk/python/lib/pulumi/automation/_remote_stack.py
@@ -0,0 +1,156 @@
# 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.

from typing import List, Optional

from pulumi.automation._cmd import OnOutput
from pulumi.automation._output import OutputMap
from pulumi.automation._stack import (
DestroyResult,
OnEvent,
PreviewResult,
RefreshResult,
Stack,
UpResult,
UpdateSummary,
)
from pulumi.automation._workspace import Deployment


class RemoteStack:
"""
RemoteStack is an isolated, independencly configurable instance of a Pulumi program that is
operated on remotely (up/preview/refresh/destroy).
"""

__stack: Stack

@property
def name(self) -> str:
return self.__stack.name

def __init__(self, stack: Stack):
self.__stack = stack

def up(
self,
on_output: Optional[OnOutput] = None,
on_event: Optional[OnEvent] = None,
) -> UpResult:
"""
Creates or updates the resources in a stack by executing the program in the Workspace.
https://www.pulumi.com/docs/reference/cli/pulumi_up/
:param on_output: A function to process the stdout stream.
:param on_event: A function to process structured events from the Pulumi event stream.
:returns: UpResult
"""
return self.__stack.up(on_output=on_output, on_event=on_event)

def preview(
self,
on_output: Optional[OnOutput] = None,
on_event: Optional[OnEvent] = None,
) -> PreviewResult:
"""
Performs a dry-run update to a stack, returning pending changes.
https://www.pulumi.com/docs/reference/cli/pulumi_preview/
:param on_output: A function to process the stdout stream.
:param on_event: A function to process structured events from the Pulumi event stream.
:returns: PreviewResult
"""
return self.__stack.preview(on_output=on_output, on_event=on_event)

def refresh(
self,
on_output: Optional[OnOutput] = None,
on_event: Optional[OnEvent] = None,
) -> RefreshResult:
"""
Compares the current stack鈥檚 resource state with the state known to exist in the actual
cloud provider. Any such changes are adopted into the current stack.
:param on_output: A function to process the stdout stream.
:param on_event: A function to process structured events from the Pulumi event stream.
:returns: RefreshResult
"""
return self.__stack.refresh(on_output=on_output, on_event=on_event)

def destroy(
self,
on_output: Optional[OnOutput] = None,
on_event: Optional[OnEvent] = None,
) -> DestroyResult:
"""
Destroy deletes all resources in a stack, leaving all history and configuration intact.
:param on_output: A function to process the stdout stream.
:param on_event: A function to process structured events from the Pulumi event stream.
:returns: DestroyResult
"""
return self.__stack.destroy(on_output=on_output, on_event=on_event)

def outputs(self) -> OutputMap:
"""
Gets the current set of Stack outputs from the last Stack.up().
:returns: OutputMap
"""
return self.__stack.outputs()

def history(
self,
page_size: Optional[int] = None,
page: Optional[int] = None,
) -> List[UpdateSummary]:
"""
Returns a list summarizing all previous and current results from Stack lifecycle operations
(up/preview/refresh/destroy).
:param page_size: Paginate history entries (used in combination with page), defaults to all.
:param page: Paginate history entries (used in combination with page_size), defaults to all.
:param show_secrets: Show config secrets when they appear in history.
:returns: List[UpdateSummary]
"""
# Note: Find a way to allow show_secrets as an option that doesn't require loading the project.
return self.__stack.history(page_size=page_size, page=page, show_secrets=False)

def cancel(self) -> None:
"""
Cancel stops a stack's currently running update. It returns an error if no update is currently running.
Note that this operation is _very dangerous_, and may leave the stack in an inconsistent state
if a resource operation was pending when the update was canceled.
This command is not supported for local backends.
"""
self.__stack.cancel()

def export_stack(self) -> Deployment:
"""
export_stack exports the deployment state of the stack.
This can be combined with Stack.import_state to edit a stack's state (such as recovery from failed deployments).
:returns: Deployment
"""
return self.__stack.export_stack()

def import_stack(self, state: Deployment) -> None:
"""
import_stack imports the specified deployment state into a pre-existing stack.
This can be combined with Stack.export_state to edit a stack's state (such as recovery from failed deployments).
:param state: The deployment state to import.
"""
self.__stack.import_stack(state=state)

0 comments on commit 322227c

Please sign in to comment.