Skip to content

Commit

Permalink
feat: render build secrets in the Application
Browse files Browse the repository at this point in the history
This feature is gated behind a feature flag so the concrete applications
must set `enable_build_secrets` to True. When this is done, during the
loading of the project the application will also render the HOST_SECRET
directives. This is currently only supported in destructive mode.
  • Loading branch information
tigarmo committed Oct 17, 2023
1 parent 89232cf commit ef5620d
Show file tree
Hide file tree
Showing 5 changed files with 102 additions and 1 deletion.
16 changes: 15 additions & 1 deletion craft_application/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
import craft_providers
from xdg.BaseDirectory import save_cache_path # type: ignore[import]

from craft_application import commands, models, util
from craft_application import commands, models, secrets, util
from craft_application.models import BuildInfo

if TYPE_CHECKING:
Expand Down Expand Up @@ -94,6 +94,10 @@ class Application:
:ivar services: A ServiceFactory for this application
"""

# Feature flag for build-time secrets. Applications that want to use the feature
# must set this to True.
enable_build_secrets = False

def __init__(
self,
app: AppMetadata,
Expand Down Expand Up @@ -379,6 +383,10 @@ def _transform_project_yaml(self, yaml_data: dict[str, Any]) -> dict[str, Any]:
# Perform variable expansion.
self._expand_environment(yaml_data)

# Handle build secrets.
if self.enable_build_secrets:
self._render_secrets(yaml_data)

# Perform extra, application-specific transformations.
yaml_data = self._extra_yaml_transform(yaml_data)

Expand Down Expand Up @@ -412,6 +420,12 @@ def _set_global_environment(self, info: craft_parts.ProjectInfo) -> None:
}
)

def _render_secrets(self, yaml_data: dict[str, Any]) -> None:
"""Render build-secrets, in-place."""
secret_values = secrets.render_secrets(yaml_data)

craft_cli.emit.set_secrets(list(secret_values))

def _extra_yaml_transform(self, yaml_data: dict[str, Any]) -> dict[str, Any]:
"""Perform additional transformations on a project's yaml data.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
A source file
15 changes: 15 additions & 0 deletions tests/integration/data/build-secrets/testcraft.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: build-secrets
description: A project with build-time secrets
version: git
base: ["ubuntu", "22.04"]

parts:
my-part:
plugin: dump
source: $(HOST_SECRET:echo ${HOST_SOURCE_FOLDER})-folder
build-environment:
- SECRET_VAR: $(HOST_SECRET:echo ${HOST_SECRET_VAR})
override-build: |
craftctl default
echo "Dumping SECRET_VAR: ${SECRET_VAR}"
echo ${SECRET_VAR} >> ${CRAFT_PART_INSTALL}/build-file.txt
30 changes: 30 additions & 0 deletions tests/integration/test_application.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,3 +257,33 @@ def test_global_environment(
assert variables["project_name"] == "environment-project"
assert variables["project_dir"] == str(tmp_path)
assert variables["project_version"] == "1.2.3"


def test_build_secrets_destructive(create_app, monkeypatch, tmp_path, capsys):
"""Test the use of build secrets in destructive mode."""
monkeypatch.setenv("CRAFT_DEBUG", "1")
monkeypatch.chdir(tmp_path)
shutil.copytree(TEST_DATA_DIR / "build-secrets", tmp_path, dirs_exist_ok=True)

# Run in destructive mode
monkeypatch.setattr("sys.argv", ["testcraft", "prime", "-v", "--destructive-mode"])

app = create_app()
app.enable_build_secrets = True

# Set the environment variables that the project needs
monkeypatch.setenv("HOST_SOURCE_FOLDER", "secret-source")
monkeypatch.setenv("HOST_SECRET_VAR", "my-secret")
app.run()

prime_dir = tmp_path / "prime"
assert (prime_dir / "source-file.txt").read_text().strip() == "A source file"
assert (prime_dir / "build-file.txt").read_text().strip() == "my-secret"

# Check that the "secrets" were masked in console output and the logfile
_, stderr = capsys.readouterr()
log_contents = craft_cli.emit._log_filepath.read_text()

for target in (stderr, log_contents):
assert "Dumping SECRET_VAR: my-secret" not in target
assert "Dumping SECRET_VAR: *****" in target
41 changes: 41 additions & 0 deletions tests/unit/test_application.py
Original file line number Diff line number Diff line change
Expand Up @@ -513,3 +513,44 @@ def test_application_expand_environment(app_metadata, fake_services):
project = app.project

assert project.parts["mypart"]["source-tag"] == "v1.2.3"


@pytest.fixture()
def build_secrets_project(monkeypatch, tmp_path):
project_dir = tmp_path / "project"
project_dir.mkdir()
project_path = project_dir / "testcraft.yaml"
project_path.write_text(
dedent(
"""
name: myproject
version: 1.2.3
parts:
mypart:
plugin: nil
source: $(HOST_SECRET:echo ${SECRET_VAR_1})/project
build-environment:
- MY_VAR: $(HOST_SECRET:echo ${SECRET_VAR_2})
"""
)
)
monkeypatch.chdir(project_dir)

return project_path


@pytest.mark.usefixtures("build_secrets_project")
def test_application_build_secrets(app_metadata, fake_services, monkeypatch, mocker):
monkeypatch.setenv("SECRET_VAR_1", "source-folder")
monkeypatch.setenv("SECRET_VAR_2", "secret-value")
spied_set_secrets = mocker.spy(craft_cli.emit, "set_secrets")

app = application.Application(app_metadata, fake_services)
app.enable_build_secrets = True
project = app.project

mypart = project.parts["mypart"]
assert mypart["source"] == "source-folder/project"
assert mypart["build-environment"][0]["MY_VAR"] == "secret-value"

spied_set_secrets.assert_called_once_with(list({"source-folder", "secret-value"}))

0 comments on commit ef5620d

Please sign in to comment.