Skip to content

Commit

Permalink
Merge pull request #5192 from systeminit/migrate-workspace-endpoints
Browse files Browse the repository at this point in the history
chore(sdf): Migrate workspace API endpoints to v2
  • Loading branch information
stack72 authored Dec 23, 2024
2 parents acb81b0 + 6cde8df commit 711b283
Show file tree
Hide file tree
Showing 7 changed files with 150 additions and 82 deletions.
6 changes: 3 additions & 3 deletions app/web/src/store/module.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ export const useModuleStore = () => {
"modules",
];

const WORKSPACE_API_PREFIX = ["v2", "workspaces", { workspaceId }];

return addStoreHooks(
workspaceId,
changeSetId,
Expand Down Expand Up @@ -439,8 +441,7 @@ export const useModuleStore = () => {

return new ApiRequest<{ id: string }>({
method: "post",
url: "/module/export_workspace",
params: { ...getVisibilityParams() },
url: WORKSPACE_API_PREFIX.concat(["export"]),
onSuccess: (response) => {
this.exportingWorkspaceOperationId = response.id;
},
Expand Down Expand Up @@ -473,7 +474,6 @@ export const useModuleStore = () => {
{
eventType: "AsyncFinish",
callback: async ({ id }: { id: string }) => {
await this.LOAD_LOCAL_MODULES();
if (id === this.exportingWorkspaceOperationId) {
this.exportingWorkspaceOperationRunning = false;
}
Expand Down
5 changes: 1 addition & 4 deletions app/web/src/store/workspaces.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,7 @@ export const useWorkspacesStore = () => {
this.importError = undefined;
return new ApiRequest<{ id: string }>({
method: "post",
url: "/module/install_workspace",
params: {
id: moduleId,
},
url: `/v2/workspaces/${moduleId}/install`,
onSuccess: (data) => {
this.workspaceImportSummary = null;
this.importId = data.id;
Expand Down
10 changes: 0 additions & 10 deletions lib/sdf-server/src/service/module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,9 @@ const MAX_NAME_SEARCH_ATTEMPTS: usize = 100;

pub mod approval_process;
pub mod builtin_module_spec;
mod export_workspace;
pub mod get_module;
pub mod import_workspace_vote;
pub mod install_module;
mod install_workspace;
pub mod reject_module;
pub mod remote_module_spec;

Expand Down Expand Up @@ -243,16 +241,8 @@ pub async fn pkg_open(builder: &DalContextBuilder, file_name: &str) -> ModuleRes

pub fn routes() -> Router<AppState> {
Router::new()
.route(
"/export_workspace",
post(export_workspace::export_workspace),
)
.route("/get_module_by_hash", get(get_module::get_module_by_hash))
.route("/install_module", post(install_module::install_module))
.route(
"/install_workspace",
post(install_workspace::install_workspace),
)
.route(
"/remote_module_spec",
get(remote_module_spec::remote_module_spec),
Expand Down
6 changes: 4 additions & 2 deletions lib/sdf-server/src/service/v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ pub mod management;
pub mod module;
pub mod variant;
pub mod view;
pub mod workspace;

const PREFIX: &str = "/workspaces/:workspace_id/change-sets/:change_set_id";

// use this if you don't need to pass in the change_set id
const CHANGE_SET_PREFIX: &str = "/workspaces/:workspace_id/change-sets";

const INTEGRATIONS_PREFIX: &str = "/workspaces/:workspace_id";
const WORKSPACES_PREFIX: &str = "/workspaces/:workspace_id";

pub fn routes(state: AppState) -> Router<AppState> {
Router::new()
Expand All @@ -29,8 +30,9 @@ pub fn routes(state: AppState) -> Router<AppState> {
.nest(&format!("{PREFIX}/schema-variants"), variant::v2_routes())
.nest(&format!("{PREFIX}/management"), management::v2_routes())
.nest(&format!("{PREFIX}/views"), view::v2_routes())
.nest(WORKSPACES_PREFIX, workspace::v2_routes())
.nest(
&format!("{INTEGRATIONS_PREFIX}/integrations"),
&format!("{WORKSPACES_PREFIX}/integrations"),
integrations::v2_routes(),
)
}
54 changes: 54 additions & 0 deletions lib/sdf-server/src/service/v2/workspace.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
use crate::{app_state::AppState, service::ApiError};
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
routing::post,
Router,
};
use dal::{TransactionsError, UserError, UserPk, WorkspaceError, WorkspacePk};
use thiserror::Error;

mod export_workspace;
mod install_workspace;

#[remain::sorted]
#[derive(Debug, Error)]
pub enum WorkspaceAPIError {
#[error("Trying to export from/import into root tenancy")]
ExportingImportingWithRootTenancy,
#[error("invalid user: {0}")]
InvalidUser(UserPk),
#[error("Module index: {0}")]
ModuleIndex(#[from] module_index_client::ModuleIndexClientError),
#[error("Module index not configured")]
ModuleIndexNotConfigured,
#[error("transactions error: {0}")]
Transactions(#[from] TransactionsError),
#[error("Unable to parse URL: {0}")]
Url(#[from] url::ParseError),
#[error("user error: {0}")]
User(#[from] UserError),
#[error("workspace error: {0}")]
Workspace(#[from] WorkspaceError),
#[error("Could not find current workspace {0}")]
WorkspaceNotFound(WorkspacePk),
}

pub type WorkspaceAPIResult<T> = Result<T, WorkspaceAPIError>;

impl IntoResponse for WorkspaceAPIError {
fn into_response(self) -> Response {
let (status_code, error_message) = match self {
WorkspaceAPIError::WorkspaceNotFound(_) => (StatusCode::NOT_FOUND, self.to_string()),
_ => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()),
};

ApiError::new(status_code, error_message).into_response()
}
}

pub fn v2_routes() -> Router<AppState> {
Router::new()
.route("/install", post(install_workspace::install_workspace))
.route("/export", post(export_workspace::export_workspace))
}
Original file line number Diff line number Diff line change
@@ -1,30 +1,22 @@
use axum::{
extract::{Host, OriginalUri},
extract::{Host, OriginalUri, Path},
http::Uri,
Json,
};
use chrono::Utc;
use dal::{DalContext, HistoryActor, User, Visibility, Workspace, WorkspaceError, WsEvent};
use dal::{DalContext, HistoryActor, User, Workspace, WorkspacePk, WsEvent};
use serde::{Deserialize, Serialize};
use si_events::audit_log::AuditLogKind;
use telemetry::prelude::*;
use telemetry::prelude::info;
use ulid::Ulid;

use crate::{
extract::{AccessBuilder, HandlerContext, PosthogClient, RawAccessToken},
service::{
async_route::handle_error,
module::{ModuleError, ModuleResult},
},
service::async_route::handle_error,
track,
};

#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ExportWorkspaceRequest {
#[serde(flatten)]
pub visibility: Visibility,
}
use super::{WorkspaceAPIError, WorkspaceAPIResult};

#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
Expand All @@ -39,24 +31,26 @@ pub async fn export_workspace(
PosthogClient(posthog_client): PosthogClient,
OriginalUri(original_uri): OriginalUri,
Host(host_name): Host,
Json(request): Json<ExportWorkspaceRequest>,
) -> ModuleResult<Json<ExportWorkspaceResponse>> {
let ctx = builder.build(request_ctx.build(request.visibility)).await?;
Path(_workspace_pk): Path<WorkspacePk>,
) -> WorkspaceAPIResult<Json<ExportWorkspaceResponse>> {
let ctx = builder.build_head(request_ctx).await?;

let current_workspace = {
let workspace_pk = ctx
.tenancy()
.workspace_pk_opt()
.ok_or(WorkspaceAPIError::ExportingImportingWithRootTenancy)?;
Workspace::get_by_pk(&ctx, &workspace_pk)
.await?
.ok_or(WorkspaceAPIError::WorkspaceNotFound(workspace_pk))?
};

let task_id = Ulid::new();

let workspace_pk = ctx
.tenancy()
.workspace_pk_opt()
.ok_or(ModuleError::ExportingImportingWithRootTenancy)?;
let workspace = Workspace::get_by_pk(&ctx, &workspace_pk)
.await?
.ok_or(ModuleError::WorkspaceNotFound(workspace_pk))?;

tokio::task::spawn(async move {
if let Err(err) = export_workspace_inner(
&ctx,
workspace,
current_workspace,
&original_uri,
&host_name,
PosthogClient(posthog_client),
Expand Down Expand Up @@ -84,38 +78,44 @@ pub async fn export_workspace(

pub async fn export_workspace_inner(
ctx: &DalContext,
workspace: Workspace,
current_workspace: Workspace,
original_uri: &Uri,
host_name: &String,
PosthogClient(posthog_client): PosthogClient,
RawAccessToken(raw_access_token): RawAccessToken,
) -> ModuleResult<()> {
) -> WorkspaceAPIResult<()> {
info!("Exporting workspace backup");
let version = Utc::now().format("%Y-%m-%d_%H:%M:%S").to_string();

let index_client = {
let module_index_url = match ctx.module_index_url() {
Some(url) => url,
None => return Err(ModuleError::ModuleIndexNotConfigured),
None => return Err(WorkspaceAPIError::ModuleIndexNotConfigured),
};

module_index_client::ModuleIndexClient::new(module_index_url.try_into()?, &raw_access_token)
};

let workspace_payload = workspace.generate_export_data(ctx, &version).await?;
let workspace_payload = current_workspace
.generate_export_data(ctx, &version)
.await?;

index_client
.upload_workspace(workspace.name().as_str(), &version, workspace_payload)
.upload_workspace(
current_workspace.name().as_str(),
&version,
workspace_payload,
)
.await?;

let workspace_id = *workspace.pk();
let workspace_id = *current_workspace.pk();
ctx.write_audit_log(
AuditLogKind::ExportWorkspace {
id: workspace_id,
name: workspace.name().clone(),
name: current_workspace.name().clone(),
version: version.clone(),
},
workspace.name().to_string(),
current_workspace.name().to_string(),
)
.await?;

Expand All @@ -124,7 +124,7 @@ pub async fn export_workspace_inner(
let created_by = if let HistoryActor::User(user_pk) = ctx.history_actor() {
let user = User::get_by_pk(ctx, *user_pk)
.await?
.ok_or(WorkspaceError::InvalidUser(*user_pk))?;
.ok_or(WorkspaceAPIError::InvalidUser(*user_pk))?;

user.email().clone()
} else {
Expand All @@ -138,7 +138,7 @@ pub async fn export_workspace_inner(
host_name,
"export_workspace",
serde_json::json!({
"pkg_name": workspace.name().to_owned(),
"pkg_name": current_workspace.name().to_owned(),
"pkg_version": version,
"pkg_created_by_email": created_by,
}),
Expand Down
Loading

0 comments on commit 711b283

Please sign in to comment.