greentic-deployer-dev 1.1.27434236067

Greentic deployer runtime for plan construction and deployment-pack dispatch
use std::collections::BTreeMap;
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::str::FromStr;

use greentic_deployer::pack_introspect::build_plan;
use greentic_deployer::{
    config::{DeployerConfig, OutputFormat, Provider},
    contract::DeployerCapability,
};
use greentic_types::SemverReq;
use greentic_types::cbor::encode_pack_manifest;
use greentic_types::component::{ComponentCapabilities, ComponentManifest, ComponentProfiles};
use greentic_types::flow::{
    ComponentRef, Flow, FlowHasher, FlowKind, FlowMetadata, InputMapping, Node, OutputMapping,
    Routing,
};
use greentic_types::pack_manifest::{PackDependency, PackKind, PackManifest};
use greentic_types::{ComponentId, FlowId, NodeId, PackId};
use indexmap::IndexMap;
use semver::Version;
use tar::Builder;

fn sample_component(id: &str, http_server: bool) -> ComponentManifest {
    let host_caps = greentic_types::component::HostCapabilities {
        http: Some(greentic_types::component::HttpCapabilities {
            client: true,
            server: http_server,
        }),
        ..Default::default()
    };
    ComponentManifest {
        id: ComponentId::from_str(id).unwrap(),
        version: Version::new(0, 1, 0),
        supports: vec![FlowKind::Messaging, FlowKind::Http],
        world: "greentic:test/world".to_string(),
        profiles: ComponentProfiles {
            default: Some("http_endpoint".to_string()),
            supported: vec![
                "http_endpoint".to_string(),
                "long_lived_service".to_string(),
            ],
        },
        capabilities: ComponentCapabilities {
            host: host_caps,
            ..Default::default()
        },
        configurators: None,
        operations: Vec::new(),
        config_schema: None,
        resources: Default::default(),
        dev_flows: Default::default(),
    }
}

fn sample_flow(id: &str, kind: FlowKind, component: &ComponentManifest) -> Flow {
    let mut nodes: IndexMap<NodeId, Node, FlowHasher> = IndexMap::default();
    nodes.insert(
        NodeId::from_str("start").unwrap(),
        Node {
            id: NodeId::from_str("start").unwrap(),
            component: ComponentRef {
                id: component.id.clone(),
                pack_alias: None,
                operation: None,
            },
            input: InputMapping {
                mapping: serde_json::Value::Null,
            },
            output: OutputMapping {
                mapping: serde_json::Value::Null,
            },
            err_map: None,
            routing: Routing::End,
            telemetry: Default::default(),
        },
    );

    let mut entrypoints = BTreeMap::new();
    entrypoints.insert("default".to_string(), serde_json::Value::Null);

    Flow {
        schema_version: "flowir-v1".to_string(),
        id: FlowId::from_str(id).unwrap(),
        kind,
        entrypoints,
        nodes,
        metadata: FlowMetadata::default(),
    }
}

fn sample_manifest() -> PackManifest {
    let http_component = sample_component("dev.greentic.http", true);
    let msg_component = sample_component("dev.greentic.msg", false);

    let flows = vec![
        greentic_types::pack_manifest::PackFlowEntry {
            id: FlowId::from_str("chat_flow").unwrap(),
            kind: FlowKind::Messaging,
            flow: sample_flow("chat_flow", FlowKind::Messaging, &msg_component),
            tags: vec!["messaging".to_string()],
            entrypoints: vec!["default".to_string()],
        },
        greentic_types::pack_manifest::PackFlowEntry {
            id: FlowId::from_str("http_flow").unwrap(),
            kind: FlowKind::Http,
            flow: sample_flow("http_flow", FlowKind::Http, &http_component),
            tags: vec!["http".to_string()],
            entrypoints: vec!["default".to_string()],
        },
    ];

    PackManifest {
        agents: Default::default(),
        schema_version: "pack-v1".to_string(),
        pack_id: PackId::from_str("dev.greentic.sample").unwrap(),
        name: None,
        version: Version::new(0, 1, 0),
        kind: PackKind::Application,
        publisher: "greentic".to_string(),
        secret_requirements: Vec::new(),
        components: vec![http_component, msg_component],
        flows,
        dependencies: vec![PackDependency {
            alias: "common".to_string(),
            pack_id: PackId::from_str("dev.greentic.common").unwrap(),
            version_req: SemverReq::parse("*").unwrap(),
            required_capabilities: vec![],
        }],
        capabilities: Vec::new(),
        signatures: Default::default(),
        bootstrap: None,
        extensions: None,
    }
}

