diff --git a/.github/workflows.src/build.inc.yml b/.github/workflows.src/build.inc.yml index e27dd9503..49aa69f9d 100644 --- a/.github/workflows.src/build.inc.yml +++ b/.github/workflows.src/build.inc.yml @@ -36,7 +36,7 @@ curl -s https://packages.edgedb.com/apt/.jsonindexes/$idx_file > /tmp/$idx_file fi out=$(cat /tmp/$idx_file | jq -r --arg REV "$rev" --arg ARCH "<< tgt.arch >>" "$jq_filter") - if [ -n "$out" ]; then + if [ -n "$out" ]; then echo 'Skip rebuilding existing << tgt.name >>' val=false fi diff --git a/src/cloud/ops.rs b/src/cloud/ops.rs index b27e2e6ca..18ea7a4f4 100644 --- a/src/cloud/ops.rs +++ b/src/cloud/ops.rs @@ -25,12 +25,23 @@ pub struct CloudInstance { pub status: String, pub version: String, pub region: String, + pub tier: CloudTier, #[serde(skip_serializing_if = "Option::is_none")] tls_ca: Option, #[serde(skip_serializing_if = "Option::is_none")] pub ui_url: Option, + pub billables: Vec, } +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct CloudInstanceResource { + pub name: String, + pub display_name: String, + pub display_unit: String, + pub display_quota: String, +} + + impl CloudInstance { pub async fn as_credentials(&self, secret_key: &str) -> anyhow::Result { let config = Builder::new() @@ -112,6 +123,14 @@ pub struct CloudInstanceCreate { // pub default_user: Option, } +#[derive(Debug, serde::Serialize)] +pub struct CloudInstanceResize { + pub name: String, + pub org: String, + pub requested_resources: Option>, + pub tier: Option, +} + #[derive(Debug, serde::Serialize)] pub struct CloudInstanceUpgrade { pub name: String, @@ -267,6 +286,27 @@ pub async fn create_cloud_instance( Ok(()) } +#[tokio::main] +pub async fn resize_cloud_instance( + client: &CloudClient, + request: &CloudInstanceResize, +) -> anyhow::Result<()> { + let url = format!("orgs/{}/instances/{}", request.org, request.name); + let operation: CloudOperation = client + .put(url, request) + .await + .or_else(|e| match e.downcast_ref::() { + Some(ErrorResponse { code: reqwest::StatusCode::NOT_FOUND, .. }) => { + anyhow::bail!( + "Instance \"{}/{}\" does not exist.", request.org, request.name); + } + _ => Err(e), + })?; + wait_instance_available_after_operation( + operation, &request.org, &request.name, client).await?; + Ok(()) +} + #[tokio::main] pub async fn upgrade_cloud_instance( client: &CloudClient, diff --git a/src/portable/create.rs b/src/portable/create.rs index e7921ed7c..d54344355 100644 --- a/src/portable/create.rs +++ b/src/portable/create.rs @@ -105,14 +105,14 @@ pub fn create(cmd: &Create, opts: &crate::options::Options) -> anyhow::Result<() ))?; } - if cp.compute_size.is_some() { + if cp.billables.compute_size.is_some() { return Err(opts.error( clap::error::ErrorKind::ArgumentConflict, cformat!("The --compute-size option is only applicable to cloud instances."), ))?; } - if cp.storage_size.is_some() { + if cp.billables.storage_size.is_some() { return Err(opts.error( clap::error::ErrorKind::ArgumentConflict, cformat!("The --storage-size option is only applicable to cloud instances."), @@ -222,10 +222,10 @@ fn create_cloud( let server_ver = cloud::versions::get_version(&query, client)?; - let compute_size = cp.and_then(|p| p.compute_size); - let storage_size = cp.and_then(|p| p.storage_size); + let compute_size = cp.and_then(|p| p.billables.compute_size); + let storage_size = cp.and_then(|p| p.billables.storage_size); - let tier = if let Some(tier) = cp.and_then(|p| p.tier) { + let tier = if let Some(tier) = cp.and_then(|p| p.billables.tier) { tier } else if compute_size.is_some() || storage_size.is_some() || org.preferred_payment_method.is_some() { cloud::ops::CloudTier::Pro diff --git a/src/portable/main.rs b/src/portable/main.rs index ec61bfadc..52deb2221 100644 --- a/src/portable/main.rs +++ b/src/portable/main.rs @@ -11,6 +11,7 @@ use crate::portable::install; use crate::portable::link; use crate::portable::list_versions; use crate::portable::project; +use crate::portable::resize; use crate::portable::revert; use crate::portable::status; use crate::portable::uninstall; @@ -46,6 +47,7 @@ pub fn instance_main(cmd: &ServerInstanceCommand, options: &Options) Link(c) => link::link(c, &options), List(c) if cfg!(windows) => windows::list(c, options), List(c) => status::list(c, options), + Resize(c) => resize::resize(c, options), Upgrade(c) => upgrade::upgrade(c, options), Start(c) => control::start(c), Stop(c) => control::stop(c), diff --git a/src/portable/mod.rs b/src/portable/mod.rs index dea5b0e04..ad6ad17dd 100644 --- a/src/portable/mod.rs +++ b/src/portable/mod.rs @@ -20,6 +20,7 @@ pub mod install; mod link; mod list_versions; mod reset_password; +mod resize; mod revert; pub mod status; mod uninstall; diff --git a/src/portable/options.rs b/src/portable/options.rs index 83d91a235..d5a4cc5ae 100644 --- a/src/portable/options.rs +++ b/src/portable/options.rs @@ -54,6 +54,8 @@ pub enum InstanceCommand { Unlink(Unlink), /// Show logs for an instance Logs(Logs), + /// Resize a Cloud instance + Resize(Resize), /// Upgrade installations and instances Upgrade(Upgrade), /// Revert a major instance upgrade @@ -145,11 +147,7 @@ pub enum InstanceName { } #[derive(clap::Args, IntoArgs, Debug, Clone)] -pub struct CloudInstanceParams { - /// The region in which to create the instance (for cloud instances) - #[arg(long)] - pub region: Option, - +pub struct CloudInstanceBillables { /// Cloud instance subscription tier. #[arg(long, value_name="tier")] #[arg(value_enum)] @@ -166,6 +164,16 @@ pub struct CloudInstanceParams { pub storage_size: Option, } +#[derive(clap::Args, IntoArgs, Debug, Clone)] +pub struct CloudInstanceParams { + /// The region in which to create the instance (for cloud instances) + #[arg(long)] + pub region: Option, + + #[command(flatten)] + pub billables: CloudInstanceBillables, +} + #[derive(clap::Args, IntoArgs, Debug, Clone)] pub struct Create { #[command(flatten)] @@ -443,6 +451,24 @@ pub struct Logs { pub follow: bool, } +#[derive(clap::Args, IntoArgs, Debug, Clone)] +pub struct Resize { + #[command(flatten)] + pub cloud_opts: CloudOptions, + + /// Instance to resize + #[arg(short='I', long, required=true)] + #[arg(value_hint=ValueHint::Other)] // TODO complete instance name + pub instance: InstanceName, + + #[command(flatten)] + pub billables: CloudInstanceBillables, + + /// Do not ask questions + #[arg(long)] + pub non_interactive: bool, +} + #[derive(clap::Args, IntoArgs, Debug, Clone)] pub struct Upgrade { #[command(flatten)] diff --git a/src/portable/resize.rs b/src/portable/resize.rs new file mode 100644 index 000000000..053ab03b0 --- /dev/null +++ b/src/portable/resize.rs @@ -0,0 +1,159 @@ +use color_print::cformat; + +use crate::cloud; +use crate::portable::options::{Resize, InstanceName}; +use crate::print::echo; +use crate::question; + + +pub fn resize(cmd: &Resize, opts: &crate::options::Options) -> anyhow::Result<()> { + match &cmd.instance { + InstanceName::Local(_) => { + Err(opts.error( + clap::error::ErrorKind::InvalidValue, + cformat!("Only Cloud instances can be resized."), + ))? + }, + InstanceName::Cloud { org_slug: org, name } => { + resize_cloud_cmd(cmd, org, name, opts) + }, + } +} + +fn resize_cloud_cmd( + cmd: &Resize, + org_slug: &str, + name: &str, + opts: &crate::options::Options, +) -> anyhow::Result<()> { + let billables = &cmd.billables; + + if billables.tier.is_none() + && billables.compute_size.is_none() + && billables.storage_size.is_none() + { + return Err(opts.error( + clap::error::ErrorKind::MissingRequiredArgument, + cformat!("Either --tier, --compute-size, \ + or --storage-size must be specified."), + ))?; + } + + if billables.compute_size.is_some() && billables.storage_size.is_some() { + return Err(opts.error( + clap::error::ErrorKind::MissingRequiredArgument, + cformat!("--compute-size, \ + and --storage-size cannot be modified at the same time."), + ))?; + } + + let client = cloud::client::CloudClient::new(&opts.cloud_options)?; + client.ensure_authenticated()?; + + let inst_name = InstanceName::Cloud { + org_slug: org_slug.to_string(), + name: name.to_string(), + }; + + let inst = cloud::ops::find_cloud_instance_by_name(name, org_slug, &client)? + .ok_or_else(|| anyhow::anyhow!("instance not found"))?; + + let compute_size = billables.compute_size; + let storage_size = billables.storage_size; + let mut resources_display_vec: Vec = vec![]; + + if let Some(tier) = billables.tier { + if tier == inst.tier && compute_size.is_none() && storage_size.is_none() { + return Err(opts.error( + clap::error::ErrorKind::InvalidValue, + cformat!("Instance \"{org_slug}/{name}\" is already a {tier:?} \ + instance."), + ))?; + } + + if tier == cloud::ops::CloudTier::Free { + if compute_size.is_some() { + return Err(opts.error( + clap::error::ErrorKind::ArgumentConflict, + cformat!("The --compute-size option can \ + only be specified for Pro instances."), + ))?; + } + if storage_size.is_some() { + return Err(opts.error( + clap::error::ErrorKind::ArgumentConflict, + cformat!("The --storage-size option can \ + only be specified for Pro instances."), + ))?; + } + } + + if tier != inst.tier { + resources_display_vec.push(format!( + "New Tier: {tier:?}", + )); + } + } + + let mut req_resources: Vec = vec![]; + + if let Some(compute_size) = compute_size { + req_resources.push( + cloud::ops::CloudInstanceResourceRequest{ + name: "compute".to_string(), + value: compute_size, + }, + ); + resources_display_vec.push(format!( + "New Compute Size: {} compute unit{}", + compute_size, + if compute_size == 1 {""} else {"s"}, + )); + } + + if let Some(storage_size) = storage_size { + req_resources.push( + cloud::ops::CloudInstanceResourceRequest{ + name: "storage".to_string(), + value: storage_size, + }, + ); + resources_display_vec.push(format!( + "New Storage Size: {} gigabyte{}", + storage_size, + if storage_size == 1 {""} else {"s"}, + )); + } + + let mut resources_display = resources_display_vec.join("\n"); + if resources_display != "" { + resources_display = format!("\n{resources_display}"); + } + + let prompt = format!( + "Will resize the \"{inst_name}\" Cloud instance as follows:\ + \n\ + {resources_display}\ + \n\nContinue?", + ); + + if !cmd.non_interactive && !question::Confirm::new(prompt).ask()? { + return Ok(()); + } + + let request = cloud::ops::CloudInstanceResize { + name: name.to_string(), + org: org_slug.to_string(), + requested_resources: Some(req_resources), + tier: billables.tier, + }; + cloud::ops::resize_cloud_instance(&client, &request)?; + echo!( + "EdgeDB Cloud instance", + inst_name, + "has been resized successfuly." + ); + echo!("To connect to the instance run:"); + echo!(" edgedb -I", inst_name); + return Ok(()) +}