Skip to content

Commit

Permalink
run terraform plan when setting --dry-run flag on `experimental-d…
Browse files Browse the repository at this point in the history
…eploy` goal (#20488)

Changed the following things to achieve this
- Add a `--dry-run` flag to the `experimental-deploy` goal to handle dry
runs such as `terraform plan`
- Run `plan` rather than `apply` when setting the `--dry-run` flag while
deploying a `terraform_deployment`
- Changed Helm deployments to use the new `--dry-run` flag instead of a
passthrough arg for dry-running

Tested 
- the terraform change via a unit test and also tried it out manually in
my own project.
- the helm change via a unit test.

Closes #18490
  • Loading branch information
agoblet authored Feb 16, 2024
1 parent a47a6f2 commit 7c1e0b7
Show file tree
Hide file tree
Showing 8 changed files with 122 additions and 14 deletions.
8 changes: 7 additions & 1 deletion docs/docs/helm/deployments.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ Continuing with the example in the previous section, we can deploy it into Kuber
3. Post-process the Kubernetes manifests generated by Helm by replacing all references to first-party Docker images by their real final registry destination.
4. Initiate the deployment of the final Kubernetes resources resulting from the post-processing.

The `experimental-deploy` goal also supports default Helm pass-through arguments that allow to change the deployment behaviour to be either atomic or a dry-run or even what is the Kubernetes config file (the `kubeconfig` file) and target context to be used in the deployment.
The `experimental-deploy` goal also supports default Helm pass-through arguments that allow to change the deployment behaviour to be atomic or even what is the Kubernetes config file (the `kubeconfig` file) and target context to be used in the deployment.

Please note that the list of valid pass-through arguments has been limited to those that do not alter the reproducibility of the deployment (i.e. `--create-namespace` is not a valid pass-through argument). Those arguments will have equivalent fields in the `helm_deployment` target.

Expand All @@ -351,6 +351,12 @@ For example, to make an atomic deployment into a non-default Kubernetes context
pants experimental-deploy src/deployments:prod -- --kube-context my-custom-kube-context --atomic
```

To perform a dry run, use the `--dry-run` flag of the `experimental-deploy` goal.

```
pants experimental-deploy --dry-run src/deployments:prod
```

:::note How does Pants authenticate with the Kubernetes cluster?
Short answer is: it doesn't.
Pants will invoke Helm under the hood with the appropriate arguments to only perform the deployment. Any authentication steps that may be needed to perform the given deployment have to be done before invoking the `experimental-deploy` goal. If you are planning to run the deployment procedure from your CI/CD pipelines, ensure that all necessary preliminary steps (including authentication with the cluster) are done before the one that triggers the deployment.
Expand Down
6 changes: 6 additions & 0 deletions docs/docs/terraform/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -175,3 +175,9 @@ Apply complete! Resources: 4 added, 0 changed, 0 destroyed.
```

You can set auto approve by adding `-auto-approve` to the `[download-terraform].args` setting in `pants.toml`. You can also set it for a single pants invocation with `--download-terraform-args='-auto-approve'`, for example `pants experimental-deploy "--download-terraform-args='-auto-approve'"`.

To run `terraform plan`, use the `--dry-run` flag of the `experimental-deploy` goal.

```
pants experimental-deploy --dry-run ::
```
10 changes: 8 additions & 2 deletions src/python/pants/backend/helm/goals/deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from pants.backend.helm.util_rules import post_renderer
from pants.backend.helm.util_rules.post_renderer import HelmDeploymentPostRendererRequest
from pants.backend.helm.util_rules.renderer import HelmDeploymentCmd, HelmDeploymentRequest
from pants.core.goals.deploy import DeployFieldSet, DeployProcess
from pants.core.goals.deploy import DeployFieldSet, DeployProcess, DeploySubsystem
from pants.engine.process import InteractiveProcess
from pants.engine.rules import Get, MultiGet, collect_rules, rule
from pants.engine.target import DependenciesRequest, Targets
Expand All @@ -37,15 +37,20 @@ class DeployHelmDeploymentFieldSet(HelmDeploymentFieldSet, DeployFieldSet):

@rule(desc="Run Helm deploy process", level=LogLevel.DEBUG)
async def run_helm_deploy(
field_set: DeployHelmDeploymentFieldSet, helm_subsystem: HelmSubsystem
field_set: DeployHelmDeploymentFieldSet,
helm_subsystem: HelmSubsystem,
deploy_subsystem: DeploySubsystem,
) -> DeployProcess:
dry_run_args = ["--dry-run"] if deploy_subsystem.dry_run else []
passthrough_args = helm_subsystem.valid_args(
extra_help=softwrap(
f"""
Most invalid arguments have equivalent fields in the `{HelmDeploymentTarget.alias}` target.
Usage of fields is encouraged over passthrough arguments as that enables repeatable deployments.
Please run `{bin_name()} help {HelmDeploymentTarget.alias}` for more information.
To use `--dry-run`, run `{bin_name()} experimental-deploy --dry-run ::`.
"""
)
)
Expand All @@ -68,6 +73,7 @@ async def run_helm_deploy(
"--install",
*(("--timeout", f"{field_set.timeout.value}s") if field_set.timeout.value else ()),
*passthrough_args,
*dry_run_args,
],
post_renderer=post_renderer,
description=f"Running Helm deployment: {field_set.address}",
Expand Down
55 changes: 52 additions & 3 deletions src/python/pants/backend/helm/goals/deploy_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,10 @@ def test_run_helm_deploy(rule_runner: RuleRunner) -> None:
)


