Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ User-visible changes to `voltage`, newest first. Each pull request that changes

## Unreleased

- `voltage environments create --org ORG_ID --name NAME` adds an environment to an organization with a browser login. Without `--execute` it only describes the request and exits 6. The new environment's ID, generated unless you pass `--id`, is in `resource_id`.
- Filter `payments list` with `--check-only true|false`, and `wallets ledger list` with `--event-type adjustment`, from the refreshed API contract. Omit `--event-type` for payment ledger entries. Filters carry across pagination.
- `webhooks test WEBHOOK_ID` without `--data` sends a test event, with a new `delivery_id` and a payload of type `test` and event `created`. Before, it failed with "This operation requires --data". `--data` still sends your own payload.
- A list that returns every item in one response, such as `wallets list`, accepts `--all` and ignores it, because there are no more pages. Before, it rejected `--all`. `--all` still pages through lists that take `--limit`.
Expand Down
1 change: 1 addition & 0 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ These commands use the auth service (`https://auth.voltage.cloud/api/v1`, overri
| `voltage auth import-key` | local only |
| `voltage organizations list` | GET `/api/v1/users/current` |
| `voltage environments list` | POST `/api/v1/oauth/token`<br>GET `/api/v1/organizations/{organization_id}/environments` |
| `voltage environments create --name NAME` | POST `/api/v1/oauth/token`<br>POST `/api/v1/organizations/{organization_id}/environments` |
| `voltage context` | local only |
| `voltage profiles create NAME` | local only |
| `voltage profiles get NAME` | local only |
Expand Down
12 changes: 11 additions & 1 deletion docs/guides/login-and-profiles.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,20 @@ secret-manager-command | voltage auth import-key --stdin --account ci --org ORG_
VOLTAGE_API_KEY=... voltage wallets list --org ORG_ID
```

An imported key remembers the organization and environment it belongs to, and a command that selects a different one fails before any request is sent. `VOLTAGE_API_KEY` is used only when neither `--profile` nor `--account` is given. `organizations list` and `environments list` require a browser login.
An imported key remembers the organization and environment it belongs to, and a command that selects a different one fails before any request is sent. `VOLTAGE_API_KEY` is used only when neither `--profile` nor `--account` is given. `organizations list`, `environments list`, and `environments create` require a browser login.

With several saved credentials, select one with `--account` or through a profile.

## Create an environment

`environments create` adds an environment to the selected organization through the auth service. Like any change, it only describes the request (exit code 6) until you add `--execute`:

```sh
voltage environments create --account me --org ORG_ID --name staging --execute
```

The CLI generates the environment's ID unless you pass `--id UUID`, and reports it in `resource_id` and in the result's `environment_id`, so the next command can use it. On a terminal, the result suggests a `voltage profiles create` command for the new environment. The description omits the name unless you pass `--show-secrets`. If the request gets no response, the environment may have been created: run `voltage environments list` and look for the reported ID before you run the command again.

## Scope

A resource ID is positional, or passed as `--id ID`, as in `voltage wallets get --id WALLET_ID`. A command that acts on one wallet also takes it as `--wallet WALLET_ID`; a profile's or `VOLTAGE_WALLET_ID`'s wallet never selects it. The enclosing scope comes from `--org`, `--env`, `--wallet`, and `--webhook`:
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ When you sign in as the same user, the new login replaces the saved one, and the

### needs a browser login

`organizations list` and `environments list` need a browser login; an API key cannot list them. Pass `--account NAME` for a saved login, or unset `VOLTAGE_API_KEY`.
`organizations list`, `environments list`, and `environments create` need a browser login; an API key cannot run them. Pass `--account NAME` for a saved login, or unset `VOLTAGE_API_KEY`.

### HTTP 401

Expand Down
1 change: 1 addition & 0 deletions scripts/check-coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
('voltage auth import-key', []),
('voltage organizations list', [('GET', '/api/v1/users/current')]),
('voltage environments list', [('POST', '/api/v1/oauth/token'), ('GET', '/api/v1/organizations/{organization_id}/environments')]),
('voltage environments create --name NAME', [('POST', '/api/v1/oauth/token'), ('POST', '/api/v1/organizations/{organization_id}/environments')]),
('voltage context', []),
('voltage profiles create NAME', []),
('voltage profiles get NAME', []),
Expand Down
53 changes: 40 additions & 13 deletions src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -617,16 +617,9 @@ pub async fn execute(
dry_run::announce(terminal, invocation, &described);
}
out.write(described, &[])?;
return Err(Error::new(
ErrorKind::NotExecuted,
format!(
"Described this change without sending it; pass --execute or set {EXECUTE_VARIABLE}=1 to send it"
),
));
return Err(not_sent());
}
Execution::SendFromEnvironment => global.terminal().notice(format!(
"Sending this change because {EXECUTE_VARIABLE} is set."
)),
Execution::SendFromEnvironment => sending_from_environment(global.terminal()),
Execution::Send => {}
}
if operation.id.returns_one_time_secret() && !out.secure_destination() {
Expand Down Expand Up @@ -762,8 +755,15 @@ impl Execution {
/// changes are described.
pub fn resolve(invocation: &ApiInvocation, execute_variable: Option<bool>) -> Self {
match invocation.requested {
Requested::Execute => Self::Send,
Requested::Unspecified if invocation.operation.method == Method::Get => Self::Send,
requested => Self::change(requested, execute_variable),
}
}

/// `resolve` for a change, such as `environments create`, which is never a read.
pub fn change(requested: Requested, execute_variable: Option<bool>) -> Self {
match requested {
Requested::Execute => Self::Send,
Requested::Unspecified if execute_variable == Some(true) => Self::SendFromEnvironment,
Requested::Unspecified => Self::DescribeChange,
}
Expand All @@ -775,6 +775,23 @@ impl Execution {
}
}

/// The error after a described change, whose exit code tells a script nothing was sent.
pub fn not_sent() -> Error {
Error::new(
ErrorKind::NotExecuted,
format!(
"Described this change without sending it; pass --execute or set {EXECUTE_VARIABLE}=1 to send it"
),
)
}

/// Say that `VOLTAGE_EXECUTE`, not the command line, sends this change.
pub fn sending_from_environment(terminal: Terminal) {
terminal.notice(format!(
"Sending this change because {EXECUTE_VARIABLE} is set."
));
}

pub const OFFSET_PAGINATION_DEPRECATED: &str =
"Offset pagination is deprecated by the API; omit --offset and --pagination to page by cursor.";

Expand Down Expand Up @@ -1359,7 +1376,7 @@ pub(crate) enum SessionChange {
}

struct SubmittedWrite {
operation: OperationId,
payment: bool,
resource_id: Option<Uuid>,
organization_id: Option<Uuid>,
environment_ids: Vec<Uuid>,
Expand All @@ -1369,13 +1386,23 @@ impl SubmissionState {
/// A command sends at most one mutation, so the first record is the only one.
fn record(&self, operation: OperationId, resource_id: Option<Uuid>, scope: &Scope) {
let _ = self.write.set(SubmittedWrite {
operation,
payment: operation.submits_payment(),
resource_id,
organization_id: scope.org,
environment_ids: scope.envs.clone(),
});
}

/// `record` for a change sent to the auth service, which is never a payment.
pub(crate) fn record_auth_change(&self, resource_id: Uuid, scope: &Scope) {
let _ = self.write.set(SubmittedWrite {
payment: false,
resource_id: Some(resource_id),
organization_id: scope.org,
environment_ids: scope.envs.clone(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit. When the user sends Ctrl-C during the send, this records scope.envs. With a profile that selects environment E1, the error reports environment_ids: [E1] next to the new environment's ID. The no-response path in create_environment passes Vec::new(). The two paths should report the same thing, and an empty list is correct for a change to the auth service:

Suggested change
environment_ids: scope.envs.clone(),
environment_ids: Vec::new(),

});
}

pub(crate) fn session_changing(&self, change: SessionChange) {
*self.session.lock().unwrap_or_else(PoisonError::into_inner) = Some(change);
}
Expand Down Expand Up @@ -1442,7 +1469,7 @@ impl SubmissionState {
};
};
let message = match write.resource_id {
Some(id) if write.operation.submits_payment() => format!(
Some(id) if write.payment => format!(
"Interrupted after payment {id} may have been submitted; query its original ID before resubmitting. The payment was not cancelled."
),
_ => "Interrupted after the request may have been submitted; check its result before retrying. The request was not cancelled.".to_owned(),
Expand Down
Loading
Loading