Skip to content

Commit 98bd510

Browse files
committed
Add env command for managing environments
- Add env list/get/create/delete/set/unset commands - Add Environment types to backend - Add migration for unique (user_id, name) constraint on environments
1 parent 3242c18 commit 98bd510

9 files changed

Lines changed: 657 additions & 3 deletions

File tree

README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,33 @@ ow workers rm my-api
7878

7979
Supported file types: `.js`, `.ts`, `.wasm`
8080

81+
### Environments
82+
83+
Manage environment variables and secrets.
84+
85+
```bash
86+
# List environments
87+
ow env list
88+
89+
# Get environment details
90+
ow env get production
91+
92+
# Create an environment
93+
ow env create production -d "Production environment"
94+
95+
# Set a variable
96+
ow env set production API_URL "https://api.example.com"
97+
98+
# Set a secret (value will be masked)
99+
ow env set production API_KEY "secret-key" --secret
100+
101+
# Remove a variable
102+
ow env unset production API_URL
103+
104+
# Delete an environment
105+
ow env delete production
106+
```
107+
81108
### Database Operations
82109

83110
Requires a `db` type alias.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- Add unique constraint on environment name per user
2+
ALTER TABLE environments ADD CONSTRAINT environments_user_name_unique UNIQUE (user_id, name);

src/backend/api.rs

Lines changed: 128 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
use super::{Backend, BackendError, CreateWorkerInput, DeployInput, Deployment, Worker};
1+
use super::{
2+
Backend, BackendError, CreateEnvironmentInput, CreateWorkerInput, DeployInput, Deployment,
3+
Environment, UpdateEnvironmentInput, Worker,
4+
};
25
use reqwest::Client;
36

47
pub struct ApiBackend {
@@ -154,4 +157,128 @@ impl Backend for ApiBackend {
154157
let deployment: Deployment = response.json().await?;
155158
Ok(deployment)
156159
}
160+
161+
async fn list_environments(&self) -> Result<Vec<Environment>, BackendError> {
162+
let response = self
163+
.request(reqwest::Method::GET, "/environments")
164+
.send()
165+
.await?;
166+
167+
if response.status() == reqwest::StatusCode::UNAUTHORIZED {
168+
return Err(BackendError::Unauthorized);
169+
}
170+
171+
if !response.status().is_success() {
172+
let text = response.text().await.unwrap_or_default();
173+
return Err(BackendError::Api(text));
174+
}
175+
176+
let environments: Vec<Environment> = response.json().await?;
177+
Ok(environments)
178+
}
179+
180+
async fn get_environment(&self, name: &str) -> Result<Environment, BackendError> {
181+
let response = self
182+
.request(reqwest::Method::GET, &format!("/environments/{}", name))
183+
.send()
184+
.await?;
185+
186+
if response.status() == reqwest::StatusCode::NOT_FOUND {
187+
return Err(BackendError::NotFound(format!(
188+
"Environment '{}' not found",
189+
name
190+
)));
191+
}
192+
193+
if response.status() == reqwest::StatusCode::UNAUTHORIZED {
194+
return Err(BackendError::Unauthorized);
195+
}
196+
197+
if !response.status().is_success() {
198+
let text = response.text().await.unwrap_or_default();
199+
return Err(BackendError::Api(text));
200+
}
201+
202+
let environment: Environment = response.json().await?;
203+
Ok(environment)
204+
}
205+
206+
async fn create_environment(
207+
&self,
208+
input: CreateEnvironmentInput,
209+
) -> Result<Environment, BackendError> {
210+
let response = self
211+
.request(reqwest::Method::POST, "/environments")
212+
.json(&input)
213+
.send()
214+
.await?;
215+
216+
if response.status() == reqwest::StatusCode::UNAUTHORIZED {
217+
return Err(BackendError::Unauthorized);
218+
}
219+
220+
if !response.status().is_success() {
221+
let text = response.text().await.unwrap_or_default();
222+
return Err(BackendError::Api(text));
223+
}
224+
225+
let environment: Environment = response.json().await?;
226+
Ok(environment)
227+
}
228+
229+
async fn update_environment(
230+
&self,
231+
name: &str,
232+
input: UpdateEnvironmentInput,
233+
) -> Result<Environment, BackendError> {
234+
let response = self
235+
.request(reqwest::Method::PATCH, &format!("/environments/{}", name))
236+
.json(&input)
237+
.send()
238+
.await?;
239+
240+
if response.status() == reqwest::StatusCode::NOT_FOUND {
241+
return Err(BackendError::NotFound(format!(
242+
"Environment '{}' not found",
243+
name
244+
)));
245+
}
246+
247+
if response.status() == reqwest::StatusCode::UNAUTHORIZED {
248+
return Err(BackendError::Unauthorized);
249+
}
250+
251+
if !response.status().is_success() {
252+
let text = response.text().await.unwrap_or_default();
253+
return Err(BackendError::Api(text));
254+
}
255+
256+
let environment: Environment = response.json().await?;
257+
Ok(environment)
258+
}
259+
260+
async fn delete_environment(&self, name: &str) -> Result<(), BackendError> {
261+
let response = self
262+
.request(reqwest::Method::DELETE, &format!("/environments/{}", name))
263+
.send()
264+
.await?;
265+
266+
if response.status() == reqwest::StatusCode::NOT_FOUND {
267+
return Err(BackendError::NotFound(format!(
268+
"Environment '{}' not found",
269+
name
270+
)));
271+
}
272+
273+
if response.status() == reqwest::StatusCode::UNAUTHORIZED {
274+
return Err(BackendError::Unauthorized);
275+
}
276+
277+
if !response.status().is_success() {
278+
let text = response.text().await.unwrap_or_default();
279+
return Err(BackendError::Api(text));
280+
}
281+
282+
Ok(())
283+
}
157284
}

