use super::*;
impl ResourceProvisioner {
pub(super) fn create_ecr_repository(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let repository_name = props
.get("RepositoryName")
.and_then(|v| v.as_str())
.unwrap_or(&resource.logical_id)
.to_string();
let image_tag_mutability = props
.get("ImageTagMutability")
.and_then(|v| v.as_str())
.unwrap_or("MUTABLE")
.to_string();
let scan_on_push = props
.get("ImageScanningConfiguration")
.and_then(|v| v.get("ScanOnPush"))
.and_then(|v| v.as_bool())
.unwrap_or(false);
let encryption_type = props
.get("EncryptionConfiguration")
.and_then(|v| v.get("EncryptionType"))
.and_then(|v| v.as_str())
.unwrap_or("AES256")
.to_string();
let kms_key = props
.get("EncryptionConfiguration")
.and_then(|v| v.get("KmsKey"))
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let policy_text = props
.get("RepositoryPolicyText")
.map(|v| {
if v.is_string() {
v.as_str().unwrap_or("").to_string()
} else {
serde_json::to_string(v).unwrap_or_default()
}
})
.filter(|s| !s.is_empty());
let lifecycle_policy = props
.get("LifecyclePolicy")
.and_then(|v| v.get("LifecyclePolicyText"))
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let mut tags: BTreeMap<String, String> = BTreeMap::new();
if let Some(arr) = props.get("Tags").and_then(|v| v.as_array()) {
for t in arr {
if let (Some(k), Some(v)) = (
t.get("Key").and_then(|x| x.as_str()),
t.get("Value").and_then(|x| x.as_str()),
) {
tags.insert(k.to_string(), v.to_string());
}
}
}
let empty_on_delete = props
.get("EmptyOnDelete")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let mut accounts = self.ecr_state.write();
let endpoint = accounts.endpoint().to_string();
let state = accounts.get_or_create(&self.account_id);
if state.repositories.contains_key(&repository_name) {
return Err(format!("Repository {repository_name} already exists"));
}
let arn = state.repository_arn(&repository_name);
let registry_id = state.account_id.clone();
let mut repo = Repository::new(&repository_name, arn.clone(), ®istry_id, &endpoint);
repo.image_tag_mutability = image_tag_mutability;
repo.image_scanning_configuration.scan_on_push = scan_on_push;
repo.encryption_configuration.encryption_type = encryption_type;
repo.encryption_configuration.kms_key = kms_key;
repo.policy = policy_text;
if let Some(policy) = lifecycle_policy.as_ref() {
let prune = fakecloud_ecr::evaluate_lifecycle_policy(&repo, policy);
for digest in &prune {
repo.images.remove(digest);
repo.image_tags.retain(|_, d| d != digest);
}
repo.lifecycle_policy_last_evaluated_at = Some(Utc::now());
}
repo.lifecycle_policy = lifecycle_policy;
repo.tags = tags;
let uri = repo.repository_uri.clone();
state.repositories.insert(repository_name.clone(), repo);
Ok(ProvisionResult::new(repository_name)
.with("Arn", arn)
.with("RepositoryUri", uri)
.with("RegistryId", registry_id)
.with("EmptyOnDelete", empty_on_delete.to_string()))
}
pub(super) fn delete_ecr_repository(&self, physical_id: &str) -> Result<(), String> {
let mut accounts = self.ecr_state.write();
let state = accounts.get_or_create(&self.account_id);
state.repositories.remove(physical_id);
Ok(())
}
pub(super) fn create_ecr_repository_policy(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let repository_name = props
.get("RepositoryName")
.and_then(|v| v.as_str())
.ok_or_else(|| "RepositoryName is required".to_string())?
.to_string();
let policy_text = props
.get("PolicyText")
.map(|v| {
if v.is_string() {
v.as_str().unwrap_or("").to_string()
} else {
serde_json::to_string(v).unwrap_or_default()
}
})
.ok_or_else(|| "PolicyText is required".to_string())?;
let mut accounts = self.ecr_state.write();
let state = accounts.get_or_create(&self.account_id);
let repo = state
.repositories
.get_mut(&repository_name)
.ok_or_else(|| format!("Repository {repository_name} does not exist"))?;
repo.policy = Some(policy_text);
Ok(ProvisionResult::new(format!(
"{}/{}",
self.account_id, repository_name
)))
}
pub(super) fn delete_ecr_repository_policy(&self, physical_id: &str) -> Result<(), String> {
let repository_name = physical_id
.split_once('/')
.map(|(_, n)| n.to_string())
.unwrap_or_else(|| physical_id.to_string());
let mut accounts = self.ecr_state.write();
let state = accounts.get_or_create(&self.account_id);
if let Some(repo) = state.repositories.get_mut(&repository_name) {
repo.policy = None;
}
Ok(())
}
pub(super) fn create_ecr_registry_policy(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let policy_text = props
.get("PolicyText")
.map(|v| {
if v.is_string() {
v.as_str().unwrap_or("").to_string()
} else {
serde_json::to_string(v).unwrap_or_default()
}
})
.ok_or_else(|| "PolicyText is required".to_string())?;
let mut accounts = self.ecr_state.write();
let state = accounts.get_or_create(&self.account_id);
state.registry_policy = Some(policy_text);
Ok(ProvisionResult::new(self.account_id.clone())
.with("RegistryId", self.account_id.clone()))
}
pub(super) fn delete_ecr_registry_policy(&self) -> Result<(), String> {
let mut accounts = self.ecr_state.write();
let state = accounts.get_or_create(&self.account_id);
state.registry_policy = None;
Ok(())
}
pub(super) fn create_ecr_replication_configuration(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
use fakecloud_ecr::{
ReplicationConfiguration, ReplicationDestination, ReplicationRule, RepositoryFilter,
};
let cfg = resource
.properties
.get("ReplicationConfiguration")
.ok_or_else(|| "ReplicationConfiguration is required".to_string())?;
let rules: Vec<ReplicationRule> = cfg
.get("Rules")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.map(|r| {
let destinations: Vec<ReplicationDestination> = r
.get("Destinations")
.and_then(|v| v.as_array())
.map(|d| {
d.iter()
.map(|dest| ReplicationDestination {
region: dest
.get("Region")
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_string(),
registry_id: dest
.get("RegistryId")
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_string(),
})
.collect()
})
.unwrap_or_default();
let repository_filters: Vec<RepositoryFilter> = r
.get("RepositoryFilters")
.and_then(|v| v.as_array())
.map(|f| {
f.iter()
.map(|f| RepositoryFilter {
filter: f
.get("Filter")
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_string(),
filter_type: f
.get("FilterType")
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_string(),
})
.collect()
})
.unwrap_or_default();
ReplicationRule {
destinations,
repository_filters,
}
})
.collect()
})
.unwrap_or_default();
let mut accounts = self.ecr_state.write();
let state = accounts.get_or_create(&self.account_id);
state.replication_configuration = Some(ReplicationConfiguration { rules });
Ok(ProvisionResult::new(self.account_id.clone()))
}
pub(super) fn delete_ecr_replication_configuration(&self) -> Result<(), String> {
let mut accounts = self.ecr_state.write();
let state = accounts.get_or_create(&self.account_id);
state.replication_configuration = None;
Ok(())
}
pub(super) fn create_ecr_pull_through_cache_rule(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
use fakecloud_ecr::PullThroughCacheRule;
let props = &resource.properties;
let prefix = props
.get("EcrRepositoryPrefix")
.and_then(|v| v.as_str())
.ok_or_else(|| "EcrRepositoryPrefix is required".to_string())?
.to_string();
let upstream_url = props
.get("UpstreamRegistryUrl")
.and_then(|v| v.as_str())
.ok_or_else(|| "UpstreamRegistryUrl is required".to_string())?
.to_string();
let upstream_registry = props
.get("UpstreamRegistry")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let credential_arn = props
.get("CredentialArn")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let custom_role_arn = props
.get("CustomRoleArn")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let now = Utc::now();
let rule = PullThroughCacheRule {
ecr_repository_prefix: prefix.clone(),
upstream_registry_url: upstream_url,
upstream_registry,
credential_arn,
created_at: now,
updated_at: now,
custom_role_arn,
};
let mut accounts = self.ecr_state.write();
let state = accounts.get_or_create(&self.account_id);
state.pull_through_cache_rules.insert(prefix.clone(), rule);
Ok(ProvisionResult::new(prefix))
}
pub(super) fn delete_ecr_pull_through_cache_rule(
&self,
physical_id: &str,
) -> Result<(), String> {
let mut accounts = self.ecr_state.write();
let state = accounts.get_or_create(&self.account_id);
state.pull_through_cache_rules.remove(physical_id);
Ok(())
}
pub(super) fn create_ecr_lifecycle_policy(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let repository_name = props
.get("RepositoryName")
.and_then(|v| v.as_str())
.ok_or_else(|| "RepositoryName is required".to_string())?
.to_string();
let policy_text = props
.get("LifecyclePolicyText")
.map(|v| {
if v.is_string() {
v.as_str().unwrap_or("").to_string()
} else {
serde_json::to_string(v).unwrap_or_default()
}
})
.ok_or_else(|| "LifecyclePolicyText is required".to_string())?;
let _registry_id = props
.get("RegistryId")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
let mut accounts = self.ecr_state.write();
let state = accounts.get_or_create(&self.account_id);
let repo = state
.repositories
.get_mut(&repository_name)
.ok_or_else(|| format!("Repository {repository_name} does not exist"))?;
let prune = fakecloud_ecr::evaluate_lifecycle_policy(repo, &policy_text);
for digest in &prune {
repo.images.remove(digest);
repo.image_tags.retain(|_, d| d != digest);
}
repo.lifecycle_policy = Some(policy_text);
repo.lifecycle_policy_last_evaluated_at = Some(Utc::now());
let registry_id = repo.registry_id.clone();
Ok(
ProvisionResult::new(format!("{}/{}", self.account_id, repository_name))
.with("RepositoryName", repository_name)
.with("RegistryId", registry_id),
)
}
pub(super) fn delete_ecr_lifecycle_policy(&self, physical_id: &str) -> Result<(), String> {
let repository_name = physical_id
.split_once('/')
.map(|(_, n)| n.to_string())
.unwrap_or_else(|| physical_id.to_string());
let mut accounts = self.ecr_state.write();
let state = accounts.get_or_create(&self.account_id);
if let Some(repo) = state.repositories.get_mut(&repository_name) {
repo.lifecycle_policy = None;
repo.lifecycle_policy_last_evaluated_at = None;
}
Ok(())
}
pub(super) fn create_ecr_registry_scanning_configuration(
&self,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
use fakecloud_ecr::{
RegistryScanningConfiguration, RegistryScanningRule, RepositoryFilter,
};
let props = &resource.properties;
let scan_type = props
.get("ScanType")
.and_then(|v| v.as_str())
.unwrap_or("BASIC")
.to_string();
let rules: Vec<RegistryScanningRule> = props
.get("Rules")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.map(|r| {
let scan_frequency = r
.get("ScanFrequency")
.and_then(|v| v.as_str())
.unwrap_or("MANUAL")
.to_string();
let repository_filters: Vec<RepositoryFilter> = r
.get("RepositoryFilters")
.and_then(|v| v.as_array())
.map(|f| {
f.iter()
.map(|f| RepositoryFilter {
filter: f
.get("Filter")
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_string(),
filter_type: f
.get("FilterType")
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_string(),
})
.collect()
})
.unwrap_or_default();
RegistryScanningRule {
scan_frequency,
repository_filters,
}
})
.collect()
})
.unwrap_or_default();
let mut accounts = self.ecr_state.write();
let state = accounts.get_or_create(&self.account_id);
state.registry_scanning_configuration = RegistryScanningConfiguration { scan_type, rules };
Ok(ProvisionResult::new(self.account_id.clone()))
}
pub(super) fn delete_ecr_registry_scanning_configuration(&self) -> Result<(), String> {
use fakecloud_ecr::RegistryScanningConfiguration;
let mut accounts = self.ecr_state.write();
let state = accounts.get_or_create(&self.account_id);
state.registry_scanning_configuration = RegistryScanningConfiguration::default();
Ok(())
}
pub(super) fn update_ecr_repository(
&self,
existing: &StackResource,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let repository_name = existing.physical_id.clone();
let mut accounts = self.ecr_state.write();
let state = accounts.get_or_create(&self.account_id);
let repo = state
.repositories
.get_mut(&repository_name)
.ok_or_else(|| format!("Repository {repository_name} no longer exists"))?;
if let Some(s) = props.get("ImageTagMutability").and_then(|v| v.as_str()) {
repo.image_tag_mutability = s.to_string();
}
if let Some(b) = props
.get("ImageScanningConfiguration")
.and_then(|v| v.get("ScanOnPush"))
.and_then(|v| v.as_bool())
{
repo.image_scanning_configuration.scan_on_push = b;
}
if let Some(cfg) = props.get("EncryptionConfiguration") {
if let Some(s) = cfg.get("EncryptionType").and_then(|v| v.as_str()) {
repo.encryption_configuration.encryption_type = s.to_string();
}
if let Some(s) = cfg.get("KmsKey").and_then(|v| v.as_str()) {
repo.encryption_configuration.kms_key = Some(s.to_string());
}
}
if let Some(v) = props.get("RepositoryPolicyText") {
let text = if v.is_string() {
v.as_str().unwrap_or("").to_string()
} else {
serde_json::to_string(v).unwrap_or_default()
};
repo.policy = if text.is_empty() { None } else { Some(text) };
}
if let Some(text) = props
.get("LifecyclePolicy")
.and_then(|v| v.get("LifecyclePolicyText"))
.and_then(|v| v.as_str())
{
let prune = fakecloud_ecr::evaluate_lifecycle_policy(repo, text);
for digest in &prune {
repo.images.remove(digest);
repo.image_tags.retain(|_, d| d != digest);
}
repo.lifecycle_policy = Some(text.to_string());
repo.lifecycle_policy_last_evaluated_at = Some(Utc::now());
}
if let Some(arr) = props.get("Tags").and_then(|v| v.as_array()) {
let mut tags: BTreeMap<String, String> = BTreeMap::new();
for t in arr {
if let (Some(k), Some(v)) = (
t.get("Key").and_then(|x| x.as_str()),
t.get("Value").and_then(|x| x.as_str()),
) {
tags.insert(k.to_string(), v.to_string());
}
}
repo.tags = tags;
}
let arn = repo.repository_arn.clone();
let uri = repo.repository_uri.clone();
let registry_id = repo.registry_id.clone();
Ok(ProvisionResult::new(repository_name)
.with("Arn", arn)
.with("RepositoryUri", uri)
.with("RegistryId", registry_id))
}
pub(super) fn update_ecr_repository_policy(
&self,
existing: &StackResource,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let physical_id = existing.physical_id.clone();
let repository_name = physical_id
.split_once('/')
.map(|(_, n)| n.to_string())
.unwrap_or_else(|| physical_id.clone());
let policy_text = props
.get("PolicyText")
.map(|v| {
if v.is_string() {
v.as_str().unwrap_or("").to_string()
} else {
serde_json::to_string(v).unwrap_or_default()
}
})
.ok_or_else(|| "PolicyText is required".to_string())?;
let mut accounts = self.ecr_state.write();
let state = accounts.get_or_create(&self.account_id);
let repo = state
.repositories
.get_mut(&repository_name)
.ok_or_else(|| format!("Repository {repository_name} does not exist"))?;
repo.policy = Some(policy_text);
Ok(ProvisionResult::new(physical_id))
}
pub(super) fn update_ecr_lifecycle_policy(
&self,
existing: &StackResource,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let physical_id = existing.physical_id.clone();
let repository_name = physical_id
.split_once('/')
.map(|(_, n)| n.to_string())
.unwrap_or_else(|| physical_id.clone());
let policy_text = props
.get("LifecyclePolicyText")
.map(|v| {
if v.is_string() {
v.as_str().unwrap_or("").to_string()
} else {
serde_json::to_string(v).unwrap_or_default()
}
})
.ok_or_else(|| "LifecyclePolicyText is required".to_string())?;
let mut accounts = self.ecr_state.write();
let state = accounts.get_or_create(&self.account_id);
let repo = state
.repositories
.get_mut(&repository_name)
.ok_or_else(|| format!("Repository {repository_name} does not exist"))?;
let prune = fakecloud_ecr::evaluate_lifecycle_policy(repo, &policy_text);
for digest in &prune {
repo.images.remove(digest);
repo.image_tags.retain(|_, d| d != digest);
}
repo.lifecycle_policy = Some(policy_text);
repo.lifecycle_policy_last_evaluated_at = Some(Utc::now());
let registry_id = repo.registry_id.clone();
Ok(ProvisionResult::new(physical_id)
.with("RepositoryName", repository_name)
.with("RegistryId", registry_id))
}
pub(super) fn update_ecr_registry_policy(
&self,
existing: &StackResource,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let policy_text = props
.get("PolicyText")
.map(|v| {
if v.is_string() {
v.as_str().unwrap_or("").to_string()
} else {
serde_json::to_string(v).unwrap_or_default()
}
})
.ok_or_else(|| "PolicyText is required".to_string())?;
let mut accounts = self.ecr_state.write();
let state = accounts.get_or_create(&self.account_id);
state.registry_policy = Some(policy_text);
Ok(ProvisionResult::new(existing.physical_id.clone())
.with("RegistryId", self.account_id.clone()))
}
pub(super) fn update_ecr_replication_configuration(
&self,
existing: &StackResource,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let result = self.create_ecr_replication_configuration(resource)?;
Ok(ProvisionResult::new(existing.physical_id.clone()).merge_attributes(result.attributes))
}
pub(super) fn update_ecr_registry_scanning_configuration(
&self,
existing: &StackResource,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let result = self.create_ecr_registry_scanning_configuration(resource)?;
Ok(ProvisionResult::new(existing.physical_id.clone()).merge_attributes(result.attributes))
}
pub(super) fn update_ecr_pull_through_cache_rule(
&self,
existing: &StackResource,
resource: &ResourceDefinition,
) -> Result<ProvisionResult, String> {
let props = &resource.properties;
let prefix = existing.physical_id.clone();
let mut accounts = self.ecr_state.write();
let state = accounts.get_or_create(&self.account_id);
let rule = state
.pull_through_cache_rules
.get_mut(&prefix)
.ok_or_else(|| format!("PullThroughCacheRule {prefix} no longer exists"))?;
if let Some(s) = props.get("UpstreamRegistryUrl").and_then(|v| v.as_str()) {
rule.upstream_registry_url = s.to_string();
}
if let Some(s) = props.get("UpstreamRegistry").and_then(|v| v.as_str()) {
rule.upstream_registry = Some(s.to_string());
}
if let Some(s) = props.get("CredentialArn").and_then(|v| v.as_str()) {
rule.credential_arn = Some(s.to_string());
}
if let Some(s) = props.get("CustomRoleArn").and_then(|v| v.as_str()) {
rule.custom_role_arn = Some(s.to_string());
}
rule.updated_at = Utc::now();
Ok(ProvisionResult::new(prefix))
}
pub(super) fn get_att_ecr_repository(
&self,
physical_id: &str,
attribute: &str,
) -> Option<String> {
let mut accounts = self.ecr_state.write();
let state = accounts.get_or_create(&self.account_id);
let repo = state.repositories.get(physical_id)?;
match attribute {
"Arn" => Some(repo.repository_arn.clone()),
"RepositoryUri" => Some(repo.repository_uri.clone()),
"RegistryId" => Some(repo.registry_id.clone()),
_ => None,
}
}
}