Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(sdf): Migrate workspace API endpoints to v2 #5192

Merged
merged 1 commit into from
Dec 23, 2024
Merged
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
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"]),
zacharyhamm marked this conversation as resolved.
Show resolved Hide resolved
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
Loading