aegis-admin-tool 0.2.1

Deploy and administer an Aegis installation with local GCP credentials.
use super::{database, gcloud::Gcloud, identity, store};
use aegis_dto::{NamespaceId, namespace::ApiEndpoint};
use anyhow::{Context, Result, ensure};
use serde::{Deserialize, Serialize};
use std::{fs, path::PathBuf};

#[derive(Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub(super) struct Connection {
    pub project: String,
    pub database: String,
    pub endpoint: String,
    pub namespace: NamespaceId,
}

impl Connection {
    pub fn load(project: Option<String>) -> Result<Self> {
        let project = match project {
            Some(project) => project,
            None => {
                Gcloud::active_project()?.context("Select a gcloud project or pass --project")?
            }
        };
        let path = path(&project)?;
        let connection: Self = serde_json::from_slice(&fs::read(&path).with_context(|| format!(
            "No Aegis administration connection for {project}; run `aegis-admin setup` or `aegis-admin connect NAMESPACE --project {project} --database DATABASE`"
        ))?)?;
        ensure!(
            connection.project == project,
            "administration connection project mismatch"
        );
        connection.validate()?;
        Ok(connection)
    }
    fn validate(&self) -> Result<()> {
        Gcloud::new(self.project.clone())?;
        store::AdminOptions {
            project_id: self.project.clone(),
            database_id: self.database.clone(),
        }
        .validate()?;
        let endpoint = ApiEndpoint::parse(&self.endpoint).map_err(anyhow::Error::msg)?;
        ensure!(
            self.endpoint.starts_with("https://")
                && endpoint.namespace().is_none()
                && endpoint.service_url() == self.endpoint,
            "administration endpoint must be a canonical HTTPS service URL"
        );
        Ok(())
    }
    pub fn persist(&self) -> Result<()> {
        self.validate()?;
        capulus::store::atomic_write(
            &path(&self.project)?,
            &serde_json::to_vec_pretty(self)?,
            Some(0o600),
            Some(0o700),
        )
    }
    pub fn open(&self) -> Result<store::Admin> {
        self.validate()?;
        let token = Gcloud::new(self.project.clone())?.token()?;
        let admin = database(
            "Connecting with local GCP credentials",
            store::AdminOptions {
                project_id: self.project.clone(),
                database_id: self.database.clone(),
            }
            .validate()?
            .connect(token),
        )?;
        let issuer = database("Checking deployment identity", async {
            Ok(identity::store(&admin.db)?
                .load_config()
                .await?
                .api_token
                .iss)
        })?;
        ensure!(
            issuer == self.endpoint,
            "stored issuer changed; inspect the deployment before reconnecting"
        );
        Ok(admin)
    }
    pub fn attach(
        project: Option<String>,
        database_id: String,
        namespace: NamespaceId,
    ) -> Result<()> {
        let project = match project {
            Some(project) => project,
            None => {
                Gcloud::active_project()?.context("Select a gcloud project or pass --project")?
            }
        };
        let options = store::AdminOptions {
            project_id: project.clone(),
            database_id: database_id.clone(),
        }
        .validate()?;
        let admin = database(
            "Opening existing deployment",
            options.connect(Gcloud::new(project.clone())?.token()?),
        )?;
        let issuer = database("Discovering deployment identity", async {
            ensure!(
                admin.namespaces().await?.contains(&namespace),
                "namespace does not exist in this database"
            );
            Ok(identity::store(&admin.db)?
                .load_config()
                .await?
                .api_token
                .iss)
        })?;
        let connection = Self {
            project,
            database: database_id,
            namespace,
            endpoint: issuer,
        };
        connection.persist()?;
        aegis_tool::client::UserContext {
            api_base: ApiEndpoint::parse(&connection.endpoint)
                .map_err(anyhow::Error::msg)?
                .with_namespace(connection.namespace)
                .base_url(),
        }
        .persist()?;
        aegis_tool::ui::success(
            "Administration connection and namespace saved; cloud configuration was not changed",
        );
        Ok(())
    }
}
fn path(project: &str) -> Result<PathBuf> {
    Gcloud::new(project.into())?;
    Ok(aegis_tool::client::app_dir()?
        .join("administration")
        .join(format!("{project}.json")))
}