Skip to content
Open
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
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ serde_json = "1.0.140"
serde_yaml = "0.9"
snapbox = "0.6"
tar = "0.4"
olpc-cjson = "0.1"
toml = "0.9"
temp-env = "0.3"
tempfile = "3.10"
Expand Down
43 changes: 43 additions & 0 deletions crates/alien-aws-clients/src/aws/ecr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ pub trait EcrApi: Send + Sync + Debug {
) -> Result<GetAuthorizationTokenResponse>;
async fn batch_get_image(&self, request: BatchGetImageRequest)
-> Result<BatchGetImageResponse>;
async fn batch_delete_image(
&self,
request: BatchDeleteImageRequest,
) -> Result<BatchDeleteImageResponse>;
async fn get_download_url_for_layer(
&self,
request: GetDownloadUrlForLayerRequest,
Expand Down Expand Up @@ -431,6 +435,23 @@ impl EcrApi for EcrClient {
.await
}

async fn batch_delete_image(
&self,
request: BatchDeleteImageRequest,
) -> Result<BatchDeleteImageResponse> {
let body = serde_json::to_string(&request).into_alien_error().context(
ErrorData::SerializationError {
message: format!(
"Failed to serialize BatchDeleteImageRequest for repository '{}'",
request.repository_name
),
},
)?;

self.post_json("BatchDeleteImage", body, &request.repository_name)
.await
}

async fn get_download_url_for_layer(
&self,
request: GetDownloadUrlForLayerRequest,
Expand Down Expand Up @@ -765,6 +786,28 @@ pub struct BatchGetImageResponse {
pub failures: Vec<ImageFailure>,
}

// -------------------------------------------------------------------------
// BatchDeleteImage
// -------------------------------------------------------------------------

#[derive(Serialize, Debug, Clone, Builder)]
#[serde(rename_all = "camelCase")]
pub struct BatchDeleteImageRequest {
pub repository_name: String,
pub image_ids: Vec<ImageIdentifier>,
#[serde(skip_serializing_if = "Option::is_none")]
pub registry_id: Option<String>,
}

#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct BatchDeleteImageResponse {
#[serde(default)]
pub image_ids: Vec<ImageIdentifier>,
#[serde(default)]
pub failures: Vec<ImageFailure>,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct ImageIdentifier {
Expand Down
90 changes: 85 additions & 5 deletions crates/alien-cloudformation/src/emitters/aws/artifact_registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ use crate::{
},
template::{CfExpression, CfResource},
};
use alien_core::{import::EmitContext, ArtifactRegistry, Result, ServiceAccount};
use alien_core::{import::EmitContext, ArtifactRegistry, RemoteBindings, Result, ServiceAccount};
use alien_error::AlienError;

#[derive(Debug, Clone, Copy, Default)]
pub struct AwsArtifactRegistryEmitter;
Expand Down Expand Up @@ -51,9 +52,13 @@ impl CfEmitter for AwsArtifactRegistryEmitter {
let mut pull_role = ecr_access_role(ctx, &pull_role_id, "registry-pull", false)?;
let mut push_role = ecr_access_role(ctx, &push_role_id, "registry-push", true)?;
pull_role.depends_on.push(repository_id.clone());
push_role.depends_on.push(repository_id);
push_role.depends_on.push(repository_id.clone());

Ok(vec![repository, pull_role, push_role])
let mut resources = vec![repository, pull_role, push_role];
if alien_core::remote_bindings::remote_binding_for_entry(ctx.resource).is_some() {
resources.push(remote_access_policy(ctx, &repository_id)?);
}
Ok(resources)
}

fn emit_import_ref(&self, ctx: &EmitContext<'_>) -> Result<CfExpression> {
Expand Down Expand Up @@ -170,8 +175,6 @@ fn ecr_policy_document(ctx: &EmitContext<'_>, push: bool) -> Result<CfExpression
if push {
repository_actions.extend([
"ecr:CompleteLayerUpload",
"ecr:CreateRepository",
"ecr:DeleteRepository",
"ecr:InitiateLayerUpload",
"ecr:PutImage",
"ecr:UploadLayerPart",
Expand Down Expand Up @@ -212,6 +215,83 @@ fn ecr_policy_document(ctx: &EmitContext<'_>, push: bool) -> Result<CfExpression
]))
}

fn remote_access_policy(ctx: &EmitContext<'_>, repository_id: &str) -> Result<CfResource> {
let access_role_id = ctx
.stack
.resources()
.find_map(|(id, entry)| {
(entry.config.resource_type() == RemoteBindings::RESOURCE_TYPE)
.then(|| ctx.name_for(id))
.flatten()
})
.map(|id| format!("{id}Role"))
.ok_or_else(|| {
AlienError::new(alien_core::ErrorData::GenericError {
message: "remote ArtifactRegistry has no Remote Bindings identity".to_string(),
})
})?;
let permission = alien_permissions::get_permission_set("artifact-registry/remote-read-write")
.and_then(|set| set.platforms.aws.as_ref())
.ok_or_else(|| {
AlienError::new(alien_core::ErrorData::GenericError {
message: "artifact-registry/remote-read-write has no AWS permissions".to_string(),
})
})?;
let actions = permission
.iter()
.flat_map(|entry| entry.grant.actions.iter().flatten())
.cloned()
.collect::<Vec<_>>();
let mut policy = CfResource::new(
format!("{repository_id}RemoteRegistryPolicy"),
"AWS::IAM::Policy".to_string(),
);
policy.properties.insert(
"PolicyName".to_string(),
CfExpression::sub(format!(
"${{AWS::StackName}}-{}-registry-access",
ctx.resource_id
)),
);
policy.properties.insert(
"Roles".to_string(),
CfExpression::list([CfExpression::ref_(access_role_id)]),
);
policy.properties.insert(
"PolicyDocument".to_string(),
CfExpression::object([
("Version", CfExpression::from("2012-10-17")),
(
"Statement",
CfExpression::list([
CfExpression::object([
("Effect", CfExpression::from("Allow")),
("Action", CfExpression::from("ecr:GetAuthorizationToken")),
("Resource", CfExpression::from("*")),
]),
CfExpression::object([
("Effect", CfExpression::from("Allow")),
(
"Action",
CfExpression::list(
actions
.into_iter()
.filter(|action| action != "ecr:GetAuthorizationToken")
.map(CfExpression::from),
),
),
(
"Resource",
CfExpression::sub(format!("${{{repository_id}.Arn}}-*")),
),
]),
]),
),
]),
);
Ok(policy)
}

fn ecr_lifecycle_policy() -> CfExpression {
CfExpression::object([(
"rules",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,12 @@ fn resource_scoped_aws_permission_context(
.with_resource_id(resource_id.to_string());
context.resource_name = None;

if resource_entry.config.downcast_ref::<Worker>().is_some() {
if resource_entry.config.downcast_ref::<Worker>().is_some()
|| resource_entry
.config
.downcast_ref::<alien_core::ArtifactRegistry>()
.is_some()
{
return context.with_resource_name(format!("${{AWS::StackName}}-{resource_id}"));
}

Expand All @@ -295,7 +300,9 @@ fn resource_scoped_aws_permission_context(
#[cfg(test)]
mod tests {
use super::*;
use alien_core::{Resource, ResourceEntry, ResourceLifecycle, Worker, WorkerCode};
use alien_core::{
ArtifactRegistry, Resource, ResourceEntry, ResourceLifecycle, Worker, WorkerCode,
};

fn live_worker_entry(id: &str) -> ResourceEntry {
ResourceEntry {
Expand Down Expand Up @@ -336,4 +343,21 @@ mod tests {
Some("${AWS::StackName}-jobs")
);
}

#[test]
fn aws_remote_management_names_artifact_registry_child_prefix() {
let entry = ResourceEntry {
enabled_when: None,
config: Resource::new(ArtifactRegistry::new("images".to_string()).build()),
lifecycle: ResourceLifecycle::Frozen,
dependencies: Vec::new(),
remote_access: true,
};
let context =
resource_scoped_aws_permission_context("images", &entry, &permission_context());
assert_eq!(
context.resource_name.as_deref(),
Some("${AWS::StackName}-images")
);
}
}
30 changes: 28 additions & 2 deletions crates/alien-cloudformation/tests/generator/aws_compute_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ use super::helpers::render_built_ins;
use alien_cloudformation::{generate_cloudformation_template, CfRegistry, RegistrationMode};
use alien_core::{
ArtifactRegistry, Build, CapacityGroup, ComputeCluster, ErrorData, ManagementPermissions,
Network, NetworkSettings, PermissionProfile, Platform, RemoteStackManagement,
ResourceLifecycle, Stack, StackSettings, Worker, WorkerCode,
Network, NetworkSettings, PermissionProfile, Platform, RemoteBindings, RemoteStackManagement,
ResourceLifecycle, ResourceRef, Stack, StackSettings, Worker, WorkerCode,
};

#[test]
Expand All @@ -32,6 +32,32 @@ fn aws_artifact_registry_renders_ecr_repository() {
insta::assert_snapshot!("aws_artifact_registry", yaml);
}

#[test]
fn aws_remote_artifact_registry_policy_is_data_only() {
let mut stack = Stack::new("remote-registry".to_string())
.add_with_remote_access(
ArtifactRegistry::new("registry".to_string()).build(),
ResourceLifecycle::Frozen,
)
.add(
RemoteBindings::new("access".to_string()).build(),
ResourceLifecycle::Frozen,
)
.build();
stack.resources.get_mut("registry").unwrap().dependencies =
vec![ResourceRef::new(RemoteBindings::RESOURCE_TYPE, "access")];
let yaml = render_built_ins(
&stack,
StackSettings::default(),
RegistrationMode::OutputsFallback,
"aws_remote_artifact_registry",
);
assert!(yaml.contains("ecr:GetAuthorizationToken"));
assert!(yaml.contains("ecr:PutImage"));
assert!(!yaml.contains("ecr:CreateRepository"));
assert!(!yaml.contains("ecr:DeleteRepository"));
}

#[test]
fn aws_build_renders_codebuild_project() {
let stack = Stack::new("acme-build".to_string())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +183,6 @@ Resources:
- ecr:GetDownloadUrlForLayer
- ecr:ListImages
- ecr:CompleteLayerUpload
- ecr:CreateRepository
- ecr:DeleteRepository
- ecr:InitiateLayerUpload
- ecr:PutImage
- ecr:UploadLayerPart
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1497,8 +1497,6 @@ Resources:
- ecr:GetDownloadUrlForLayer
- ecr:ListImages
- ecr:CompleteLayerUpload
- ecr:CreateRepository
- ecr:DeleteRepository
- ecr:InitiateLayerUpload
- ecr:PutImage
- ecr:UploadLayerPart
Expand Down
9 changes: 9 additions & 0 deletions crates/alien-core/src/remote_bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pub enum RemoteBindingKind {
Storage,
Key,
Ai,
ArtifactRegistry,
}

/// One resource type's provider-neutral Remote Bindings contract.
Expand Down Expand Up @@ -51,6 +52,14 @@ const DEFINITIONS: &[RemoteBindingDefinition] = &[
setup_support_resource_types: &["azure_resource_group", "service_activation"],
revision: 1,
},
RemoteBindingDefinition {
resource_type: "artifact-registry",
permission_set: "artifact-registry/remote-read-write",
kind: RemoteBindingKind::ArtifactRegistry,
description: "Pull and push OCI artifacts in this registry",
setup_support_resource_types: &["azure_resource_group", "service_activation"],
revision: 1,
},
];

pub fn remote_binding_definition(
Expand Down
32 changes: 32 additions & 0 deletions crates/alien-gcp-clients/src/gcp/artifactregistry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,15 @@ pub trait ArtifactRegistryApi: Send + Sync + Debug {
repository_id: String,
) -> Result<Operation>;

/// Deletes a package and all of its versions and tags.
async fn delete_package(
&self,
project_id: String,
location: String,
repository_id: String,
package_name: String,
) -> Result<Operation>;

/// Gets a repository.
async fn get_repository(
&self,
Expand Down Expand Up @@ -189,6 +198,29 @@ impl ArtifactRegistryApi for ArtifactRegistryClient {
.await
}

async fn delete_package(
&self,
project_id: String,
location: String,
repository_id: String,
package_name: String,
) -> Result<Operation> {
let encoded_package = urlencoding::encode(&package_name);
let path = format!(
"projects/{project_id}/locations/{location}/repositories/{repository_id}/packages/{encoded_package}"
);

self.base
.execute_request(
Method::DELETE,
&path,
None,
Option::<()>::None,
&package_name,
)
.await
}

/// Gets a repository.
///
/// # Arguments
Expand Down
Loading
Loading