fn write_gtpack_tar(manifest: &PackManifest, path: &Path) {
    let encoded = encode_pack_manifest(manifest).expect("encode manifest");
    let mut builder = Builder::new(Vec::new());

    let mut manifest_header = tar::Header::new_gnu();
    manifest_header.set_size(encoded.len() as u64);
    manifest_header.set_mode(0o644);
    manifest_header.set_cksum();
    builder
        .append_data(&mut manifest_header, "manifest.cbor", encoded.as_slice())
        .expect("append manifest");

    let dummy = b"wasm";
    let mut comp_header = tar::Header::new_gnu();
    comp_header.set_size(dummy.len() as u64);
    comp_header.set_mode(0o644);
    comp_header.set_cksum();
    builder
        .append_data(
            &mut comp_header,
            "components/dev.greentic.http.wasm",
            dummy.as_slice(),
        )
        .expect("append component");

    let bytes = builder.into_inner().expect("tar bytes");
    let mut file = fs::File::create(path).expect("create tar");
    file.write_all(&bytes).expect("write tar");
}

fn write_directory_pack(manifest: &PackManifest, root: &Path) {
    let encoded = encode_pack_manifest(manifest).expect("encode manifest");
    fs::create_dir_all(root.join("components")).expect("mkdir components");
    fs::write(root.join("manifest.cbor"), encoded).expect("write manifest");
    fs::write(root.join("components/dev.greentic.http.wasm"), b"wasm").expect("write component");
}

fn default_config(pack_path: PathBuf) -> DeployerConfig {
    DeployerConfig {
        capability: DeployerCapability::Plan,
        provider: Provider::Aws,
        strategy: "iac-only".into(),
        tenant: "acme".into(),
        environment: "dev".into(),
        pack_path,
        bundle_root: None,
        providers_dir: PathBuf::from("providers/deployer"),
        packs_dir: PathBuf::from("packs"),
        provider_pack: None,
        pack_ref: None,
        distributor_url: None,
        distributor_token: None,
        preview: false,
        dry_run: false,
        execute_local: false,
        output: OutputFormat::Text,
        greentic: greentic_config::ConfigResolver::new()
            .load()
            .expect("load default config")
            .config,
        provenance: greentic_config::ProvenanceMap::new(),
        config_warnings: Vec::new(),
        deploy_pack_id_override: None,
        deploy_flow_id_override: None,
        bundle_source: None,
        bundle_digest: None,
        repo_registry_base: None,
        store_registry_base: None,
    }
}

#[test]
fn builds_plan_from_tar_gtpack() {
    let manifest = sample_manifest();
    let base = std::env::current_dir()
        .expect("cwd")
        .join("target/tmp-tests");
    std::fs::create_dir_all(&base).expect("create tmp base");
    let dir = tempfile::tempdir_in(base).expect("temp dir");
    let tar_path = dir.path().join("sample.gtpack");
    write_gtpack_tar(&manifest, &tar_path);

    let config = default_config(tar_path);
    let plan = build_plan(&config).expect("plan builds");
    assert_eq!(plan.plan.pack_id, manifest.pack_id.to_string());
    assert!(!plan.plan.channels.is_empty());
    assert!(!plan.components.is_empty());
}

#[test]
fn builds_plan_from_directory_pack() {
    let manifest = sample_manifest();
    let base = std::env::current_dir()
        .expect("cwd")
        .join("target/tmp-tests");
    std::fs::create_dir_all(&base).expect("create tmp base");
    let dir = tempfile::tempdir_in(base).expect("temp dir");
    write_directory_pack(&manifest, dir.path());

    let config = default_config(dir.path().to_path_buf());
    let plan = build_plan(&config).expect("plan builds");
    assert_eq!(plan.plan.pack_version, manifest.version);
    assert_eq!(plan.target, greentic_deployer::plan::Target::Aws);
    assert_eq!(plan.plan.tenant, "acme");
}