aegis-admin-tool 0.3.8

Deploy and administer an Aegis installation with local GCP credentials.
use serde_json::{Value, json};
use std::{fs, os::unix::fs::PermissionsExt, process::Command, time::Duration};

#[test]
fn fresh_setup_guides_sign_in_configuration_before_creating_resources() {
    let dir = tempfile::tempdir().unwrap();
    let executable = dir.path().join("gcloud");
    fs::write(&executable, include_str!("fixtures/gcloud.py")).unwrap();
    fs::set_permissions(&executable, fs::Permissions::from_mode(0o700)).unwrap();
    let state = dir.path().join("cloud.json");
    let output = capulus::process::CaptureOptions::default()
        .validate()
        .unwrap()
        .run(
            Command::new(env!("CARGO_BIN_EXE_aegis-admin"))
                .env("HOME", dir.path())
                .env_remove("SUDO_USER")
                .env("AEGIS_TEST_CLOUD_STATE", &state)
                .env(
                    "PATH",
                    format!(
                        "{}:{}",
                        dir.path().display(),
                        std::env::var("PATH").unwrap()
                    ),
                )
                .args([
                    "--progress",
                    "plain",
                    "--color",
                    "never",
                    "setup",
                    "--project",
                    "aegis-fresh-test",
                    "--region",
                    "australia-southeast1",
                    "--yes",
                    "--no-enroll",
                    "--oauth",
                ]),
            None,
        )
        .unwrap();
    assert!(!output.status.success());
    assert!(output.stdout.is_empty());
    assert!(!output.stderr.contains('\x1b'));
    for expected in [
        "Use operator@example.com in the Google Cloud Console.",
        "https://console.cloud.google.com/auth/overview?project=aegis-fresh-test",
        "https://console.cloud.google.com/auth/clients?project=aegis-fresh-test",
        "Audience → Test users",
        "https://aegis-api-1234567890.australia-southeast1.run.app/v2/oauth/callback",
        "--oauth-client FILE",
    ] {
        assert!(output.stderr.contains(expected), "{}", output.stderr);
    }
    let cloud: Value = serde_json::from_slice(&fs::read(state).unwrap()).unwrap();
    let calls = cloud["calls"].as_array().unwrap();
    assert_eq!(calls.len(), 3);
    assert_eq!(calls[0][0], "services");
    assert!(
        calls[0]
            .as_array()
            .unwrap()
            .contains(&json!("cloudresourcemanager.googleapis.com"))
    );
    assert!(
        calls[0]
            .as_array()
            .unwrap()
            .contains(&json!("cloudbilling.googleapis.com"))
    );
    assert_eq!(calls[1][0], "projects");
    assert_eq!(calls[1][1], "describe");
    assert_eq!(calls[2][0], "auth");
    assert_eq!(calls[2][1], "list");
    assert!(
        !dir.path()
            .join(".aegis/deployments/aegis-fresh-test.json")
            .exists()
    );
}

#[test]
fn administration_help_and_validation_do_not_require_a_saved_endpoint() {
    let dir = tempfile::tempdir().unwrap();
    for args in [
        vec!["--help"],
        vec!["setup", "--help"],
        vec!["authorize", "--help"],
        vec!["configure", "--help"],
        vec!["namespace-template"],
    ] {
        let result = capulus::process::CaptureOptions::default()
            .validate()
            .unwrap()
            .run(
                Command::new(env!("CARGO_BIN_EXE_aegis-admin"))
                    .args(args)
                    .env("HOME", dir.path()),
                None,
            )
            .unwrap();
        assert!(result.status.success(), "{}", result.stderr);
        assert!(!result.stdout.contains("api.hoek.io"));
    }
}

