Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions core/crates/ployz-build/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ pub struct Request<'a> {
pub pull: bool,
/// Hide unreadable progress lines when this Build reads Sealed values.
pub sealed: bool,
/// The GitHub Actions cache scope, in place of each target's own name.
pub cache_scope: Option<&'a str>,
}

/// One image to produce, named by the caller.
Expand Down Expand Up @@ -707,11 +709,12 @@ fn bake_arguments(
// cache export writes to it, so uploading cache never delays the image.
if github_cache(request) {
let bake = &planned.bake;
let scope = request.cache_scope.unwrap_or(bake);
arguments.push("--set".to_owned());
arguments.push(format!("{bake}.cache-from=type=gha,scope={bake}"));
arguments.push(format!("{bake}.cache-from=type=gha,scope={scope}"));
if produce == Produce::Cache {
arguments.push("--set".to_owned());
arguments.push(format!("{bake}.cache-to=type=gha,scope={bake},mode=max"));
arguments.push(format!("{bake}.cache-to=type=gha,scope={scope},mode=max"));
}
}
// Platforms travel in the captured Compose file, which upstream reads.
Expand Down
1 change: 1 addition & 0 deletions core/crates/ployz-build/src/scrub.rs
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,7 @@ exit 0
image_contexts: &BTreeMap::new(),
compose_file: &root.path().join("compose.yaml"),
sealed_override: None,
cache_scope: None,
working_dir: root.path(),
environment: &BTreeMap::new(),
docker: Some(&docker),
Expand Down
10 changes: 10 additions & 0 deletions core/crates/ployz-build/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ exit 0
no_cache: false,
pull: false,
sealed: false,
cache_scope: None,
};
let admission = Admission::try_acquire_with(&HostPolicy {
docker: program.clone(),
Expand Down Expand Up @@ -300,6 +301,7 @@ exit 0
no_cache: false,
pull: false,
sealed: false,
cache_scope: None,
};
let admission = Admission::try_acquire_with(&HostPolicy {
docker: program.clone(),
Expand Down Expand Up @@ -391,6 +393,7 @@ fn requested_output_selects_exclusive_bake_behavior() {
no_cache: true,
pull: false,
sealed: false,
cache_scope: None,
};

let validate = bake_arguments(
Expand Down Expand Up @@ -566,6 +569,7 @@ esac
no_cache: false,
pull: false,
sealed: false,
cache_scope: None,
};
execute(&request, &Cancellation::new(), None, &|_| ()).unwrap();
std::fs::remove_dir_all(directory).unwrap();
Expand Down Expand Up @@ -625,6 +629,7 @@ fn docker_preflight_holds_quarantine_and_clears_confirmed_failures() {
no_cache: false,
pull: false,
sealed: false,
cache_scope: None,
};
for stop_at in ["context show", "buildx version"] {
executable(
Expand Down Expand Up @@ -720,6 +725,7 @@ exit 0
no_cache: false,
pull: false,
sealed: false,
cache_scope: None,
};
let admission = Admission::try_acquire_with(&HostPolicy {
state_directory: directory.clone(),
Expand Down Expand Up @@ -783,6 +789,7 @@ exit 0
no_cache: false,
pull: false,
sealed: false,
cache_scope: None,
};
let admission = Admission::try_acquire_with(&HostPolicy {
state_directory: directory.clone(),
Expand Down Expand Up @@ -853,6 +860,7 @@ fn a_cache_export_runs_nothing(
no_cache: false,
pull: false,
sealed: false,
cache_scope: None,
};
// Any Docker call would fail: this Docker does not exist.
assert_eq!(export_cache(&request, &Cancellation::new(), None), Ok(()));
Expand Down Expand Up @@ -890,6 +898,7 @@ fn railpack_prepares_a_python_requirements_app() {
no_cache: false,
pull: false,
sealed: false,
cache_scope: None,
};
let docker = Docker {
program: Path::new("docker"),
Expand Down Expand Up @@ -978,6 +987,7 @@ fn railpack_delivers_a_sealed_value_to_a_build_step_and_keeps_it_out_of_the_imag
no_cache: false,
pull: false,
sealed: true,
cache_scope: None,
};
let reported = std::sync::Mutex::new(String::new());
let progress = |progress: Progress| {
Expand Down
1 change: 1 addition & 0 deletions core/crates/ployz-build/src/upload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,7 @@ impl CompletedUpload {
no_cache: definition.no_cache,
pull: definition.pull,
sealed: !opened.is_empty(),
cache_scope: None,
},
admission,
progress,
Expand Down
3 changes: 3 additions & 0 deletions core/crates/ployz-build/tests/host_policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ exit 0
image_contexts: &BTreeMap::new(),
compose_file: &host.file("compose.yaml"),
sealed_override: None,
cache_scope: None,
working_dir: &host.policy.state_directory,
environment: &BTreeMap::from([("PATH".into(), "/usr/bin:/bin".into())]),
docker: Some(&host.policy.docker),
Expand Down Expand Up @@ -203,6 +204,7 @@ fn resource_launch_failure_reports_preparation_and_releases_confirmed_ownership(
image_contexts: &BTreeMap::new(),
compose_file: &host.file("compose.yaml"),
sealed_override: None,
cache_scope: None,
working_dir: &host.policy.state_directory,
environment: &BTreeMap::new(),
docker: Some(&host.policy.docker),
Expand Down Expand Up @@ -336,6 +338,7 @@ echo "$*" >> "$root/mutations"
image_contexts: &BTreeMap::new(),
compose_file: &host.file("compose.yaml"),
sealed_override: None,
cache_scope: None,
working_dir: &host.policy.state_directory,
environment: &environment,
docker: Some(&host.policy.docker),
Expand Down
2 changes: 2 additions & 0 deletions core/crates/ployz-core/src/authority/refusal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,6 @@ pub enum AuthorityRefusal {
NoClusterIdentity,
#[error("operation {operation} already ran this step; a new restart signs a new approval")]
Replayed { operation: OperationId },
#[error("this GitHub token already minted a Build Grant; a build asks for a new one")]
ProofReplayed,
}
29 changes: 25 additions & 4 deletions core/crates/ployz-core/src/authority/rule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,21 @@ pub struct Builder {
pub sha: CommitSha,
}

/// The commit of `getployz/build` whose reusable workflow a rule approved from the CLI
/// pins by default.
pub const BUILDER_SHA: &str = "5f35eaa8e1372d0d86ff82788adadf308e5f3f97";

impl Builder {
/// `getployz/build`'s reusable workflow at [`BUILDER_SHA`].
#[must_use]
pub fn pinned() -> Self {
Self {
r#ref: format!("getployz/build/.github/workflows/build.yml@{BUILDER_SHA}"),
sha: CommitSha::parse(BUILDER_SHA).expect("the pinned builder is a commit"),
}
}
}

/// The repository, branch and workflow a rule builds from.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, TS)]
#[serde(deny_unknown_fields)]
Expand Down Expand Up @@ -317,6 +332,12 @@ impl Statement for AutoDeployRule {
}

impl AutoDeployRule {
/// What the rule approves for `name`, or `None` when it names no such Service.
#[must_use]
pub fn service(&self, name: &ServiceName) -> Option<&RuleService> {
self.services.iter().find(|service| service.name == *name)
}

/// Check a verified GitHub token's claims against this rule, in the order
/// `d-build-proof.md` lists them. The audience is the caller's: it depends on the
/// holder, recipient or image in hand.
Expand Down Expand Up @@ -349,14 +370,14 @@ impl AutoDeployRule {
claims.workflow_ref
== format!("{}/{}@{git_ref}", source.repository, source.workflow),
),
(
"job_workflow_ref",
claims.job_workflow_ref == source.builder.r#ref,
),
(
"job_workflow_sha",
claims.job_workflow_sha == source.builder.sha,
),
(
"job_workflow_ref",
claims.job_workflow_ref == source.builder.r#ref,
),
(
"runner_environment",
claims.runner_environment == source.runner.as_str(),
Expand Down
6 changes: 6 additions & 0 deletions core/crates/ployz-core/src/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2125,6 +2125,12 @@ pub struct ContractDescription {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
pub sealing: Option<crate::Sealing>,
/// The build attest key the Machine derives from that Sealing Key, which seals build
/// secrets to a rule build's recipient. Absent like `sealing`, and from a daemon that
/// predates rule builds.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional = nullable)]
pub attest: Option<crate::authority::AttestKey>,
}

impl ContractDescription {
Expand Down
56 changes: 56 additions & 0 deletions core/crates/ployz-core/src/rpc/build_grant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,61 @@ pub struct MintBuildGrantRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub sealed: Option<crate::SealedRecord>,
/// A build under an automatic deploy rule: the signed rule and the GitHub token
/// whose audience names this grant. Sent only to a daemon advertising
/// [`crate::BUILD_GRANT_RULE_CAPABILITY`]. Such a daemon checks the token and the
/// commit's authority before it opens `sealed`, and serves the opened values sealed
/// to the build's recipient key only.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub rule: Option<RuleGrant>,
}

/// What a builder under a rule asks a grant with. Everything but the rule and the token
/// is the builder's own word, which the Machine checks by recomputing the token's
/// audience from it.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, TS)]
#[serde(deny_unknown_fields)]
pub struct RuleGrant {
#[ts(type = "unknown")]
pub rule: crate::authority::Signed<crate::authority::AutoDeployRule>,
/// The raw GitHub Actions OIDC token.
pub proof: GithubToken,
pub service: crate::ServiceName,
/// The commit the builder checked out.
pub commit: crate::authority::CommitSha,
pub authority: crate::authority::CommitAuthority,
/// The key the build secrets are sealed to.
pub recipient: crate::authority::RecipientKey,
/// A token that may read the repository's branch head, such as an installation
/// token narrowed to `contents: read`. `None` reads a public repository.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional)]
pub reader: Option<GithubToken>,
}

/// A GitHub token. Its Debug never prints it.
#[derive(Clone, Eq, PartialEq, Serialize, Deserialize, TS)]
#[serde(transparent)]
#[ts(type = "string")]
pub struct GithubToken(String);

impl GithubToken {
#[must_use]
pub const fn new(token: String) -> Self {
Self(token)
}

#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}

impl std::fmt::Debug for GithubToken {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("GithubToken(..)")
}
}

/// A minted grant. The Machine keeps it in memory only; a restart ends it.
Expand Down Expand Up @@ -135,6 +190,7 @@ mod tests {
repository,
holder: None,
sealed: None,
rule: None,
}
);
assert_eq!(
Expand Down
1 change: 1 addition & 0 deletions core/crates/ployz-core/src/rpc_catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ macro_rules! rpc_catalog {
BUILD_SEALED_CAPABILITY: ("ployz.build.sealed.v1", Always),
/// The daemon mints a Build Grant for a holder's own key and returns no grant secret.
BUILD_GRANT_HOLDER_CAPABILITY: ("ployz.build.grant.holder.v1", Always),
BUILD_GRANT_RULE_CAPABILITY: ("ployz.build.grant.rule.v1", Always),
/// `EnsureContainer` takes a Device-signed restart approval and, under Device enforcement, refuses an Operator's restart step without one.
RESTART_APPROVAL_CAPABILITY: ("ployz.restart.device-approval.v2", Always),
/// A restart or deploy approval may be signed by a browser key under a trusted passkey's session grant that rides in its envelope. The grant names its Environment's ID, and a deploy under it opens only Sealed values sealed for that Environment.
Expand Down
Loading
Loading