use std::path::PathBuf;
use clap::{Args, Parser, Subcommand};
use greentic_deploy_spec::CapabilitySlot;
use crate::environment::LocalFsStore;
use super::{OpError, OpFlags, OpOutcome, render_error};
#[derive(Parser, Debug)]
#[command(
after_help = "Nouns: env, env-packs, bundles, revisions, traffic, config, credentials, secrets.\n\
Every verb honors:\n\
--schema dump the JSON schema of the payload it would accept, then exit\n\
--answers <PATH> read the payload from a JSON or YAML file\n\n\
Examples:\n\
greentic-operator op env create --answers env.json\n\
greentic-operator op revisions warm --answers warm.yaml\n\
greentic-operator op env show <env-id>\n\n\
Errors are written to stderr as a JSON envelope:\n\
{\"op\":\"<verb>\",\"noun\":\"<noun>\",\"error\":{\"kind\":\"…\",\"message\":\"…\"}}\n\
Success output goes to stdout as:\n\
{\"op\":\"<verb>\",\"noun\":\"<noun>\",\"result\":…}"
)]
pub struct OpCommand {
#[arg(long, global = true)]
pub store_root: Option<PathBuf>,
#[arg(long, global = true)]
pub schema: bool,
#[arg(long, global = true)]
pub answers: Option<PathBuf>,
#[command(subcommand)]
pub noun: OpNoun,
}
#[derive(Subcommand, Debug)]
pub enum OpNoun {
Env {
#[command(subcommand)]
verb: EnvVerb,
},
EnvPacks {
#[command(subcommand)]
verb: EnvPacksVerb,
},
Bundles {
#[command(subcommand)]
verb: BundlesVerb,
},
Revisions {
#[command(subcommand)]
verb: RevisionsVerb,
},
Traffic {
#[command(subcommand)]
verb: TrafficVerb,
},
Config {
#[command(subcommand)]
verb: ConfigVerb,
},
Credentials {
#[command(subcommand)]
verb: CredentialsVerb,
},
Secrets {
#[command(subcommand)]
verb: SecretsVerb,
},
}
#[derive(Subcommand, Debug)]
pub enum EnvVerb {
Init,
Create,
Update,
List,
Show {
env_id: String,
},
Doctor {
env_id: String,
},
ToolCheck {
env_id: String,
},
Destroy {
env_id: String,
#[arg(long)]
confirm: bool,
},
MigrateDev {
target: String,
#[arg(long, conflicts_with = "apply")]
check: bool,
#[arg(long, conflicts_with = "check")]
apply: bool,
},
MigrateState {
target: String,
#[arg(long, conflicts_with = "apply")]
check: bool,
#[arg(long, conflicts_with = "check")]
apply: bool,
#[arg(long = "state-dir")]
state_dir: Option<PathBuf>,
},
}
#[derive(Subcommand, Debug)]
pub enum EnvPacksVerb {
Add,
Update,
Remove,
Rollback,
List { env_id: String },
}
#[derive(Subcommand, Debug)]
pub enum BundlesVerb {
Add,
Update,
Remove,
List { env_id: String },
}
#[derive(Subcommand, Debug)]
pub enum RevisionsVerb {
Stage,
Warm,
Drain,
Archive,
List { env_id: String },
}
#[derive(Subcommand, Debug)]
pub enum TrafficVerb {
Set(TrafficSetArgs),
Show(TrafficTargetArgs),
Rollback(TrafficTargetArgs),
}
#[derive(Args, Debug)]
pub struct TrafficSetArgs {
pub env_id: Option<String>,
pub entries: Vec<String>,
#[arg(long)]
pub deployment: Option<String>,
#[arg(long)]
pub idempotency_key: Option<String>,
#[arg(long)]
pub updated_by: Option<String>,
#[arg(long)]
pub authorization_ref: Option<PathBuf>,
}
#[derive(Args, Debug)]
pub struct TrafficTargetArgs {
pub env_id: Option<String>,
#[arg(long)]
pub deployment: Option<String>,
}
#[derive(Subcommand, Debug)]
pub enum ConfigVerb {
Show,
Set,
}
#[derive(Subcommand, Debug)]
pub enum CredentialsVerb {
Requirements,
Bootstrap,
Rotate,
}
#[derive(Subcommand, Debug)]
pub enum SecretsVerb {
List,
Put,
Get,
Rotate,
}
#[derive(Args, Debug)]
pub struct PayloadArg {
#[arg(long, conflicts_with = "answers")]
pub payload_json: Option<String>,
}
pub fn build_store(cmd: &OpCommand) -> Result<LocalFsStore, OpError> {
let root = match &cmd.store_root {
Some(p) => p.clone(),
None => LocalFsStore::default_root().ok_or_else(|| {
OpError::InvalidArgument("no --store-root and HOME / USERPROFILE not set".to_string())
})?,
};
Ok(LocalFsStore::new(root))
}
pub fn print_outcome(outcome: &OpOutcome) -> Result<(), OpError> {
let value = serde_json::to_value(outcome)
.map_err(|e| OpError::InvalidArgument(format!("serialize outcome: {e}")))?;
println!("{value}");
Ok(())
}
pub fn print_error(noun: &'static str, op: &'static str, err: &OpError) {
let value = render_error(noun, op, err);
eprintln!("{value}");
}
pub fn dispatch_op(cmd: OpCommand) -> Result<(), OpError> {
let flags = OpFlags {
schema_only: cmd.schema,
answers: cmd.answers.clone(),
};
let (noun, verb) = noun_verb_labels(&cmd.noun);
let store = build_store(&cmd).inspect_err(|err| print_error(noun, verb, err))?;
let result = match cmd.noun {
OpNoun::Env { verb } => dispatch_env(&store, &flags, verb),
OpNoun::EnvPacks { verb } => dispatch_env_packs(&store, &flags, verb),
OpNoun::Bundles { verb } => dispatch_bundles(&store, &flags, verb),
OpNoun::Revisions { verb } => dispatch_revisions(&store, &flags, verb),
OpNoun::Traffic { verb } => dispatch_traffic(&store, &flags, verb),
OpNoun::Config { verb } => dispatch_config(&store, &flags, verb),
OpNoun::Credentials { verb } => dispatch_credentials(&store, &flags, verb),
OpNoun::Secrets { verb } => dispatch_secrets(&store, &flags, verb),
};
result.inspect_err(|err| print_error(noun, verb, err))
}
pub fn noun_verb_labels(noun: &OpNoun) -> (&'static str, &'static str) {
match noun {
OpNoun::Env { verb } => (
"env",
match verb {
EnvVerb::Init => "init",
EnvVerb::Create => "create",
EnvVerb::Update => "update",
EnvVerb::List => "list",
EnvVerb::Show { .. } => "show",
EnvVerb::Doctor { .. } => "doctor",
EnvVerb::ToolCheck { .. } => "tool-check",
EnvVerb::Destroy { .. } => "destroy",
EnvVerb::MigrateDev { .. } => "migrate-dev",
EnvVerb::MigrateState { .. } => "migrate-state",
},
),
OpNoun::EnvPacks { verb } => (
"env-packs",
match verb {
EnvPacksVerb::Add => "add",
EnvPacksVerb::Update => "update",
EnvPacksVerb::Remove => "remove",
EnvPacksVerb::Rollback => "rollback",
EnvPacksVerb::List { .. } => "list",
},
),
OpNoun::Bundles { verb } => (
"bundles",
match verb {
BundlesVerb::Add => "add",
BundlesVerb::Update => "update",
BundlesVerb::Remove => "remove",
BundlesVerb::List { .. } => "list",
},
),
OpNoun::Revisions { verb } => (
"revisions",
match verb {
RevisionsVerb::Stage => "stage",
RevisionsVerb::Warm => "warm",
RevisionsVerb::Drain => "drain",
RevisionsVerb::Archive => "archive",
RevisionsVerb::List { .. } => "list",
},
),
OpNoun::Traffic { verb } => (
"traffic",
match verb {
TrafficVerb::Set(_) => "set",
TrafficVerb::Show(_) => "show",
TrafficVerb::Rollback(_) => "rollback",
},
),
OpNoun::Config { verb } => (
"config",
match verb {
ConfigVerb::Show => "show",
ConfigVerb::Set => "set",
},
),
OpNoun::Credentials { verb } => (
"credentials",
match verb {
CredentialsVerb::Requirements => "requirements",
CredentialsVerb::Bootstrap => "bootstrap",
CredentialsVerb::Rotate => "rotate",
},
),
OpNoun::Secrets { verb } => (
"secrets",
match verb {
SecretsVerb::List => "list",
SecretsVerb::Put => "put",
SecretsVerb::Get => "get",
SecretsVerb::Rotate => "rotate",
},
),
}
}
fn dispatch_env(store: &LocalFsStore, flags: &OpFlags, verb: EnvVerb) -> Result<(), OpError> {
let outcome = match verb {
EnvVerb::Init => super::env::init(store, flags)?,
EnvVerb::Create => super::env::create(store, flags, None)?,
EnvVerb::Update => super::env::update(store, flags, None)?,
EnvVerb::List => super::env::list(store, flags)?,
EnvVerb::Show { env_id } => super::env::show(store, flags, &env_id)?,
EnvVerb::Doctor { env_id } => super::env::doctor(store, flags, &env_id)?,
EnvVerb::ToolCheck { env_id } => super::env::tool_check(store, flags, &env_id)?,
EnvVerb::Destroy { env_id, confirm } => {
super::env::destroy(store, flags, &env_id, confirm)?
}
EnvVerb::MigrateDev {
target,
check,
apply,
} => {
if !(check ^ apply) {
return Err(OpError::InvalidArgument(
"migrate-dev requires exactly one of --check or --apply".to_string(),
));
}
if check {
super::migrate::check(store, flags, &target)?
} else {
super::migrate::apply(store, flags, &target)?
}
}
EnvVerb::MigrateState {
target,
check,
apply,
state_dir,
} => {
if !(check ^ apply) {
return Err(OpError::InvalidArgument(
"migrate-state requires exactly one of --check or --apply".to_string(),
));
}
if check {
super::migrate_state::check(store, flags, &target, state_dir.as_deref())?
} else {
super::migrate_state::apply(store, flags, &target, state_dir.as_deref())?
}
}
};
print_outcome(&outcome)
}
fn dispatch_env_packs(
store: &LocalFsStore,
flags: &OpFlags,
verb: EnvPacksVerb,
) -> Result<(), OpError> {
let outcome = match verb {
EnvPacksVerb::Add => super::env_packs::add(store, flags, None)?,
EnvPacksVerb::Update => super::env_packs::update(store, flags, None)?,
EnvPacksVerb::Remove => super::env_packs::remove(store, flags, None)?,
EnvPacksVerb::Rollback => super::env_packs::rollback(store, flags, None)?,
EnvPacksVerb::List { env_id } => super::env_packs::list(store, flags, &env_id)?,
};
print_outcome(&outcome)
}
fn dispatch_bundles(
store: &LocalFsStore,
flags: &OpFlags,
verb: BundlesVerb,
) -> Result<(), OpError> {
let outcome = match verb {
BundlesVerb::Add => super::bundles::add(store, flags, None)?,
BundlesVerb::Update => super::bundles::update(store, flags, None)?,
BundlesVerb::Remove => super::bundles::remove(store, flags, None)?,
BundlesVerb::List { env_id } => super::bundles::list(store, flags, &env_id)?,
};
print_outcome(&outcome)
}
fn dispatch_revisions(
store: &LocalFsStore,
flags: &OpFlags,
verb: RevisionsVerb,
) -> Result<(), OpError> {
let outcome = match verb {
RevisionsVerb::Stage => super::revisions::stage(store, flags, None)?,
RevisionsVerb::Warm => super::revisions::warm(store, flags, None)?,
RevisionsVerb::Drain => super::revisions::drain(store, flags, None)?,
RevisionsVerb::Archive => super::revisions::archive(store, flags, None)?,
RevisionsVerb::List { env_id } => super::revisions::list(store, flags, &env_id)?,
};
print_outcome(&outcome)
}
fn dispatch_traffic(
store: &LocalFsStore,
flags: &OpFlags,
verb: TrafficVerb,
) -> Result<(), OpError> {
let outcome = match verb {
TrafficVerb::Set(args) => {
let payload = super::traffic::payload_from_set_args(args)?;
super::traffic::set(store, flags, payload)?
}
TrafficVerb::Show(args) => {
let payload = super::traffic::payload_from_target_args(args)?;
super::traffic::show(store, flags, payload)?
}
TrafficVerb::Rollback(args) => {
let payload = super::traffic::payload_from_target_args(args)?;
super::traffic::rollback(store, flags, payload)?
}
};
print_outcome(&outcome)
}
fn dispatch_config(store: &LocalFsStore, flags: &OpFlags, verb: ConfigVerb) -> Result<(), OpError> {
let outcome = match verb {
ConfigVerb::Show => super::config::show(store, flags, None)?,
ConfigVerb::Set => super::config::set(store, flags, None)?,
};
print_outcome(&outcome)
}
fn dispatch_credentials(
store: &LocalFsStore,
flags: &OpFlags,
verb: CredentialsVerb,
) -> Result<(), OpError> {
let outcome = match verb {
CredentialsVerb::Requirements => super::credentials::requirements(store, flags, None)?,
CredentialsVerb::Bootstrap => super::credentials::bootstrap(store, flags, None)?,
CredentialsVerb::Rotate => super::credentials::rotate(store, flags, None)?,
};
print_outcome(&outcome)
}
fn dispatch_secrets(
store: &LocalFsStore,
flags: &OpFlags,
verb: SecretsVerb,
) -> Result<(), OpError> {
let outcome = match verb {
SecretsVerb::List => super::secrets::list(store, flags, None)?,
SecretsVerb::Put => super::secrets::put(store, flags, None)?,
SecretsVerb::Get => super::secrets::get(store, flags, None)?,
SecretsVerb::Rotate => super::secrets::rotate(store, flags, None)?,
};
print_outcome(&outcome)
}
#[allow(dead_code)]
fn _slot_anchor(_: CapabilitySlot) {}