use http::Method;
use serde_derive::{Deserialize, Serialize};
use serde_with::{DeserializeFromStr, SerializeDisplay};
use std::borrow::Borrow;
use std::collections::HashMap;
use strum::{Display, EnumString};
use typed_builder::TypedBuilder;
use crate::client::*;
use crate::entity::{Entity, Id};
use crate::error::Result;
use crate::role::RoleFullname;
pub type AWSIntegration = Entity<AWSIntegrationValue>;
pub type AWSIntegrationId = Id<AWSIntegrationValue>;
#[derive(PartialEq, Clone, Debug, TypedBuilder, Serialize, Deserialize)]
#[builder(field_defaults(setter(into)))]
#[serde(rename_all = "camelCase")]
pub struct AWSIntegrationValue {
pub name: String,
#[builder(default)]
pub memo: String,
#[builder(default, setter(strip_option))]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub key: Option<String>,
#[builder(default, setter(strip_option))]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub role_arn: Option<String>,
#[builder(default, setter(strip_option))]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub external_id: Option<String>,
#[serde(default)]
pub region: String,
#[builder(default)]
#[serde(default)]
pub included_tags: String,
#[builder(default)]
#[serde(default)]
pub excluded_tags: String,
#[builder(default)]
#[serde(default)]
pub services: HashMap<AWSServiceName, AWSServiceConfig>,
}
#[derive(PartialEq, Clone, Debug, TypedBuilder, Serialize, Deserialize)]
#[builder(field_defaults(setter(into)))]
#[serde(rename_all = "camelCase")]
pub struct AWSServiceConfig {
#[builder(default = true)]
pub enable: bool,
#[builder(default, setter(strip_option))]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub role: Option<RoleFullname>,
#[builder(
default,
setter(transform = |metrics: impl IntoIterator<Item = impl AsRef<str>>| metrics
.into_iter().map(|metric| metric.as_ref().to_owned()).collect::<Vec<_>>()),
)]
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub included_metrics: Vec<String>,
#[builder(
default,
setter(transform = |metrics: impl IntoIterator<Item = impl AsRef<str>>| metrics
.into_iter().map(|metric| metric.as_ref().to_owned()).collect::<Vec<_>>()),
)]
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub excluded_metrics: Vec<String>,
#[builder(default)]
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub retire_automatically: bool,
}
#[derive(
PartialEq, Eq, Clone, Hash, Debug, Display, EnumString, SerializeDisplay, DeserializeFromStr,
)]
pub enum AWSServiceName {
EC2,
ELB,
ALB,
NLB,
RDS,
Redshift,
ElastiCache,
SQS,
Lambda,
DynamoDB,
CloudFront,
APIGateway,
Kinesis,
S3,
ES,
ECSCluster,
SES,
States,
EFS,
Firehose,
Batch,
WAF,
Billing,
Route53,
Connect,
DocDB,
CodeBuild,
Athena,
#[strum(default)]
Unknown(String),
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
use serde_json::json;
fn aws_integration_example1() -> AWSIntegration {
AWSIntegration::builder()
.id("aws1")
.value(
AWSIntegrationValue::builder()
.name("AWS integration setting")
.key("aws-access-key")
.region("ap-northeast-1")
.build(),
)
.build()
}
fn json_example1() -> serde_json::Value {
json!({
"id": "aws1",
"name": "AWS integration setting",
"memo": "",
"key": "aws-access-key",
"region": "ap-northeast-1",
"includedTags": "",
"excludedTags": "",
"services": {},
})
}
fn aws_integration_example2() -> AWSIntegration {
AWSIntegration::builder()
.id("aws2")
.value(
AWSIntegrationValue::builder()
.name("AWS integration setting")
.memo("This is an AWS integration memo.")
.role_arn("aws-role-arn")
.external_id("aws-integration-external-id")
.region("ap-northeast-2")
.included_tags("tag1, tag2")
.excluded_tags("tag3, tag4")
.services([
(
AWSServiceName::EC2,
AWSServiceConfig::builder()
.retire_automatically(true)
.build(),
),
(
AWSServiceName::NLB,
AWSServiceConfig::builder()
.role("aws:nlb")
.included_metrics([
"nlb.host_count.#.healthy",
"nlb.host_count.#.unhealthy",
])
.build(),
),
(
AWSServiceName::S3,
AWSServiceConfig::builder()
.enable(false)
.role("aws:s3")
.excluded_metrics(["s3.bucket_size.*"])
.build(),
),
])
.build(),
)
.build()
}
fn json_example2() -> serde_json::Value {
json!({
"id": "aws2",
"name": "AWS integration setting",
"memo": "This is an AWS integration memo.",
"roleArn": "aws-role-arn",
"externalId": "aws-integration-external-id",
"region": "ap-northeast-2",
"includedTags": "tag1, tag2",
"excludedTags": "tag3, tag4",
"services": {
"EC2": {
"enable": true,
"retireAutomatically": true,
},
"NLB": {
"enable": true,
"role": "aws:nlb",
"includedMetrics": [
"nlb.host_count.#.healthy",
"nlb.host_count.#.unhealthy",
],
},
"S3": {
"enable": false,
"role": "aws:s3",
"excludedMetrics": ["s3.bucket_size.*"],
},
},
})
}
#[rstest]
#[case(aws_integration_example1(), json_example1())]
#[case(aws_integration_example2(), json_example2())]
fn test_aws_integration_json(
#[case] aws_integration: AWSIntegration,
#[case] json: serde_json::Value,
) {
assert_eq!(serde_json::to_value(&aws_integration).unwrap(), json);
assert_eq!(aws_integration, serde_json::from_value(json).unwrap());
}
#[rstest]
#[case(AWSServiceName::EC2, "EC2")]
#[case(AWSServiceName::ELB, "ELB")]
#[case(AWSServiceName::ALB, "ALB")]
#[case(AWSServiceName::NLB, "NLB")]
#[case(AWSServiceName::RDS, "RDS")]
#[case(AWSServiceName::Redshift, "Redshift")]
#[case(AWSServiceName::ElastiCache, "ElastiCache")]
#[case(AWSServiceName::SQS, "SQS")]
#[case(AWSServiceName::Lambda, "Lambda")]
#[case(AWSServiceName::DynamoDB, "DynamoDB")]
#[case(AWSServiceName::CloudFront, "CloudFront")]
#[case(AWSServiceName::APIGateway, "APIGateway")]
#[case(AWSServiceName::Kinesis, "Kinesis")]
#[case(AWSServiceName::S3, "S3")]
#[case(AWSServiceName::ES, "ES")]
#[case(AWSServiceName::ECSCluster, "ECSCluster")]
#[case(AWSServiceName::SES, "SES")]
#[case(AWSServiceName::States, "States")]
#[case(AWSServiceName::EFS, "EFS")]
#[case(AWSServiceName::Firehose, "Firehose")]
#[case(AWSServiceName::Batch, "Batch")]
#[case(AWSServiceName::WAF, "WAF")]
#[case(AWSServiceName::Billing, "Billing")]
#[case(AWSServiceName::Route53, "Route53")]
#[case(AWSServiceName::Connect, "Connect")]
#[case(AWSServiceName::DocDB, "DocDB")]
#[case(AWSServiceName::CodeBuild, "CodeBuild")]
#[case(AWSServiceName::Athena, "Athena")]
fn test_aws_service(#[case] aws_service: AWSServiceName, #[case] aws_service_str: &str) {
assert_eq!(aws_service.to_string(), aws_service_str);
assert_eq!(aws_service, aws_service_str.parse().unwrap());
assert_eq!(
aws_service,
serde_json::from_value(aws_service_str.into()).unwrap()
);
assert_eq!(serde_json::to_value(aws_service).unwrap(), aws_service_str);
}
}
impl Client {
pub async fn list_aws_integrations(&self) -> Result<Vec<AWSIntegration>> {
self.request(
Method::GET,
"/api/v0/aws-integrations",
query_params![],
request_body![],
response_body! { aws_integrations: Vec<AWSIntegration> },
)
.await
}
pub async fn create_aws_integration(
&self,
aws_integration_value: impl Borrow<AWSIntegrationValue>,
) -> Result<AWSIntegration> {
self.request(
Method::POST,
"/api/v0/aws-integrations",
query_params![],
request_body!(aws_integration_value.borrow()),
response_body!(..),
)
.await
}
pub async fn get_aws_integration(
&self,
aws_integration_id: impl Into<AWSIntegrationId>,
) -> Result<AWSIntegration> {
self.request(
Method::GET,
format_url!("/api/v0/aws-integrations/{}", aws_integration_id),
query_params![],
request_body![],
response_body!(..),
)
.await
}
pub async fn update_aws_integration(
&self,
aws_integration_id: impl Into<AWSIntegrationId>,
aws_integration_value: impl Borrow<AWSIntegrationValue>,
) -> Result<AWSIntegration> {
self.request(
Method::PUT,
format_url!("/api/v0/aws-integrations/{}", aws_integration_id),
query_params![],
request_body!(aws_integration_value.borrow()),
response_body!(..),
)
.await
}
pub async fn delete_aws_integration(
&self,
aws_integration_id: impl Into<AWSIntegrationId>,
) -> Result<AWSIntegration> {
self.request(
Method::DELETE,
format_url!("/api/v0/aws-integrations/{}", aws_integration_id),
query_params![],
request_body![],
response_body!(..),
)
.await
}
pub async fn create_aws_integration_external_id(&self) -> Result<String> {
self.request(
Method::POST,
"/api/v0/aws-integrations-external-id",
query_params![],
request_body![],
response_body! { externalId: String },
)
.await
}
pub async fn list_aws_integration_metrics(
&self,
) -> Result<HashMap<AWSServiceName, Vec<String>>> {
self.request(
Method::GET,
"/api/v0/aws-integrations-excludable-metrics",
query_params![],
request_body![],
response_body!(..),
)
.await
}
}
#[cfg(test)]
mod client_tests {
use serde_json::json;
use crate::aws_integration::*;
use crate::tests::*;
fn value_example() -> AWSIntegrationValue {
AWSIntegrationValue::builder()
.name("AWS integration setting")
.memo("This is an AWS integration memo.")
.role_arn("aws-role-arn")
.external_id("aws-integration-external-id")
.region("ap-northeast-1")
.services([(
AWSServiceName::EC2,
AWSServiceConfig::builder()
.role("aws: ec2")
.retire_automatically(true)
.build(),
)])
.build()
}
fn entity_example() -> AWSIntegration {
AWSIntegration {
id: AWSIntegrationId::from("aws0"),
value: value_example(),
}
}
fn value_json_example() -> serde_json::Value {
json!({
"name": "AWS integration setting",
"memo": "This is an AWS integration memo.",
"roleArn": "aws-role-arn",
"externalId": "aws-integration-external-id",
"region": "ap-northeast-1",
"includedTags": "",
"excludedTags": "",
"services": {
"EC2": {
"enable": true,
"role": "aws:ec2",
"retireAutomatically": true,
},
},
})
}
fn entity_json_example() -> serde_json::Value {
let mut json = value_json_example();
json["id"] = json!("aws0");
json
}
#[async_std::test]
async fn list_aws_integrations() {
let server = test_server! {
method = GET,
path = "/api/v0/aws-integrations",
response = json!({
"aws_integrations": [entity_json_example()],
}),
};
assert_eq!(
test_client!(server).list_aws_integrations().await,
Ok(vec![entity_example()]),
);
}
#[async_std::test]
async fn create_aws_integration() {
let server = test_server! {
method = POST,
path = "/api/v0/aws-integrations",
request = value_json_example(),
response = entity_json_example(),
};
assert_eq!(
test_client!(server)
.create_aws_integration(value_example())
.await,
Ok(entity_example()),
);
assert_eq!(
test_client!(server)
.create_aws_integration(&value_example())
.await,
Ok(entity_example()),
);
}
#[async_std::test]
async fn get_aws_integration() {
let server = test_server! {
method = GET,
path = "/api/v0/aws-integrations/aws0",
response = entity_json_example(),
};
assert_eq!(
test_client!(server).get_aws_integration("aws0").await,
Ok(entity_example()),
);
assert_eq!(
test_client!(server)
.get_aws_integration(AWSIntegrationId::from("aws0"))
.await,
Ok(entity_example()),
);
}
#[async_std::test]
async fn update_aws_integration() {
let server = test_server! {
method = PUT,
path = "/api/v0/aws-integrations/aws0",
request = value_json_example(),
response = entity_json_example(),
};
assert_eq!(
test_client!(server)
.update_aws_integration("aws0", value_example())
.await,
Ok(entity_example()),
);
assert_eq!(
test_client!(server)
.update_aws_integration(AWSIntegrationId::from("aws0"), &value_example())
.await,
Ok(entity_example()),
);
}
#[async_std::test]
async fn delete_aws_integration() {
let server = test_server! {
method = DELETE,
path = "/api/v0/aws-integrations/aws0",
response = entity_json_example(),
};
assert_eq!(
test_client!(server).delete_aws_integration("aws0").await,
Ok(entity_example()),
);
assert_eq!(
test_client!(server)
.delete_aws_integration(AWSIntegrationId::from("aws0"))
.await,
Ok(entity_example()),
);
}
#[async_std::test]
async fn create_aws_integration_external_id() {
let server = test_server! {
method = POST,
path = "/api/v0/aws-integrations-external-id",
response = json!({ "externalId": "external0" }),
};
assert_eq!(
test_client!(server)
.create_aws_integration_external_id()
.await,
Ok("external0".to_owned())
);
}
#[async_std::test]
async fn list_aws_integration_metrics() {
let server = test_server! {
method = GET,
path = "/api/v0/aws-integrations-excludable-metrics",
response = json!({
"EC2": ["ec2.cpu.used"],
"ELB": ["elb.count.request_count"],
"ALB": ["alb.request.count"],
"NLB": ["nlb.flowcount.active"],
"RDS": ["rds.cpu.used"],
"Redshift": ["redshift.cpu.used"],
"ElastiCache": ["elasticache.cpu.used"],
"SQS": ["sqs.oldest_message.age"],
"Lambda": ["lambda.count.invocations"],
"DynamoDB": ["dynamodb.read_capacity_units.provisioned"],
"CloudFront": ["cloudfront.requests.request"],
"APIGateway": ["apigateway.requests.count"],
"Kinesis": ["kinesis.bytes.get_records"],
"S3": ["s3.requests.all_requests"],
"ES": ["es.cluster_status.green"],
"ECSCluster": ["ecs.cpu_utilization.maximum"],
"SES": ["ses.email_sending_events.send"],
"States": ["states.executions.aborted"],
"EFS": ["efs.burst_credit_balance.minimum"],
"Firehose": ["firehose.service_bytes.backup_to_s3"],
"Batch": ["batch.job_queue_status.#.succeeded"],
"WAF": ["waf.web_acl_requests.#.allowed"],
"Billing": ["billing.estimated_charges.total"],
"Route53": ["route53.dns_queries.*"],
"Connect": ["connect.voice_calls.breaching_concurrency_quota"],
"DocDB": ["docdb.cpu.used"],
"CodeBuild": ["codebuild.builds.count"],
"Athena": ["athena.processed_bytes.#.average"],
"UnknownService": [],
}),
};
assert_eq!(
test_client!(server)
.list_aws_integration_metrics()
.await
.map(|_| ()),
Ok(())
);
}
}