#[test]
#[ignore = "requires a loopback FIRESTORE_EMULATOR_HOST"]
fn externally_hosted_configuration_and_discovery_do_not_provision_cloud_resources() {
    assert!(
        std::env::var("FIRESTORE_EMULATOR_HOST")
            .unwrap()
            .starts_with("127.0.0.1:")
    );
    let dir = tempfile::tempdir().unwrap();
    let executable = dir.path().join("gcloud");
    fs::write(&executable, include_str!("fixtures/gcloud.py")).unwrap();
    fs::set_permissions(&executable, fs::Permissions::from_mode(0o700)).unwrap();
    let oauth = dir.path().join("oauth.json");
    fs::write(
        &oauth,
        serde_json::to_vec(&json!({"web": {
            "client_id": "external-client", "client_secret": "external-secret-never-log",
            "redirect_uris": ["https://fleet.example/custom/oauth/callback"]
        }}))
        .unwrap(),
    )
    .unwrap();
    let project = format!(
        "aegis-external-{}",
        &uuid::Uuid::new_v4().simple().to_string()[..12]
    );
    let state = dir.path().join("cloud.json");
    for discover in [false, false, true] {
        let mut command = Command::new(env!("CARGO_BIN_EXE_aegis-admin"));
        command
            .env("HOME", dir.path())
            .env_remove("SUDO_USER")
            .env("AEGIS_TEST_CLOUD_STATE", &state)
            .env(
                "PATH",
                format!(
                    "{}:{}",
                    dir.path().display(),
                    std::env::var("PATH").unwrap()
                ),
            )
            .args(["--progress", "plain"]);
        if discover {
            command.args(["connect", "personal"]);
        } else {
            command.args(["configure", "--endpoint", "https://fleet.example/custom"]);
            command.arg("--oauth-client").arg(&oauth);
        }
        command.args(["--project", &project, "--database", "custom-database"]);
        let output = capulus::process::CaptureOptions::default()
            .validate()
            .unwrap()
            .run(&mut command, None)
            .unwrap();
        assert!(output.status.success(), "{}", output.stderr);
        assert!(!output.stderr.contains("external-secret-never-log"));
        assert!(output.stdout.is_empty());
    }
    let cloud: Value = serde_json::from_slice(&fs::read(state).unwrap()).unwrap();
    assert!(
        cloud["calls"]
            .as_array()
            .unwrap()
            .iter()
            .all(|call| call[0] == "auth" && call[1] == "print-access-token")
    );
    let context = fs::read_to_string(dir.path().join(".aegis/context.toml")).unwrap();
    assert!(context.contains("https://fleet.example/custom/namespaces/personal"));
}

#[test]
#[ignore = "requires a loopback FIRESTORE_EMULATOR_HOST"]
fn setup_resumes_after_deploy_failure_without_replacing_keys_or_secrets() {
    setup_resume(None, true);
}

#[test]
#[ignore = "requires a loopback FIRESTORE_EMULATOR_HOST"]
fn proxied_setup_retains_cloud_run_authentication() {
    setup_resume(Some("proxy@example-project.iam.gserviceaccount.com"), true);
}

