Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: hopinc/cli
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v0.2.40
Choose a base ref
...
head repository: hopinc/cli
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: main
Choose a head ref

Commits on Jan 31, 2023

  1. feat: start on premade forms

    pxseu committed Jan 31, 2023

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    35a0e0c View commit details

Commits on May 13, 2023

  1. fix: windows installer logo

    pxseu committed May 13, 2023
    Copy the full SHA
    2d9bb3e View commit details
  2. Copy the full SHA
    3bce5b2 View commit details
  3. Copy the full SHA
    9f97c33 View commit details
  4. feat: v0.2.41

    pxseu committed May 13, 2023
    Copy the full SHA
    9734c5f View commit details
  5. fix: test

    pxseu committed May 13, 2023
    Copy the full SHA
    9d9d218 View commit details
  6. feat: v0.2.42

    pxseu committed May 13, 2023
    Copy the full SHA
    36ee04b View commit details

Commits on May 15, 2023

  1. feat: add recreate command

    pxseu committed May 15, 2023
    Copy the full SHA
    aec28d5 View commit details
  2. feat: v0.2.43

    pxseu committed May 15, 2023
    Copy the full SHA
    9c51c44 View commit details

Commits on May 23, 2023

  1. Copy the full SHA
    80e0381 View commit details

Commits on May 27, 2023

  1. fix: price estimate

    pxseu committed May 27, 2023
    Copy the full SHA
    6c5a998 View commit details
  2. feat: v0.2.44

    pxseu committed May 27, 2023
    Copy the full SHA
    5e652a9 View commit details
  3. fix: tests

    pxseu committed May 27, 2023
    Copy the full SHA
    dc9be7c View commit details
  4. feat: v0.2.45

    pxseu committed May 27, 2023
    Copy the full SHA
    f607f75 View commit details

Commits on May 29, 2023

  1. feat: better quota error

    pxseu committed May 29, 2023
    Copy the full SHA
    34f3699 View commit details
  2. feat: v0.2.46

    pxseu committed May 29, 2023
    Copy the full SHA
    0406f21 View commit details

Commits on Jun 6, 2023

  1. Copy the full SHA
    4e03256 View commit details
  2. feat: v0.2.47

    pxseu committed Jun 6, 2023
    Copy the full SHA
    f6e33f2 View commit details

Commits on Jun 8, 2023

  1. Copy the full SHA
    928cbbc View commit details
  2. feat: v0.2.48

    pxseu committed Jun 8, 2023
    Copy the full SHA
    902a452 View commit details

Commits on Jun 10, 2023

  1. chore: format

    pxseu committed Jun 10, 2023
    Copy the full SHA
    7014700 View commit details
  2. feat: container metrics command

    pxseu committed Jun 10, 2023
    Copy the full SHA
    863b192 View commit details
  3. feat: v0.2.49

    pxseu committed Jun 10, 2023
    Copy the full SHA
    93f64ff View commit details
  4. feat: move to clap 4.1 (lol)

    pxseu committed Jun 10, 2023
    Copy the full SHA
    dcd5dd3 View commit details

Commits on Jun 11, 2023

  1. feat: bump all deps

    pxseu committed Jun 11, 2023
    Copy the full SHA
    35a2826 View commit details
  2. feat: fix fslike

    pxseu committed Jun 11, 2023
    Copy the full SHA
    6331788 View commit details
  3. feat: fix update on windows

    pxseu committed Jun 11, 2023
    Copy the full SHA
    dd9c439 View commit details
  4. feat: v0.2.50

    pxseu committed Jun 11, 2023
    Copy the full SHA
    43ce277 View commit details

Commits on Jun 15, 2023

  1. Copy the full SHA
    56c1768 View commit details
  2. feat: finish templates

    pxseu committed Jun 15, 2023
    Copy the full SHA
    9ff580b View commit details
  3. feat: v0.2.51

    pxseu committed Jun 15, 2023
    Copy the full SHA
    eb3553c View commit details

Commits on Jun 16, 2023

  1. Copy the full SHA
    a866efe View commit details
  2. feat: v0.2.52

    pxseu committed Jun 16, 2023
    Copy the full SHA
    79d950c View commit details

Commits on Aug 5, 2023

  1. Copy the full SHA
    2b30ecf View commit details
  2. feat: v0.2.53

    pxseu committed Aug 5, 2023
    Copy the full SHA
    dd615ac View commit details

Commits on Sep 15, 2023

  1. Copy the full SHA
    876c1db View commit details

Commits on Sep 17, 2023

  1. feat: simple backup command

    pxseu committed Sep 17, 2023
    Copy the full SHA
    68b6b70 View commit details

Commits on Sep 18, 2023

  1. feat: better logs

    pxseu committed Sep 18, 2023
    Copy the full SHA
    6b79cd2 View commit details
  2. feat: v0.2.54

    pxseu committed Sep 18, 2023
    Copy the full SHA
    2fcb26b View commit details

Commits on Sep 28, 2023

  1. feat: new arisu

    pxseu committed Sep 28, 2023
    Copy the full SHA
    18f9f14 View commit details

Commits on Oct 2, 2023

  1. feat: arisu unsub

    pxseu committed Oct 2, 2023
    Copy the full SHA
    43fc370 View commit details
  2. feat: v0.2.55

    pxseu committed Oct 2, 2023
    1
    Copy the full SHA
    a0c067e View commit details

Commits on Oct 8, 2023

  1. feat: webhooks

    pxseu committed Oct 8, 2023
    Copy the full SHA
    f650c7e View commit details
  2. feat: v0.2.56

    pxseu committed Oct 8, 2023
    Copy the full SHA
    3384527 View commit details
  3. feat: linux

    pxseu committed Oct 8, 2023
    Copy the full SHA
    f8f6d4e View commit details
  4. feat: v0.2.57

    pxseu committed Oct 8, 2023
    Copy the full SHA
    f7703c3 View commit details
  5. chore: update workflow

    pxseu committed Oct 8, 2023
    Copy the full SHA
    8b73a0e View commit details

Commits on Oct 10, 2023

  1. feat: make things faster

    pxseu committed Oct 10, 2023
    Copy the full SHA
    bbed289 View commit details
  2. feat: v0.2.58

    pxseu committed Oct 10, 2023
    Copy the full SHA
    842481f View commit details
  3. feat: fix workflow

    pxseu committed Oct 10, 2023
    Copy the full SHA
    6e75d31 View commit details
Showing with 3,895 additions and 1,190 deletions.
  1. +17 −33 .github/workflows/release.yml
  2. +695 −581 Cargo.lock
  3. +26 −10 Cargo.toml
  4. BIN build/windows/resources/Dialog.bmp
  5. +1 −0 src/commands/auth/docker.rs
  6. +1 −0 src/commands/auth/list.rs
  7. +1 −1 src/commands/auth/login/browser_auth.rs
  8. +1 −0 src/commands/auth/login/mod.rs
  9. +1 −0 src/commands/auth/logout.rs
  10. +1 −0 src/commands/auth/mod.rs
  11. +1 −0 src/commands/auth/switch.rs
  12. +1 −0 src/commands/channels/create.rs
  13. +1 −0 src/commands/channels/delete.rs
  14. +1 −0 src/commands/channels/list.rs
  15. +1 −0 src/commands/channels/message.rs
  16. +1 −0 src/commands/channels/mod.rs
  17. +1 −0 src/commands/channels/subscribe.rs
  18. +1 −0 src/commands/channels/tokens/create.rs
  19. +1 −0 src/commands/channels/tokens/delete.rs
  20. +1 −0 src/commands/channels/tokens/list.rs
  21. +1 −0 src/commands/channels/tokens/messages.rs
  22. +1 −0 src/commands/channels/tokens/mod.rs
  23. +1 −0 src/commands/completions/mod.rs
  24. +18 −12 src/commands/containers/create.rs
  25. +17 −11 src/commands/containers/delete.rs
  26. +101 −0 src/commands/containers/inspect.rs
  27. +20 −13 src/commands/containers/list.rs
  28. +24 −13 src/commands/containers/logs.rs
  29. +100 −0 src/commands/containers/metrics.rs
  30. +13 −0 src/commands/containers/mod.rs
  31. +92 −0 src/commands/containers/recreate.rs
  32. +38 −3 src/commands/containers/types.rs
  33. +65 −11 src/commands/containers/utils.rs
  34. +3 −0 src/commands/deploy/local/mod.rs
  35. +2 −0 src/commands/deploy/mod.rs
  36. +16 −10 src/commands/domains/attach.rs
  37. +16 −10 src/commands/domains/delete.rs
  38. +16 −10 src/commands/domains/list.rs
  39. +1 −0 src/commands/domains/mod.rs
  40. +19 −13 src/commands/gateways/create.rs
  41. +17 −11 src/commands/gateways/delete.rs
  42. +17 −11 src/commands/gateways/list.rs
  43. +1 −0 src/commands/gateways/mod.rs
  44. +17 −11 src/commands/gateways/update.rs
  45. +1 −3 src/commands/gateways/util.rs
  46. +16 −10 src/commands/ignite/builds/cancel.rs
  47. +19 −14 src/commands/ignite/builds/list.rs
  48. +1 −0 src/commands/ignite/builds/mod.rs
  49. +2 −0 src/commands/ignite/create.rs
  50. +22 −14 src/commands/ignite/delete.rs
  51. +2 −0 src/commands/ignite/from_compose/mod.rs
  52. +2 −3 src/commands/ignite/from_compose/types.rs
  53. +20 −13 src/commands/ignite/get_env.rs
  54. +69 −0 src/commands/ignite/groups/create.rs
  55. +39 −0 src/commands/ignite/groups/delete.rs
  56. +39 −0 src/commands/ignite/groups/list.rs
  57. +38 −0 src/commands/ignite/groups/mod.rs
  58. +91 −0 src/commands/ignite/groups/move.rs
  59. +199 −0 src/commands/ignite/groups/utils.rs
  60. +19 −14 src/commands/ignite/health/create.rs
  61. +16 −11 src/commands/ignite/health/delete.rs
  62. +19 −14 src/commands/ignite/health/list.rs
  63. +1 −0 src/commands/ignite/health/mod.rs
  64. +19 −14 src/commands/ignite/health/state.rs
  65. +121 −0 src/commands/ignite/inspect.rs
  66. +9 −6 src/commands/ignite/list.rs
  67. +10 −1 src/commands/ignite/mod.rs
  68. +18 −10 src/commands/ignite/promote.rs
  69. +20 −14 src/commands/ignite/rollout.rs
  70. +20 −13 src/commands/ignite/scale.rs
  71. +147 −14 src/commands/ignite/templates.rs
  72. +110 −2 src/commands/ignite/types.rs
  73. +22 −16 src/commands/ignite/update.rs
  74. +133 −43 src/commands/ignite/utils.rs
  75. +19 −15 src/commands/link/mod.rs
  76. +7 −0 src/commands/mod.rs
  77. +19 −10 src/commands/oops/mod.rs
  78. +1 −0 src/commands/payment/due.rs
  79. +1 −0 src/commands/payment/list.rs
  80. +1 −0 src/commands/payment/mod.rs
  81. +1 −0 src/commands/projects/create/mod.rs
  82. +1 −1 src/commands/projects/create/utils.rs
  83. +1 −0 src/commands/projects/delete.rs
  84. +2 −2 src/commands/projects/finance/mod.rs
  85. +9 −9 src/commands/projects/finance/types.rs
  86. +17 −17 src/commands/projects/finance/utils.rs
  87. +1 −0 src/commands/projects/info.rs
  88. +1 −0 src/commands/projects/list.rs
  89. +1 −0 src/commands/projects/mod.rs
  90. +1 −0 src/commands/projects/switch.rs
  91. +222 −0 src/commands/projects/types.rs
  92. +17 −4 src/commands/projects/utils.rs
  93. +1 −0 src/commands/secrets/delete.rs
  94. +1 −0 src/commands/secrets/list.rs
  95. +1 −0 src/commands/secrets/mod.rs
  96. +1 −0 src/commands/secrets/set.rs
  97. +3 −1 src/commands/tunnel/mod.rs
  98. +2 −2 src/commands/tunnel/utils.rs
  99. +1 −1 src/commands/update/checker.rs
  100. +1 −0 src/commands/update/command.rs
  101. +1 −1 src/commands/update/mod.rs
  102. +22 −13 src/commands/update/util.rs
  103. +38 −0 src/commands/volumes/backup.rs
  104. +7 −5 src/commands/volumes/copy/fslike.rs
  105. +2 −1 src/commands/volumes/copy/mod.rs
  106. +4 −2 src/commands/volumes/copy/utils.rs
  107. +1 −0 src/commands/volumes/delete.rs
  108. +2 −1 src/commands/volumes/list.rs
  109. +51 −0 src/commands/volumes/mkdir.rs
  110. +12 −1 src/commands/volumes/mod.rs
  111. +44 −0 src/commands/volumes/move.rs
  112. +16 −1 src/commands/volumes/types.rs
  113. +49 −1 src/commands/volumes/utils.rs
  114. +72 −0 src/commands/webhooks/create.rs
  115. +39 −0 src/commands/webhooks/delete.rs
  116. +35 −0 src/commands/webhooks/list.rs
  117. +43 −0 src/commands/webhooks/mod.rs
  118. +48 −0 src/commands/webhooks/regenerate.rs
  119. +83 −0 src/commands/webhooks/update.rs
  120. +52 −0 src/commands/webhooks/utils.rs
  121. +1 −0 src/commands/whoami/mod.rs
  122. +17 −8 src/lib.rs
  123. +1 −1 src/main.rs
  124. +3 −4 src/state/http/mod.rs
  125. +34 −17 src/state/mod.rs
  126. +2 −2 src/store/macros.rs
  127. +2 −1 src/store/mod.rs
  128. +92 −7 src/utils/arisu/mod.rs
  129. +42 −24 src/utils/arisu/shard.rs
  130. +60 −16 src/utils/arisu/types.rs
  131. +4 −5 src/utils/browser.rs
  132. +18 −0 src/utils/deser.rs
  133. +3 −2 src/utils/mod.rs
  134. +48 −22 src/utils/size.rs