def test_raises_error_when_using_invalid_passthrough_args(rule_runner: RuleRunner) -> None:
@pytest.mark.parametrize("invalid_passthrough_args", [["--namespace", "foo"], ["--dry-run"]])
def test_raises_error_when_using_invalid_passthrough_args(
rule_runner: RuleRunner, invalid_passthrough_args: list[str]
) -> None:
rule_runner.write_files(
{
"src/chart/BUILD": """helm_chart(registries=["oci://www.example.com/external"])""",
Expand All @@ -182,10 +185,12 @@ def test_raises_error_when_using_invalid_passthrough_args(rule_runner: RuleRunne
)

source_root_patterns = ["/src/*"]
deploy_args = ["--force", "--debug", "--kubeconfig=./kubeconfig", "--namespace", "foo"]
deploy_args = ["--force", "--debug", "--kubeconfig=./kubeconfig", *invalid_passthrough_args]

invalid_passthrough_args_as_string = " ".join(invalid_passthrough_args)
with pytest.raises(
ExecutionError, match="The following command line arguments are not valid: --namespace foo."
ExecutionError,
match=f"The following command line arguments are not valid: {invalid_passthrough_args_as_string}.",
):
_run_deployment(
rule_runner,
Expand Down Expand Up @@ -226,3 +231,47 @@ def test_can_deploy_3rd_party_chart(rule_runner: RuleRunner) -> None:

assert deploy_process.process
assert len(deploy_process.publish_dependencies) == 0


@pytest.mark.parametrize(
"dry_run_args,expected",
[
([], False),
(["--experimental-deploy-dry-run=False"], False),
(["--experimental-deploy-dry-run"], True),
],
)
def test_run_helm_deploy_adheres_to_dry_run_flag(
rule_runner: RuleRunner, dry_run_args: list[str], expected: bool
) -> None:
rule_runner.write_files(
{
"src/chart/BUILD": """helm_chart(registries=["oci://www.example.com/external"])""",
"src/chart/Chart.yaml": HELM_CHART_FILE,
"src/deployment/BUILD": dedent(
"""\
helm_deployment(
name="bar",
namespace="uat",
chart="//src/chart",
sources=["*.yaml", "subdir/*.yml"]
)
"""
),
}
)

expected_build_number = "34"
expected_ns_suffix = "quxx"

deploy_args = ["--kubeconfig", "./kubeconfig", "--create-namespace"]
deploy_process = _run_deployment(
rule_runner,
"src/deployment",
"bar",
args=[f"--helm-args={repr(deploy_args)}", *dry_run_args],
env={"BUILD_NUMBER": expected_build_number, "NS_SUFFIX": expected_ns_suffix},
)

assert deploy_process.process
assert ("--dry-run" in deploy_process.process.process.argv) == expected
5 changes: 3 additions & 2 deletions src/python/pants/backend/helm/subsystems/helm.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
"--cleanup-on-fail",
"--create-namespace",
"--debug",
"--dry-run",
"--force",
"--wait",
"--wait-for-jobs",
Expand Down Expand Up @@ -165,7 +164,7 @@ class HelmSubsystem(TemplatedExternalTool):
)

args = ArgsListOption(
example="--dry-run",
example="--force",
passthrough=True,
extra_help=softwrap(
f"""
Expand All @@ -180,6 +179,8 @@ class HelmSubsystem(TemplatedExternalTool):
Before attempting to use passthrough arguments, check the reference of each of the available target types
to see what fields are accepted in each of them.
To pass `--dry-run`, use the `--experimental-deploy-dry-run` flag.
"""
),
)
Expand Down
11 changes: 7 additions & 4 deletions src/python/pants/backend/terraform/goals/deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from pants.backend.terraform.target_types import TerraformDeploymentFieldSet
from pants.backend.terraform.tool import TerraformProcess, TerraformTool
from pants.backend.terraform.utils import terraform_arg, terraform_relpath
from pants.core.goals.deploy import DeployFieldSet, DeployProcess
from pants.core.goals.deploy import DeployFieldSet, DeployProcess, DeploySubsystem
from pants.core.util_rules.source_files import SourceFiles, SourceFilesRequest
from pants.engine.engine_aware import EngineAwareParameter
from pants.engine.internals.native_engine import Digest, MergeDigests
Expand All @@ -39,7 +39,9 @@ class TerraformDeploymentRequest(EngineAwareParameter):

@rule
async def prepare_terraform_deployment(
request: TerraformDeploymentRequest, terraform_subsystem: TerraformTool
request: TerraformDeploymentRequest,
terraform_subsystem: TerraformTool,
deploy_subsystem: DeploySubsystem,
) -> InteractiveProcess:
initialised_terraform = await Get(
TerraformInitResponse,
Expand All @@ -50,7 +52,8 @@ async def prepare_terraform_deployment(
),
)

args = ["apply"]
terraform_command = "plan" if deploy_subsystem.dry_run else "apply"
args = [terraform_command]

invocation_files = await Get(
TerraformDeploymentInvocationFiles,
Expand Down Expand Up @@ -78,7 +81,7 @@ async def prepare_terraform_deployment(
TerraformProcess(
args=tuple(args),
input_digest=with_vars,
description="Terraform apply",
description=f"Terraform {terraform_command}",
chdir=initialised_terraform.chdir,
),
)
Expand Down
28 changes: 27 additions & 1 deletion src/python/pants/backend/terraform/goals/deploy_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

import json

import pytest

from pants.backend.terraform.goals.deploy import DeployTerraformFieldSet
from pants.backend.terraform.testutil import rule_runner_with_auto_approve, standard_deployment
from pants.core.goals.deploy import Deploy, DeployProcess
Expand Down Expand Up @@ -47,7 +49,31 @@ def test_deploy_terraform_forwards_args(rule_runner: RuleRunner, standard_deploy
assert "-chdir=src/tf" in argv, "Did not find expected -chdir"
assert "-var-file=stg.tfvars" in argv, "Did not find expected -var-file"
assert "-auto-approve" in argv, "Did not find expected passthrough args"
# assert standard_deployment.state_file.check()


@pytest.mark.parametrize(
"options,action,not_action",
[
([], "apply", "plan"),
(["--experimental-deploy-dry-run=False"], "apply", "plan"),
(["--experimental-deploy-dry-run"], "plan", "apply"),
],
)
def test_deploy_terraform_adheres_to_dry_run_flag(
rule_runner: RuleRunner, standard_deployment, options: list[str], action: str, not_action: str
) -> None:
rule_runner.write_files(standard_deployment.files)
rule_runner.set_options(options)

target = rule_runner.get_target(Address("src/tf", target_name="stg"))
field_set = DeployTerraformFieldSet.create(target)
deploy_process = rule_runner.request(DeployProcess, [field_set])
assert deploy_process.process

argv = deploy_process.process.process.argv

assert action in argv, f"Expected {action} in argv"
assert not_action not in argv, f"Did not expect {not_action} in argv"


def test_deploy_terraform_with_module(rule_runner: RuleRunner) -> None:
Expand Down
13 changes: 12 additions & 1 deletion src/python/pants/core/goals/deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
TargetRootsToFieldSetsRequest,
)
from pants.engine.unions import union
from pants.util.strutil import pluralize
from pants.option.option_types import BoolOption
from pants.util.strutil import pluralize, softwrap

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -83,6 +84,16 @@ class DeploySubsystem(GoalSubsystem):
name = "experimental-deploy"
help = "Perform a deployment process."

dry_run = BoolOption(
default=False,
help=softwrap(
"""
If true, perform a dry run without deploying anything.
For example, when deploying a terraform_deployment, a plan will be executed instead of an apply.
"""
),
)

required_union_implementation = (DeployFieldSet,)


Expand Down

0 comments on commit 7c1e0b7

Please sign in to comment.