src/backend/db.rs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
use super::{Backend, BackendError, CreateWorkerInput, DeployInput, Deployment, Worker};
1+
use super::{
2+
Backend, BackendError, CreateEnvironmentInput, CreateWorkerInput, DeployInput, Deployment,
3+
Environment, UpdateEnvironmentInput, Worker,
4+
};
25
use sha2::{Digest, Sha256};
36
use sqlx::{PgPool, Row};
47

@@ -176,4 +179,41 @@ impl Backend for DbBackend {
176179
message: row.get("message"),
177180
})
178181
}
182+
183+
async fn list_environments(&self) -> Result<Vec<Environment>, BackendError> {
184+
Err(BackendError::Api(
185+
"Environments require API access. Use an API alias.".to_string(),
186+
))
187+
}
188+
189+
async fn get_environment(&self, _name: &str) -> Result<Environment, BackendError> {
190+
Err(BackendError::Api(
191+
"Environments require API access. Use an API alias.".to_string(),
192+
))
193+
}
194+
195+
async fn create_environment(
196+
&self,
197+
_input: CreateEnvironmentInput,
198+
) -> Result<Environment, BackendError> {
199+
Err(BackendError::Api(
200+
"Environments require API access. Use an API alias.".to_string(),
201+
))
202+
}
203+
204+
async fn update_environment(
205+
&self,
206+
_name: &str,
207+
_input: UpdateEnvironmentInput,
208+
) -> Result<Environment, BackendError> {
209+
Err(BackendError::Api(
210+
"Environments require API access. Use an API alias.".to_string(),
211+
))
212+
}
213+
214+
async fn delete_environment(&self, _name: &str) -> Result<(), BackendError> {
215+
Err(BackendError::Api(
216+
"Environments require API access. Use an API alias.".to_string(),
217+
))
218+
}
179219
}

src/backend/mock.rs

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
use super::{Backend, BackendError, CreateWorkerInput, DeployInput, Deployment, Worker};
1+
use super::{
2+
Backend, BackendError, CreateEnvironmentInput, CreateWorkerInput, DeployInput, Deployment,
3+
Environment, UpdateEnvironmentInput, Worker,
4+
};
25
use chrono::Utc;
36
use sha2::{Digest, Sha256};
47
use std::collections::HashMap;
@@ -8,6 +11,7 @@ use std::sync::{Arc, Mutex};
811
struct MockState {
912
workers: HashMap<String, Worker>,
1013
deployments: HashMap<String, Vec<Deployment>>,
14+
environments: HashMap<String, Environment>,
1115
}
1216

1317
#[derive(Default, Clone)]
@@ -154,4 +158,80 @@ impl Backend for MockBackend {
154158

155159
Ok(deployment)
156160
}
161+
162+
async fn list_environments(&self) -> Result<Vec<Environment>, BackendError> {
163+
let state = self.state.lock().unwrap();
164+
let mut environments: Vec<Environment> = state.environments.values().cloned().collect();
165+
environments.sort_by(|a, b| a.name.cmp(&b.name));
166+
Ok(environments)
167+
}
168+
169+
async fn get_environment(&self, name: &str) -> Result<Environment, BackendError> {
170+
let state = self.state.lock().unwrap();
171+
state
172+
.environments
173+
.get(name)
174+
.cloned()
175+
.ok_or_else(|| BackendError::NotFound(format!("Environment '{}' not found", name)))
176+
}
177+
178+
async fn create_environment(
179+
&self,
180+
input: CreateEnvironmentInput,
181+
) -> Result<Environment, BackendError> {
182+
let mut state = self.state.lock().unwrap();
183+
184+
if state.environments.contains_key(&input.name) {
185+
return Err(BackendError::Api(format!(
186+
"Environment '{}' already exists",
187+
input.name
188+
)));
189+
}
190+
191+
let environment = Environment {
192+
id: uuid::Uuid::new_v4().to_string(),
193+
name: input.name.clone(),
194+
description: input.desc,
195+
values: vec![],
196+
created_at: Utc::now(),
197+
updated_at: Utc::now(),
198+
};
199+
200+
state.environments.insert(input.name, environment.clone());
201+
Ok(environment)
202+
}
203+
204+
async fn update_environment(
205+
&self,
206+
name: &str,
207+
input: UpdateEnvironmentInput,
208+
) -> Result<Environment, BackendError> {
209+
let mut state = self.state.lock().unwrap();
210+
211+
let environment = state
212+
.environments
213+
.get_mut(name)
214+
.ok_or_else(|| BackendError::NotFound(format!("Environment '{}' not found", name)))?;
215+
216+
if let Some(new_name) = input.name {
217+
environment.name = new_name;
218+
}
219+
220+
environment.updated_at = Utc::now();
221+
222+
Ok(environment.clone())
223+
}
224+
225+
async fn delete_environment(&self, name: &str) -> Result<(), BackendError> {
226+
let mut state = self.state.lock().unwrap();
227+
228+
if state.environments.remove(name).is_none() {
229+
return Err(BackendError::NotFound(format!(
230+
"Environment '{}' not found",
231+
name
232+
)));
233+
}
234+
235+
Ok(())
236+
}
157237
}

0 commit comments

Comments
 (0)