fn setup_resume(proxy_invoker: Option<&str>, oauth_enabled: bool) {
    assert!(
        std::env::var("FIRESTORE_EMULATOR_HOST")
            .unwrap()
            .starts_with("127.0.0.1:")
    );
    let dir = tempfile::tempdir().unwrap();
    let executable = dir.path().join("gcloud");
    fs::write(&executable, include_str!("fixtures/gcloud.py")).unwrap();
    fs::set_permissions(&executable, fs::Permissions::from_mode(0o700)).unwrap();
    let oauth = dir.path().join("oauth.json");
    fs::write(
        &oauth,
        serde_json::to_vec(
            &json!({"web": {"client_id":"test-client", "client_secret":"test-secret-never-log",
        "redirect_uris":["https://fleet.example/custom/oauth/callback"]}}),
        )
        .unwrap(),
    )
    .unwrap();
    let project = format!(
        "aegis-wizard-{}",
        &uuid::Uuid::new_v4().simple().to_string()[..12]
    );
    let image = format!("ghcr.io/khoek/aegis-api@sha256:{}", "a".repeat(64));
    let state = dir.path().join("cloud.json");
    for attempt in 0..3 {
        let mut command = Command::new(env!("CARGO_BIN_EXE_aegis-admin"));
        command
            .env("HOME", dir.path())
            .env_remove("SUDO_USER")
            .env("AEGIS_TEST_CLOUD_STATE", &state)
            .env(
                "PATH",
                format!(
                    "{}:{}",
                    dir.path().display(),
                    std::env::var("PATH").unwrap()
                ),
            )
            .args([
                "--progress",
                "plain",
                "--color",
                "never",
                "setup",
                "--project",
                &project,
                "--yes",
                "--no-enroll",
            ]);
        if let Some(invoker) = proxy_invoker {
            command
                .env("AEGIS_TEST_PROXY_INVOKER", invoker)
                .args(["--proxy-invoker", invoker]);
        }
        if attempt == 0 {
            command.args([
                "--region",
                "us-central1",
                "--endpoint",
                "https://fleet.example/custom",
                "--image",
                &image,
            ]);
            if oauth_enabled {
                command.arg("--oauth-client").arg(&oauth);
            }
        } else if attempt == 2 && oauth_enabled {
            command.arg("--oauth-client").arg(&oauth);
        }
        let output = capulus::process::CaptureOptions {
            timeout: Duration::from_secs(90),
            ..Default::default()
        }
        .validate()
        .unwrap()
        .run(&mut command, None)
        .unwrap();
        assert!(!output.status.success());
        assert!(
            output.stderr.contains("simulated image pull failure"),
            "{}",
            output.stderr
        );
        assert!(output.stderr.contains("Resources and keys are retained"));
        if attempt == 0 {
            assert!(
                output.stderr.contains("GCP still reports SERVICE_DISABLED"),
                "{}",
                output.stderr
            );
            assert!(
                output.stderr.contains("IAM access is not yet usable"),
                "{}",
                output.stderr
            );
        }
        assert!(output.stderr.contains("API runtime access verified"));
        assert!(!output.stderr.contains("test-secret-never-log"));
        assert!(!output.stderr.contains('\x1b'));
        assert!(output.stdout.is_empty(), "{}", output.stdout);
        let receipt = dir
            .path()
            .join(".aegis/deployments")
            .join(format!("{project}.json"));
        let mut receipt_value: Value =
            serde_json::from_slice(&fs::read(&receipt).unwrap()).unwrap();
        assert_eq!(
            receipt_value["secret_version"],
            if oauth_enabled {
                json!("1")
            } else {
                Value::Null
            }
        );
        assert_eq!(receipt_value["config"]["login"].is_null(), !oauth_enabled);
        assert_eq!(
            receipt_value["completed"],
            json!(["GCP resources", "Identity and certificate authorities"])
        );
        if attempt == 1 {
            // Model a process dying after Secret Manager committed but before its receipt was saved.
            receipt_value["secret_version"] = Value::Null;
            fs::write(receipt, serde_json::to_vec(&receipt_value).unwrap()).unwrap();
        }
    }
    let state: Value = serde_json::from_slice(&fs::read(state).unwrap()).unwrap();
    assert_eq!(state["secret_versions"], u64::from(oauth_enabled));
    let calls = state["calls"].as_array().unwrap();
    assert!(calls.iter().any(|call| {
        call[0] == "iam"
            && call[1] == "service-accounts"
            && call[2] == "add-iam-policy-binding"
            && call[3] == format!("aegis-api@{project}.iam.gserviceaccount.com")
            && call
                .as_array()
                .unwrap()
                .contains(&json!("--role=roles/iam.serviceAccountTokenCreator"))
    }));
    if !oauth_enabled {
        for call in state["calls"].as_array().unwrap() {
            assert_ne!(call[0], "secrets");
            assert!(
                !call
                    .as_array()
                    .unwrap()
                    .contains(&json!("secretmanager.googleapis.com"))
            );
            assert!(!call.as_array().unwrap().contains(&json!("--set-secrets")));
        }
    }
}

#[test]
#[ignore = "requires a loopback FIRESTORE_EMULATOR_HOST"]
fn default_setup_resumes_without_oauth_or_secret_manager() {
    setup_resume(None, false);
}