fundaia 0.55.0

Command line for the Fundaia deployment platform: projects, services, variables, deployments, logs and metrics
use super::*;

pub struct FreezeDraft<'a> {
    pub reason: &'a str,
    pub environment: Option<&'a str>,
    pub until: Option<&'a str>,
    pub window: Option<&'a str>,
    pub timezone: Option<&'a str>,
}

pub struct ScheduledActionDraft<'a> {
    pub action: &'a str,
    pub at: Option<&'a str>,
    pub cron: Option<&'a str>,
    pub timezone: Option<&'a str>,
    pub confirm: Option<&'a str>,
}

pub struct AutoPromotionDraft<'a> {
    pub from: &'a str,
    pub to: &'a str,
    pub services: &'a [String],
    pub healthy_minutes: Option<u32>,
    pub confirm: Option<&'a str>,
}

impl ApiClient {
    pub async fn freeze_status(&self, project_id: &str) -> Result<Answer<FreezeStatus>> {
        self.get_answer(&format!(
            "/api/infra/projects/{project_id}/freezes{}",
            self.environment_query('?')
        ))
        .await
    }

    pub async fn freeze(
        &self,
        project_id: &str,
        draft: &FreezeDraft<'_>,
    ) -> Result<Answer<FreezeAnswer>> {
        self.post_answer(
            &format!("/api/infra/projects/{project_id}/freezes"),
            &freeze_body(draft),
        )
        .await
    }

    pub async fn lift_freeze(
        &self,
        project_id: &str,
        freeze_id: &str,
    ) -> Result<Answer<FreezeAnswer>> {
        self.answer(
            Method::DELETE,
            &format!(
                "/api/infra/projects/{project_id}/freezes/{}",
                utf8_percent_encode(freeze_id, NON_ALPHANUMERIC)
            ),
            None::<&()>,
        )
        .await
    }

    pub async fn protect_environment(
        &self,
        project_id: &str,
        environment: &str,
        protect: bool,
        confirm: Option<&str>,
    ) -> Result<Answer<ProtectionAnswer>> {
        let mut body = serde_json::Map::new();
        body.insert("protected".into(), protect.into());
        if let Some(confirm) = confirm {
            body.insert("confirm".into(), confirm.into());
        }
        self.answer(
            Method::PUT,
            &format!(
                "/api/infra/projects/{project_id}/environments/{}/protection",
                utf8_percent_encode(environment, NON_ALPHANUMERIC)
            ),
            Some(&serde_json::Value::Object(body)),
        )
        .await
    }

    pub async fn scheduled_actions(
        &self,
        service_environment_id: &str,
    ) -> Result<Answer<ScheduledActionsPage>> {
        self.get_answer(&format!(
            "/api/infra/services/{service_environment_id}/scheduled-actions"
        ))
        .await
    }

    pub async fn schedule_action(
        &self,
        service_environment_id: &str,
        draft: &ScheduledActionDraft<'_>,
    ) -> Result<Answer<ScheduledActionAnswer>> {
        self.post_answer(
            &format!("/api/infra/services/{service_environment_id}/scheduled-actions"),
            &action_body(draft),
        )
        .await
    }

    pub async fn remove_scheduled_action(
        &self,
        service_environment_id: &str,
        action_id: &str,
    ) -> Result<serde_json::Value> {
        self.delete_for(&format!(
            "/api/infra/services/{service_environment_id}/scheduled-actions/{}",
            utf8_percent_encode(action_id, NON_ALPHANUMERIC)
        ))
        .await
    }

    pub async fn auto_promotions(&self, project_id: &str) -> Result<Answer<AutoPromotionsPage>> {
        self.get_answer(&format!("/api/infra/projects/{project_id}/auto-promotions"))
            .await
    }

    pub async fn add_auto_promotion(
        &self,
        project_id: &str,
        draft: &AutoPromotionDraft<'_>,
    ) -> Result<Answer<AutoPromotionAnswer>> {
        self.post_answer(
            &format!("/api/infra/projects/{project_id}/auto-promotions"),
            &rule_body(draft),
        )
        .await
    }

    pub async fn remove_auto_promotion(
        &self,
        project_id: &str,
        rule_id: &str,
    ) -> Result<serde_json::Value> {
        self.delete_for(&format!(
            "/api/infra/projects/{project_id}/auto-promotions/{}",
            utf8_percent_encode(rule_id, NON_ALPHANUMERIC)
        ))
        .await
    }
}

fn freeze_body(draft: &FreezeDraft<'_>) -> serde_json::Value {
    let mut body = serde_json::Map::new();
    body.insert("reason".into(), draft.reason.into());
    for (key, value) in [
        ("environment", draft.environment),
        ("until", draft.until),
        ("window", draft.window),
        ("timezone", draft.timezone),
    ] {
        if let Some(value) = value {
            body.insert(key.into(), value.into());
        }
    }
    serde_json::Value::Object(body)
}

fn action_body(draft: &ScheduledActionDraft<'_>) -> serde_json::Value {
    let mut body = serde_json::Map::new();
    body.insert("action".into(), draft.action.into());
    for (key, value) in [
        ("at", draft.at),
        ("cron", draft.cron),
        ("timezone", draft.timezone),
        ("confirm", draft.confirm),
    ] {
        if let Some(value) = value {
            body.insert(key.into(), value.into());
        }
    }
    serde_json::Value::Object(body)
}

fn rule_body(draft: &AutoPromotionDraft<'_>) -> serde_json::Value {
    let mut body = serde_json::Map::new();
    body.insert("from".into(), draft.from.into());
    body.insert("to".into(), draft.to.into());
    if !draft.services.is_empty() {
        body.insert("services".into(), draft.services.to_vec().into());
    }
    if let Some(minutes) = draft.healthy_minutes {
        body.insert("healthyMinutes".into(), minutes.into());
    }
    if let Some(confirm) = draft.confirm {
        body.insert("confirm".into(), confirm.into());
    }
    serde_json::Value::Object(body)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_should_send_only_the_reason_for_a_freeze_from_now() {
        let draft = FreezeDraft {
            reason: "Black Friday",
            environment: None,
            until: None,
            window: None,
            timezone: None,
        };
        assert_eq!(
            freeze_body(&draft),
            serde_json::json!({ "reason": "Black Friday" })
        );
    }

    #[test]
    fn it_should_send_a_recurring_window_with_its_environment() {
        let draft = FreezeDraft {
            reason: "Fin de semana",
            environment: Some("production"),
            until: None,
            window: Some("V 18:00 - L 08:00"),
            timezone: None,
        };
        assert_eq!(
            freeze_body(&draft),
            serde_json::json!({ "reason": "Fin de semana", "environment": "production", "window": "V 18:00 - L 08:00" })
        );
    }

    #[test]
    fn it_should_leave_the_services_out_of_a_rule_for_all_of_them() {
        let draft = AutoPromotionDraft {
            from: "staging",
            to: "production",
            services: &[],
            healthy_minutes: Some(15),
            confirm: None,
        };
        assert_eq!(
            rule_body(&draft),
            serde_json::json!({ "from": "staging", "to": "production", "healthyMinutes": 15 })
        );
    }
}