50 changes: 17 additions & 33 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -15,7 +15,7 @@ jobs:
tag_name: ${{ steps.tag.outputs.tag_name }}
steps:
- name: Checkout the repo
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 1

@@ -42,7 +42,6 @@ jobs:
name: Build Release Assets
needs: ["draft-release"]
runs-on: ${{ matrix.os }}
continue-on-error: true
strategy:
fail-fast: false
matrix:
@@ -100,34 +99,30 @@ jobs:

steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 1

- name: Install Rust
uses: actions-rs/toolchain@v1
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
target: ${{ matrix.target }}
profile: minimal
override: true
targets: ${{ matrix.target }}
components: rustfmt, clippy

- name: Install Wix [Windows]
if: matrix.os == 'windows-latest'
uses: actions-rs/cargo@v1
with:
command: install
args: cargo-wix
run: cargo install cargo-wix

- name: Install Cross [Linux]
if: matrix.os == 'ubuntu-latest'
run: cargo install cross

- name: Cache
uses: Swatinem/rust-cache@v2

- name: Build release binary
uses: actions-rs/cargo@v1
with:
command: build
args: --release --locked ${{ matrix.flags }} --target ${{ matrix.target }} --package hop-cli
use-cross: ${{ matrix.os == 'ubuntu-latest' }}
run: ${{ matrix.os == 'ubuntu-latest' && 'cross' || 'cargo' }} build --release --locked ${{ matrix.flags }} --target ${{ matrix.target }} --package hop-cli

- name: Prepare binaries [*nix]
if: matrix.os != 'windows-latest'
@@ -147,10 +142,7 @@ jobs:
- name: Build installer [Windows]
if: matrix.os == 'windows-latest'
uses: actions-rs/cargo@v1
with:
command: wix
args: -I .\build\windows\main.wxs -v --no-build --nocapture --target ${{ matrix.target }} --output target\wix\hop-${{ matrix.platform }}.msi --package hop-cli
run: cargo wix -I .\build\windows\main.wxs -v --no-build --nocapture --target ${{ matrix.target }} --output target\wix\hop-${{ matrix.platform }}.msi --package hop-cli

- name: Upload binaries
uses: actions/upload-artifact@v3
@@ -197,29 +189,21 @@ jobs:
environment: prod
steps:
- name: Checkout the repo
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: 1

- name: Install Rust
uses: actions-rs/toolchain@v1
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
profile: minimal
override: true
components: rustfmt, clippy

- name: Login to Crates
uses: actions-rs/cargo@v1
with:
command: login
args: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: cargo login ${{ secrets.CARGO_REGISTRY_TOKEN }}

- name: Publish Crates
uses: actions-rs/cargo@v1
with:
command: publish
args: --no-verify
use-cross: ${{ matrix.os == 'ubuntu-latest' }}
run: cargo publish --locked --no-verify

publish-aur:
name: Publish to AUR
1,276 changes: 695 additions & 581 deletions Cargo.lock

Large diffs are not rendered by default.

36 changes: 26 additions & 10 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "hop-cli"
version = "0.2.40"
version = "0.2.61"
edition = "2021"
license = "MPL-2.0"
authors = ["Hop, Inc."]
@@ -14,8 +14,14 @@ categories = ["command-line-utilities"]
[profile.release]
strip = true
lto = true
opt-level = 3
panic = "abort"

[profile.test]
strip = true
lto = true
opt-level = 3

