diff --git a/Cargo.lock b/Cargo.lock index ff39e49..5007f7e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -62,8 +62,8 @@ checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" [[package]] name = "amp-client" -version = "0.7.4" -source = "git+https://github.com/amphitheatre-app/amp-client-rust?tag=v0.7.4#44813d305ffed8951dad053327d56538fca96bb3" +version = "0.7.6" +source = "git+https://github.com/amphitheatre-app/amp-client-rust?tag=v0.7.6#082f4db7d0668b8249e1cb7f3e744149711e9ee3" dependencies = [ "amp-common", "futures", @@ -75,8 +75,8 @@ dependencies = [ [[package]] name = "amp-common" -version = "0.7.4" -source = "git+https://github.com/amphitheatre-app/common?tag=v0.7.4#d4f8b4b6cfee8766a2d19bcc96291d09db6d0228" +version = "0.7.7" +source = "git+https://github.com/amphitheatre-app/common?tag=v0.7.7#663f18822f1a3cad634ef38396698c9add0e2f22" dependencies = [ "anyhow", "chrono", diff --git a/Cargo.toml b/Cargo.toml index cfc6148..0591e40 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,8 +14,8 @@ name = "playground" path = "src/lib.rs" [dependencies] -amp-client = { git = "https://github.com/amphitheatre-app/amp-client-rust", tag = "v0.7.4" } -amp-common = { git = "https://github.com/amphitheatre-app/common", tag = "v0.7.4" } +amp-client = { git = "https://github.com/amphitheatre-app/amp-client-rust", tag = "v0.7.6" } +amp-common = { git = "https://github.com/amphitheatre-app/common", tag = "v0.7.7" } anyhow = "1.0" axum = { version = "0.7.4" } clap = { version = "4.4.12", features = ["derive", "env"] } diff --git a/src/errors.rs b/src/errors.rs index 44b55de..1cf1228 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -59,6 +59,9 @@ pub enum ApiError { #[error("Not Found Repo: {0}")] NotFoundRepo(anyhow::Error), + + #[error("Bad Playbook Request: {0}")] + BadPlaybookRequest(String), } impl IntoResponse for ApiError { @@ -76,6 +79,7 @@ impl IntoResponse for ApiError { Self::FailedToSynchronize(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()), Self::BadPlaybook(e) => (StatusCode::BAD_REQUEST, e.to_string()), Self::NotFoundRepo(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()), + Self::BadPlaybookRequest(e) => (StatusCode::BAD_REQUEST, e.to_string()), }; error!("{} - {}", status, message); diff --git a/src/handlers/file.rs b/src/handlers/file.rs index ab4a68e..d4b5bcd 100644 --- a/src/handlers/file.rs +++ b/src/handlers/file.rs @@ -29,10 +29,9 @@ use crate::services::FileService; /// Returns a file's content. #[utoipa::path( - get, path = "/v1/playbooks/{id}/files/{reference}/{path}", + get, path = "/v1/playbooks/{id}/files/{path}", params( ("id" = Uuid, description = "The id of playbook"), - ("reference" = String, description = "The name of the commit/branch/tag."), ("path" = String, description = "The file path relative to the root of the repository."), ), responses( @@ -43,19 +42,15 @@ use crate::services::FileService; ), tag = "Files" )] -pub async fn get( - State(ctx): State>, - Path((id, reference, path)): Path<(Uuid, String, String)>, -) -> Result { - Ok(Json(FileService::get(ctx, id, reference, path).await?)) +pub async fn get(State(ctx): State>, Path((id, path)): Path<(Uuid, String)>) -> Result { + Ok(Json(FileService::get(ctx, id, path).await?)) } /// Create a file #[utoipa::path( - post, path = "/v1/playbooks/{id}/files/{reference}/{path}", + post, path = "/v1/playbooks/{id}/files/{path}", params( ("id" = Uuid, description = "The id of playbook"), - ("reference" = String, description = "The name of the commit/branch/tag."), ("path" = String, description = "The file path relative to the root of the repository."), ), request_body( @@ -74,20 +69,18 @@ pub async fn create( State(ctx): State>, Path(id): Path, - Path(reference): Path, Path(path): Path, Json(req): Json, ) -> Result { - Ok((StatusCode::CREATED, Json(FileService::create(ctx, id, reference, path, req.content).await?))) + Ok((StatusCode::CREATED, Json(FileService::create(ctx, id, path, req.content).await?))) } /// Update a file #[utoipa::path( - put, path = "/v1/playbooks/{id}/files/{reference}/{path}", + put, path = "/v1/playbooks/{id}/files/{path}", params( ("id" = Uuid, description = "The id of playbook"), - ("reference" = String, description = "The name of the commit/branch/tag. Default: default branch."), ("path" = String, description = "The file path relative to the root of the repository."), ), request_body( @@ -107,20 +100,18 @@ pub async fn update( State(ctx): State>, Path(id): Path, - Path(reference): Path, Path(path): Path, Json(req): Json, ) -> Result { - Ok(Json(FileService::update(ctx, id, reference, path, req.content).await?)) + Ok(Json(FileService::update(ctx, id, path, req.content).await?)) } /// Delete a file #[utoipa::path( - delete, path = "/v1/playbooks/{id}/files/{reference}/{path}", + delete, path = "/v1/playbooks/{id}/files/{path}", params( ("id" = Uuid, description = "The id of playbook"), - ("reference" = String, description = "The name of the commit/branch/tag."), ("path" = String, description = "The file path relative to the root of the repository."), ), responses( @@ -135,20 +126,18 @@ pub async fn delete( State(ctx): State>, Path(id): Path, - Path(reference): Path, Path(path): Path, ) -> Result { - FileService::delete(ctx, id, reference, path).await?; + FileService::delete(ctx, id, path).await?; Ok(StatusCode::NO_CONTENT) } /// Copy a file #[utoipa::path( - post, path = "/v1/playbooks/{id}/files/{reference}/{path}/actions/copy", + post, path = "/v1/playbooks/{id}/files/{path}/actions/copy", params( ("id" = Uuid, description = "The id of playbook"), - ("reference" = String, description = "The name of the commit/branch/tag. Default: default branch."), ("path" = String, description = "The file path relative to the root of the repository."), ), request_body( @@ -168,20 +157,18 @@ pub async fn copy( State(ctx): State>, Path(id): Path, - Path(reference): Path, Path(path): Path, Json(req): Json, ) -> Result { - Ok(Json(FileService::copy(ctx, id, reference, path, req.destination).await?)) + Ok(Json(FileService::copy(ctx, id, path, req.destination).await?)) } /// Move a file #[utoipa::path( - post, path = "/v1/playbooks/{id}/files/{reference}/{path}/actions/move", + post, path = "/v1/playbooks/{id}/files/{path}/actions/move", params( ("id" = Uuid, description = "The id of playbook"), - ("reference" = String, description = "The name of the commit/branch/tag. Default: default branch."), ("path" = String, description = "The file path relative to the root of the repository."), ), request_body( @@ -201,10 +188,9 @@ pub async fn rename( State(ctx): State>, Path(id): Path, - Path(reference): Path, Path(path): Path, Json(req): Json, ) -> Result { - Ok(Json(FileService::rename(ctx, id, reference, path, req.destination).await?)) + Ok(Json(FileService::rename(ctx, id, path, req.destination).await?)) } diff --git a/src/handlers/folder.rs b/src/handlers/folder.rs index 8b308f2..8db8714 100644 --- a/src/handlers/folder.rs +++ b/src/handlers/folder.rs @@ -28,14 +28,32 @@ use crate::services::FolderService; // The Folders Service Handlers. -/// Returns a folder's tree. +/// Gets the file list of a directory in a repository. #[utoipa::path( - get, path = "/v1/playbooks/{id}/folders/{reference}/{path}", + get, path = "/v1/playbooks/{id}/folders/{path}", params( ("id" = Uuid, description = "The id of playbook"), - ("reference" = String, description = "The name of the commit/branch/tag."), ("path" = String, description = "The file path relative to the root of the repository."), ), + responses( + (status = 200, description = "The folder tree", body = Vec), + (status = 404, description = "Playbook not found"), + (status = 404, description = "Folder not found"), + (status = 500, description = "Internal Server Error"), + ), + tag = "Folders" +)] +pub async fn get(State(ctx): State>, Path((id, path)): Path<(Uuid, String)>) -> Result { + Ok(Json(FolderService::get(ctx, id, path).await?)) +} + +/// Returns a folder's tree. + +#[utoipa::path( + get, path = "/v1/playbooks/{id}/tree", + params( + ("id" = Uuid, description = "The id of playbook"), + ), responses( (status = 200, description = "The folder tree", body = Tree), (status = 404, description = "Playbook not found"), @@ -44,20 +62,19 @@ use crate::services::FolderService; ), tag = "Folders" )] -pub async fn get( +pub async fn tree( State(ctx): State>, - Path((id, reference, path)): Path<(Uuid, String, Option)>, + Path(id): Path, Query(params): Query>, ) -> Result { - Ok(Json(FolderService::get(ctx, id, reference, path, params.get("recursive")).await?)) + Ok(Json(FolderService::tree(ctx, id, params.get("recursive")).await?)) } /// Create a folder #[utoipa::path( - post, path = "/v1/playbooks/{id}/folders/{reference}/{path}", + post, path = "/v1/playbooks/{id}/folders/{path}", params( ("id" = Uuid, description = "The id of playbook"), - ("reference" = String, description = "The name of the commit/branch/tag."), ("path" = String, description = "The file path relative to the root of the repository."), ), responses( @@ -71,18 +88,16 @@ pub async fn create( State(ctx): State>, Path(id): Path, - Path(reference): Path, Path(path): Path, ) -> Result { - Ok((StatusCode::CREATED, Json(FolderService::create(ctx, id, reference, path).await?))) + Ok((StatusCode::CREATED, Json(FolderService::create(ctx, id, path).await?))) } /// Delete a folder #[utoipa::path( - delete, path = "/v1/playbooks/{id}/folders/{reference}/{path}", + delete, path = "/v1/playbooks/{id}/folders/{path}", params( ("id" = Uuid, description = "The id of playbook"), - ("reference" = String, description = "The name of the commit/branch/tag."), ("path" = String, description = "The file path relative to the root of the repository."), ), responses( @@ -97,20 +112,18 @@ pub async fn delete( State(ctx): State>, Path(id): Path, - Path(reference): Path, Path(path): Path, ) -> Result { - FolderService::delete(ctx, id, reference, path).await?; + FolderService::delete(ctx, id, path).await?; Ok(StatusCode::NO_CONTENT) } /// Copy a folder #[utoipa::path( - post, path = "/v1/playbooks/{id}/folders/{reference}/{path}/actions/copy", + post, path = "/v1/playbooks/{id}/folders/{path}/actions/copy", params( ("id" = Uuid, description = "The id of playbook"), - ("reference" = String, description = "The name of the commit/branch/tag. Default: default branch."), ("path" = String, description = "The file path relative to the root of the repository."), ), request_body( @@ -130,20 +143,18 @@ pub async fn copy( State(ctx): State>, Path(id): Path, - Path(reference): Path, Path(path): Path, Json(req): Json, ) -> Result { - Ok(Json(FolderService::copy(ctx, id, reference, path, req.destination).await?)) + Ok(Json(FolderService::copy(ctx, id, path, req.destination).await?)) } /// Move a folder #[utoipa::path( - post, path = "/v1/playbooks/{id}/folders/{reference}/{path}/actions/move", + post, path = "/v1/playbooks/{id}/folders/{path}/actions/move", params( ("id" = Uuid, description = "The id of playbook"), - ("reference" = String, description = "The name of the commit/branch/tag. Default: default branch."), ("path" = String, description = "The file path relative to the root of the repository."), ), request_body( @@ -163,10 +174,9 @@ pub async fn rename( State(ctx): State>, Path(id): Path, - Path(reference): Path, Path(path): Path, Json(req): Json, ) -> Result { - Ok(Json(FolderService::rename(ctx, id, reference, path, req.destination).await?)) + Ok(Json(FolderService::rename(ctx, id, path, req.destination).await?)) } diff --git a/src/requests/playbook.rs b/src/requests/playbook.rs index 2828966..2b10d49 100644 --- a/src/requests/playbook.rs +++ b/src/requests/playbook.rs @@ -17,6 +17,18 @@ use utoipa::ToSchema; #[derive(Debug, Serialize, Deserialize, ToSchema)] pub struct CreatePlaybookRequest { + /// Source code repository the partner should be cloned from. + /// e.g. https://github.com/amphitheatre-app/amphitheatre.git. pub repo: String, - pub reference: Option, + /// Git branch the partner should be cloned from. eg. master or main + #[serde(skip_serializing_if = "Option::is_none")] + pub branch: Option, + /// Git tag the partner should be cloned from. eg. v1.0 + #[serde(skip_serializing_if = "Option::is_none")] + pub tag: Option, + /// A commit hash like rev = "4c59b707", or a named reference exposed by + /// the remote repository such as rev = "refs/pull/493/head". What references + /// are available varies by where the repo is hosted. + #[serde(skip_serializing_if = "Option::is_none")] + pub rev: Option, } diff --git a/src/routes.rs b/src/routes.rs index ca84cbc..2fd1760 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -31,17 +31,18 @@ pub fn build() -> Router> { .route("/v1/playbooks/:id/logs", get(logger::logs)) // // files - .route("/v1/playbooks/:id/files/:reference/:path", get(file::get)) - .route("/v1/playbooks/:id/files/:reference/:path", post(file::create)) - .route("/v1/playbooks/:id/files/:reference/:path", put(file::update)) - .route("/v1/playbooks/:id/files/:reference/:path", delete(file::delete)) - .route("/v1/playbooks/:id/files/:reference/:path/actions/copy", post(file::copy)) - .route("/v1/playbooks/:id/files/:reference/:path/actions/move", post(file::rename)) + .route("/v1/playbooks/:id/files/:path", get(file::get)) + .route("/v1/playbooks/:id/files/:path", post(file::create)) + .route("/v1/playbooks/:id/files/:path", put(file::update)) + .route("/v1/playbooks/:id/files/:path", delete(file::delete)) + .route("/v1/playbooks/:id/files/:path/actions/copy", post(file::copy)) + .route("/v1/playbooks/:id/files/:path/actions/move", post(file::rename)) // // folders - .route("/v1/playbooks/:id/folders/:reference/:path", get(folder::get)) - .route("/v1/playbooks/:id/folders/:reference/:path", post(folder::create)) - .route("/v1/playbooks/:id/folders/:reference/:path", delete(folder::delete)) - .route("/v1/playbooks/:id/folders/:reference/:path/actions/copy", post(folder::copy)) - .route("/v1/playbooks/:id/folders/:reference/:path/actions/move", post(folder::rename)) + .route("/v1/playbooks/:id/folders/:path", get(folder::get)) + .route("/v1/playbooks/:id/tree", get(folder::tree)) + .route("/v1/playbooks/:id/folders/:path", post(folder::create)) + .route("/v1/playbooks/:id/folders/:path", delete(folder::delete)) + .route("/v1/playbooks/:id/folders/:path/actions/copy", post(folder::copy)) + .route("/v1/playbooks/:id/folders/:path/actions/move", post(folder::rename)) } diff --git a/src/services/file.rs b/src/services/file.rs index f959380..0ec6fb3 100644 --- a/src/services/file.rs +++ b/src/services/file.rs @@ -27,24 +27,18 @@ pub struct FileService; impl FileService { /// Get a file content from the remote git repository. - pub async fn get(ctx: Arc, id: Uuid, reference: String, path: String) -> Result { + pub async fn get(ctx: Arc, id: Uuid, path: String) -> Result { let playbook = ctx.client.playbooks().get(&id.to_string()).map_err(ApiError::NotFoundPlaybook)?; let source = playbook.preface.repository.unwrap(); ctx.github_client .contents() - .find(&utils::repo(&source.repo)?, &path, &reference) + .find(&utils::repo(&source.repo)?, &path, &source.branch.unwrap_or_default()) .map_err(|e| ApiError::NotFoundContent(e.to_string())) } /// Create a file to the workspace. - pub async fn create( - ctx: Arc, - id: Uuid, - _reference: String, - path: String, - content: String, - ) -> Result { + pub async fn create(ctx: Arc, id: Uuid, path: String, content: String) -> Result { let data = content.into_bytes(); let req = Synchronization { kind: EventKinds::Create, @@ -61,43 +55,25 @@ impl FileService { } /// Update a file to the workspace. - pub async fn update( - _ctx: Arc, - _id: Uuid, - _reference: String, - _path: String, - _content: String, - ) -> Result { + pub async fn update(_ctx: Arc, _id: Uuid, _path: String, _content: String) -> Result { // refer to create() method. todo!() } /// Delete a file from the workspace. - pub async fn delete(_ctx: Arc, _id: Uuid, _reference: String, _path: String) -> Result<()> { + pub async fn delete(_ctx: Arc, _id: Uuid, _path: String) -> Result<()> { // refer to create() method. todo!() } /// Copy a file to the destination path on the workspace. - pub async fn copy( - _ctx: Arc, - _id: Uuid, - _reference: String, - _path: String, - _destination: String, - ) -> Result { + pub async fn copy(_ctx: Arc, _id: Uuid, _path: String, _destination: String) -> Result { // refer to create() method. todo!() } /// Move a file to the destination path on the workspace. - pub async fn rename( - _ctx: Arc, - _id: Uuid, - _reference: String, - _path: String, - _destination: String, - ) -> Result { + pub async fn rename(_ctx: Arc, _id: Uuid, _path: String, _destination: String) -> Result { // refer to create() method. todo!() } diff --git a/src/services/folder.rs b/src/services/folder.rs index e26cd9a..a84b38d 100644 --- a/src/services/folder.rs +++ b/src/services/folder.rs @@ -12,67 +12,58 @@ // See the License for the specific language governing permissions and // limitations under the License. -use amp_common::scm::git::Tree; use std::sync::Arc; use uuid::Uuid; -use amp_common::scm::content::Content; +use amp_common::scm::content::{Content, File}; +use amp_common::scm::git::Tree; use crate::context::Context; use crate::errors::{ApiError, Result}; use crate::utils; +use crate::utils::unwrap_or_error; pub struct FolderService; impl FolderService { - pub async fn get( - ctx: Arc, - id: Uuid, - _reference: String, - path: Option, - recursive: Option<&String>, - ) -> Result { + pub async fn get(ctx: Arc, id: Uuid, path: String) -> Result, ApiError> { let playbook = ctx.client.playbooks().get(&id.to_string()).map_err(ApiError::NotFoundPlaybook)?; - let source = playbook.preface.repository.unwrap(); - let content = ctx - .github_client + let source = unwrap_or_error(playbook.preface.repository, "The repository is none")?; + let reference = unwrap_or_error(source.reference(), "The reference is none")?; + + ctx.github_client .contents() - .find(&utils::repo(&source.repo)?, &path.unwrap_or_default(), &source.branch.unwrap_or_default()) - .map_err(|e| ApiError::NotFoundContent(e.to_string()))?; + .list(&utils::repo(&source.repo)?, &path, &reference) + .map_err(|e| ApiError::NotFoundContent(e.to_string())) + } + + pub async fn tree(ctx: Arc, id: Uuid, recursive: Option<&String>) -> Result { + let playbook = ctx.client.playbooks().get(&id.to_string()).map_err(ApiError::NotFoundPlaybook)?; + + let source = unwrap_or_error(playbook.preface.repository, "The repository is none")?; + let reference = unwrap_or_error(source.reference(), "The reference is none")?; ctx.github_client .git() - .get_tree(&utils::repo(&source.repo)?, &content.sha, Option::from(recursive.is_some())) + .get_tree(&utils::repo(&source.repo)?, &reference, Option::from(recursive.is_some())) .map_err(|e| ApiError::NotFoundFolder(e.to_string()))? .ok_or(ApiError::NotFoundFolder("The folder is none".to_string())) } - pub async fn create(_ctx: Arc, _id: Uuid, _reference: String, _path: String) -> Result { + pub async fn create(_ctx: Arc, _id: Uuid, _path: String) -> Result { todo!() } - pub async fn delete(_ctx: Arc, _id: Uuid, _reference: String, _path: String) -> Result<()> { + pub async fn delete(_ctx: Arc, _id: Uuid, _path: String) -> Result<()> { todo!() } - pub async fn copy( - _ctx: Arc, - _id: Uuid, - _reference: String, - _path: String, - _destination: String, - ) -> Result { + pub async fn copy(_ctx: Arc, _id: Uuid, _path: String, _destination: String) -> Result { todo!() } - pub async fn rename( - _ctx: Arc, - _id: Uuid, - _reference: String, - _path: String, - _destination: String, - ) -> Result { + pub async fn rename(_ctx: Arc, _id: Uuid, _path: String, _destination: String) -> Result { todo!() } } diff --git a/src/services/playbook.rs b/src/services/playbook.rs index 978321a..b260714 100644 --- a/src/services/playbook.rs +++ b/src/services/playbook.rs @@ -33,15 +33,17 @@ impl PlaybookService { let repo = repo(&req.repo)?; let repository = ctx.github_client.repositories().find(&repo).map_err(ApiError::NotFoundRepo)?; let description = repository.and_then(|r| r.description).unwrap_or_default(); - let preface = Preface { - name: req.repo.clone(), - repository: Some(GitReference { - repo: req.repo.clone(), - branch: req.reference.clone(), - ..GitReference::default() - }), - ..Preface::default() + let repository = GitReference { + repo: req.repo.clone(), + branch: req.branch.clone(), + tag: req.tag.clone(), + rev: req.rev.clone(), + ..GitReference::default() }; + if repository.reference().is_none() { + return Err(ApiError::BadPlaybookRequest("Requires either branch, tag or rev".to_string())); + } + let preface = Preface { name: req.repo.clone(), repository: Some(repository), ..Preface::default() }; let payload = PlaybookPayload { title: repo, description, preface }; ctx.client.playbooks().create(payload).map_err(ApiError::FailedToCreatePlaybook) diff --git a/src/swagger.rs b/src/swagger.rs index b82d8ec..5a14407 100644 --- a/src/swagger.rs +++ b/src/swagger.rs @@ -34,6 +34,7 @@ use crate::{handlers, requests}; handlers::file::rename, handlers::folder::get, + handlers::folder::tree, handlers::folder::create, handlers::folder::delete, handlers::folder::copy, @@ -63,6 +64,7 @@ use crate::{handlers, requests}; amp_common::schema::Service, amp_common::scm::content::Content, + amp_common::scm::content::File, amp_common::scm::git::Tree, amp_common::scm::git::TreeEntry, ) diff --git a/src/utils.rs b/src/utils.rs index a0c4081..b04ac5f 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -24,3 +24,9 @@ pub fn repo(url: &str) -> Result { Ok(repo) } +pub fn unwrap_or_error(option: Option, error_message: &str) -> Result { + match option { + Some(value) => Ok(value), + None => Err(ApiError::BadPlaybookRequest(error_message.to_string())), + } +}