[[bin]]
name = "hop"
path = "./src/main.rs"
@@ -27,7 +33,8 @@ update = []
[dependencies]
ms = "0.1"
log = "0.4"
dirs = "4.0"
dirs = "5.0"
rand = "0.8"
regex = "1.6"
runas = "1.0"
anyhow = "1.0"
@@ -44,20 +51,25 @@ serde_json = "1.0"
serde_repr = "0.1"
async-trait = "0.1"
futures-util = "0.3"
clap_complete = "3.2"
clap = { version = "3.2", features = ["derive"] }
clap_complete = "4.1"
clap = { version = "4.1", features = ["derive"] }
fern = { version = "0.6", features = ["colored"] }
tokio = { version = "1.20", features = ["full"] }
# version > 1.29 makes us have 2x deps
tokio = { version = "=1.29", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
hyper = { version = "0.14", features = ["server"] }
ctrlc = { version = "3.2", features = ["termination"] }
chrono = { version = "0.4", features = ["serde"] }
async_zip = { version = "0.0", features = ["full"] }
async-compression = { version = "0.3", features = ["tokio", "gzip"] }
async-compression = { version = "0.4", features = ["tokio", "gzip"] }


# *nix only deps
[target.'cfg(all(not(windows), not(macos)))'.dependencies]
hop = { version = "0.1", features = [
"chrono",
"rustls-tls-webpki-roots",
], default-features = false }
leap_client_rs = { version = "0.1", features = [
"zlib",
"rustls-tls-webpki-roots",
@@ -67,17 +79,21 @@ reqwest = { version = "0.11", features = [
"multipart",
"rustls-tls-webpki-roots",
], default-features = false }
tokio-rustls = { version = "0.23", default-features = false }
tokio-rustls = { version = "0.24", default-features = false }
webpki = "0.22"
webpki-roots = "0.22"
async-tungstenite = { version = "0.20", features = [
webpki-roots = "0.25"
async-tungstenite = { version = "0.23", features = [
"tokio-runtime",
"tokio-rustls-webpki-roots",
] }


# windows only deps
[target.'cfg(any(windows, macos))'.dependencies]
hop = { version = "0.1", features = [
"chrono",
"native-tls",
], default-features = false }
reqwest = { version = "0.11", features = [
"json",
"multipart",
@@ -89,7 +105,7 @@ leap_client_rs = { version = "0.1", features = [
], default-features = false }
native-tls = "0.2"
tokio-native-tls = "0.3"
async-tungstenite = { version = "0.20", features = [
async-tungstenite = { version = "0.23", features = [
"tokio-runtime",
"tokio-native-tls",
] }
Binary file modified build/windows/resources/Dialog.bmp
Binary file not shown.
1 change: 1 addition & 0 deletions src/commands/auth/docker.rs
Original file line number Diff line number Diff line change
@@ -10,6 +10,7 @@ use crate::utils::in_path;

#[derive(Debug, Parser)]
#[clap(about = "Authenticate the current user with Docker")]
#[group(skip)]
pub struct Options {}

pub async fn handle(_options: &Options, state: &mut State) -> Result<()> {
1 change: 1 addition & 0 deletions src/commands/auth/list.rs
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@ use crate::state::State;

#[derive(Debug, Parser)]
#[clap(about = "List all authenticated users")]
#[group(skip)]
pub struct Options {
#[clap(short, long, help = "Only print the IDs of the authorized users")]
pub quiet: bool,
2 changes: 1 addition & 1 deletion src/commands/auth/login/browser_auth.rs
Original file line number Diff line number Diff line change
@@ -16,7 +16,7 @@ pub async fn browser_login() -> Result<String> {

let url = format!(
"{WEB_AUTH_URL}?{}",
vec!["callback", &format!("http://localhost:{port}/")].join("=")
["callback", &format!("http://localhost:{port}/")].join("=")
);

// lunch a web server to handle the auth request
1 change: 1 addition & 0 deletions src/commands/auth/login/mod.rs
Original file line number Diff line number Diff line change
@@ -17,6 +17,7 @@ const PAT_FALLBACK_URL: &str = "https://console.hop.io/settings/pats";

#[derive(Debug, Parser, Default, PartialEq, Eq)]
#[clap(about = "Login to Hop")]
#[group(skip)]
pub struct Options {
#[clap(
long,
1 change: 1 addition & 0 deletions src/commands/auth/logout.rs
Original file line number Diff line number Diff line change
@@ -9,6 +9,7 @@ use crate::store::Store;

#[derive(Debug, Parser)]
#[clap(about = "Logout the current user")]
#[group(skip)]
pub struct Options {}

pub async fn handle(_options: Options, mut state: State) -> Result<()> {
1 change: 1 addition & 0 deletions src/commands/auth/mod.rs
Original file line number Diff line number Diff line change
@@ -24,6 +24,7 @@ pub enum Commands {

#[derive(Debug, Parser)]
#[clap(about = "Authenticate with Hop")]
#[group(skip)]
pub struct Options {
#[clap(subcommand)]
pub commands: Commands,
1 change: 1 addition & 0 deletions src/commands/auth/switch.rs
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@ use crate::state::State;

#[derive(Debug, Parser)]
#[clap(about = "Switch to a different user")]
#[group(skip)]
pub struct Options {}

pub async fn handle(_options: Options, state: State) -> Result<()> {
1 change: 1 addition & 0 deletions src/commands/channels/create.rs
Original file line number Diff line number Diff line change
@@ -9,6 +9,7 @@ use crate::utils::validate_json_non_null;

#[derive(Debug, Parser, Default, PartialEq, Eq)]
#[clap(about = "Create a new Channel")]
#[group(skip)]
pub struct Options {
#[clap(short = 'i', long = "id", help = "Custom ID for the channel")]
custom_id: Option<String>,
1 change: 1 addition & 0 deletions src/commands/channels/delete.rs
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@ use crate::state::State;

#[derive(Debug, Parser)]
#[clap(about = "Delete Channels")]
#[group(skip)]
pub struct Options {
#[clap(help = "IDs of the Channels")]
channels: Vec<String>,
1 change: 1 addition & 0 deletions src/commands/channels/list.rs
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@ use crate::state::State;

#[derive(Debug, Parser, Default, PartialEq, Eq)]
#[clap(about = "List all Channel")]
#[group(skip)]
pub struct Options {
#[clap(short, long, help = "Only print the IDs of the Channels")]
pub quiet: bool,
1 change: 1 addition & 0 deletions src/commands/channels/message.rs
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@ use crate::state::State;

#[derive(Debug, Parser, Default, PartialEq, Eq)]
#[clap(about = "Send a message to a Channel")]
#[group(skip)]
pub struct Options {
#[clap(short, long, help = "The ID of the Channel to send the message to")]
channel: Option<String>,
1 change: 1 addition & 0 deletions src/commands/channels/mod.rs
Original file line number Diff line number Diff line change
@@ -30,6 +30,7 @@ pub enum Commands {

#[derive(Debug, Parser)]
#[clap(about = "Interact with Channels")]
#[group(skip)]
pub struct Options {
#[clap(subcommand)]
pub commands: Commands,
1 change: 1 addition & 0 deletions src/commands/channels/subscribe.rs
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@ use crate::state::State;

#[derive(Debug, Parser, Default, PartialEq, Eq)]
#[clap(about = "Subscribe a Leap Token to a Channel")]
#[group(skip)]
pub struct Options {
#[clap(short, long, help = "The ID of the Channel to subscribe to")]
channel: Option<String>,
1 change: 1 addition & 0 deletions src/commands/channels/tokens/create.rs
Original file line number Diff line number Diff line change
@@ -9,6 +9,7 @@ use crate::utils::validate_json;

#[derive(Debug, Parser, Default, PartialEq, Eq)]
#[clap(about = "Create a new Leap Token")]
#[group(skip)]
pub struct Options {
#[clap(
short,
1 change: 1 addition & 0 deletions src/commands/channels/tokens/delete.rs
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@ use crate::state::State;

#[derive(Debug, Parser)]
#[clap(about = "Delete Leap Tokens")]
#[group(skip)]
pub struct Options {
#[clap(help = "IDs of the Leap Tokens")]
tokens: Vec<String>,
1 change: 1 addition & 0 deletions src/commands/channels/tokens/list.rs
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@ use crate::state::State;

#[derive(Debug, Parser, Default, PartialEq, Eq)]
#[clap(about = "List all Leap Tokens")]
#[group(skip)]
pub struct Options {
#[clap(short, long, help = "Only print the IDs of the Tokens")]
quiet: bool,
1 change: 1 addition & 0 deletions src/commands/channels/tokens/messages.rs
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@ use crate::state::State;

#[derive(Debug, Parser, Default, PartialEq, Eq)]
#[clap(about = "Send a message to a Leap Token")]
#[group(skip)]
pub struct Options {
#[clap(short, long, help = "The ID of the Token to send the message to")]
token: Option<String>,
1 change: 1 addition & 0 deletions src/commands/channels/tokens/mod.rs
Original file line number Diff line number Diff line change
@@ -24,6 +24,7 @@ pub enum Commands {

#[derive(Debug, Parser)]
#[clap(about = "Interact with Channel Tokens")]
#[group(skip)]
pub struct Options {
#[clap(subcommand)]
pub commands: Commands,
1 change: 1 addition & 0 deletions src/commands/completions/mod.rs
Original file line number Diff line number Diff line change
@@ -9,6 +9,7 @@ use crate::CLI;

#[derive(Debug, Parser)]
#[clap(about = "Generate completion scripts for the specified shell")]
#[group(skip)]
pub struct Options {
#[clap(help = "The shell to print the completion script for")]
shell: CompletionShell,
30 changes: 18 additions & 12 deletions src/commands/containers/create.rs
Original file line number Diff line number Diff line change
@@ -2,11 +2,12 @@ use anyhow::{ensure, Result};
use clap::Parser;

use crate::commands::containers::utils::create_containers;
use crate::commands::ignite::utils::{format_deployments, get_all_deployments};
use crate::commands::ignite::groups::utils::fetch_grouped_deployments;
use crate::state::State;

#[derive(Debug, Parser)]
#[clap(about = "Create containers for a deployment")]
#[group(skip)]
pub struct Options {
#[clap(short, long, help = "ID of the deployment")]
deployment: Option<String>,
@@ -20,17 +21,22 @@ pub async fn handle(options: Options, state: State) -> Result<()> {
Some(id) => id,

None => {
let project_id = state.ctx.current_project_error()?.id;

let deployments = get_all_deployments(&state.http, &project_id).await?;
ensure!(!deployments.is_empty(), "No deployments found");
let deployments_fmt = format_deployments(&deployments, false);

let idx = dialoguer::Select::new()
.with_prompt("Select a deployment")
.items(&deployments_fmt)
.default(0)
.interact()?;
let (deployments_fmt, deployments, validator) =
fetch_grouped_deployments(&state, false, true).await?;

let idx = loop {
let idx = dialoguer::Select::new()
.with_prompt("Select a deployment")
.items(&deployments_fmt)
.default(0)
.interact()?;

if let Ok(idx) = validator(idx) {
break idx;
}

console::Term::stderr().clear_last_lines(1)?
};

deployments[idx].id.clone()
}
28 changes: 17 additions & 11 deletions src/commands/containers/delete.rs
Original file line number Diff line number Diff line change
@@ -3,11 +3,12 @@ use clap::Parser;

use super::utils::delete_container;
use crate::commands::containers::utils::{format_containers, get_all_containers};
use crate::commands::ignite::utils::{format_deployments, get_all_deployments};
use crate::commands::ignite::groups::utils::fetch_grouped_deployments;
use crate::state::State;

#[derive(Debug, Parser)]
#[clap(about = "Delete containers")]
#[group(skip)]
pub struct Options {
#[clap(help = "IDs of the containers")]
containers: Vec<String>,
@@ -20,17 +21,22 @@ pub async fn handle(options: Options, state: State) -> Result<()> {
let containers = if !options.containers.is_empty() {
options.containers
} else {
let project_id = state.ctx.current_project_error()?.id;
let (deployments_fmt, deployments, validator) =
fetch_grouped_deployments(&state, false, true).await?;

let deployments = get_all_deployments(&state.http, &project_id).await?;
ensure!(!deployments.is_empty(), "No deployments found");
let deployments_fmt = format_deployments(&deployments, false);
let idx = loop {
let idx = dialoguer::Select::new()
.with_prompt("Select a deployment")
.items(&deployments_fmt)
.default(0)
.interact()?;

let idx = dialoguer::Select::new()
.with_prompt("Select a deployment")
.items(&deployments_fmt)
.default(0)
.interact()?;
if let Ok(idx) = validator(idx) {
break idx;
}

console::Term::stderr().clear_last_lines(1)?
};

let containers = get_all_containers(&state.http, &deployments[idx].id).await?;
ensure!(!containers.is_empty(), "No containers found");
@@ -66,7 +72,7 @@ pub async fn handle(options: Options, state: State) -> Result<()> {
for container in &containers {
log::info!("Deleting container `{}`", container);

if let Err(err) = delete_container(&state.http, container).await {
if let Err(err) = delete_container(&state.http, container, false).await {
log::error!("Failed to delete container `{}`: {}", container, err);
} else {
delete_count += 1;
101 changes: 101 additions & 0 deletions src/commands/containers/inspect.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
use std::io::Write;

use anyhow::{ensure, Result};
use clap::Parser;
use tabwriter::TabWriter;

use super::utils::{format_containers, get_all_containers, get_container, UNAVAILABLE_ELEMENT};
use crate::commands::containers::utils::format_single_metrics;
use crate::commands::ignite::groups::utils::fetch_grouped_deployments;
use crate::commands::ignite::utils::get_deployment;
use crate::state::State;
use crate::utils::relative_time;

#[derive(Debug, Parser)]
#[clap(about = "Inspect a container")]
#[group(skip)]
pub struct Options {
#[clap(help = "ID of the container")]
pub container: Option<String>,
}

pub async fn handle(options: Options, state: State) -> Result<()> {
let (container, deployment) = if let Some(container_id) = options.container {
let container = get_container(&state.http, &container_id).await?;
let deployment = get_deployment(&state.http, &container.deployment_id).await?;

(container, deployment)
} else {
let (deployments_fmt, deployments, validator) =
fetch_grouped_deployments(&state, false, true).await?;

let idx = loop {
let idx = dialoguer::Select::new()
.with_prompt("Select a deployment")
.items(&deployments_fmt)
.default(0)
.interact()?;

if let Ok(idx) = validator(idx) {
break idx;
}

console::Term::stderr().clear_last_lines(1)?
};

let deployment = deployments[idx].to_owned();

let containers = get_all_containers(&state.http, &deployment.id).await?;
ensure!(!containers.is_empty(), "No containers found");
let containers_fmt = format_containers(&containers, false);

let idx = dialoguer::Select::new()
.with_prompt("Select container")
.items(&containers_fmt)
.default(0)
.interact()?;

(containers[idx].to_owned(), deployment)
};

let mut tw = TabWriter::new(vec![]);

writeln!(tw, "{}", container.id)?;
writeln!(tw, " Metadata")?;
writeln!(tw, "\tDeployment: {} ({})", deployment.name, deployment.id)?;
writeln!(tw, "\tCreated: {} ago", relative_time(container.created_at))?;
writeln!(tw, "\tState: {}", container.state)?;
writeln!(
tw,
"\tUptime: {}",
container
.uptime
.as_ref()
.map(|u| {
u.last_start
.map(relative_time)
.unwrap_or_else(|| UNAVAILABLE_ELEMENT.to_string())
})
.unwrap_or_else(|| UNAVAILABLE_ELEMENT.to_string())
)?;
writeln!(
tw,
"\tInternal IP: {}",
container
.internal_ip
.unwrap_or_else(|| UNAVAILABLE_ELEMENT.to_string())
)?;
writeln!(tw, "\tRegion: {}", container.region)?;
writeln!(tw, "\tType: {}", container.type_)?;
writeln!(tw, " Metrics")?;

for metric in format_single_metrics(&container.metrics, &deployment)? {
writeln!(tw, "\t{}", metric)?;
}

tw.flush()?;

print!("{}", String::from_utf8(tw.into_inner()?)?);

Ok(())
}
33 changes: 20 additions & 13 deletions src/commands/containers/list.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
use anyhow::{ensure, Result};
use anyhow::Result;
use clap::Parser;

use crate::commands::containers::utils::{format_containers, get_all_containers};
use crate::commands::ignite::utils::{format_deployments, get_all_deployments, get_deployment};
use crate::commands::ignite::groups::utils::fetch_grouped_deployments;
use crate::commands::ignite::utils::get_deployment;
use crate::state::State;

#[derive(Debug, Parser)]
#[clap(about = "List all containers")]
#[group(skip)]
pub struct Options {
#[clap(help = "ID of the deployment")]
pub deployment: Option<String>,
@@ -20,17 +22,22 @@ pub async fn handle(options: Options, state: State) -> Result<()> {
Some(id) => get_deployment(&state.http, &id).await?,

None => {
let project_id = state.ctx.current_project_error()?.id;

let deployments = get_all_deployments(&state.http, &project_id).await?;
ensure!(!deployments.is_empty(), "No deployments found");
let deployments_fmt = format_deployments(&deployments, false);

let idx = dialoguer::Select::new()
.with_prompt("Select a deployment")
.items(&deployments_fmt)
.default(0)
.interact()?;
let (deployments_fmt, deployments, validator) =
fetch_grouped_deployments(&state, false, true).await?;

let idx = loop {
let idx = dialoguer::Select::new()
.with_prompt("Select a deployment")
.items(&deployments_fmt)
.default(0)
.interact()?;

if let Ok(idx) = validator(idx) {
break idx;
}

console::Term::stderr().clear_last_lines(1)?
};

deployments[idx].clone()
}
37 changes: 24 additions & 13 deletions src/commands/containers/logs.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
use std::env::temp_dir;

use anyhow::{ensure, Result};
use anyhow::{ensure, Context, Result};
use clap::Parser;
use futures_util::StreamExt;
use tokio::fs;
use tokio::process::Command;

use super::utils::{format_containers, format_logs, get_all_containers, get_container_logs};
use crate::commands::ignite::utils::{format_deployments, get_all_deployments};
use crate::commands::ignite::groups::utils::fetch_grouped_deployments;
use crate::config::DEFAULT_EDITOR;
use crate::state::State;
use crate::utils::arisu::{ArisuClient, ArisuMessage};
use crate::utils::in_path;

#[derive(Debug, Parser)]
#[clap(about = "Get logs of a container")]
#[group(skip)]
pub struct Options {
#[clap(help = "ID of the container")]
container: Option<String>,
@@ -45,17 +46,22 @@ pub async fn handle(options: Options, state: State) -> Result<()> {
Some(id) => id,

None => {
let project_id = state.ctx.current_project_error()?.id;
let (deployments_fmt, deployments, validator) =
fetch_grouped_deployments(&state, false, true).await?;

let deployments = get_all_deployments(&state.http, &project_id).await?;
ensure!(!deployments.is_empty(), "No deployments found");
let deployments_fmt = format_deployments(&deployments, false);
let idx = loop {
let idx = dialoguer::Select::new()
.with_prompt("Select a deployment")
.items(&deployments_fmt)
.default(0)
.interact()?;

let idx = dialoguer::Select::new()
.with_prompt("Select a deployment")
.items(&deployments_fmt)
.default(0)
.interact()?;
if let Ok(idx) = validator(idx) {
break idx;
}

console::Term::stderr().clear_last_lines(1)?
};

let containers = get_all_containers(&state.http, &deployments[idx].id).await?;
ensure!(!containers.is_empty(), "No containers found");
@@ -118,19 +124,24 @@ pub async fn handle(options: Options, state: State) -> Result<()> {
format_logs(&logs, true, options.timestamps, options.details).join("\n")
);

let token = state.token().unwrap();
let token = state.token().context("No token found")?;

let mut arisu = ArisuClient::new(&container, &token).await?;

while let Some(message) = arisu.next().await {
match message {
ArisuMessage::Open => arisu.request_logs().await?,

ArisuMessage::ServiceMessage(data) => log::info!("Service: {data}"),
ArisuMessage::Out(log) => {

ArisuMessage::Logs(log) => {
print!(
"{}",
format_logs(&[log], true, options.timestamps, options.details)[0]
);
}

_ => {}
}
}

100 changes: 100 additions & 0 deletions src/commands/containers/metrics.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
use std::io::Write;

use anyhow::{ensure, Context, Result};
use clap::Parser;
use console::Term;
use futures_util::StreamExt;

use super::utils::{format_containers, get_all_containers, get_container};
use crate::commands::containers::utils::format_single_metrics;
use crate::commands::ignite::groups::utils::fetch_grouped_deployments;
use crate::commands::ignite::utils::get_deployment;
use crate::state::State;
use crate::utils::arisu::{ArisuClient, ArisuMessage};

#[derive(Debug, Parser)]
#[clap(about = "Get metrics for a container")]
#[group(skip)]
pub struct Options {
#[clap(help = "ID of the container")]
pub container: Option<String>,

#[clap(short, long, help = "Show metrics in real time")]
pub follow: bool,
}

pub async fn handle(options: Options, state: State) -> Result<()> {
let (container, deployment) = if let Some(container_id) = options.container {
let container = get_container(&state.http, &container_id).await?;
let deployment = get_deployment(&state.http, &container.deployment_id).await?;

(container, deployment)
} else {
let (deployments_fmt, deployments, validator) =
fetch_grouped_deployments(&state, false, true).await?;

let idx = loop {
let idx = dialoguer::Select::new()
.with_prompt("Select a deployment")
.items(&deployments_fmt)
.default(0)
.interact()?;

if let Ok(idx) = validator(idx) {
break idx;
}

console::Term::stderr().clear_last_lines(1)?
};

let deployment = deployments[idx].to_owned();

let containers = get_all_containers(&state.http, &deployment.id).await?;
ensure!(!containers.is_empty(), "No containers found");
let containers_fmt = format_containers(&containers, false);

let idx = dialoguer::Select::new()
.with_prompt("Select container")
.items(&containers_fmt)
.default(0)
.interact()?;

(containers[idx].to_owned(), deployment)
};

let mut term = Term::stdout();

writeln!(
term,
"{}",
format_single_metrics(&container.metrics, &deployment)?.join("\n")
)?;

if !options.follow {
return Ok(());
}

let token = state.token().context("No token found")?;

let mut arisu = ArisuClient::new(&container.id, &token).await?;

while let Some(message) = arisu.next().await {
match message {
ArisuMessage::Open => arisu.request_metrics().await?,

ArisuMessage::Metrics(metrics) => {
let metrics = format_single_metrics(&Some(metrics), &deployment)?;

if !state.debug {
term.clear_last_lines(metrics.len())?;
}

writeln!(term, "{}", metrics.join("\n"))?
}

_ => {}
}
}

Ok(())
}
13 changes: 13 additions & 0 deletions src/commands/containers/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
mod create;
mod delete;
mod inspect;
mod list;
mod logs;
pub mod metrics;
mod recreate;
pub mod types;
pub mod utils;

@@ -16,15 +19,22 @@ pub enum Commands {
Create(create::Options),
#[clap(name = "rm", alias = "delete")]
Delete(delete::Options),
#[clap(name = "recreate")]
Recreate(recreate::Options),
#[clap(name = "ls", alias = "list")]
List(list::Options),
#[clap(alias = "info")]
Inspect(inspect::Options),
#[clap(alias = "stats")]
Metrics(metrics::Options),

#[clap(name = "logs", alias = "log")]
Log(logs::Options),
}

#[derive(Debug, Parser)]
#[clap(about = "Interact with Ignite containers")]
#[group(skip)]
pub struct Options {
#[clap(subcommand)]
pub commands: Commands,
@@ -36,5 +46,8 @@ pub async fn handle(options: Options, state: State) -> Result<()> {
Commands::Delete(options) => delete::handle(options, state).await,
Commands::List(options) => list::handle(options, state).await,
Commands::Log(options) => logs::handle(options, state).await,
Commands::Recreate(options) => recreate::handle(options, state).await,
Commands::Inspect(options) => inspect::handle(options, state).await,
Commands::Metrics(options) => metrics::handle(options, state).await,
}
}
92 changes: 92 additions & 0 deletions src/commands/containers/recreate.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
use anyhow::{bail, ensure, Result};
use clap::Parser;

use super::utils::delete_container;
use crate::commands::containers::types::Container;
use crate::commands::containers::utils::{format_containers, get_all_containers};
use crate::commands::ignite::groups::utils::fetch_grouped_deployments;
use crate::state::State;

#[derive(Debug, Parser)]
#[clap(about = "Recreate containers")]
#[group(skip)]
pub struct Options {
#[clap(help = "IDs of the containers")]
containers: Vec<String>,

#[clap(short, long, help = "Skip confirmation")]
force: bool,
}

pub async fn handle(options: Options, state: State) -> Result<()> {
let containers = if !options.containers.is_empty() {
options.containers
} else {
let (deployments_fmt, deployments, validator) =
fetch_grouped_deployments(&state, false, true).await?;

let idx = loop {
let idx = dialoguer::Select::new()
.with_prompt("Select a deployment")
.items(&deployments_fmt)
.default(0)
.interact()?;

if let Ok(idx) = validator(idx) {
break idx;
}

console::Term::stderr().clear_last_lines(1)?
};

let containers = get_all_containers(&state.http, &deployments[idx].id).await?;
ensure!(!containers.is_empty(), "No containers found");
let containers_fmt = format_containers(&containers, false);

let idxs = dialoguer::MultiSelect::new()
.with_prompt("Select containers to recreate")
.items(&containers_fmt)
.interact()?;

containers
.iter()
.enumerate()
.filter(|(i, _)| idxs.contains(i))
.map(|(_, c)| c.id.clone())
.collect()
};

if !options.force
&& !dialoguer::Confirm::new()
.with_prompt(format!(
"Are you sure you want to recreate {} containers?",
containers.len()
))
.interact_opt()?
.unwrap_or(false)
{
bail!("Aborted");
}

let mut recreated_count = 0;

for container in &containers {
log::info!("Recreating container `{container}`");

match delete_container(&state.http, container, true).await {
Ok(Some(Container { id, .. })) => {
log::info!("Recreated container `{container}`, new ID: `{id}`");
recreated_count += 1;
}
Ok(None) => log::error!("Failed to recreate container `{container}`"),
Err(err) => log::error!("Failed to recreate container `{container}`: {err}"),
}
}

log::info!(
"Recreated {recreated_count}/{} containers",
containers.len()
);

Ok(())
}
41 changes: 38 additions & 3 deletions src/commands/containers/types.rs
Original file line number Diff line number Diff line change
@@ -55,15 +55,16 @@ impl Display for ContainerState {
}
}

#[derive(Debug, Deserialize, Serialize)]
#[derive(Debug, Deserialize, Clone)]
pub struct Uptime {
pub last_start: Option<DateTime<Utc>>,
}
#[derive(Debug, Deserialize)]
#[derive(Debug, Deserialize, Clone)]
pub struct Container {
pub id: String,
pub created_at: String,
pub created_at: DateTime<Utc>,
pub state: ContainerState,
pub metrics: Option<Metrics>,
pub deployment_id: String,
pub internal_ip: Option<String>,
pub region: String,
@@ -113,3 +114,37 @@ pub struct Log {
pub struct LogsResponse {
pub logs: Vec<Log>,
}

#[derive(Debug, Deserialize)]
pub struct SingleContainer {
pub container: Container,
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Metrics {
pub cpu_usage_percent: f64,
pub memory_usage_bytes: u64,
}

/// Reusable metrics functions
impl Metrics {
/// Normalize the metrics to the number of vcpus
pub fn cpu_usage_percent(&self, cpu_count: f64) -> f64 {
// 100% = 4vcpu
self.cpu_usage_percent / cpu_count / 4.0
}

/// Normalize the metrics to the amount of memory
pub fn memory_usage_percent(&self, memory: u64) -> f64 {
self.memory_usage_bytes as f64 / (memory as f64) * 100.0
}
}

#[derive(Debug, Deserialize)]
#[serde(tag = "e", content = "d", rename_all = "SCREAMING_SNAKE_CASE")]
pub enum ContainerEvents {
ContainerMetricsUpdate {
container_id: String,
metrics: Metrics,
},
}
76 changes: 65 additions & 11 deletions src/commands/containers/utils.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
use std::borrow::Borrow;
use std::io::Write;
use std::vec;

use anyhow::{anyhow, Result};
use console::style;
use serde_json::Value;
use tabwriter::TabWriter;

use super::types::{
Container, ContainerState, CreateContainers, Log, LogsResponse, MultipleContainersResponse,
Container, ContainerState, CreateContainers, Log, LogsResponse, Metrics,
MultipleContainersResponse, SingleContainer,
};
use crate::commands::ignite::types::Deployment;
use crate::state::http::HttpClient;
use crate::utils::relative_time;
use crate::utils::size::{parse_size, user_friendly_size};

pub async fn create_containers(
http: &HttpClient,
@@ -34,15 +37,27 @@ pub async fn create_containers(
Ok(response.containers)
}

pub async fn delete_container(http: &HttpClient, container_id: &str) -> Result<()> {
http.request::<Value>(
"DELETE",
&format!("/ignite/containers/{container_id}"),
None,
)
.await?;
pub async fn delete_container(
http: &HttpClient,
container_id: &str,
recreate: bool,
) -> Result<Option<Container>> {
Ok(http
.request::<SingleContainer>(
"DELETE",
&format!("/ignite/containers/{container_id}?recreate={recreate}"),
None,
)
.await?
.map(|response| response.container))
}

Ok(())
pub async fn get_container(http: &HttpClient, container_id: &str) -> Result<Container> {
Ok(http
.request::<SingleContainer>("GET", &format!("/ignite/containers/{container_id}"), None)
.await?
.ok_or_else(|| anyhow!("Error while parsing response"))?
.container)
}

pub async fn get_all_containers(http: &HttpClient, deployment_id: &str) -> Result<Vec<Container>> {
@@ -78,7 +93,7 @@ pub async fn get_container_logs(
Ok(response.logs)
}

const UNAVAILABLE_ELEMENT: &str = "-";
pub const UNAVAILABLE_ELEMENT: &str = "-";

pub fn format_containers(containers: &Vec<Container>, title: bool) -> Vec<String> {
let mut tw = TabWriter::new(vec![]);
@@ -159,3 +174,42 @@ fn format_log(log: &Log, colors: bool, timestamps: bool, details: bool) -> Strin

format!("{timestamp}{log_level}{}", log.message)
}

pub fn format_single_metrics(
metrics: &Option<Metrics>,
deployment: &Deployment,
) -> Result<Vec<String>> {
let mut buff = vec![];

buff.push(format!(
"CPU: {}",
metrics
.clone()
.map(|m| format!(
"{:.2}%/{} vcpu",
m.cpu_usage_percent(deployment.config.resources.vcpu),
deployment.config.resources.vcpu
))
.unwrap_or_else(|| UNAVAILABLE_ELEMENT.to_string())
));

buff.push(format!(
"Memory: {}",
metrics
.clone()
.map(|m| -> Result<String> {
let ram = parse_size(&deployment.config.resources.ram)?;

Ok(format!(
"{:.2}% {}/{}",
m.memory_usage_percent(ram),
user_friendly_size(m.memory_usage_bytes)?,
user_friendly_size(ram)?
))
})
.transpose()?
.unwrap_or_else(|| UNAVAILABLE_ELEMENT.to_string())
));

Ok(buff)
}
3 changes: 3 additions & 0 deletions src/commands/deploy/local/mod.rs
Original file line number Diff line number Diff line change
@@ -55,6 +55,8 @@ pub async fn build(
.arg(dir)
.arg("-t")
.arg(image)
.arg("--progress=plain")
.arg("--platform=linux/amd64")
.args(build_args)
.status()
.await?;
@@ -87,6 +89,7 @@ pub async fn build(
.arg("build")
.arg("-n")
.arg(image)
.arg("--platform=linux/amd64")
.arg(dir)
.status()
.await?;
2 changes: 2 additions & 0 deletions src/commands/deploy/mod.rs
Original file line number Diff line number Diff line change
@@ -34,6 +34,7 @@ const HOP_BUILD_BASE_URL: &str = "https://builder.hop.io/v1";

#[derive(Debug, Parser)]
#[clap(about = "Deploy a new container")]
#[group(skip)]
pub struct Options {
#[clap(
name = "dir",
@@ -167,6 +168,7 @@ pub async fn handle(options: Options, state: State) -> Result<()> {
&Deployment::default(),
&Some(default_name),
false,
&project,
)
.await?
};
26 changes: 16 additions & 10 deletions src/commands/domains/attach.rs
Original file line number Diff line number Diff line change
@@ -3,11 +3,12 @@ use clap::Parser;

use super::util::attach_domain;
use crate::commands::gateways::util::{format_gateways, get_all_gateways};
use crate::commands::ignite::utils::{format_deployments, get_all_deployments};
use crate::commands::ignite::groups::utils::fetch_grouped_deployments;
use crate::state::State;

#[derive(Debug, Parser)]
#[clap(about = "Attach a domain to a Gateway")]
#[group(skip)]
pub struct Options {
#[clap(help = "ID of the Gateway")]
pub gateway: Option<String>,
@@ -21,17 +22,22 @@ pub async fn handle(options: Options, state: State) -> Result<()> {
Some(id) => id,

None => {
let project_id = state.ctx.current_project_error()?.id;
let (deployments_fmt, deployments, validator) =
fetch_grouped_deployments(&state, false, true).await?;

let deployments = get_all_deployments(&state.http, &project_id).await?;
ensure!(!deployments.is_empty(), "No deployments found");
let deployments_fmt = format_deployments(&deployments, false);
let idx = loop {
let idx = dialoguer::Select::new()
.with_prompt("Select a deployment")
.items(&deployments_fmt)
.default(0)
.interact()?;

let idx = dialoguer::Select::new()
.with_prompt("Select a deployment")
.items(&deployments_fmt)
.default(0)
.interact()?;
if let Ok(idx) = validator(idx) {
break idx;
}

console::Term::stderr().clear_last_lines(1)?
};

let gateways = get_all_gateways(&state.http, &deployments[idx].id).await?;
ensure!(!gateways.is_empty(), "No Gateways found");
26 changes: 16 additions & 10 deletions src/commands/domains/delete.rs
Original file line number Diff line number Diff line change
@@ -3,11 +3,12 @@ use clap::Parser;

use super::util::{delete_domain, format_domains, get_all_domains};
use crate::commands::gateways::util::{format_gateways, get_all_gateways};
use crate::commands::ignite::utils::{format_deployments, get_all_deployments};
use crate::commands::ignite::groups::utils::fetch_grouped_deployments;
use crate::state::State;

#[derive(Debug, Parser)]
#[clap(about = "Detach a domain from a Gateway")]
#[group(skip)]
pub struct Options {
#[clap(help = "ID of the domain")]
pub domain: Option<String>,
@@ -18,17 +19,22 @@ pub async fn handle(options: Options, state: State) -> Result<()> {
Some(id) => id,

None => {
let project_id = state.ctx.current_project_error()?.id;
let (deployments_fmt, deployments, validator) =
fetch_grouped_deployments(&state, false, true).await?;

let deployments = get_all_deployments(&state.http, &project_id).await?;
ensure!(!deployments.is_empty(), "No deployments found");
let deployments_fmt = format_deployments(&deployments, false);
let idx = loop {
let idx = dialoguer::Select::new()
.with_prompt("Select a deployment")
.items(&deployments_fmt)
.default(0)
.interact()?;

let idx = dialoguer::Select::new()
.with_prompt("Select a deployment")
.items(&deployments_fmt)
.default(0)
.interact()?;
if let Ok(idx) = validator(idx) {
break idx;
}

console::Term::stderr().clear_last_lines(1)?
};

let gateways = get_all_gateways(&state.http, &deployments[idx].id).await?;
ensure!(!gateways.is_empty(), "No Gateways found");
26 changes: 16 additions & 10 deletions src/commands/domains/list.rs
Original file line number Diff line number Diff line change
@@ -3,11 +3,12 @@ use clap::Parser;

use super::util::{format_domains, get_all_domains};
use crate::commands::gateways::util::{format_gateways, get_all_gateways};
use crate::commands::ignite::utils::{format_deployments, get_all_deployments};
use crate::commands::ignite::groups::utils::fetch_grouped_deployments;
use crate::state::State;

#[derive(Debug, Parser)]
#[clap(about = "List all domains attached to a Gateway")]
#[group(skip)]
pub struct Options {
#[clap(help = "ID of the Gateway")]
pub gateway: Option<String>,
@@ -21,17 +22,22 @@ pub async fn handle(options: Options, state: State) -> Result<()> {
Some(id) => id,

None => {
let project_id = state.ctx.current_project_error()?.id;
let (deployments_fmt, deployments, validator) =
fetch_grouped_deployments(&state, false, true).await?;

let deployments = get_all_deployments(&state.http, &project_id).await?;
ensure!(!deployments.is_empty(), "No deployments found");
let deployments_fmt = format_deployments(&deployments, false);
let idx = loop {
let idx = dialoguer::Select::new()
.with_prompt("Select a deployment")
.items(&deployments_fmt)
.default(0)
.interact()?;

let idx = dialoguer::Select::new()
.with_prompt("Select a deployment")
.items(&deployments_fmt)
.default(0)
.interact()?;
if let Ok(idx) = validator(idx) {
break idx;
}

console::Term::stderr().clear_last_lines(1)?
};

let gateways = get_all_gateways(&state.http, &deployments[idx].id).await?;
ensure!(!gateways.is_empty(), "No Gateways found");
1 change: 1 addition & 0 deletions src/commands/domains/mod.rs
Original file line number Diff line number Diff line change
@@ -21,6 +21,7 @@ pub enum Commands {

#[derive(Debug, Parser)]
#[clap(about = "Interact with domains")]
#[group(skip)]
pub struct Options {
#[clap(subcommand)]
pub commands: Commands,
32 changes: 19 additions & 13 deletions src/commands/gateways/create.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use anyhow::{ensure, Result};
use anyhow::Result;
use clap::Parser;

use super::types::{GatewayProtocol, GatewayType};
use crate::commands::gateways::types::GatewayConfig;
use crate::commands::gateways::util::{create_gateway, update_gateway_config};
use crate::commands::ignite::utils::{format_deployments, get_all_deployments};
use crate::commands::ignite::groups::utils::fetch_grouped_deployments;
use crate::state::State;
use crate::utils::urlify;

@@ -28,6 +28,7 @@ pub struct GatewayOptions {

#[derive(Debug, Parser)]
#[clap(about = "Create a Gateway")]
#[group(skip)]
pub struct Options {
#[clap(name = "deployment", help = "ID of the deployment")]
pub deployment: Option<String>,
@@ -41,17 +42,22 @@ pub async fn handle(options: Options, state: State) -> Result<()> {
Some(deployment) => deployment,

None => {
let project_id = state.ctx.current_project_error()?.id;

let deployments = get_all_deployments(&state.http, &project_id).await?;
ensure!(!deployments.is_empty(), "No deployments found");
let deployments_fmt = format_deployments(&deployments, false);

let idx = dialoguer::Select::new()
.with_prompt("Select a deployment")
.items(&deployments_fmt)
.default(0)
.interact()?;
let (deployments_fmt, deployments, validator) =
fetch_grouped_deployments(&state, false, true).await?;

let idx = loop {
let idx = dialoguer::Select::new()
.with_prompt("Select a deployment")
.items(&deployments_fmt)
.default(0)
.interact()?;

if let Ok(idx) = validator(idx) {
break idx;
}

console::Term::stderr().clear_last_lines(1)?
};

deployments[idx].id.clone()
}
28 changes: 17 additions & 11 deletions src/commands/gateways/delete.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
use anyhow::{bail, ensure, Result};
use anyhow::{bail, Result};
use clap::Parser;

use crate::commands::gateways::util::{delete_gateway, format_gateways, get_all_gateways};
use crate::commands::ignite::utils::{format_deployments, get_all_deployments};
use crate::commands::ignite::groups::utils::fetch_grouped_deployments;
use crate::state::State;

#[derive(Debug, Parser)]
#[clap(about = "Delete gateways")]
#[group(skip)]
pub struct Options {
#[clap(name = "gateways", help = "IDs of the gateways")]
gateways: Vec<String>,
@@ -19,17 +20,22 @@ pub async fn handle(options: Options, state: State) -> Result<()> {
let gateways = if !options.gateways.is_empty() {
options.gateways
} else {
let project_id = state.ctx.current_project_error()?.id;
let (deployments_fmt, deployments, validator) =
fetch_grouped_deployments(&state, false, true).await?;

let deployments = get_all_deployments(&state.http, &project_id).await?;
ensure!(!deployments.is_empty(), "No deployments found");
let deployments_fmt = format_deployments(&deployments, false);
let idx = loop {
let idx = dialoguer::Select::new()
.with_prompt("Select a deployment")
.items(&deployments_fmt)
.default(0)
.interact()?;

let idx = dialoguer::Select::new()
.with_prompt("Select a deployment")
.items(&deployments_fmt)
.default(0)
.interact()?;
if let Ok(idx) = validator(idx) {
break idx;
}

console::Term::stderr().clear_last_lines(1)?
};

let gateways = get_all_gateways(&state.http, &deployments[idx].id).await?;
let gateways_fmt = format_gateways(&gateways, false);
28 changes: 17 additions & 11 deletions src/commands/gateways/list.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
use anyhow::{ensure, Result};
use anyhow::Result;
use clap::Parser;

use crate::commands::gateways::util::{format_gateways, get_all_gateways};
use crate::commands::ignite::utils::{format_deployments, get_all_deployments};
use crate::commands::ignite::groups::utils::fetch_grouped_deployments;
use crate::state::State;

#[derive(Debug, Parser)]
#[clap(about = "List all Gateways")]
#[group(skip)]
pub struct Options {
#[clap(name = "deployment", help = "ID of the deployment")]
pub deployment: Option<String>,
@@ -24,17 +25,22 @@ pub async fn handle(options: Options, state: State) -> Result<()> {
Some(deployment) => deployment,

None => {
let project_id = state.ctx.current_project_error()?.id;
let (deployments_fmt, deployments, validator) =
fetch_grouped_deployments(&state, false, true).await?;

let deployments = get_all_deployments(&state.http, &project_id).await?;
ensure!(!deployments.is_empty(), "No deployments found");
let deployments_fmt = format_deployments(&deployments, false);
let idx = loop {
let idx = dialoguer::Select::new()
.with_prompt("Select a deployment")
.items(&deployments_fmt)
.default(0)
.interact()?;

let idx = dialoguer::Select::new()
.with_prompt("Select a deployment")
.items(&deployments_fmt)
.default(0)
.interact()?;
if let Ok(idx) = validator(idx) {
break idx;
}

console::Term::stderr().clear_last_lines(1)?
};

deployments[idx].id.clone()
}
1 change: 1 addition & 0 deletions src/commands/gateways/mod.rs
Original file line number Diff line number Diff line change
@@ -24,6 +24,7 @@ pub enum Commands {

#[derive(Debug, Parser)]
#[clap(name = "gateways", about = "Interact with Ignite Gateways")]
#[group(skip)]
pub struct Options {
#[clap(subcommand)]
pub commands: Commands,
28 changes: 17 additions & 11 deletions src/commands/gateways/update.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
use anyhow::{ensure, Result};
use anyhow::Result;
use clap::Parser;

use super::create::GatewayOptions;
use crate::commands::gateways::types::GatewayConfig;
use crate::commands::gateways::util::{
format_gateways, get_all_gateways, get_gateway, update_gateway, update_gateway_config,
};
use crate::commands::ignite::utils::{format_deployments, get_all_deployments};
use crate::commands::ignite::groups::utils::fetch_grouped_deployments;
use crate::state::State;

#[derive(Debug, Parser)]
#[clap(about = "Update a Gateway")]
#[group(skip)]
pub struct Options {
#[clap(name = "gateway", help = "ID of the Gateway")]
pub gateway: Option<String>,
@@ -24,17 +25,22 @@ pub async fn handle(options: Options, state: State) -> Result<()> {
Some(gateway_id) => get_gateway(&state.http, &gateway_id).await?,

None => {
let project_id = state.ctx.current_project_error()?.id;
let (deployments_fmt, deployments, validator) =
fetch_grouped_deployments(&state, false, true).await?;

let deployments = get_all_deployments(&state.http, &project_id).await?;
ensure!(!deployments.is_empty(), "No deployments found");
let deployments_fmt = format_deployments(&deployments, false);
let idx = loop {
let idx = dialoguer::Select::new()
.with_prompt("Select a deployment")
.items(&deployments_fmt)
.default(0)
.interact()?;

let idx = dialoguer::Select::new()
.with_prompt("Select a deployment")
.items(&deployments_fmt)
.default(0)
.interact()?;
if let Ok(idx) = validator(idx) {
break idx;
}

console::Term::stderr().clear_last_lines(1)?
};

let gateways = get_all_gateways(&state.http, &deployments[idx].id).await?;
let gateways_fmt = format_gateways(&gateways, false);
4 changes: 1 addition & 3 deletions src/commands/gateways/util.rs
Original file line number Diff line number Diff line change
@@ -35,9 +35,7 @@ pub async fn get_all_gateways(http: &HttpClient, deployment_id: &str) -> Result<
let response = http
.request::<MultipleGateways>(
"GET",
&format!(
"/ignite/deployments/{deployment_id}/gateways"
),
&format!("/ignite/deployments/{deployment_id}/gateways"),
None,
)
.await?
26 changes: 16 additions & 10 deletions src/commands/ignite/builds/cancel.rs
Original file line number Diff line number Diff line change
@@ -3,11 +3,12 @@ use clap::Parser;

use super::utils::{cancel_build, format_builds, get_all_builds};
use crate::commands::ignite::builds::types::BuildState;
use crate::commands::ignite::utils::{format_deployments, get_all_deployments};
use crate::commands::ignite::groups::utils::fetch_grouped_deployments;
use crate::state::State;

#[derive(Debug, Parser)]
#[clap(about = "Cancel a running build")]
#[group(skip)]
pub struct Options {
#[clap(help = "ID of the deployment")]
pub build: Option<String>,
@@ -21,17 +22,22 @@ pub async fn handle(options: Options, state: State) -> Result<()> {
Some(id) => id,

None => {
let project_id = state.ctx.current_project_error()?.id;
let (deployments_fmt, deployments, validator) =
fetch_grouped_deployments(&state, false, true).await?;

let deployments = get_all_deployments(&state.http, &project_id).await?;
ensure!(!deployments.is_empty(), "No deployments found");
let deployments_fmt = format_deployments(&deployments, false);
let idx = loop {
let idx = dialoguer::Select::new()
.with_prompt("Select a deployment")
.items(&deployments_fmt)
.default(0)
.interact()?;

let idx = dialoguer::Select::new()
.with_prompt("Select a deployment")
.items(&deployments_fmt)
.default(0)
.interact()?;
if let Ok(idx) = validator(idx) {
break idx;
}

console::Term::stderr().clear_last_lines(1)?
};

let builds = get_all_builds(&state.http, &deployments[idx].id)
.await?
33 changes: 19 additions & 14 deletions src/commands/ignite/builds/list.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
use anyhow::{ensure, Result};
use anyhow::Result;
use clap::Parser;

use super::utils::{format_builds, get_all_builds};
use crate::commands::ignite::utils::{format_deployments, get_all_deployments};
use crate::state::State;
use crate::{commands::ignite::groups::utils::fetch_grouped_deployments, state::State};

#[derive(Debug, Parser)]
#[clap(about = "List all builds in a deployment")]
#[group(skip)]
pub struct Options {
#[clap(help = "ID of the deployment")]
pub deployment: Option<String>,
@@ -20,17 +20,22 @@ pub async fn handle(options: Options, state: State) -> Result<()> {
Some(id) => id,

None => {
let project_id = state.ctx.current_project_error()?.id;

let deployments = get_all_deployments(&state.http, &project_id).await?;
ensure!(!deployments.is_empty(), "No deployments found");
let deployments_fmt = format_deployments(&deployments, false);

let idx = dialoguer::Select::new()
.with_prompt("Select a deployment")
.items(&deployments_fmt)
.default(0)
.interact()?;
let (deployments_fmt, deployments, validator) =
fetch_grouped_deployments(&state, false, true).await?;

let idx = loop {
let idx = dialoguer::Select::new()
.with_prompt("Select a deployment")
.items(&deployments_fmt)
.default(0)
.interact()?;

if let Ok(idx) = validator(idx) {
break idx;
}

console::Term::stderr().clear_last_lines(1)?
};

deployments[idx].id.clone()
}
1 change: 1 addition & 0 deletions src/commands/ignite/builds/mod.rs
Original file line number Diff line number Diff line change
@@ -18,6 +18,7 @@ pub enum Commands {

#[derive(Debug, Parser)]
#[clap(about = "Interact with Ignite Builds")]
#[group(skip)]
pub struct Options {
#[clap(subcommand)]
pub commands: Commands,
2 changes: 2 additions & 0 deletions src/commands/ignite/create.rs
Original file line number Diff line number Diff line change
@@ -77,6 +77,7 @@ pub struct VolumeConfig {

#[derive(Debug, Parser, Default, PartialEq, Clone)]
#[clap(about = "Create a new deployment")]
#[group(skip)]
pub struct Options {
#[clap(flatten)]
pub config: DeploymentConfig,
@@ -104,6 +105,7 @@ pub async fn handle(options: Options, state: State) -> Result<()> {
&Deployment::default(),
&None,
false,
&project,
)
.await?;

36 changes: 22 additions & 14 deletions src/commands/ignite/delete.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
use anyhow::{bail, ensure, Result};
use anyhow::{bail, Result};
use clap::Parser;

use crate::commands::ignite::utils::{delete_deployment, format_deployments, get_all_deployments};
use crate::state::State;
use crate::{
commands::ignite::{groups::utils::fetch_grouped_deployments, utils::delete_deployment},
state::State,
};

#[derive(Debug, Parser)]
#[clap(about = "Delete a deployment")]
#[group(skip)]
pub struct Options {
#[clap(help = "ID of the deployment to delete")]
deployment: Option<String>,
@@ -19,17 +22,22 @@ pub async fn handle(options: Options, state: State) -> Result<()> {
Some(id) => id,

None => {
let project_id = state.ctx.current_project_error()?.id;

let deployments = get_all_deployments(&state.http, &project_id).await?;
ensure!(!deployments.is_empty(), "No deployments found");
let deployments_fmt = format_deployments(&deployments, false);

let idx = dialoguer::Select::new()
.with_prompt("Select a deployment")
.items(&deployments_fmt)
.default(0)
.interact()?;
let (deployments_fmt, deployments, validator) =
fetch_grouped_deployments(&state, false, true).await?;

let idx = loop {
let idx = dialoguer::Select::new()
.with_prompt("Select a deployment")
.items(&deployments_fmt)
.default(0)
.interact()?;

if let Ok(idx) = validator(idx) {
break idx;
}

console::Term::stderr().clear_last_lines(1)?
};

deployments[idx].id.clone()
}
2 changes: 2 additions & 0 deletions src/commands/ignite/from_compose/mod.rs
Original file line number Diff line number Diff line change
@@ -31,6 +31,7 @@ use crate::utils::urlify;

#[derive(Debug, Parser)]
#[clap(about = "Creates new Ignite deployments from a Docker compose file")]
#[group(skip)]
pub struct Options {
#[clap(help = "The file to read from. Defaults to docker-compose.yml")]
pub file: Option<PathBuf>,
@@ -135,6 +136,7 @@ pub async fn handle(options: Options, state: State) -> Result<()> {
&deployment,
&Some(name.clone()),
false,
&project,
)
.await?;

5 changes: 2 additions & 3 deletions src/commands/ignite/from_compose/types.rs
Original file line number Diff line number Diff line change
@@ -292,9 +292,8 @@ impl<'de> Deserialize<'de> for HealthCheckTest {
let value = Value::deserialize(deserializer)?;

// regex to extract the hostname port and path from a given curl command
let re =
Regex::new(r#"^curl\s?((?:-|--)[A-Za-z]+)*\s+(https?://)?([^/:\s]+)(:\d+)?(/.*)?$"#)
.unwrap();
let re = Regex::new(r"^curl\s?((?:-|--)[A-Za-z]+)*\s+(https?://)?([^/:\s]+)(:\d+)?(/.*)?$")
.unwrap();

let test_string = match value {
Value::String(s) => s,
33 changes: 20 additions & 13 deletions src/commands/ignite/get_env.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
use std::io::Write;

use anyhow::{ensure, Result};
use anyhow::Result;
use clap::Parser;

use crate::commands::ignite::utils::{format_deployments, get_all_deployments, get_deployment};
use crate::commands::ignite::groups::utils::fetch_grouped_deployments;
use crate::commands::ignite::utils::get_deployment;
use crate::commands::secrets::utils::get_secret_name;
use crate::state::State;

#[derive(Debug, Parser)]
#[clap(about = "Get current deployments env values")]
#[group(skip)]
pub struct Options {
#[clap(help = "ID of the deployment to get env values")]
pub deployment: Option<String>,
@@ -19,17 +21,22 @@ pub async fn handle(options: Options, state: State) -> Result<()> {
Some(id) => get_deployment(&state.http, &id).await?,

None => {
let project_id = state.ctx.current_project_error()?.id;

let deployments = get_all_deployments(&state.http, &project_id).await?;
ensure!(!deployments.is_empty(), "No deployments found");
let deployments_fmt = format_deployments(&deployments, false);

let idx = dialoguer::Select::new()
.with_prompt("Select a deployment")
.items(&deployments_fmt)
.default(0)
.interact()?;
let (deployments_fmt, deployments, validator) =
fetch_grouped_deployments(&state, false, true).await?;

let idx = loop {
let idx = dialoguer::Select::new()
.with_prompt("Select a deployment")
.items(&deployments_fmt)
.default(0)
.interact()?;

if let Ok(idx) = validator(idx) {
break idx;
}

console::Term::stderr().clear_last_lines(1)?
};

deployments[idx].clone()
}
69 changes: 69 additions & 0 deletions src/commands/ignite/groups/create.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
use anyhow::Result;
use clap::Parser;

use crate::commands::ignite::groups::utils::fetch_grouped_deployments;
use crate::state::State;

#[derive(Debug, Parser)]
#[clap(about = "Create a ne Ignite group")]
#[group(skip)]
pub struct Options {
#[clap(help = "The name of the group")]
pub name: Option<String>,
#[clap(short, long, help = "The deployments to add to the group")]
pub deployments: Vec<String>,
}

pub async fn handle(options: Options, state: State) -> Result<()> {
let project = state.ctx.current_project_error()?;

let name = if let Some(name) = options.name {
name
} else {
dialoguer::Input::new()
.with_prompt("Group Name")
.interact_text()?
};

let deployments = if !options.deployments.is_empty() {
options.deployments
} else {
let (deployments_fmt, deployments, validator) =
fetch_grouped_deployments(&state, false, true).await?;

let idxs = loop {
let idxs = dialoguer::MultiSelect::new()
.with_prompt("Select deployments")
.items(&deployments_fmt)
.interact()?;

if !idxs.is_empty() && idxs.iter().all(|idx| validator(*idx).is_ok()) {
break idxs;
}

console::Term::stderr().clear_last_lines(1)?
}
.into_iter()
.map(|idx| validator(idx).unwrap())
.collect::<Vec<_>>();

idxs.into_iter()
.map(|idx| deployments[idx].id.clone())
.collect()
};

let group = state
.hop
.ignite
.groups
.create(
&project.id,
&name,
&deployments.iter().map(|id| id.as_str()).collect::<Vec<_>>(),
)
.await?;

log::info!("Group successfully created. ID: {}\n", group.id);

Ok(())
}
39 changes: 39 additions & 0 deletions src/commands/ignite/groups/delete.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
use anyhow::{ensure, Result};
use clap::Parser;

use crate::state::State;

use super::utils::format_groups;

#[derive(Debug, Parser)]
#[clap(about = "Delete an Ignite group")]
#[group(skip)]
pub struct Options {
#[clap(help = "The ID of the group")]
pub group: Option<String>,
}

pub async fn handle(options: Options, state: State) -> Result<()> {
let group = if let Some(group) = options.group {
group
} else {
let project = state.ctx.current_project_error()?;

let mut groups = state.hop.ignite.groups.get_all(&project.id).await?;

ensure!(!groups.is_empty(), "No groups found");

groups.sort_unstable_by_key(|group| group.position);

let dialoguer_groups = dialoguer::Select::new()
.with_prompt("Select group")
.items(&format_groups(&groups)?)
.interact()?;

groups[dialoguer_groups].id.clone()
};

state.hop.ignite.groups.delete(&group).await?;

Ok(())
}
39 changes: 39 additions & 0 deletions src/commands/ignite/groups/list.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
use anyhow::Result;
use clap::Parser;

use crate::commands::ignite::groups::utils::format_groups;
use crate::state::State;

#[derive(Debug, Parser)]
#[clap(about = "List all Ignite groups")]
#[group(skip)]
pub struct Options {
#[clap(
short = 'q',
long = "quiet",
help = "Only print the IDs of the deployments"
)]
pub quiet: bool,
}

pub async fn handle(options: Options, state: State) -> Result<()> {
let project = state.ctx.current_project_error()?;

let groups = state.hop.ignite.groups.get_all(&project.id).await?;

if options.quiet {
let ids = groups
.iter()
.map(|d| d.id.as_str())
.collect::<Vec<_>>()
.join(" ");

println!("{ids}");
} else {
let formated = format_groups(&groups)?;

println!("{}", formated.join("\n"))
}

Ok(())
}
38 changes: 38 additions & 0 deletions src/commands/ignite/groups/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
mod create;
mod delete;
mod list;
mod r#move;
pub mod utils;

use anyhow::Result;
use clap::{Parser, Subcommand};

use crate::state::State;

#[derive(Debug, Subcommand)]
pub enum Commands {
#[clap(name = "new", alias = "create")]
Create(create::Options),
#[clap(name = "rm", alias = "delete")]
Delete(delete::Options),
#[clap(name = "move", alias = "add-deployment", alias = "add")]
Move(r#move::Options),
List(list::Options),
}

#[derive(Debug, Parser)]
#[clap(about = "Manage groups")]
#[group(skip)]
pub struct Options {
#[clap(subcommand)]
pub commands: Commands,
}

pub async fn handle(options: Options, state: State) -> Result<()> {
match options.commands {
Commands::Create(options) => create::handle(options, state).await,
Commands::Delete(options) => delete::handle(options, state).await,
Commands::Move(options) => r#move::handle(options, state).await,
Commands::List(options) => list::handle(options, state).await,
}
}
91 changes: 91 additions & 0 deletions src/commands/ignite/groups/move.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
use anyhow::{ensure, Result};
use clap::Parser;
use console::style;

use crate::commands::ignite::groups::utils::{fetch_grouped_deployments, format_groups};
use crate::commands::ignite::utils::get_deployment;
use crate::config::EXEC_NAME;
use crate::state::State;

#[derive(Debug, Parser)]
#[clap(about = "Move a Deployment to a group")]
#[group(skip)]
pub struct Options {
#[clap(help = "The ID of the group, or \"none\" to remove from a group")]
pub group: Option<String>,
#[clap(help = "Deployment ID to add")]
pub deployment: Option<String>,
}

pub async fn handle(options: Options, state: State) -> Result<()> {
let project = state.ctx.current_project_error()?;

let deployment = if let Some(deployment) = options.deployment {
get_deployment(&state.http, &deployment).await?
} else {
let (deployments_fmt, deployments, validator) =
fetch_grouped_deployments(&state, false, true).await?;

let idx = loop {
let idx = dialoguer::Select::new()
.with_prompt("Select a deployment")
.items(&deployments_fmt)
.default(0)
.interact()?;

if let Ok(idx) = validator(idx) {
break idx;
}

console::Term::stderr().clear_last_lines(1)?
};

deployments[idx].to_owned()
};

let group = if let Some(group) = options.group {
if group.is_empty() || ["none", "null"].contains(&group.to_lowercase().as_str()) {
None
} else {
Some(group)
}
} else {
let mut groups = state.hop.ignite.groups.get_all(&project.id).await?;

ensure!(
!groups.is_empty(),
"No groups found, create one with `{EXEC_NAME} ignite groups create`"
);

groups.sort_unstable_by_key(|group| group.position);

let mut formated = format_groups(&groups)?;

if deployment.group_id.is_some() {
formated.push(style("None (remove from a group)").white().to_string());
}

let dialoguer_groups = dialoguer::Select::new()
.with_prompt("Select group")
.default(0)
.items(&formated)
.interact()?;

if dialoguer_groups == groups.len() - 1 {
None
} else {
Some(groups[dialoguer_groups].id.clone())
}
};

state
.hop
.ignite
.groups
.move_deployment(group.as_deref(), &deployment.id)
.await?;

log::info!("Added deployment to group");

Ok(())
}
199 changes: 199 additions & 0 deletions src/commands/ignite/groups/utils.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
use std::{io::Write, str::FromStr};

use anyhow::{anyhow, ensure, Error, Ok, Result};
use chrono::{DateTime, Utc};
use hop::ignite::groups::types::Group;
use tabwriter::TabWriter;

use crate::{
commands::ignite::{types::Deployment, utils::get_all_deployments},
state::State,
utils::relative_time,
};

pub fn format_groups(groups: &[Group]) -> Result<Vec<String>> {
let mut tw = TabWriter::new(vec![]);

for group in groups {
writeln!(
tw,
"{}\t({}) - {}",
group.name,
group.id,
relative_time(group.created_at)
)?;
}

Ok(String::from_utf8(tw.into_inner()?)?
.lines()
.map(std::string::ToString::to_string)
.collect())
}

const BOTTOM_RIGHT: char = '├';
const UP_RIGHT: char = '└';
const HORIZONTAL: char = '─';
const MIN_LINE_PAD: usize = 4;

pub fn format_grouped_deployments_and_order(
groups: &[Group],
deployments: &[Deployment],
details: bool,
compact: bool,
) -> Result<(Vec<String>, Vec<Deployment>, Vec<usize>)> {
let mut tw = TabWriter::new(vec![]);
let mut deployments_ord = vec![];
let mut ignore_list = vec![];

if !groups.is_empty() {
let horiz_pad = (groups
.iter()
.map(|group| group.name.len())
.max()
.unwrap_or_default()
/ 2
+ 1)
.min(MIN_LINE_PAD);

for group in groups {
let deployments = deployments
.iter()
.filter(|d| d.group_id == Some(group.id.clone()));

let last_idx = deployments.clone().count() - 1;

for (idx, deployment) in deployments.enumerate() {
deployments_ord.push(deployment.to_owned());

match idx {
_ if idx == last_idx => {
writeln!(
tw,
"{}",
format_deployment(
deployment,
details,
Some(&pad(&UP_RIGHT.to_string(), horiz_pad))
)?,
)?;
}
idx => {
if idx == 0usize {
writeln!(tw, "{}", group.name)?;
ignore_list.push(deployments_ord.len() - 1 + ignore_list.len());
}

writeln!(
tw,
"{}",
format_deployment(
deployment,
details,
Some(&pad(&BOTTOM_RIGHT.to_string(), horiz_pad))
)?,
)?;
}
}
}

if !compact {
writeln!(tw)?;
}
}
}

for deployment in deployments.iter().filter(|dep| dep.group_id.is_none()) {
deployments_ord.push(deployment.to_owned());
writeln!(tw, "{}", format_deployment(deployment, details, None)?)?;
}

Ok((
String::from_utf8(tw.into_inner()?)?
.lines()
.map(std::string::ToString::to_string)
.collect(),
deployments_ord,
ignore_list,
))
}

/// Add horizontal lines to the end of a string until it reaches the specified length
fn pad(s: &str, len: usize) -> String {
let mut s = s.to_string();

while s.chars().count() < len {
s.push(HORIZONTAL)
}

s
}

fn format_deployment(
deployment: &Deployment,
details: bool,
prefix: Option<&str>,
) -> Result<String> {
Ok(format!(
"{}{}\t{}/{}\t{}\t{}",
if let Some(prefix) = prefix {
format!("{} ", prefix)
} else {
"".to_string()
},
if !details {
deployment.name.clone()
} else {
format!("{}\t({})", deployment.name, deployment.id)
},
deployment.container_count,
deployment.target_container_count,
relative_time(DateTime::<Utc>::from_str(&deployment.created_at)?),
deployment.config.type_,
))
}

pub async fn fetch_grouped_deployments(
state: &State,
details: bool,
compact: bool,
) -> Result<(
Vec<String>,
Vec<Deployment>,
impl Fn(usize) -> Result<usize, Error>,
)> {
let project_id = state.ctx.current_project_error()?.id;

let (groups, deployments) = tokio::join!(
state.hop.ignite.groups.get_all(&project_id),
get_all_deployments(&state.http, &project_id)
);

let (groups, deployments) = (groups?, deployments?);

ensure!(!deployments.is_empty(), "No deployments found");

let start = tokio::time::Instant::now();

let (fmt, deps, ignore_list) =
format_grouped_deployments_and_order(&groups, &deployments, details, compact)?;

log::debug!(
"format_grouped_deployments_and_order took {:?}",
start.elapsed()
);

// write a closure that will return an error if the index is in the ignore list
let closure = move |idx| {
let search = ignore_list.binary_search(&idx);

// because indexes are ordered, we can subtract the possible index from the current index
// to get the actual index in the `deps` vector
if let Err(possible) = search {
Ok(idx - possible)
} else {
Err(anyhow!("Invalid index selected"))
}
};

Ok((fmt, deps, closure))
}
33 changes: 19 additions & 14 deletions src/commands/ignite/health/create.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
use anyhow::{ensure, Result};
use anyhow::Result;
use clap::Parser;

use super::utils::{create_health_check, create_health_check_config};
use crate::commands::ignite::utils::{format_deployments, get_all_deployments};
use crate::state::State;
use crate::{commands::ignite::groups::utils::fetch_grouped_deployments, state::State};

#[derive(Debug, Parser)]
#[clap(about = "Create Health Checks for a deployment")]
#[group(skip)]
pub struct Options {
#[clap(name = "deployment", help = "ID of the deployment")]
pub deployment: Option<String>,
@@ -41,17 +41,22 @@ pub async fn handle(options: Options, state: State) -> Result<()> {
Some(id) => id,

None => {
let project_id = state.ctx.current_project_error()?.id;

let deployments = get_all_deployments(&state.http, &project_id).await?;
ensure!(!deployments.is_empty(), "No deployments found");
let deployments_fmt = format_deployments(&deployments, false);

let idx = dialoguer::Select::new()
.with_prompt("Select a deployment")
.items(&deployments_fmt)
.default(0)
.interact()?;
let (deployments_fmt, deployments, validator) =
fetch_grouped_deployments(&state, false, true).await?;

let idx = loop {
let idx = dialoguer::Select::new()
.with_prompt("Select a deployment")
.items(&deployments_fmt)
.default(0)
.interact()?;

if let Ok(idx) = validator(idx) {
break idx;
}

console::Term::stderr().clear_last_lines(1)?
};

deployments[idx].id.clone()
}
27 changes: 16 additions & 11 deletions src/commands/ignite/health/delete.rs
Original file line number Diff line number Diff line change
@@ -2,11 +2,11 @@ use anyhow::{bail, ensure, Result};
use clap::Parser;

use super::utils::{delete_health_check, format_health_checks, get_all_health_checks};
use crate::commands::ignite::utils::{format_deployments, get_all_deployments};
use crate::state::State;
use crate::{commands::ignite::groups::utils::fetch_grouped_deployments, state::State};

#[derive(Debug, Parser)]
#[clap(about = "Delete a Health Check")]
#[group(skip)]
pub struct Options {
#[clap(name = "heath-checks", help = "IDs of the Health Check")]
pub health_checks: Vec<String>,
@@ -19,17 +19,22 @@ pub async fn handle(options: Options, state: State) -> Result<()> {
let health_checks = if !options.health_checks.is_empty() {
options.health_checks
} else {
let project_id = state.ctx.current_project_error()?.id;
let (deployments_fmt, deployments, validator) =
fetch_grouped_deployments(&state, false, true).await?;

let deployments = get_all_deployments(&state.http, &project_id).await?;
ensure!(!deployments.is_empty(), "No deployments found");
let deployments_fmt = format_deployments(&deployments, false);
let idx = loop {
let idx = dialoguer::Select::new()
.with_prompt("Select a deployment")
.items(&deployments_fmt)
.default(0)
.interact()?;

let idx = dialoguer::Select::new()
.with_prompt("Select a deployment")
.items(&deployments_fmt)
.default(0)
.interact()?;
if let Ok(idx) = validator(idx) {
break idx;
}

console::Term::stderr().clear_last_lines(1)?
};

let health_checks = get_all_health_checks(&state.http, &deployments[idx].id).await?;
ensure!(!health_checks.is_empty(), "No health checks found");
33 changes: 19 additions & 14 deletions src/commands/ignite/health/list.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
use anyhow::{ensure, Result};
use anyhow::Result;
use clap::Parser;

use super::utils::{format_health_checks, get_all_health_checks};
use crate::commands::ignite::utils::{format_deployments, get_all_deployments};
use crate::state::State;
use crate::{commands::ignite::groups::utils::fetch_grouped_deployments, state::State};

#[derive(Debug, Parser)]
#[clap(about = "List Health Checks in a deployment")]
#[group(skip)]
pub struct Options {
#[clap(help = "ID of the deployment")]
pub deployment: Option<String>,
@@ -20,17 +20,22 @@ pub async fn handle(options: Options, state: State) -> Result<()> {
Some(id) => id,

None => {
let project_id = state.ctx.current_project_error()?.id;

let deployments = get_all_deployments(&state.http, &project_id).await?;
ensure!(!deployments.is_empty(), "No deployments found");
let deployments_fmt = format_deployments(&deployments, false);

let idx = dialoguer::Select::new()
.with_prompt("Select a deployment")
.items(&deployments_fmt)
.default(0)
.interact()?;
let (deployments_fmt, deployments, validator) =
fetch_grouped_deployments(&state, false, true).await?;

let idx = loop {
let idx = dialoguer::Select::new()
.with_prompt("Select a deployment")
.items(&deployments_fmt)
.default(0)
.interact()?;

if let Ok(idx) = validator(idx) {
break idx;
}

console::Term::stderr().clear_last_lines(1)?
};

deployments[idx].id.clone()
}
1 change: 1 addition & 0 deletions src/commands/ignite/health/mod.rs
Original file line number Diff line number Diff line change
@@ -24,6 +24,7 @@ pub enum Commands {

#[derive(Debug, Parser)]
#[clap(about = "Interact with Ignite Health Checks")]
#[group(skip)]
pub struct Options {
#[clap(subcommand)]
pub commands: Commands,
33 changes: 19 additions & 14 deletions src/commands/ignite/health/state.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
use anyhow::{ensure, Result};
use anyhow::Result;
use clap::Parser;

use super::utils::{format_health_state, get_health_state};
use crate::commands::ignite::utils::{format_deployments, get_all_deployments};
use crate::state::State;
use crate::{commands::ignite::groups::utils::fetch_grouped_deployments, state::State};

#[derive(Debug, Parser)]
#[clap(about = "Create Health Checks for a deployment")]
#[group(skip)]
pub struct Options {
#[clap(help = "ID of the Deployment")]
pub deployment: Option<String>,
@@ -17,17 +17,22 @@ pub async fn handle(options: Options, state: State) -> Result<()> {
Some(id) => id,

None => {
let project_id = state.ctx.current_project_error()?.id;

let deployments = get_all_deployments(&state.http, &project_id).await?;
ensure!(!deployments.is_empty(), "No deployments found");
let deployments_fmt = format_deployments(&deployments, false);

let idx = dialoguer::Select::new()
.with_prompt("Select a deployment")
.items(&deployments_fmt)
.default(0)
.interact()?;
let (deployments_fmt, deployments, validator) =
fetch_grouped_deployments(&state, false, true).await?;

let idx = loop {
let idx = dialoguer::Select::new()
.with_prompt("Select a deployment")
.items(&deployments_fmt)
.default(0)
.interact()?;

if let Ok(idx) = validator(idx) {
break idx;
}

console::Term::stderr().clear_last_lines(1)?
};

deployments[idx].id.clone()
}
121 changes: 121 additions & 0 deletions src/commands/ignite/inspect.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
use std::io::Write;

use anyhow::Result;
use clap::Parser;
use tabwriter::TabWriter;

use super::utils::get_tiers;
use crate::commands::ignite::groups::utils::fetch_grouped_deployments;
use crate::commands::ignite::utils::get_storage;
use crate::state::State;

#[derive(Debug, Parser)]
#[clap(about = "Inspect a deployment")]
#[group(skip)]
pub struct Options {
#[clap(help = "The ID or name of the deployment")]
pub deployment: Option<String>,
}

pub async fn handle(options: Options, state: State) -> Result<()> {
let mut deployment = if let Some(id_or_name) = options.deployment {
state.get_deployment_by_name_or_id(&id_or_name).await?
} else {
let (deployments_fmt, deployments, validator) =
fetch_grouped_deployments(&state, false, true).await?;

let idx = loop {
let idx = dialoguer::Select::new()
.with_prompt("Select a deployment")
.items(&deployments_fmt)
.default(0)
.interact()?;

if let Ok(idx) = validator(idx) {
break idx;
}

console::Term::stderr().clear_last_lines(1)?
};

deployments[idx].clone()
};

let (tiers, storage) = tokio::join!(
get_tiers(&state.http),
get_storage(&state.http, &deployment.id)
);
let (tiers, storage) = (tiers?, storage?);

let mut tw = TabWriter::new(vec![]);

writeln!(tw, "{} ({})", deployment.name, deployment.id)?;
writeln!(tw, " Metadata")?;
writeln!(tw, "\tImage: {}", deployment.config.image.name)?;
writeln!(tw, "\tCreated: {}", deployment.created_at)?;
writeln!(
tw,
"\tContainers: {}/{}",
deployment.container_count, deployment.target_container_count
)?;
writeln!(
tw,
"\tRestart Policy: {}",
deployment.config.restart_policy.take().unwrap_or_default()
)?;
writeln!(
tw,
"\tUses ephemeral containers: {}",
if deployment.is_ephemeral() {
"Yes"
} else {
"No"
}
)?;
writeln!(
tw,
"\tEntrypoint: {}",
deployment
.config
.entrypoint
.map(|s| serde_json::to_string(&s).unwrap())
.unwrap_or_else(|| "None".to_string())
)?;
writeln!(
tw,
"\tCommand: {}",
deployment
.config
.cmd
.map(|s| serde_json::to_string(&s).unwrap())
.unwrap_or_else(|| "None".to_string())
)?;
writeln!(tw, " Resources")?;
writeln!(
tw,
"\tTier: {}",
deployment.config.resources.get_tier_name(&tiers)?
)?;
writeln!(
tw,
"\tVolume: {}",
storage
.volume
.map(|s| s.to_string())
.unwrap_or_else(|| "None".to_string())
)?;
writeln!(
tw,
"\tBuild Cache: {}",
storage
.build_cache
.map(|s| s.to_string())
.unwrap_or_else(|| "None".to_string())
)?;

tw.flush()?;

print!("{}", String::from_utf8(tw.into_inner()?)?);

Ok(())
}
15 changes: 9 additions & 6 deletions src/commands/ignite/list.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
use anyhow::Result;
use clap::Parser;

use crate::commands::ignite::utils::{format_deployments, get_all_deployments};
use crate::state::State;
use crate::{
commands::ignite::{groups::utils::fetch_grouped_deployments, utils::get_all_deployments},
state::State,
};

#[derive(Debug, Parser)]
#[clap(about = "List all deployments")]
#[group(skip)]
pub struct Options {
#[clap(short, long, help = "Only print the IDs of the deployments")]
pub quiet: bool,
}

pub async fn handle(options: Options, state: State) -> Result<()> {
let project_id = state.ctx.current_project_error()?.id;
if options.quiet {
let project_id = state.ctx.current_project_error()?.id;

let deployments = get_all_deployments(&state.http, &project_id).await?;
let deployments = get_all_deployments(&state.http, &project_id).await?;

if options.quiet {
let ids = deployments
.iter()
.map(|d| d.id.as_str())
@@ -25,7 +28,7 @@ pub async fn handle(options: Options, state: State) -> Result<()> {

println!("{ids}");
} else {
let deployments_fmt = format_deployments(&deployments, true);
let deployments_fmt = fetch_grouped_deployments(&state, false, false).await?.0;

println!("{}", deployments_fmt.join("\n"));
}
11 changes: 10 additions & 1 deletion src/commands/ignite/mod.rs
Original file line number Diff line number Diff line change
@@ -3,7 +3,9 @@ pub mod create;
mod delete;
pub mod from_compose;
mod get_env;
pub mod groups;
mod health;
mod inspect;
mod list;
mod promote;
pub mod rollout;
@@ -26,9 +28,11 @@ pub enum Commands {
List(list::Options),
#[clap(name = "rm", alias = "delete")]
Delete(delete::Options),
Update(update::Options),
#[clap(alias = "info")]
Inspect(inspect::Options),
#[clap(alias = "rollouts")]
Rollout(rollout::Options),
Update(update::Options),
Scale(scale::Options),
#[clap(name = "get-env")]
GetEnv(get_env::Options),
@@ -38,6 +42,8 @@ pub enum Commands {
Health(health::Options),
#[clap(alias = "build")]
Builds(builds::Options),
#[clap(alias = "gr")]
Groups(groups::Options),
#[clap(alias = "rollback")]
Promote(promote::Options),
#[clap(alias = "template")]
@@ -52,6 +58,7 @@ pub enum Commands {

#[derive(Debug, Parser)]
#[clap(about = "Interact with Ignite deployments")]
#[group(skip)]
pub struct Options {
#[clap(subcommand)]
pub commands: Commands,
@@ -63,6 +70,7 @@ pub async fn handle(options: Options, state: State) -> Result<()> {
Commands::Create(options) => create::handle(options, state).await,
Commands::Delete(options) => delete::handle(options, state).await,
Commands::Update(options) => update::handle(options, state).await,
Commands::Inspect(options) => inspect::handle(options, state).await,
Commands::Rollout(options) => rollout::handle(options, state).await,
Commands::Scale(options) => scale::handle(options, state).await,
Commands::GetEnv(options) => get_env::handle(options, state).await,
@@ -74,5 +82,6 @@ pub async fn handle(options: Options, state: State) -> Result<()> {
Commands::FromCompose(options) => from_compose::handle(options, state).await,
Commands::Tunnel(options) => super::tunnel::handle(&options, state).await,
Commands::Templates(options) => templates::handle(options, state).await,
Commands::Groups(options) => groups::handle(options, state).await,
}
}
28 changes: 18 additions & 10 deletions src/commands/ignite/promote.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
use anyhow::{ensure, Result};
use clap::Parser;

use super::utils::{format_deployments, get_all_deployments, promote};
use super::utils::promote;
use crate::commands::ignite::builds::types::BuildState;
use crate::commands::ignite::builds::utils::get_all_builds;
use crate::commands::ignite::groups::utils::fetch_grouped_deployments;
use crate::state::State;

#[derive(Debug, Parser)]
#[clap(about = "Rollback containers in a deployment")]
#[group(skip)]
pub struct Options {
#[clap(help = "ID of the deployment")]
pub deployment: Option<String>,
@@ -21,17 +23,22 @@ pub async fn handle(options: Options, state: State) -> Result<()> {
Some(id) => id,

None => {
let project_id = state.ctx.current_project_error()?.id;
let (deployments_fmt, deployments, validator) =
fetch_grouped_deployments(&state, false, true).await?;

let deployments = get_all_deployments(&state.http, &project_id).await?;
ensure!(!deployments.is_empty(), "No deployments found");
let deployments_fmt = format_deployments(&deployments, false);
let idx = loop {
let idx = dialoguer::Select::new()
.with_prompt("Select a deployment")
.items(&deployments_fmt)
.default(0)
.interact()?;

let idx = dialoguer::Select::new()
.with_prompt("Select a deployment")
.items(&deployments_fmt)
.default(0)
.interact()?;
if let Ok(idx) = validator(idx) {
break idx;
}

console::Term::stderr().clear_last_lines(1)?
};

deployments[idx].id.clone()
}
@@ -46,6 +53,7 @@ pub async fn handle(options: Options, state: State) -> Result<()> {
.into_iter()
.filter(|b| matches!(b.state, BuildState::Succeeded))
.collect::<Vec<_>>();

ensure!(!builds.is_empty(), "No successful builds found");

let idx = dialoguer::Select::new()
34 changes: 20 additions & 14 deletions src/commands/ignite/rollout.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
use anyhow::{ensure, Result};
use anyhow::Result;
use clap::Parser;

use super::utils::{format_deployments, get_all_deployments, rollout};
use crate::state::State;
use super::utils::rollout;
use crate::{commands::ignite::groups::utils::fetch_grouped_deployments, state::State};

#[derive(Debug, Parser)]
#[clap(about = "Rollout new containers to a deployment")]
#[group(skip)]
pub struct Options {
#[clap(help = "ID of the deployment")]
pub deployment: Option<String>,
@@ -16,17 +17,22 @@ pub async fn handle(options: Options, state: State) -> Result<()> {
Some(id) => id,

None => {
let project_id = state.ctx.current_project_error()?.id;

let deployments = get_all_deployments(&state.http, &project_id).await?;
ensure!(!deployments.is_empty(), "No deployments found");
let deployments_fmt = format_deployments(&deployments, false);

let idx = dialoguer::Select::new()
.with_prompt("Select a deployment")
.items(&deployments_fmt)
.default(0)
.interact()?;
let (deployments_fmt, deployments, validator) =
fetch_grouped_deployments(&state, false, true).await?;

let idx = loop {
let idx = dialoguer::Select::new()
.with_prompt("Select a deployment")
.items(&deployments_fmt)
.default(0)
.interact()?;

if let Ok(idx) = validator(idx) {
break idx;
}

console::Term::stderr().clear_last_lines(1)?
};

deployments[idx].id.clone()
}
33 changes: 20 additions & 13 deletions src/commands/ignite/scale.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
use anyhow::{ensure, Result};
use anyhow::Result;
use clap::Parser;

use super::utils::{format_deployments, get_all_deployments, scale};
use super::utils::scale;
use crate::commands::ignite::groups::utils::fetch_grouped_deployments;
use crate::commands::ignite::utils::get_deployment;
use crate::state::State;

#[derive(Debug, Parser)]
#[clap(about = "Scale a deployment")]
#[group(skip)]
pub struct Options {
#[clap(help = "ID of the deployment to scale")]
pub deployment: Option<String>,
@@ -20,17 +22,22 @@ pub async fn handle(options: Options, state: State) -> Result<()> {
Some(id) => get_deployment(&state.http, &id).await?,

None => {
let project_id = state.ctx.current_project_error()?.id;

let deployments = get_all_deployments(&state.http, &project_id).await?;
ensure!(!deployments.is_empty(), "No deployments found");
let deployments_fmt = format_deployments(&deployments, false);

let idx = dialoguer::Select::new()
.with_prompt("Select a deployment")
.items(&deployments_fmt)
.default(0)
.interact()?;
let (deployments_fmt, deployments, validator) =
fetch_grouped_deployments(&state, false, true).await?;

let idx = loop {
let idx = dialoguer::Select::new()
.with_prompt("Select a deployment")
.items(&deployments_fmt)
.default(0)
.interact()?;

if let Ok(idx) = validator(idx) {
break idx;
}

console::Term::stderr().clear_last_lines(1)?
};

deployments[idx].clone()
}
161 changes: 147 additions & 14 deletions src/commands/ignite/templates.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
use anyhow::{anyhow, Result};
use clap::Parser;
use rand::Rng;
use regex::Regex;

use super::create::DeploymentConfig;
use crate::commands::containers::utils::create_containers;
use crate::commands::ignite::create::Options as CreateOptions;
use crate::commands::ignite::types::{Config, Deployment, Image, Volume};
use crate::commands::ignite::types::{
Autogen, Config, Deployment, Image, MapTo, PremadeInput, Volume,
};
use crate::commands::ignite::utils::{
create_deployment, format_premade, get_premade, update_deployment_config, WEB_IGNITE_URL,
};
@@ -14,6 +18,7 @@ use crate::utils::urlify;

#[derive(Debug, Parser, Default, PartialEq, Clone)]
#[clap(about = "Create a new deployment")]
#[group(skip)]
pub struct Options {
#[clap(flatten)]
pub config: DeploymentConfig,
@@ -46,6 +51,145 @@ pub async fn handle(options: Options, state: State) -> Result<()> {
&premades[selection]
};

let mut deployment = Deployment {
config: Config {
entrypoint: premade.entrypoint.clone(),
env: premade.environment.clone().unwrap_or_default(),
volume: Some(Volume {
fs: premade.filesystem.clone().unwrap_or_default(),
mount_path: premade.mountpath.clone(),
size: "".to_string(),
}),
..Default::default()
},

..Default::default()
};

if let Some(form) = &premade.form {
log::info!("This template requires some additional information");

for field in &form.fields {
let value = match &field.input {
PremadeInput::String {
default,
autogen,
max_length,
validator,
required,
} => {
let mut input = dialoguer::Input::<String>::new();

if let Some(default) = default {
input.default(default.clone());
} else if let Some(autogen) = autogen {
input.default(match autogen {
Autogen::ProjectNamespace => project.namespace.clone(),

Autogen::SecureToken => {
// generate random bits securely
let mut rng = rand::thread_rng();

// generate a random string of 24 characters
std::iter::repeat(())
.map(|()| rng.sample(rand::distributions::Alphanumeric))
.take(24)
.map(|b| b as char)
.collect()
}
});
}

input.validate_with(|input: &String| -> Result<(), String> {
if input.len() > *max_length {
return Err(
format!("Input must be less than {max_length} characters",),
);
}

let validator = {
let valid = validator.split('/').collect::<Vec<_>>();

if valid.len() == 3 {
valid[1]
} else {
return Err(format!("Invalid validator `{validator}`",));
}
};

if !Regex::new(validator)
.map_err(|e| e.to_string())?
.is_match(input)
{
return Err(format!("Input must match regex `{validator}`",));
}

Ok(())
});

if let Some(description) = &field.description {
input.with_prompt(format!("{} ({})", field.title, description));
} else {
input.with_prompt(&field.title);
}

input.allow_empty(!required);

let value = input.interact()?;

if *required {
value
} else {
continue;
}
}

PremadeInput::Range {
default,
min,
max,
increment,
unit,
} => {
let items = std::iter::repeat(())
.enumerate()
.map(|(i, _)| format!("{}{}", min + (i as u64 * increment), unit))
.take(((max - min) / increment) as usize)
.collect::<Vec<_>>();

let mut input = dialoguer::Select::new();

input.default(
items
.iter()
.position(|i| i == &format!("{default}{unit}"))
.unwrap_or(0),
);

input.with_prompt(&field.title);

input.items(&items);

items[input.interact()?].clone()
}
};

for place in &field.map_to {
match place {
MapTo::Env { key } => {
deployment.config.env.insert(key.clone(), value.clone());
}
MapTo::VolumeSize => {
deployment.config.volume = deployment.config.volume.take().map(|mut v| {
v.size = value.clone();
v
});
}
}
}
}
}

let (mut deployment_config, container_options) = update_deployment_config(
&state.http,
CreateOptions {
@@ -54,21 +198,10 @@ pub async fn handle(options: Options, state: State) -> Result<()> {
image: Some("".to_string()),
},
options == Options::default(),
&Deployment {
config: Config {
entrypoint: premade.entrypoint.clone(),
env: premade.environment.clone().unwrap_or_default(),
volume: Some(Volume {
fs: premade.filesystem.clone().unwrap_or_default(),
mount_path: premade.mountpath.clone(),
size: "".to_string(),
}),
..Default::default()
},
..Default::default()
},
&deployment,
&Some(premade.name.clone()),
false,
&project,
)
.await?;

Loading