From e4c61cb66a061513031dbec9ee7f9b737196706b Mon Sep 17 00:00:00 2001 From: liuhaoming Date: Mon, 11 Nov 2024 20:57:56 +0800 Subject: [PATCH] feat: add workspace config apis for kusion server --- api/openapispec/docs.go | 1037 +++++++++++++++-- api/openapispec/swagger.json | 1037 +++++++++++++++-- api/openapispec/swagger.yaml | 625 +++++++++- pkg/domain/request/workspace_request.go | 14 +- pkg/server/handler/module/handler.go | 4 +- pkg/server/handler/stack/execute.go | 142 +-- pkg/server/handler/stack/execute_async.go | 142 +-- pkg/server/handler/stack/handler.go | 148 +-- pkg/server/handler/stack/run.go | 86 +- pkg/server/handler/workspace/configs.go | 133 +++ pkg/server/handler/workspace/handler_test.go | 162 ++- pkg/server/manager/workspace/configs.go | 255 ++++ pkg/server/manager/workspace/types.go | 21 + .../manager/workspace/workspace_manager.go | 18 + pkg/server/route/route.go | 14 +- 15 files changed, 3351 insertions(+), 487 deletions(-) create mode 100644 pkg/server/handler/workspace/configs.go create mode 100644 pkg/server/manager/workspace/configs.go diff --git a/api/openapispec/docs.go b/api/openapispec/docs.go index d2c410755..9b17b528c 100644 --- a/api/openapispec/docs.go +++ b/api/openapispec/docs.go @@ -1137,6 +1137,182 @@ const docTemplate = `{ } } }, + "/api/v1/runs": { + "get": { + "description": "List all runs", + "produces": [ + "application/json" + ], + "tags": [ + "stack" + ], + "summary": "List runs", + "operationId": "listRun", + "parameters": [ + { + "type": "integer", + "description": "ProjectID to filter runs by. Default to all", + "name": "projectID", + "in": "query" + }, + { + "type": "integer", + "description": "OrgID to filter runs by. Default to all", + "name": "orgID", + "in": "query" + }, + { + "type": "string", + "description": "ProjectName to filter runs by. Default to all", + "name": "projectName", + "in": "query" + }, + { + "type": "string", + "description": "Cloud to filter runs by. Default to all", + "name": "cloud", + "in": "query" + }, + { + "type": "string", + "description": "Environment to filter runs by. Default to all", + "name": "env", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/entity.Stack" + } + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "429": { + "description": "Too Many Requests", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/api/v1/runs/{run_id}": { + "get": { + "description": "Get run information by run ID", + "produces": [ + "application/json" + ], + "tags": [ + "run" + ], + "summary": "Get run", + "operationId": "getRun", + "parameters": [ + { + "type": "integer", + "description": "Run ID", + "name": "run", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/entity.Run" + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "429": { + "description": "Too Many Requests", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/api/v1/runs/{run_id}/result": { + "get": { + "description": "Get run result by run ID", + "produces": [ + "application/json" + ], + "tags": [ + "run" + ], + "summary": "Get run result", + "operationId": "getRunResult", + "parameters": [ + { + "type": "integer", + "description": "Run ID", + "name": "run", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/entity.Run" + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "429": { + "description": "Too Many Requests", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, "/api/v1/sources": { "get": { "description": "List source information by source ID", @@ -1430,12 +1606,6 @@ const docTemplate = `{ "description": "Environment to filter stacks by. Default to all", "name": "env", "in": "query" - }, - { - "type": "boolean", - "description": "Whether to get last synced base revision. Default to false", - "name": "getLastSyncedBase", - "in": "query" } ], "responses": { @@ -1713,6 +1883,27 @@ const docTemplate = `{ "in": "path", "required": true }, + { + "description": "The resources to import during the stack preview", + "name": "importedResources", + "in": "body", + "schema": { + "$ref": "#/definitions/request.StackImportRequest" + } + }, + { + "type": "string", + "description": "The target workspace to preview the spec in.", + "name": "workspace", + "in": "query", + "required": true + }, + { + "type": "boolean", + "description": "Import existing resources during the stack preview", + "name": "importResources", + "in": "query" + }, { "type": "string", "description": "The Spec ID to use for the apply. Will generate a new spec if omitted.", @@ -1762,6 +1953,95 @@ const docTemplate = `{ } } }, + "/api/v1/stacks/{stack_id}/apply/async": { + "post": { + "description": "Start a run and asynchronously apply stack changes by stack ID", + "produces": [ + "application/json" + ], + "tags": [ + "stack" + ], + "summary": "Asynchronously apply stack", + "operationId": "applyStackAsync", + "parameters": [ + { + "type": "integer", + "description": "Stack ID", + "name": "stack_id", + "in": "path", + "required": true + }, + { + "description": "The resources to import during the stack preview", + "name": "importedResources", + "in": "body", + "schema": { + "$ref": "#/definitions/request.StackImportRequest" + } + }, + { + "type": "string", + "description": "The target workspace to preview the spec in.", + "name": "workspace", + "in": "query", + "required": true + }, + { + "type": "boolean", + "description": "Import existing resources during the stack preview", + "name": "importResources", + "in": "query" + }, + { + "type": "string", + "description": "The Spec ID to use for the apply. Will generate a new spec if omitted.", + "name": "specID", + "in": "query" + }, + { + "type": "boolean", + "description": "Force the apply even when the stack is locked. May cause concurrency issues!!!", + "name": "force", + "in": "query" + }, + { + "type": "boolean", + "description": "Apply in dry-run mode", + "name": "dryrun", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/entity.Run" + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "429": { + "description": "Too Many Requests", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, "/api/v1/stacks/{stack_id}/destroy": { "post": { "description": "Destroy stack information by stack ID", @@ -1781,6 +2061,13 @@ const docTemplate = `{ "in": "path", "required": true }, + { + "type": "string", + "description": "The target workspace to preview the spec in.", + "name": "workspace", + "in": "query", + "required": true + }, { "type": "boolean", "description": "Force the destroy even when the stack is locked. May cause concurrency issues!!!", @@ -1788,17 +2075,413 @@ const docTemplate = `{ "in": "query" }, { - "type": "boolean", - "description": "Destroy in dry-run mode", - "name": "dryrun", - "in": "query" + "type": "boolean", + "description": "Destroy in dry-run mode", + "name": "dryrun", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "429": { + "description": "Too Many Requests", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/api/v1/stacks/{stack_id}/destroy/async": { + "post": { + "description": "Start a run and asynchronously destroy stack resources by stack ID", + "produces": [ + "application/json" + ], + "tags": [ + "stack" + ], + "summary": "Asynchronously destroy stack", + "operationId": "destroyStackAsync", + "parameters": [ + { + "type": "integer", + "description": "Stack ID", + "name": "stack_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "The target workspace to preview the spec in.", + "name": "workspace", + "in": "query", + "required": true + }, + { + "type": "boolean", + "description": "Force the destroy even when the stack is locked. May cause concurrency issues!!!", + "name": "force", + "in": "query" + }, + { + "type": "boolean", + "description": "Destroy in dry-run mode", + "name": "dryrun", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "429": { + "description": "Too Many Requests", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/api/v1/stacks/{stack_id}/generate": { + "post": { + "description": "Generate stack information by stack ID", + "produces": [ + "application/json" + ], + "tags": [ + "stack" + ], + "summary": "Generate stack", + "operationId": "generateStack", + "parameters": [ + { + "type": "integer", + "description": "Stack ID", + "name": "stack_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "The target workspace to preview the spec in.", + "name": "workspace", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "The format to generate the spec in. Choices are: spec. Default to spec.", + "name": "format", + "in": "query" + }, + { + "type": "boolean", + "description": "Force the generate even when the stack is locked", + "name": "force", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/v1.Spec" + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "429": { + "description": "Too Many Requests", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/api/v1/stacks/{stack_id}/generate/async": { + "post": { + "description": "Start a run and asynchronously generate stack spec by stack ID", + "produces": [ + "application/json" + ], + "tags": [ + "stack" + ], + "summary": "Asynchronously generate stack", + "operationId": "generateStackAsync", + "parameters": [ + { + "type": "integer", + "description": "Stack ID", + "name": "stack_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "The target workspace to preview the spec in.", + "name": "workspace", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "The format to generate the spec in. Choices are: spec. Default to spec.", + "name": "format", + "in": "query" + }, + { + "type": "boolean", + "description": "Force the generate even when the stack is locked", + "name": "force", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/v1.Spec" + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "429": { + "description": "Too Many Requests", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/api/v1/stacks/{stack_id}/preview": { + "post": { + "description": "Start a run and asynchronously preview stack changes by stack ID", + "produces": [ + "application/json" + ], + "tags": [ + "stack" + ], + "summary": "Asynchronously preview stack", + "operationId": "previewStackAsync", + "parameters": [ + { + "type": "integer", + "description": "Stack ID", + "name": "stack_id", + "in": "path", + "required": true + }, + { + "description": "The resources to import during the stack preview", + "name": "importedResources", + "in": "body", + "schema": { + "$ref": "#/definitions/request.StackImportRequest" + } + }, + { + "type": "string", + "description": "The target workspace to preview the spec in.", + "name": "workspace", + "in": "query", + "required": true + }, + { + "type": "boolean", + "description": "Import existing resources during the stack preview", + "name": "importResources", + "in": "query" + }, + { + "type": "string", + "description": "Output format. Choices are: json, default. Default to default output format in Kusion.", + "name": "output", + "in": "query" + }, + { + "type": "boolean", + "description": "Show detailed output", + "name": "detail", + "in": "query" + }, + { + "type": "string", + "description": "The Spec ID to use for the preview. Default to the last one generated.", + "name": "specID", + "in": "query" + }, + { + "type": "boolean", + "description": "Force the preview even when the stack is locked", + "name": "force", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/entity.Run" + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "429": { + "description": "Too Many Requests", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/api/v1/workspaces": { + "get": { + "description": "List all workspaces", + "produces": [ + "application/json" + ], + "tags": [ + "workspace" + ], + "summary": "List workspaces", + "operationId": "listWorkspace", + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/entity.Workspace" + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "429": { + "description": "Too Many Requests", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + }, + "post": { + "description": "Create a new workspace", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "workspace" + ], + "summary": "Create workspace", + "operationId": "createWorkspace", + "parameters": [ + { + "description": "Created workspace", + "name": "workspace", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.CreateWorkspaceRequest" + } } ], "responses": { "200": { "description": "Success", "schema": { - "type": "string" + "$ref": "#/definitions/entity.Workspace" } }, "400": { @@ -1824,43 +2507,34 @@ const docTemplate = `{ } } }, - "/api/v1/stacks/{stack_id}/generate": { + "/api/v1/workspaces/configs/mod-deps/{id}": { "post": { - "description": "Generate stack information by stack ID", - "produces": [ + "description": "Create the module dependencies in kcl.mod of the specified workspace", + "consumes": [ "application/json" ], + "produces": [ + "text/plain" + ], "tags": [ - "stack" + "workspace" ], - "summary": "Generate stack", - "operationId": "generateStack", + "summary": "Create the module dependencies of the workspace", + "operationId": "createWorkspaceModDeps", "parameters": [ { "type": "integer", - "description": "Stack ID", - "name": "stack_id", + "description": "Workspace ID", + "name": "id", "in": "path", "required": true - }, - { - "type": "string", - "description": "The format to generate the spec in. Choices are: spec. Default to spec.", - "name": "format", - "in": "query" - }, - { - "type": "boolean", - "description": "Force the generate even when the stack is locked", - "name": "force", - "in": "query" } ], "responses": { "200": { "description": "Success", "schema": { - "$ref": "#/definitions/v1.Spec" + "type": "string" } }, "400": { @@ -1886,55 +2560,36 @@ const docTemplate = `{ } } }, - "/api/v1/stacks/{stack_id}/preview": { + "/api/v1/workspaces/configs/validate": { "post": { - "description": "Preview stack information by stack ID", + "description": "Validate the configurations in the specified workspace", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ - "stack" + "workspace" ], - "summary": "Preview stack", - "operationId": "previewStack", + "summary": "Validate workspace configurations", + "operationId": "validateWorkspaceConfigs", "parameters": [ { - "type": "integer", - "description": "Stack ID", - "name": "stack_id", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "Output format. Choices are: json, default. Default to default output format in Kusion.", - "name": "output", - "in": "query" - }, - { - "type": "boolean", - "description": "Show detailed output", - "name": "detail", - "in": "query" - }, - { - "type": "string", - "description": "The Spec ID to use for the preview. Default to the last one generated.", - "name": "specID", - "in": "query" - }, - { - "type": "boolean", - "description": "Force the preview even when the stack is locked", - "name": "force", - "in": "query" + "description": "Workspace configurations to be validated", + "name": "workspace", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WorkspaceConfigs" + } } ], "responses": { "200": { "description": "Success", "schema": { - "$ref": "#/definitions/models.Changes" + "$ref": "#/definitions/request.WorkspaceConfigs" } }, "400": { @@ -1960,22 +2615,34 @@ const docTemplate = `{ } } }, - "/api/v1/workspaces": { + "/api/v1/workspaces/configs/{id}": { "get": { - "description": "List all workspaces", + "description": "Get configurations in the specified workspace", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ "workspace" ], - "summary": "List workspaces", - "operationId": "listWorkspace", + "summary": "get workspace configurations", + "operationId": "getWorkspaceConfigs", + "parameters": [ + { + "type": "integer", + "description": "Workspace ID", + "name": "id", + "in": "path", + "required": true + } + ], "responses": { "200": { "description": "Success", "schema": { - "$ref": "#/definitions/entity.Workspace" + "$ref": "#/definitions/request.WorkspaceConfigs" } }, "400": { @@ -2000,8 +2667,8 @@ const docTemplate = `{ } } }, - "post": { - "description": "Create a new workspace", + "put": { + "description": "Update the configurations in the specified workspace", "consumes": [ "application/json" ], @@ -2011,16 +2678,23 @@ const docTemplate = `{ "tags": [ "workspace" ], - "summary": "Create workspace", - "operationId": "createWorkspace", + "summary": "Update workspace configurations", + "operationId": "updateWorkspaceConfigs", "parameters": [ { - "description": "Created workspace", + "type": "integer", + "description": "Workspace ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Updated workspace configurations", "name": "workspace", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/request.CreateWorkspaceRequest" + "$ref": "#/definitions/request.WorkspaceConfigs" } } ], @@ -2028,7 +2702,7 @@ const docTemplate = `{ "200": { "description": "Success", "schema": { - "$ref": "#/definitions/entity.Workspace" + "$ref": "#/definitions/request.WorkspaceConfigs" } }, "400": { @@ -2237,21 +2911,55 @@ const docTemplate = `{ } }, "definitions": { + "constant.RunStatus": { + "type": "string", + "enum": [ + "Scheduling", + "InProgress", + "Failed", + "Succeeded", + "Cancelled", + "Queued" + ], + "x-enum-varnames": [ + "RunStatusScheduling", + "RunStatusInProgress", + "RunStatusFailed", + "RunStatusSucceeded", + "RunStatusCancelled", + "RunStatusQueued" + ] + }, + "constant.RunType": { + "type": "string", + "enum": [ + "Generate", + "Preview", + "Apply", + "Destroy" + ], + "x-enum-varnames": [ + "RunTypeGenerate", + "RunTypePreview", + "RunTypeApply", + "RunTypeDestroy" + ] + }, "constant.SourceProviderType": { "type": "string", "enum": [ + "git", "git", "github", "oci", - "local", - "git" + "local" ], "x-enum-varnames": [ + "DefaultSourceType", "SourceProviderTypeGit", "SourceProviderTypeGithub", "SourceProviderTypeOCI", - "SourceProviderTypeLocal", - "DefaultSourceType" + "SourceProviderTypeLocal" ] }, "constant.StackState": { @@ -2552,6 +3260,59 @@ const docTemplate = `{ } } }, + "entity.Run": { + "type": "object", + "properties": { + "creationTimestamp": { + "description": "CreationTimestamp is the timestamp of the created for the run.", + "type": "string" + }, + "id": { + "description": "ID is the id of the run.", + "type": "integer" + }, + "logs": { + "description": "Result RunResult ` + "`" + `yaml:\"result\" json:\"result\"` + "`" + `\nLogs is the logs of the run.", + "type": "string" + }, + "result": { + "description": "Result is the result of the run.", + "type": "string" + }, + "stack": { + "description": "Stack is the stack of the run.", + "allOf": [ + { + "$ref": "#/definitions/entity.Stack" + } + ] + }, + "status": { + "description": "Status is the status of the run.", + "allOf": [ + { + "$ref": "#/definitions/constant.RunStatus" + } + ] + }, + "type": { + "description": "RunType is the type of the run provider.", + "allOf": [ + { + "$ref": "#/definitions/constant.RunType" + } + ] + }, + "updateTimestamp": { + "description": "UpdateTimestamp is the timestamp of the updated for the run.", + "type": "string" + }, + "workspace": { + "description": "Workspace is the target workspace of the run.", + "type": "string" + } + } + }, "entity.Source": { "type": "object", "properties": { @@ -2574,6 +3335,10 @@ const docTemplate = `{ "type": "string" } }, + "name": { + "description": "Name is the name of the source.", + "type": "string" + }, "owners": { "description": "Owners is a list of owners for the source.", "type": "array", @@ -2951,6 +3716,7 @@ const docTemplate = `{ "request.CreateSourceRequest": { "type": "object", "required": [ + "name", "remote", "sourceProvider" ], @@ -2966,6 +3732,10 @@ const docTemplate = `{ "type": "string" } }, + "name": { + "description": "Name is the name of the source.", + "type": "string" + }, "owners": { "description": "Owners is a list of owners for the source.", "type": "array", @@ -3069,6 +3839,17 @@ const docTemplate = `{ } } }, + "request.StackImportRequest": { + "type": "object", + "properties": { + "importedResources": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, "request.UpdateBackendRequest": { "type": "object", "required": [ @@ -3218,7 +3999,10 @@ const docTemplate = `{ "request.UpdateSourceRequest": { "type": "object", "required": [ - "id" + "id", + "name", + "remote", + "sourceProvider" ], "properties": { "description": { @@ -3236,6 +4020,10 @@ const docTemplate = `{ "type": "string" } }, + "name": { + "description": "Name is the name of the source.", + "type": "string" + }, "owners": { "description": "Owners is a list of owners for the source.", "type": "array", @@ -3348,6 +4136,35 @@ const docTemplate = `{ } } }, + "request.WorkspaceConfigs": { + "type": "object", + "properties": { + "context": { + "description": "Context contains workspace-level configurations, such as runtimes, topologies, and metadata, etc.", + "allOf": [ + { + "$ref": "#/definitions/v1.GenericConfig" + } + ] + }, + "modules": { + "description": "Modules are the configs of a set of modules.", + "allOf": [ + { + "$ref": "#/definitions/v1.ModuleConfigs" + } + ] + }, + "secretStore": { + "description": "SecretStore represents a secure external location for storing secrets.", + "allOf": [ + { + "$ref": "#/definitions/v1.SecretStore" + } + ] + } + } + }, "url.URL": { "type": "object", "properties": { @@ -3475,6 +4292,19 @@ const docTemplate = `{ } } }, + "v1.Configs": { + "type": "object", + "properties": { + "default": { + "description": "Default is default block of the module config.", + "allOf": [ + { + "$ref": "#/definitions/v1.GenericConfig" + } + ] + } + } + }, "v1.FakeProvider": { "type": "object", "properties": { @@ -3510,6 +4340,45 @@ const docTemplate = `{ "type": "object", "additionalProperties": {} }, + "v1.ModuleConfig": { + "type": "object", + "properties": { + "configs": { + "description": "Configs contains all levels of module configs", + "allOf": [ + { + "$ref": "#/definitions/v1.Configs" + } + ] + }, + "path": { + "description": "Path is the path of the module. It can be a local path or a remote URL", + "type": "string" + }, + "version": { + "description": "Version is the version of the module.", + "type": "string" + } + } + }, + "v1.ModuleConfigs": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/v1.ModuleConfig" + } + }, + "v1.ModulePatcherConfig": { + "type": "object", + "properties": { + "projectSelector": { + "description": "ProjectSelector contains the selected projects.", + "type": "array", + "items": { + "type": "string" + } + } + } + }, "v1.OnPremisesProvider": { "type": "object", "properties": { diff --git a/api/openapispec/swagger.json b/api/openapispec/swagger.json index 275f181f1..56d2ea2d4 100644 --- a/api/openapispec/swagger.json +++ b/api/openapispec/swagger.json @@ -1126,6 +1126,182 @@ } } }, + "/api/v1/runs": { + "get": { + "description": "List all runs", + "produces": [ + "application/json" + ], + "tags": [ + "stack" + ], + "summary": "List runs", + "operationId": "listRun", + "parameters": [ + { + "type": "integer", + "description": "ProjectID to filter runs by. Default to all", + "name": "projectID", + "in": "query" + }, + { + "type": "integer", + "description": "OrgID to filter runs by. Default to all", + "name": "orgID", + "in": "query" + }, + { + "type": "string", + "description": "ProjectName to filter runs by. Default to all", + "name": "projectName", + "in": "query" + }, + { + "type": "string", + "description": "Cloud to filter runs by. Default to all", + "name": "cloud", + "in": "query" + }, + { + "type": "string", + "description": "Environment to filter runs by. Default to all", + "name": "env", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/entity.Stack" + } + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "429": { + "description": "Too Many Requests", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/api/v1/runs/{run_id}": { + "get": { + "description": "Get run information by run ID", + "produces": [ + "application/json" + ], + "tags": [ + "run" + ], + "summary": "Get run", + "operationId": "getRun", + "parameters": [ + { + "type": "integer", + "description": "Run ID", + "name": "run", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/entity.Run" + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "429": { + "description": "Too Many Requests", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/api/v1/runs/{run_id}/result": { + "get": { + "description": "Get run result by run ID", + "produces": [ + "application/json" + ], + "tags": [ + "run" + ], + "summary": "Get run result", + "operationId": "getRunResult", + "parameters": [ + { + "type": "integer", + "description": "Run ID", + "name": "run", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/entity.Run" + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "429": { + "description": "Too Many Requests", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, "/api/v1/sources": { "get": { "description": "List source information by source ID", @@ -1419,12 +1595,6 @@ "description": "Environment to filter stacks by. Default to all", "name": "env", "in": "query" - }, - { - "type": "boolean", - "description": "Whether to get last synced base revision. Default to false", - "name": "getLastSyncedBase", - "in": "query" } ], "responses": { @@ -1702,6 +1872,27 @@ "in": "path", "required": true }, + { + "description": "The resources to import during the stack preview", + "name": "importedResources", + "in": "body", + "schema": { + "$ref": "#/definitions/request.StackImportRequest" + } + }, + { + "type": "string", + "description": "The target workspace to preview the spec in.", + "name": "workspace", + "in": "query", + "required": true + }, + { + "type": "boolean", + "description": "Import existing resources during the stack preview", + "name": "importResources", + "in": "query" + }, { "type": "string", "description": "The Spec ID to use for the apply. Will generate a new spec if omitted.", @@ -1751,6 +1942,95 @@ } } }, + "/api/v1/stacks/{stack_id}/apply/async": { + "post": { + "description": "Start a run and asynchronously apply stack changes by stack ID", + "produces": [ + "application/json" + ], + "tags": [ + "stack" + ], + "summary": "Asynchronously apply stack", + "operationId": "applyStackAsync", + "parameters": [ + { + "type": "integer", + "description": "Stack ID", + "name": "stack_id", + "in": "path", + "required": true + }, + { + "description": "The resources to import during the stack preview", + "name": "importedResources", + "in": "body", + "schema": { + "$ref": "#/definitions/request.StackImportRequest" + } + }, + { + "type": "string", + "description": "The target workspace to preview the spec in.", + "name": "workspace", + "in": "query", + "required": true + }, + { + "type": "boolean", + "description": "Import existing resources during the stack preview", + "name": "importResources", + "in": "query" + }, + { + "type": "string", + "description": "The Spec ID to use for the apply. Will generate a new spec if omitted.", + "name": "specID", + "in": "query" + }, + { + "type": "boolean", + "description": "Force the apply even when the stack is locked. May cause concurrency issues!!!", + "name": "force", + "in": "query" + }, + { + "type": "boolean", + "description": "Apply in dry-run mode", + "name": "dryrun", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/entity.Run" + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "429": { + "description": "Too Many Requests", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, "/api/v1/stacks/{stack_id}/destroy": { "post": { "description": "Destroy stack information by stack ID", @@ -1770,6 +2050,13 @@ "in": "path", "required": true }, + { + "type": "string", + "description": "The target workspace to preview the spec in.", + "name": "workspace", + "in": "query", + "required": true + }, { "type": "boolean", "description": "Force the destroy even when the stack is locked. May cause concurrency issues!!!", @@ -1777,17 +2064,413 @@ "in": "query" }, { - "type": "boolean", - "description": "Destroy in dry-run mode", - "name": "dryrun", - "in": "query" + "type": "boolean", + "description": "Destroy in dry-run mode", + "name": "dryrun", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "429": { + "description": "Too Many Requests", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/api/v1/stacks/{stack_id}/destroy/async": { + "post": { + "description": "Start a run and asynchronously destroy stack resources by stack ID", + "produces": [ + "application/json" + ], + "tags": [ + "stack" + ], + "summary": "Asynchronously destroy stack", + "operationId": "destroyStackAsync", + "parameters": [ + { + "type": "integer", + "description": "Stack ID", + "name": "stack_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "The target workspace to preview the spec in.", + "name": "workspace", + "in": "query", + "required": true + }, + { + "type": "boolean", + "description": "Force the destroy even when the stack is locked. May cause concurrency issues!!!", + "name": "force", + "in": "query" + }, + { + "type": "boolean", + "description": "Destroy in dry-run mode", + "name": "dryrun", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "429": { + "description": "Too Many Requests", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/api/v1/stacks/{stack_id}/generate": { + "post": { + "description": "Generate stack information by stack ID", + "produces": [ + "application/json" + ], + "tags": [ + "stack" + ], + "summary": "Generate stack", + "operationId": "generateStack", + "parameters": [ + { + "type": "integer", + "description": "Stack ID", + "name": "stack_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "The target workspace to preview the spec in.", + "name": "workspace", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "The format to generate the spec in. Choices are: spec. Default to spec.", + "name": "format", + "in": "query" + }, + { + "type": "boolean", + "description": "Force the generate even when the stack is locked", + "name": "force", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/v1.Spec" + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "429": { + "description": "Too Many Requests", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/api/v1/stacks/{stack_id}/generate/async": { + "post": { + "description": "Start a run and asynchronously generate stack spec by stack ID", + "produces": [ + "application/json" + ], + "tags": [ + "stack" + ], + "summary": "Asynchronously generate stack", + "operationId": "generateStackAsync", + "parameters": [ + { + "type": "integer", + "description": "Stack ID", + "name": "stack_id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "The target workspace to preview the spec in.", + "name": "workspace", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "The format to generate the spec in. Choices are: spec. Default to spec.", + "name": "format", + "in": "query" + }, + { + "type": "boolean", + "description": "Force the generate even when the stack is locked", + "name": "force", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/v1.Spec" + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "429": { + "description": "Too Many Requests", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/api/v1/stacks/{stack_id}/preview": { + "post": { + "description": "Start a run and asynchronously preview stack changes by stack ID", + "produces": [ + "application/json" + ], + "tags": [ + "stack" + ], + "summary": "Asynchronously preview stack", + "operationId": "previewStackAsync", + "parameters": [ + { + "type": "integer", + "description": "Stack ID", + "name": "stack_id", + "in": "path", + "required": true + }, + { + "description": "The resources to import during the stack preview", + "name": "importedResources", + "in": "body", + "schema": { + "$ref": "#/definitions/request.StackImportRequest" + } + }, + { + "type": "string", + "description": "The target workspace to preview the spec in.", + "name": "workspace", + "in": "query", + "required": true + }, + { + "type": "boolean", + "description": "Import existing resources during the stack preview", + "name": "importResources", + "in": "query" + }, + { + "type": "string", + "description": "Output format. Choices are: json, default. Default to default output format in Kusion.", + "name": "output", + "in": "query" + }, + { + "type": "boolean", + "description": "Show detailed output", + "name": "detail", + "in": "query" + }, + { + "type": "string", + "description": "The Spec ID to use for the preview. Default to the last one generated.", + "name": "specID", + "in": "query" + }, + { + "type": "boolean", + "description": "Force the preview even when the stack is locked", + "name": "force", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/entity.Run" + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "429": { + "description": "Too Many Requests", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + } + }, + "/api/v1/workspaces": { + "get": { + "description": "List all workspaces", + "produces": [ + "application/json" + ], + "tags": [ + "workspace" + ], + "summary": "List workspaces", + "operationId": "listWorkspace", + "responses": { + "200": { + "description": "Success", + "schema": { + "$ref": "#/definitions/entity.Workspace" + } + }, + "400": { + "description": "Bad Request", + "schema": {} + }, + "401": { + "description": "Unauthorized", + "schema": {} + }, + "404": { + "description": "Not Found", + "schema": {} + }, + "429": { + "description": "Too Many Requests", + "schema": {} + }, + "500": { + "description": "Internal Server Error", + "schema": {} + } + } + }, + "post": { + "description": "Create a new workspace", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "workspace" + ], + "summary": "Create workspace", + "operationId": "createWorkspace", + "parameters": [ + { + "description": "Created workspace", + "name": "workspace", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.CreateWorkspaceRequest" + } } ], "responses": { "200": { "description": "Success", "schema": { - "type": "string" + "$ref": "#/definitions/entity.Workspace" } }, "400": { @@ -1813,43 +2496,34 @@ } } }, - "/api/v1/stacks/{stack_id}/generate": { + "/api/v1/workspaces/configs/mod-deps/{id}": { "post": { - "description": "Generate stack information by stack ID", - "produces": [ + "description": "Create the module dependencies in kcl.mod of the specified workspace", + "consumes": [ "application/json" ], + "produces": [ + "text/plain" + ], "tags": [ - "stack" + "workspace" ], - "summary": "Generate stack", - "operationId": "generateStack", + "summary": "Create the module dependencies of the workspace", + "operationId": "createWorkspaceModDeps", "parameters": [ { "type": "integer", - "description": "Stack ID", - "name": "stack_id", + "description": "Workspace ID", + "name": "id", "in": "path", "required": true - }, - { - "type": "string", - "description": "The format to generate the spec in. Choices are: spec. Default to spec.", - "name": "format", - "in": "query" - }, - { - "type": "boolean", - "description": "Force the generate even when the stack is locked", - "name": "force", - "in": "query" } ], "responses": { "200": { "description": "Success", "schema": { - "$ref": "#/definitions/v1.Spec" + "type": "string" } }, "400": { @@ -1875,55 +2549,36 @@ } } }, - "/api/v1/stacks/{stack_id}/preview": { + "/api/v1/workspaces/configs/validate": { "post": { - "description": "Preview stack information by stack ID", + "description": "Validate the configurations in the specified workspace", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ - "stack" + "workspace" ], - "summary": "Preview stack", - "operationId": "previewStack", + "summary": "Validate workspace configurations", + "operationId": "validateWorkspaceConfigs", "parameters": [ { - "type": "integer", - "description": "Stack ID", - "name": "stack_id", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "Output format. Choices are: json, default. Default to default output format in Kusion.", - "name": "output", - "in": "query" - }, - { - "type": "boolean", - "description": "Show detailed output", - "name": "detail", - "in": "query" - }, - { - "type": "string", - "description": "The Spec ID to use for the preview. Default to the last one generated.", - "name": "specID", - "in": "query" - }, - { - "type": "boolean", - "description": "Force the preview even when the stack is locked", - "name": "force", - "in": "query" + "description": "Workspace configurations to be validated", + "name": "workspace", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WorkspaceConfigs" + } } ], "responses": { "200": { "description": "Success", "schema": { - "$ref": "#/definitions/models.Changes" + "$ref": "#/definitions/request.WorkspaceConfigs" } }, "400": { @@ -1949,22 +2604,34 @@ } } }, - "/api/v1/workspaces": { + "/api/v1/workspaces/configs/{id}": { "get": { - "description": "List all workspaces", + "description": "Get configurations in the specified workspace", + "consumes": [ + "application/json" + ], "produces": [ "application/json" ], "tags": [ "workspace" ], - "summary": "List workspaces", - "operationId": "listWorkspace", + "summary": "get workspace configurations", + "operationId": "getWorkspaceConfigs", + "parameters": [ + { + "type": "integer", + "description": "Workspace ID", + "name": "id", + "in": "path", + "required": true + } + ], "responses": { "200": { "description": "Success", "schema": { - "$ref": "#/definitions/entity.Workspace" + "$ref": "#/definitions/request.WorkspaceConfigs" } }, "400": { @@ -1989,8 +2656,8 @@ } } }, - "post": { - "description": "Create a new workspace", + "put": { + "description": "Update the configurations in the specified workspace", "consumes": [ "application/json" ], @@ -2000,16 +2667,23 @@ "tags": [ "workspace" ], - "summary": "Create workspace", - "operationId": "createWorkspace", + "summary": "Update workspace configurations", + "operationId": "updateWorkspaceConfigs", "parameters": [ { - "description": "Created workspace", + "type": "integer", + "description": "Workspace ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Updated workspace configurations", "name": "workspace", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/request.CreateWorkspaceRequest" + "$ref": "#/definitions/request.WorkspaceConfigs" } } ], @@ -2017,7 +2691,7 @@ "200": { "description": "Success", "schema": { - "$ref": "#/definitions/entity.Workspace" + "$ref": "#/definitions/request.WorkspaceConfigs" } }, "400": { @@ -2226,21 +2900,55 @@ } }, "definitions": { + "constant.RunStatus": { + "type": "string", + "enum": [ + "Scheduling", + "InProgress", + "Failed", + "Succeeded", + "Cancelled", + "Queued" + ], + "x-enum-varnames": [ + "RunStatusScheduling", + "RunStatusInProgress", + "RunStatusFailed", + "RunStatusSucceeded", + "RunStatusCancelled", + "RunStatusQueued" + ] + }, + "constant.RunType": { + "type": "string", + "enum": [ + "Generate", + "Preview", + "Apply", + "Destroy" + ], + "x-enum-varnames": [ + "RunTypeGenerate", + "RunTypePreview", + "RunTypeApply", + "RunTypeDestroy" + ] + }, "constant.SourceProviderType": { "type": "string", "enum": [ + "git", "git", "github", "oci", - "local", - "git" + "local" ], "x-enum-varnames": [ + "DefaultSourceType", "SourceProviderTypeGit", "SourceProviderTypeGithub", "SourceProviderTypeOCI", - "SourceProviderTypeLocal", - "DefaultSourceType" + "SourceProviderTypeLocal" ] }, "constant.StackState": { @@ -2541,6 +3249,59 @@ } } }, + "entity.Run": { + "type": "object", + "properties": { + "creationTimestamp": { + "description": "CreationTimestamp is the timestamp of the created for the run.", + "type": "string" + }, + "id": { + "description": "ID is the id of the run.", + "type": "integer" + }, + "logs": { + "description": "Result RunResult `yaml:\"result\" json:\"result\"`\nLogs is the logs of the run.", + "type": "string" + }, + "result": { + "description": "Result is the result of the run.", + "type": "string" + }, + "stack": { + "description": "Stack is the stack of the run.", + "allOf": [ + { + "$ref": "#/definitions/entity.Stack" + } + ] + }, + "status": { + "description": "Status is the status of the run.", + "allOf": [ + { + "$ref": "#/definitions/constant.RunStatus" + } + ] + }, + "type": { + "description": "RunType is the type of the run provider.", + "allOf": [ + { + "$ref": "#/definitions/constant.RunType" + } + ] + }, + "updateTimestamp": { + "description": "UpdateTimestamp is the timestamp of the updated for the run.", + "type": "string" + }, + "workspace": { + "description": "Workspace is the target workspace of the run.", + "type": "string" + } + } + }, "entity.Source": { "type": "object", "properties": { @@ -2563,6 +3324,10 @@ "type": "string" } }, + "name": { + "description": "Name is the name of the source.", + "type": "string" + }, "owners": { "description": "Owners is a list of owners for the source.", "type": "array", @@ -2940,6 +3705,7 @@ "request.CreateSourceRequest": { "type": "object", "required": [ + "name", "remote", "sourceProvider" ], @@ -2955,6 +3721,10 @@ "type": "string" } }, + "name": { + "description": "Name is the name of the source.", + "type": "string" + }, "owners": { "description": "Owners is a list of owners for the source.", "type": "array", @@ -3058,6 +3828,17 @@ } } }, + "request.StackImportRequest": { + "type": "object", + "properties": { + "importedResources": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, "request.UpdateBackendRequest": { "type": "object", "required": [ @@ -3207,7 +3988,10 @@ "request.UpdateSourceRequest": { "type": "object", "required": [ - "id" + "id", + "name", + "remote", + "sourceProvider" ], "properties": { "description": { @@ -3225,6 +4009,10 @@ "type": "string" } }, + "name": { + "description": "Name is the name of the source.", + "type": "string" + }, "owners": { "description": "Owners is a list of owners for the source.", "type": "array", @@ -3337,6 +4125,35 @@ } } }, + "request.WorkspaceConfigs": { + "type": "object", + "properties": { + "context": { + "description": "Context contains workspace-level configurations, such as runtimes, topologies, and metadata, etc.", + "allOf": [ + { + "$ref": "#/definitions/v1.GenericConfig" + } + ] + }, + "modules": { + "description": "Modules are the configs of a set of modules.", + "allOf": [ + { + "$ref": "#/definitions/v1.ModuleConfigs" + } + ] + }, + "secretStore": { + "description": "SecretStore represents a secure external location for storing secrets.", + "allOf": [ + { + "$ref": "#/definitions/v1.SecretStore" + } + ] + } + } + }, "url.URL": { "type": "object", "properties": { @@ -3464,6 +4281,19 @@ } } }, + "v1.Configs": { + "type": "object", + "properties": { + "default": { + "description": "Default is default block of the module config.", + "allOf": [ + { + "$ref": "#/definitions/v1.GenericConfig" + } + ] + } + } + }, "v1.FakeProvider": { "type": "object", "properties": { @@ -3499,6 +4329,45 @@ "type": "object", "additionalProperties": {} }, + "v1.ModuleConfig": { + "type": "object", + "properties": { + "configs": { + "description": "Configs contains all levels of module configs", + "allOf": [ + { + "$ref": "#/definitions/v1.Configs" + } + ] + }, + "path": { + "description": "Path is the path of the module. It can be a local path or a remote URL", + "type": "string" + }, + "version": { + "description": "Version is the version of the module.", + "type": "string" + } + } + }, + "v1.ModuleConfigs": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/v1.ModuleConfig" + } + }, + "v1.ModulePatcherConfig": { + "type": "object", + "properties": { + "projectSelector": { + "description": "ProjectSelector contains the selected projects.", + "type": "array", + "items": { + "type": "string" + } + } + } + }, "v1.OnPremisesProvider": { "type": "object", "properties": { diff --git a/api/openapispec/swagger.yaml b/api/openapispec/swagger.yaml index 5d01c631f..263351ce6 100644 --- a/api/openapispec/swagger.yaml +++ b/api/openapispec/swagger.yaml @@ -1,18 +1,46 @@ definitions: + constant.RunStatus: + enum: + - Scheduling + - InProgress + - Failed + - Succeeded + - Cancelled + - Queued + type: string + x-enum-varnames: + - RunStatusScheduling + - RunStatusInProgress + - RunStatusFailed + - RunStatusSucceeded + - RunStatusCancelled + - RunStatusQueued + constant.RunType: + enum: + - Generate + - Preview + - Apply + - Destroy + type: string + x-enum-varnames: + - RunTypeGenerate + - RunTypePreview + - RunTypeApply + - RunTypeDestroy constant.SourceProviderType: enum: - git + - git - github - oci - local - - git type: string x-enum-varnames: + - DefaultSourceType - SourceProviderTypeGit - SourceProviderTypeGithub - SourceProviderTypeOCI - SourceProviderTypeLocal - - DefaultSourceType constant.StackState: enum: - UnSynced @@ -230,6 +258,41 @@ definitions: description: UpdateTimestamp is the timestamp of the updated for the resource. type: string type: object + entity.Run: + properties: + creationTimestamp: + description: CreationTimestamp is the timestamp of the created for the run. + type: string + id: + description: ID is the id of the run. + type: integer + logs: + description: |- + Result RunResult `yaml:"result" json:"result"` + Logs is the logs of the run. + type: string + result: + description: Result is the result of the run. + type: string + stack: + allOf: + - $ref: '#/definitions/entity.Stack' + description: Stack is the stack of the run. + status: + allOf: + - $ref: '#/definitions/constant.RunStatus' + description: Status is the status of the run. + type: + allOf: + - $ref: '#/definitions/constant.RunType' + description: RunType is the type of the run provider. + updateTimestamp: + description: UpdateTimestamp is the timestamp of the updated for the run. + type: string + workspace: + description: Workspace is the target workspace of the run. + type: string + type: object entity.Source: properties: creationTimestamp: @@ -246,6 +309,9 @@ definitions: items: type: string type: array + name: + description: Name is the name of the source. + type: string owners: description: Owners is a list of owners for the source. items: @@ -518,6 +584,9 @@ definitions: items: type: string type: array + name: + description: Name is the name of the source. + type: string owners: description: Owners is a list of owners for the source. items: @@ -530,6 +599,7 @@ definitions: description: SourceProvider is the type of the source provider. type: string required: + - name - remote - sourceProvider type: object @@ -596,6 +666,13 @@ definitions: - name - owners type: object + request.StackImportRequest: + properties: + importedResources: + additionalProperties: + type: string + type: object + type: object request.UpdateBackendRequest: properties: backendConfig: @@ -713,6 +790,9 @@ definitions: items: type: string type: array + name: + description: Name is the name of the source. + type: string owners: description: Owners is a list of owners for the source. items: @@ -726,6 +806,9 @@ definitions: type: string required: - id + - name + - remote + - sourceProvider type: object request.UpdateStackRequest: properties: @@ -797,6 +880,23 @@ definitions: - id - owners type: object + request.WorkspaceConfigs: + properties: + context: + allOf: + - $ref: '#/definitions/v1.GenericConfig' + description: Context contains workspace-level configurations, such as runtimes, + topologies, and metadata, etc. + modules: + allOf: + - $ref: '#/definitions/v1.ModuleConfigs' + description: Modules are the configs of a set of modules. + secretStore: + allOf: + - $ref: '#/definitions/v1.SecretStore' + description: SecretStore represents a secure external location for storing + secrets. + type: object url.URL: properties: forceQuery: @@ -897,6 +997,13 @@ definitions: BackendTypeS3. type: string type: object + v1.Configs: + properties: + default: + allOf: + - $ref: '#/definitions/v1.GenericConfig' + description: Default is default block of the module config. + type: object v1.FakeProvider: properties: data: @@ -920,6 +1027,32 @@ definitions: v1.GenericConfig: additionalProperties: {} type: object + v1.ModuleConfig: + properties: + configs: + allOf: + - $ref: '#/definitions/v1.Configs' + description: Configs contains all levels of module configs + path: + description: Path is the path of the module. It can be a local path or a remote + URL + type: string + version: + description: Version is the version of the module. + type: string + type: object + v1.ModuleConfigs: + additionalProperties: + $ref: '#/definitions/v1.ModuleConfig' + type: object + v1.ModulePatcherConfig: + properties: + projectSelector: + description: ProjectSelector contains the selected projects. + items: + type: string + type: array + type: object v1.OnPremisesProvider: properties: attributes: @@ -1840,6 +1973,128 @@ paths: summary: Get resource tags: - resource + /api/v1/runs: + get: + description: List all runs + operationId: listRun + parameters: + - description: ProjectID to filter runs by. Default to all + in: query + name: projectID + type: integer + - description: OrgID to filter runs by. Default to all + in: query + name: orgID + type: integer + - description: ProjectName to filter runs by. Default to all + in: query + name: projectName + type: string + - description: Cloud to filter runs by. Default to all + in: query + name: cloud + type: string + - description: Environment to filter runs by. Default to all + in: query + name: env + type: string + produces: + - application/json + responses: + "200": + description: Success + schema: + items: + $ref: '#/definitions/entity.Stack' + type: array + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "429": + description: Too Many Requests + schema: {} + "500": + description: Internal Server Error + schema: {} + summary: List runs + tags: + - stack + /api/v1/runs/{run_id}: + get: + description: Get run information by run ID + operationId: getRun + parameters: + - description: Run ID + in: path + name: run + required: true + type: integer + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/entity.Run' + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "429": + description: Too Many Requests + schema: {} + "500": + description: Internal Server Error + schema: {} + summary: Get run + tags: + - run + /api/v1/runs/{run_id}/result: + get: + description: Get run result by run ID + operationId: getRunResult + parameters: + - description: Run ID + in: path + name: run + required: true + type: integer + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/entity.Run' + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "429": + description: Too Many Requests + schema: {} + "500": + description: Internal Server Error + schema: {} + summary: Get run result + tags: + - run /api/v1/sources: get: description: List source information by source ID @@ -2042,10 +2297,6 @@ paths: in: query name: env type: string - - description: Whether to get last synced base revision. Default to false - in: query - name: getLastSyncedBase - type: boolean produces: - application/json responses: @@ -2241,6 +2492,20 @@ paths: name: stack_id required: true type: integer + - description: The resources to import during the stack preview + in: body + name: importedResources + schema: + $ref: '#/definitions/request.StackImportRequest' + - description: The target workspace to preview the spec in. + in: query + name: workspace + required: true + type: string + - description: Import existing resources during the stack preview + in: query + name: importResources + type: boolean - description: The Spec ID to use for the apply. Will generate a new spec if omitted. in: query @@ -2280,6 +2545,69 @@ paths: summary: Apply stack tags: - stack + /api/v1/stacks/{stack_id}/apply/async: + post: + description: Start a run and asynchronously apply stack changes by stack ID + operationId: applyStackAsync + parameters: + - description: Stack ID + in: path + name: stack_id + required: true + type: integer + - description: The resources to import during the stack preview + in: body + name: importedResources + schema: + $ref: '#/definitions/request.StackImportRequest' + - description: The target workspace to preview the spec in. + in: query + name: workspace + required: true + type: string + - description: Import existing resources during the stack preview + in: query + name: importResources + type: boolean + - description: The Spec ID to use for the apply. Will generate a new spec if + omitted. + in: query + name: specID + type: string + - description: Force the apply even when the stack is locked. May cause concurrency + issues!!! + in: query + name: force + type: boolean + - description: Apply in dry-run mode + in: query + name: dryrun + type: boolean + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/entity.Run' + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "429": + description: Too Many Requests + schema: {} + "500": + description: Internal Server Error + schema: {} + summary: Asynchronously apply stack + tags: + - stack /api/v1/stacks/{stack_id}/destroy: post: description: Destroy stack information by stack ID @@ -2290,6 +2618,11 @@ paths: name: stack_id required: true type: integer + - description: The target workspace to preview the spec in. + in: query + name: workspace + required: true + type: string - description: Force the destroy even when the stack is locked. May cause concurrency issues!!! in: query @@ -2324,6 +2657,56 @@ paths: summary: Destroy stack tags: - stack + /api/v1/stacks/{stack_id}/destroy/async: + post: + description: Start a run and asynchronously destroy stack resources by stack + ID + operationId: destroyStackAsync + parameters: + - description: Stack ID + in: path + name: stack_id + required: true + type: integer + - description: The target workspace to preview the spec in. + in: query + name: workspace + required: true + type: string + - description: Force the destroy even when the stack is locked. May cause concurrency + issues!!! + in: query + name: force + type: boolean + - description: Destroy in dry-run mode + in: query + name: dryrun + type: boolean + produces: + - application/json + responses: + "200": + description: Success + schema: + type: string + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "429": + description: Too Many Requests + schema: {} + "500": + description: Internal Server Error + schema: {} + summary: Asynchronously destroy stack + tags: + - stack /api/v1/stacks/{stack_id}/generate: post: description: Generate stack information by stack ID @@ -2334,6 +2717,11 @@ paths: name: stack_id required: true type: integer + - description: The target workspace to preview the spec in. + in: query + name: workspace + required: true + type: string - description: 'The format to generate the spec in. Choices are: spec. Default to spec.' in: query @@ -2368,16 +2756,79 @@ paths: summary: Generate stack tags: - stack + /api/v1/stacks/{stack_id}/generate/async: + post: + description: Start a run and asynchronously generate stack spec by stack ID + operationId: generateStackAsync + parameters: + - description: Stack ID + in: path + name: stack_id + required: true + type: integer + - description: The target workspace to preview the spec in. + in: query + name: workspace + required: true + type: string + - description: 'The format to generate the spec in. Choices are: spec. Default + to spec.' + in: query + name: format + type: string + - description: Force the generate even when the stack is locked + in: query + name: force + type: boolean + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/v1.Spec' + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "429": + description: Too Many Requests + schema: {} + "500": + description: Internal Server Error + schema: {} + summary: Asynchronously generate stack + tags: + - stack /api/v1/stacks/{stack_id}/preview: post: - description: Preview stack information by stack ID - operationId: previewStack + description: Start a run and asynchronously preview stack changes by stack ID + operationId: previewStackAsync parameters: - description: Stack ID in: path name: stack_id required: true type: integer + - description: The resources to import during the stack preview + in: body + name: importedResources + schema: + $ref: '#/definitions/request.StackImportRequest' + - description: The target workspace to preview the spec in. + in: query + name: workspace + required: true + type: string + - description: Import existing resources during the stack preview + in: query + name: importResources + type: boolean - description: 'Output format. Choices are: json, default. Default to default output format in Kusion.' in: query @@ -2401,7 +2852,7 @@ paths: "200": description: Success schema: - $ref: '#/definitions/models.Changes' + $ref: '#/definitions/entity.Run' "400": description: Bad Request schema: {} @@ -2417,7 +2868,7 @@ paths: "500": description: Internal Server Error schema: {} - summary: Preview stack + summary: Asynchronously preview stack tags: - stack /api/v1/workspaces: @@ -2597,6 +3048,160 @@ paths: summary: Update workspace tags: - workspace + /api/v1/workspaces/configs/{id}: + get: + consumes: + - application/json + description: Get configurations in the specified workspace + operationId: getWorkspaceConfigs + parameters: + - description: Workspace ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/request.WorkspaceConfigs' + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "429": + description: Too Many Requests + schema: {} + "500": + description: Internal Server Error + schema: {} + summary: get workspace configurations + tags: + - workspace + put: + consumes: + - application/json + description: Update the configurations in the specified workspace + operationId: updateWorkspaceConfigs + parameters: + - description: Workspace ID + in: path + name: id + required: true + type: integer + - description: Updated workspace configurations + in: body + name: workspace + required: true + schema: + $ref: '#/definitions/request.WorkspaceConfigs' + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/request.WorkspaceConfigs' + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "429": + description: Too Many Requests + schema: {} + "500": + description: Internal Server Error + schema: {} + summary: Update workspace configurations + tags: + - workspace + /api/v1/workspaces/configs/mod-deps/{id}: + post: + consumes: + - application/json + description: Create the module dependencies in kcl.mod of the specified workspace + operationId: createWorkspaceModDeps + parameters: + - description: Workspace ID + in: path + name: id + required: true + type: integer + produces: + - text/plain + responses: + "200": + description: Success + schema: + type: string + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "429": + description: Too Many Requests + schema: {} + "500": + description: Internal Server Error + schema: {} + summary: Create the module dependencies of the workspace + tags: + - workspace + /api/v1/workspaces/configs/validate: + post: + consumes: + - application/json + description: Validate the configurations in the specified workspace + operationId: validateWorkspaceConfigs + parameters: + - description: Workspace configurations to be validated + in: body + name: workspace + required: true + schema: + $ref: '#/definitions/request.WorkspaceConfigs' + produces: + - application/json + responses: + "200": + description: Success + schema: + $ref: '#/definitions/request.WorkspaceConfigs' + "400": + description: Bad Request + schema: {} + "401": + description: Unauthorized + schema: {} + "404": + description: Not Found + schema: {} + "429": + description: Too Many Requests + schema: {} + "500": + description: Internal Server Error + schema: {} + summary: Validate workspace configurations + tags: + - workspace /endpoints: get: consumes: diff --git a/pkg/domain/request/workspace_request.go b/pkg/domain/request/workspace_request.go index baaf95a0b..c6f705a2a 100644 --- a/pkg/domain/request/workspace_request.go +++ b/pkg/domain/request/workspace_request.go @@ -1,6 +1,10 @@ package request -import "net/http" +import ( + "net/http" + + v1 "kusionstack.io/kusion/pkg/apis/api.kusion.io/v1" +) // CreateWorkspaceRequest represents the create request structure for // workspace. @@ -45,6 +49,10 @@ type WorkspaceCredentials struct { AwsRegion string `json:"awsRegion,omitempty"` } +type WorkspaceConfigs struct { + *v1.Workspace `yaml:",inline" json:",inline"` +} + func (payload *CreateWorkspaceRequest) Decode(r *http.Request) error { return decode(r, payload) } @@ -56,3 +64,7 @@ func (payload *UpdateWorkspaceRequest) Decode(r *http.Request) error { func (payload *WorkspaceCredentials) Decode(r *http.Request) error { return decode(r, payload) } + +func (payload *WorkspaceConfigs) Decode(r *http.Request) error { + return decode(r, payload) +} diff --git a/pkg/server/handler/module/handler.go b/pkg/server/handler/module/handler.go index fd45044bb..1ae3ff283 100644 --- a/pkg/server/handler/module/handler.go +++ b/pkg/server/handler/module/handler.go @@ -89,7 +89,7 @@ func (h *Handler) DeleteModule() http.HandlerFunc { // @Failure 429 {object} error "Too Many Requests" // @Failure 404 {object} error "Not Found" // @Failure 500 {object} error "Internal Server Error" -// @Router /api/v1/modules/{name} [put] +// @Router /api/v1/modules/{name} [put] func (h *Handler) UpdateModule() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Getting stuff from context. @@ -125,7 +125,7 @@ func (h *Handler) UpdateModule() http.HandlerFunc { // @Failure 429 {object} error "Too Many Requests" // @Failure 404 {object} error "Not Found" // @Failure 500 {object} error "Internal Server Error" -// @Router /api/v1/modules/{name} [get] +// @Router /api/v1/modules/{name} [get] func (h *Handler) GetModule() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Getting stuff from context. diff --git a/pkg/server/handler/stack/execute.go b/pkg/server/handler/stack/execute.go index 3af7fb02c..9da5f1e6a 100644 --- a/pkg/server/handler/stack/execute.go +++ b/pkg/server/handler/stack/execute.go @@ -14,26 +14,26 @@ import ( stackmanager "kusionstack.io/kusion/pkg/server/manager/stack" ) -// @Id previewStack -// @Summary Preview stack -// @Description Preview stack information by stack ID -// @Tags stack -// @Produce json -// @Param stack_id path int true "Stack ID" -// @Param importedResources body request.StackImportRequest false "The resources to import during the stack preview" -// @Param workspace query string true "The target workspace to preview the spec in." -// @Param importResources query bool false "Import existing resources during the stack preview" -// @Param output query string false "Output format. Choices are: json, default. Default to default output format in Kusion." -// @Param detail query bool false "Show detailed output" -// @Param specID query string false "The Spec ID to use for the preview. Default to the last one generated." -// @Param force query bool false "Force the preview even when the stack is locked" -// @Success 200 {object} models.Changes "Success" -// @Failure 400 {object} error "Bad Request" -// @Failure 401 {object} error "Unauthorized" -// @Failure 429 {object} error "Too Many Requests" -// @Failure 404 {object} error "Not Found" -// @Failure 500 {object} error "Internal Server Error" -// @Router /api/v1/stacks/{stack_id}/preview [post] +// @Id previewStack +// @Summary Preview stack +// @Description Preview stack information by stack ID +// @Tags stack +// @Produce json +// @Param stack_id path int true "Stack ID" +// @Param importedResources body request.StackImportRequest false "The resources to import during the stack preview" +// @Param workspace query string true "The target workspace to preview the spec in." +// @Param importResources query bool false "Import existing resources during the stack preview" +// @Param output query string false "Output format. Choices are: json, default. Default to default output format in Kusion." +// @Param detail query bool false "Show detailed output" +// @Param specID query string false "The Spec ID to use for the preview. Default to the last one generated." +// @Param force query bool false "Force the preview even when the stack is locked" +// @Success 200 {object} models.Changes "Success" +// @Failure 400 {object} error "Bad Request" +// @Failure 401 {object} error "Unauthorized" +// @Failure 429 {object} error "Too Many Requests" +// @Failure 404 {object} error "Not Found" +// @Failure 500 {object} error "Internal Server Error" +// @Router /api/v1/stacks/{stack_id}/preview [post] func (h *Handler) PreviewStack() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Getting stuff from context @@ -73,22 +73,22 @@ func (h *Handler) PreviewStack() http.HandlerFunc { } } -// @Id generateStack -// @Summary Generate stack -// @Description Generate stack information by stack ID -// @Tags stack -// @Produce json -// @Param stack_id path int true "Stack ID" -// @Param workspace query string true "The target workspace to preview the spec in." -// @Param format query string false "The format to generate the spec in. Choices are: spec. Default to spec." -// @Param force query bool false "Force the generate even when the stack is locked" -// @Success 200 {object} v1.Spec "Success" -// @Failure 400 {object} error "Bad Request" -// @Failure 401 {object} error "Unauthorized" -// @Failure 429 {object} error "Too Many Requests" -// @Failure 404 {object} error "Not Found" -// @Failure 500 {object} error "Internal Server Error" -// @Router /api/v1/stacks/{stack_id}/generate [post] +// @Id generateStack +// @Summary Generate stack +// @Description Generate stack information by stack ID +// @Tags stack +// @Produce json +// @Param stack_id path int true "Stack ID" +// @Param workspace query string true "The target workspace to preview the spec in." +// @Param format query string false "The format to generate the spec in. Choices are: spec. Default to spec." +// @Param force query bool false "Force the generate even when the stack is locked" +// @Success 200 {object} v1.Spec "Success" +// @Failure 400 {object} error "Bad Request" +// @Failure 401 {object} error "Unauthorized" +// @Failure 429 {object} error "Too Many Requests" +// @Failure 404 {object} error "Not Found" +// @Failure 500 {object} error "Internal Server Error" +// @Router /api/v1/stacks/{stack_id}/generate [post] func (h *Handler) GenerateStack() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Getting stuff from context @@ -111,25 +111,25 @@ func (h *Handler) GenerateStack() http.HandlerFunc { } } -// @Id applyStack -// @Summary Apply stack -// @Description Apply stack information by stack ID -// @Tags stack -// @Produce json -// @Param stack_id path int true "Stack ID" -// @Param importedResources body request.StackImportRequest false "The resources to import during the stack preview" -// @Param workspace query string true "The target workspace to preview the spec in." -// @Param importResources query bool false "Import existing resources during the stack preview" -// @Param specID query string false "The Spec ID to use for the apply. Will generate a new spec if omitted." -// @Param force query bool false "Force the apply even when the stack is locked. May cause concurrency issues!!!" -// @Param dryrun query bool false "Apply in dry-run mode" -// @Success 200 {object} string "Success" -// @Failure 400 {object} error "Bad Request" -// @Failure 401 {object} error "Unauthorized" -// @Failure 429 {object} error "Too Many Requests" -// @Failure 404 {object} error "Not Found" -// @Failure 500 {object} error "Internal Server Error" -// @Router /api/v1/stacks/{stack_id}/apply [post] +// @Id applyStack +// @Summary Apply stack +// @Description Apply stack information by stack ID +// @Tags stack +// @Produce json +// @Param stack_id path int true "Stack ID" +// @Param importedResources body request.StackImportRequest false "The resources to import during the stack preview" +// @Param workspace query string true "The target workspace to preview the spec in." +// @Param importResources query bool false "Import existing resources during the stack preview" +// @Param specID query string false "The Spec ID to use for the apply. Will generate a new spec if omitted." +// @Param force query bool false "Force the apply even when the stack is locked. May cause concurrency issues!!!" +// @Param dryrun query bool false "Apply in dry-run mode" +// @Success 200 {object} string "Success" +// @Failure 400 {object} error "Bad Request" +// @Failure 401 {object} error "Unauthorized" +// @Failure 429 {object} error "Too Many Requests" +// @Failure 404 {object} error "Not Found" +// @Failure 500 {object} error "Internal Server Error" +// @Router /api/v1/stacks/{stack_id}/apply [post] func (h *Handler) ApplyStack() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Getting stuff from context @@ -178,22 +178,22 @@ func (h *Handler) ApplyStack() http.HandlerFunc { } } -// @Id destroyStack -// @Summary Destroy stack -// @Description Destroy stack information by stack ID -// @Tags stack -// @Produce json -// @Param stack_id path int true "Stack ID" -// @Param workspace query string true "The target workspace to preview the spec in." -// @Param force query bool false "Force the destroy even when the stack is locked. May cause concurrency issues!!!" -// @Param dryrun query bool false "Destroy in dry-run mode" -// @Success 200 {object} string "Success" -// @Failure 400 {object} error "Bad Request" -// @Failure 401 {object} error "Unauthorized" -// @Failure 429 {object} error "Too Many Requests" -// @Failure 404 {object} error "Not Found" -// @Failure 500 {object} error "Internal Server Error" -// @Router /api/v1/stacks/{stack_id}/destroy [post] +// @Id destroyStack +// @Summary Destroy stack +// @Description Destroy stack information by stack ID +// @Tags stack +// @Produce json +// @Param stack_id path int true "Stack ID" +// @Param workspace query string true "The target workspace to preview the spec in." +// @Param force query bool false "Force the destroy even when the stack is locked. May cause concurrency issues!!!" +// @Param dryrun query bool false "Destroy in dry-run mode" +// @Success 200 {object} string "Success" +// @Failure 400 {object} error "Bad Request" +// @Failure 401 {object} error "Unauthorized" +// @Failure 429 {object} error "Too Many Requests" +// @Failure 404 {object} error "Not Found" +// @Failure 500 {object} error "Internal Server Error" +// @Router /api/v1/stacks/{stack_id}/destroy [post] func (h *Handler) DestroyStack() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Getting stuff from context diff --git a/pkg/server/handler/stack/execute_async.go b/pkg/server/handler/stack/execute_async.go index d07fce825..6ab7ac50d 100644 --- a/pkg/server/handler/stack/execute_async.go +++ b/pkg/server/handler/stack/execute_async.go @@ -19,26 +19,26 @@ import ( logutil "kusionstack.io/kusion/pkg/server/util/logging" ) -// @Id previewStackAsync -// @Summary Asynchronously preview stack -// @Description Start a run and asynchronously preview stack changes by stack ID -// @Tags stack -// @Produce json -// @Param stack_id path int true "Stack ID" -// @Param importedResources body request.StackImportRequest false "The resources to import during the stack preview" -// @Param workspace query string true "The target workspace to preview the spec in." -// @Param importResources query bool false "Import existing resources during the stack preview" -// @Param output query string false "Output format. Choices are: json, default. Default to default output format in Kusion." -// @Param detail query bool false "Show detailed output" -// @Param specID query string false "The Spec ID to use for the preview. Default to the last one generated." -// @Param force query bool false "Force the preview even when the stack is locked" -// @Success 200 {object} entity.Run "Success" -// @Failure 400 {object} error "Bad Request" -// @Failure 401 {object} error "Unauthorized" -// @Failure 429 {object} error "Too Many Requests" -// @Failure 404 {object} error "Not Found" -// @Failure 500 {object} error "Internal Server Error" -// @Router /api/v1/stacks/{stack_id}/preview [post] +// @Id previewStackAsync +// @Summary Asynchronously preview stack +// @Description Start a run and asynchronously preview stack changes by stack ID +// @Tags stack +// @Produce json +// @Param stack_id path int true "Stack ID" +// @Param importedResources body request.StackImportRequest false "The resources to import during the stack preview" +// @Param workspace query string true "The target workspace to preview the spec in." +// @Param importResources query bool false "Import existing resources during the stack preview" +// @Param output query string false "Output format. Choices are: json, default. Default to default output format in Kusion." +// @Param detail query bool false "Show detailed output" +// @Param specID query string false "The Spec ID to use for the preview. Default to the last one generated." +// @Param force query bool false "Force the preview even when the stack is locked" +// @Success 200 {object} entity.Run "Success" +// @Failure 400 {object} error "Bad Request" +// @Failure 401 {object} error "Unauthorized" +// @Failure 429 {object} error "Too Many Requests" +// @Failure 404 {object} error "Not Found" +// @Failure 500 {object} error "Internal Server Error" +// @Router /api/v1/stacks/{stack_id}/preview [post] func (h *Handler) PreviewStackAsync() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Getting stuff from context @@ -125,25 +125,25 @@ func (h *Handler) PreviewStackAsync() http.HandlerFunc { } } -// @Id applyStackAsync -// @Summary Asynchronously apply stack -// @Description Start a run and asynchronously apply stack changes by stack ID -// @Tags stack -// @Produce json -// @Param stack_id path int true "Stack ID" -// @Param importedResources body request.StackImportRequest false "The resources to import during the stack preview" -// @Param workspace query string true "The target workspace to preview the spec in." -// @Param importResources query bool false "Import existing resources during the stack preview" -// @Param specID query string false "The Spec ID to use for the apply. Will generate a new spec if omitted." -// @Param force query bool false "Force the apply even when the stack is locked. May cause concurrency issues!!!" -// @Param dryrun query bool false "Apply in dry-run mode" -// @Success 200 {object} entity.Run "Success" -// @Failure 400 {object} error "Bad Request" -// @Failure 401 {object} error "Unauthorized" -// @Failure 429 {object} error "Too Many Requests" -// @Failure 404 {object} error "Not Found" -// @Failure 500 {object} error "Internal Server Error" -// @Router /api/v1/stacks/{stack_id}/apply/async [post] +// @Id applyStackAsync +// @Summary Asynchronously apply stack +// @Description Start a run and asynchronously apply stack changes by stack ID +// @Tags stack +// @Produce json +// @Param stack_id path int true "Stack ID" +// @Param importedResources body request.StackImportRequest false "The resources to import during the stack preview" +// @Param workspace query string true "The target workspace to preview the spec in." +// @Param importResources query bool false "Import existing resources during the stack preview" +// @Param specID query string false "The Spec ID to use for the apply. Will generate a new spec if omitted." +// @Param force query bool false "Force the apply even when the stack is locked. May cause concurrency issues!!!" +// @Param dryrun query bool false "Apply in dry-run mode" +// @Success 200 {object} entity.Run "Success" +// @Failure 400 {object} error "Bad Request" +// @Failure 401 {object} error "Unauthorized" +// @Failure 429 {object} error "Too Many Requests" +// @Failure 404 {object} error "Not Found" +// @Failure 500 {object} error "Internal Server Error" +// @Router /api/v1/stacks/{stack_id}/apply/async [post] func (h *Handler) ApplyStackAsync() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Getting stuff from context @@ -230,22 +230,22 @@ func (h *Handler) ApplyStackAsync() http.HandlerFunc { } } -// @Id generateStackAsync -// @Summary Asynchronously generate stack -// @Description Start a run and asynchronously generate stack spec by stack ID -// @Tags stack -// @Produce json -// @Param stack_id path int true "Stack ID" -// @Param workspace query string true "The target workspace to preview the spec in." -// @Param format query string false "The format to generate the spec in. Choices are: spec. Default to spec." -// @Param force query bool false "Force the generate even when the stack is locked" -// @Success 200 {object} v1.Spec "Success" -// @Failure 400 {object} error "Bad Request" -// @Failure 401 {object} error "Unauthorized" -// @Failure 429 {object} error "Too Many Requests" -// @Failure 404 {object} error "Not Found" -// @Failure 500 {object} error "Internal Server Error" -// @Router /api/v1/stacks/{stack_id}/generate/async [post] +// @Id generateStackAsync +// @Summary Asynchronously generate stack +// @Description Start a run and asynchronously generate stack spec by stack ID +// @Tags stack +// @Produce json +// @Param stack_id path int true "Stack ID" +// @Param workspace query string true "The target workspace to preview the spec in." +// @Param format query string false "The format to generate the spec in. Choices are: spec. Default to spec." +// @Param force query bool false "Force the generate even when the stack is locked" +// @Success 200 {object} v1.Spec "Success" +// @Failure 400 {object} error "Bad Request" +// @Failure 401 {object} error "Unauthorized" +// @Failure 429 {object} error "Too Many Requests" +// @Failure 404 {object} error "Not Found" +// @Failure 500 {object} error "Internal Server Error" +// @Router /api/v1/stacks/{stack_id}/generate/async [post] func (h *Handler) GenerateStackAsync() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Getting stuff from context @@ -326,22 +326,22 @@ func (h *Handler) GenerateStackAsync() http.HandlerFunc { } } -// @Id destroyStackAsync -// @Summary Asynchronously destroy stack -// @Description Start a run and asynchronously destroy stack resources by stack ID -// @Tags stack -// @Produce json -// @Param stack_id path int true "Stack ID" -// @Param workspace query string true "The target workspace to preview the spec in." -// @Param force query bool false "Force the destroy even when the stack is locked. May cause concurrency issues!!!" -// @Param dryrun query bool false "Destroy in dry-run mode" -// @Success 200 {object} string "Success" -// @Failure 400 {object} error "Bad Request" -// @Failure 401 {object} error "Unauthorized" -// @Failure 429 {object} error "Too Many Requests" -// @Failure 404 {object} error "Not Found" -// @Failure 500 {object} error "Internal Server Error" -// @Router /api/v1/stacks/{stack_id}/destroy/async [post] +// @Id destroyStackAsync +// @Summary Asynchronously destroy stack +// @Description Start a run and asynchronously destroy stack resources by stack ID +// @Tags stack +// @Produce json +// @Param stack_id path int true "Stack ID" +// @Param workspace query string true "The target workspace to preview the spec in." +// @Param force query bool false "Force the destroy even when the stack is locked. May cause concurrency issues!!!" +// @Param dryrun query bool false "Destroy in dry-run mode" +// @Success 200 {object} string "Success" +// @Failure 400 {object} error "Bad Request" +// @Failure 401 {object} error "Unauthorized" +// @Failure 429 {object} error "Too Many Requests" +// @Failure 404 {object} error "Not Found" +// @Failure 500 {object} error "Internal Server Error" +// @Router /api/v1/stacks/{stack_id}/destroy/async [post] func (h *Handler) DestroyStackAsync() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Getting stuff from context diff --git a/pkg/server/handler/stack/handler.go b/pkg/server/handler/stack/handler.go index 6d9090758..f0e1539bb 100644 --- a/pkg/server/handler/stack/handler.go +++ b/pkg/server/handler/stack/handler.go @@ -9,22 +9,22 @@ import ( logutil "kusionstack.io/kusion/pkg/server/util/logging" ) -// @Id createStack -// @Summary Create stack -// @Description Create a new stack -// @Tags stack -// @Accept json -// @Produce json -// @Param stack body request.CreateStackRequest true "Created stack" -// @Param fromTemplate query bool false "Whether to create an AppConfig from template when creating the stack" -// @Param initTopology query bool false "Whether to initialize an AppTopology from template when creating the stack" -// @Success 200 {object} entity.Stack "Success" -// @Failure 400 {object} error "Bad Request" -// @Failure 401 {object} error "Unauthorized" -// @Failure 429 {object} error "Too Many Requests" -// @Failure 404 {object} error "Not Found" -// @Failure 500 {object} error "Internal Server Error" -// @Router /api/v1/stacks [post] +// @Id createStack +// @Summary Create stack +// @Description Create a new stack +// @Tags stack +// @Accept json +// @Produce json +// @Param stack body request.CreateStackRequest true "Created stack" +// @Param fromTemplate query bool false "Whether to create an AppConfig from template when creating the stack" +// @Param initTopology query bool false "Whether to initialize an AppTopology from template when creating the stack" +// @Success 200 {object} entity.Stack "Success" +// @Failure 400 {object} error "Bad Request" +// @Failure 401 {object} error "Unauthorized" +// @Failure 429 {object} error "Too Many Requests" +// @Failure 404 {object} error "Not Found" +// @Failure 500 {object} error "Internal Server Error" +// @Router /api/v1/stacks [post] func (h *Handler) CreateStack() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Getting stuff from context @@ -66,19 +66,19 @@ func (h *Handler) CreateStack() http.HandlerFunc { } } -// @Id deleteStack -// @Summary Delete stack -// @Description Delete specified stack by ID -// @Tags stack -// @Produce json -// @Param stack_id path int true "Stack ID" -// @Success 200 {object} string "Success" -// @Failure 400 {object} error "Bad Request" -// @Failure 401 {object} error "Unauthorized" -// @Failure 429 {object} error "Too Many Requests" -// @Failure 404 {object} error "Not Found" -// @Failure 500 {object} error "Internal Server Error" -// @Router /api/v1/stacks/{stack_id} [delete] +// @Id deleteStack +// @Summary Delete stack +// @Description Delete specified stack by ID +// @Tags stack +// @Produce json +// @Param stack_id path int true "Stack ID" +// @Success 200 {object} string "Success" +// @Failure 400 {object} error "Bad Request" +// @Failure 401 {object} error "Unauthorized" +// @Failure 429 {object} error "Too Many Requests" +// @Failure 404 {object} error "Not Found" +// @Failure 500 {object} error "Internal Server Error" +// @Router /api/v1/stacks/{stack_id} [delete] func (h *Handler) DeleteStack() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Getting stuff from context @@ -94,21 +94,21 @@ func (h *Handler) DeleteStack() http.HandlerFunc { } } -// @Id updateStack -// @Summary Update stack -// @Description Update the specified stack -// @Tags stack -// @Accept json -// @Produce json -// @Param stack_id path int true "Stack ID" -// @Param stack body request.UpdateStackRequest true "Updated stack" -// @Success 200 {object} entity.Stack "Success" -// @Failure 400 {object} error "Bad Request" -// @Failure 401 {object} error "Unauthorized" -// @Failure 429 {object} error "Too Many Requests" -// @Failure 404 {object} error "Not Found" -// @Failure 500 {object} error "Internal Server Error" -// @Router /api/v1/stacks/{stack_id} [put] +// @Id updateStack +// @Summary Update stack +// @Description Update the specified stack +// @Tags stack +// @Accept json +// @Produce json +// @Param stack_id path int true "Stack ID" +// @Param stack body request.UpdateStackRequest true "Updated stack" +// @Success 200 {object} entity.Stack "Success" +// @Failure 400 {object} error "Bad Request" +// @Failure 401 {object} error "Unauthorized" +// @Failure 429 {object} error "Too Many Requests" +// @Failure 404 {object} error "Not Found" +// @Failure 500 {object} error "Internal Server Error" +// @Router /api/v1/stacks/{stack_id} [put] func (h *Handler) UpdateStack() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Getting stuff from context @@ -131,19 +131,19 @@ func (h *Handler) UpdateStack() http.HandlerFunc { } } -// @Id getStack -// @Summary Get stack -// @Description Get stack information by stack ID -// @Tags stack -// @Produce json -// @Param stack_id path int true "Stack ID" -// @Success 200 {object} entity.Stack "Success" -// @Failure 400 {object} error "Bad Request" -// @Failure 401 {object} error "Unauthorized" -// @Failure 429 {object} error "Too Many Requests" -// @Failure 404 {object} error "Not Found" -// @Failure 500 {object} error "Internal Server Error" -// @Router /api/v1/stacks/{stack_id} [get] +// @Id getStack +// @Summary Get stack +// @Description Get stack information by stack ID +// @Tags stack +// @Produce json +// @Param stack_id path int true "Stack ID" +// @Success 200 {object} entity.Stack "Success" +// @Failure 400 {object} error "Bad Request" +// @Failure 401 {object} error "Unauthorized" +// @Failure 429 {object} error "Too Many Requests" +// @Failure 404 {object} error "Not Found" +// @Failure 500 {object} error "Internal Server Error" +// @Router /api/v1/stacks/{stack_id} [get] func (h *Handler) GetStack() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Getting stuff from context @@ -159,23 +159,23 @@ func (h *Handler) GetStack() http.HandlerFunc { } } -// @Id listStack -// @Summary List stacks -// @Description List all stacks -// @Tags stack -// @Produce json -// @Param projectID query uint false "ProjectID to filter stacks by. Default to all" -// @Param orgID query uint false "OrgID to filter stacks by. Default to all" -// @Param projectName query string false "ProjectName to filter stacks by. Default to all" -// @Param cloud query string false "Cloud to filter stacks by. Default to all" -// @Param env query string false "Environment to filter stacks by. Default to all" -// @Success 200 {object} []entity.Stack "Success" -// @Failure 400 {object} error "Bad Request" -// @Failure 401 {object} error "Unauthorized" -// @Failure 429 {object} error "Too Many Requests" -// @Failure 404 {object} error "Not Found" -// @Failure 500 {object} error "Internal Server Error" -// @Router /api/v1/stacks [get] +// @Id listStack +// @Summary List stacks +// @Description List all stacks +// @Tags stack +// @Produce json +// @Param projectID query uint false "ProjectID to filter stacks by. Default to all" +// @Param orgID query uint false "OrgID to filter stacks by. Default to all" +// @Param projectName query string false "ProjectName to filter stacks by. Default to all" +// @Param cloud query string false "Cloud to filter stacks by. Default to all" +// @Param env query string false "Environment to filter stacks by. Default to all" +// @Success 200 {object} []entity.Stack "Success" +// @Failure 400 {object} error "Bad Request" +// @Failure 401 {object} error "Unauthorized" +// @Failure 429 {object} error "Too Many Requests" +// @Failure 404 {object} error "Not Found" +// @Failure 500 {object} error "Internal Server Error" +// @Router /api/v1/stacks [get] func (h *Handler) ListStacks() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Getting stuff from context diff --git a/pkg/server/handler/stack/run.go b/pkg/server/handler/stack/run.go index ed7f61e6c..7ff0fbcd6 100644 --- a/pkg/server/handler/stack/run.go +++ b/pkg/server/handler/stack/run.go @@ -9,19 +9,19 @@ import ( logutil "kusionstack.io/kusion/pkg/server/util/logging" ) -// @Id getRun -// @Summary Get run -// @Description Get run information by run ID -// @Tags run -// @Produce json -// @Param run path int true "Run ID" -// @Success 200 {object} entity.Run "Success" -// @Failure 400 {object} error "Bad Request" -// @Failure 401 {object} error "Unauthorized" -// @Failure 429 {object} error "Too Many Requests" -// @Failure 404 {object} error "Not Found" -// @Failure 500 {object} error "Internal Server Error" -// @Router /api/v1/runs/{run_id} [get] +// @Id getRun +// @Summary Get run +// @Description Get run information by run ID +// @Tags run +// @Produce json +// @Param run path int true "Run ID" +// @Success 200 {object} entity.Run "Success" +// @Failure 400 {object} error "Bad Request" +// @Failure 401 {object} error "Unauthorized" +// @Failure 429 {object} error "Too Many Requests" +// @Failure 404 {object} error "Not Found" +// @Failure 500 {object} error "Internal Server Error" +// @Router /api/v1/runs/{run_id} [get] func (h *Handler) GetRun() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Getting stuff from context @@ -37,19 +37,19 @@ func (h *Handler) GetRun() http.HandlerFunc { } } -// @Id getRunResult -// @Summary Get run result -// @Description Get run result by run ID -// @Tags run -// @Produce json -// @Param run path int true "Run ID" -// @Success 200 {object} entity.Run "Success" -// @Failure 400 {object} error "Bad Request" -// @Failure 401 {object} error "Unauthorized" -// @Failure 429 {object} error "Too Many Requests" -// @Failure 404 {object} error "Not Found" -// @Failure 500 {object} error "Internal Server Error" -// @Router /api/v1/runs/{run_id}/result [get] +// @Id getRunResult +// @Summary Get run result +// @Description Get run result by run ID +// @Tags run +// @Produce json +// @Param run path int true "Run ID" +// @Success 200 {object} entity.Run "Success" +// @Failure 400 {object} error "Bad Request" +// @Failure 401 {object} error "Unauthorized" +// @Failure 429 {object} error "Too Many Requests" +// @Failure 404 {object} error "Not Found" +// @Failure 500 {object} error "Internal Server Error" +// @Router /api/v1/runs/{run_id}/result [get] func (h *Handler) GetRunResult() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Getting stuff from context @@ -71,23 +71,23 @@ func (h *Handler) GetRunResult() http.HandlerFunc { } } -// @Id listRun -// @Summary List runs -// @Description List all runs -// @Tags stack -// @Produce json -// @Param projectID query uint false "ProjectID to filter runs by. Default to all" -// @Param orgID query uint false "OrgID to filter runs by. Default to all" -// @Param projectName query string false "ProjectName to filter runs by. Default to all" -// @Param cloud query string false "Cloud to filter runs by. Default to all" -// @Param env query string false "Environment to filter runs by. Default to all" -// @Success 200 {object} []entity.Stack "Success" -// @Failure 400 {object} error "Bad Request" -// @Failure 401 {object} error "Unauthorized" -// @Failure 429 {object} error "Too Many Requests" -// @Failure 404 {object} error "Not Found" -// @Failure 500 {object} error "Internal Server Error" -// @Router /api/v1/runs [get] +// @Id listRun +// @Summary List runs +// @Description List all runs +// @Tags stack +// @Produce json +// @Param projectID query uint false "ProjectID to filter runs by. Default to all" +// @Param orgID query uint false "OrgID to filter runs by. Default to all" +// @Param projectName query string false "ProjectName to filter runs by. Default to all" +// @Param cloud query string false "Cloud to filter runs by. Default to all" +// @Param env query string false "Environment to filter runs by. Default to all" +// @Success 200 {object} []entity.Stack "Success" +// @Failure 400 {object} error "Bad Request" +// @Failure 401 {object} error "Unauthorized" +// @Failure 429 {object} error "Too Many Requests" +// @Failure 404 {object} error "Not Found" +// @Failure 500 {object} error "Internal Server Error" +// @Router /api/v1/runs [get] func (h *Handler) ListRuns() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Getting stuff from context diff --git a/pkg/server/handler/workspace/configs.go b/pkg/server/handler/workspace/configs.go new file mode 100644 index 000000000..9de774893 --- /dev/null +++ b/pkg/server/handler/workspace/configs.go @@ -0,0 +1,133 @@ +package workspace + +import ( + "context" + "net/http" + + "github.com/go-chi/render" + "kusionstack.io/kusion/pkg/domain/request" + "kusionstack.io/kusion/pkg/server/handler" +) + +// @Id getWorkspaceConfigs +// @Summary get workspace configurations +// @Description Get configurations in the specified workspace +// @Tags workspace +// @Accept json +// @Produce json +// @Param id path int true "Workspace ID" +// @Success 200 {object} request.WorkspaceConfigs "Success" +// @Failure 400 {object} error "Bad Request" +// @Failure 401 {object} error "Unauthorized" +// @Failure 429 {object} error "Too Many Requests" +// @Failure 404 {object} error "Not Found" +// @Failure 500 {object} error "Internal Server Error" +// @Router /api/v1/workspaces/configs/{id} [get] +func (h *Handler) GetWorkspaceConfigs() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Getting stuff from the context. + ctx, logger, params, err := requestHelper(r) + if err != nil { + render.Render(w, r, handler.FailureResponse(ctx, err)) + return + } + logger.Info("Getting workspace configs...", "workspaceID", params.WorkspaceID) + + wsConfigs, err := h.workspaceManager.GetWorkspaceConfigs(ctx, params.WorkspaceID) + handler.HandleResult(w, r, ctx, err, wsConfigs) + } +} + +// @Id validateWorkspaceConfigs +// @Summary Validate workspace configurations +// @Description Validate the configurations in the specified workspace +// @Tags workspace +// @Accept json +// @Produce json +// @Param workspace body request.WorkspaceConfigs true "Workspace configurations to be validated" +// @Success 200 {object} request.WorkspaceConfigs "Success" +// @Failure 400 {object} error "Bad Request" +// @Failure 401 {object} error "Unauthorized" +// @Failure 429 {object} error "Too Many Requests" +// @Failure 404 {object} error "Not Found" +// @Failure 500 {object} error "Internal Server Error" +// @Router /api/v1/workspaces/configs/validate [post] +func (h *Handler) ValidateWorkspaceConfigs() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Decode the request body into the payload. + var requestPayload request.WorkspaceConfigs + if err := requestPayload.Decode(r); err != nil { + render.Render(w, r, handler.FailureResponse(context.Background(), err)) + return + } + + wsConfigs, err := h.workspaceManager.ValidateWorkspaceConfigs(context.Background(), requestPayload) + handler.HandleResult(w, r, context.Background(), err, wsConfigs) + } +} + +// @Id updateWorkspaceConfigs +// @Summary Update workspace configurations +// @Description Update the configurations in the specified workspace +// @Tags workspace +// @Accept json +// @Produce json +// @Param id path int true "Workspace ID" +// @Param workspace body request.WorkspaceConfigs true "Updated workspace configurations" +// @Success 200 {object} request.WorkspaceConfigs "Success" +// @Failure 400 {object} error "Bad Request" +// @Failure 401 {object} error "Unauthorized" +// @Failure 429 {object} error "Too Many Requests" +// @Failure 404 {object} error "Not Found" +// @Failure 500 {object} error "Internal Server Error" +// @Router /api/v1/workspaces/configs/{id} [put] +func (h *Handler) UpdateWorkspaceConfigs() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Getting stuff from context. + ctx, logger, params, err := requestHelper(r) + if err != nil { + render.Render(w, r, handler.FailureResponse(ctx, err)) + return + } + logger.Info("Updating workspace configurations...", "workspaceID", params.WorkspaceID) + + // Decode the request body into the payload. + var requestPayload request.WorkspaceConfigs + if err := requestPayload.Decode(r); err != nil { + render.Render(w, r, handler.FailureResponse(ctx, err)) + return + } + + wsConfigs, err := h.workspaceManager.UpdateWorkspaceConfigs(ctx, params.WorkspaceID, requestPayload) + handler.HandleResult(w, r, ctx, err, wsConfigs) + } +} + +// @Id createWorkspaceModDeps +// @Summary Create the module dependencies of the workspace +// @Description Create the module dependencies in kcl.mod of the specified workspace +// @Tags workspace +// @Accept json +// @Produce plain +// @Param id path int true "Workspace ID" +// @Success 200 {object} string "Success" +// @Failure 400 {object} error "Bad Request" +// @Failure 401 {object} error "Unauthorized" +// @Failure 429 {object} error "Too Many Requests" +// @Failure 404 {object} error "Not Found" +// @Failure 500 {object} error "Internal Server Error" +// @Router /api/v1/workspaces/configs/mod-deps/{id} [post] +func (h *Handler) CreateWorkspaceModDeps() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Getting stuff from context. + ctx, logger, params, err := requestHelper(r) + if err != nil { + render.Render(w, r, handler.FailureResponse(ctx, err)) + return + } + logger.Info("Creating kusion module dependencies...", "workspaceID", params.WorkspaceID) + + deps, err := h.workspaceManager.CreateKCLModDependencies(ctx, params.WorkspaceID) + handler.HandleResult(w, r, ctx, err, deps) + } +} diff --git a/pkg/server/handler/workspace/handler_test.go b/pkg/server/handler/workspace/handler_test.go index 34480b69b..7c8922b63 100644 --- a/pkg/server/handler/workspace/handler_test.go +++ b/pkg/server/handler/workspace/handler_test.go @@ -10,15 +10,21 @@ import ( "testing" "github.com/DATA-DOG/go-sqlmock" + "github.com/bytedance/mockey" "github.com/go-chi/chi/v5" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gorm.io/gorm" + v1 "kusionstack.io/kusion/pkg/apis/api.kusion.io/v1" + "kusionstack.io/kusion/pkg/backend" "kusionstack.io/kusion/pkg/domain/entity" "kusionstack.io/kusion/pkg/domain/request" + "kusionstack.io/kusion/pkg/engine/release" + "kusionstack.io/kusion/pkg/engine/resource/graph" "kusionstack.io/kusion/pkg/infra/persistence" "kusionstack.io/kusion/pkg/server/handler" workspacemanager "kusionstack.io/kusion/pkg/server/manager/workspace" + "kusionstack.io/kusion/pkg/workspace" ) func TestWorkspaceHandler(t *testing.T) { @@ -89,51 +95,56 @@ func TestWorkspaceHandler(t *testing.T) { }) t.Run("CreateWorkspace", func(t *testing.T) { - sqlMock, fakeGDB, recorder, workspaceHandler := setupTest(t) - defer persistence.CloseDB(t, fakeGDB) - defer sqlMock.ExpectClose() - - // Create a new HTTP request - req, err := http.NewRequest("POST", "/workspaces", nil) - assert.NoError(t, err) - - rctx := chi.NewRouteContext() - rctx.URLParams.Add("workspaceID", "1") - req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) - - // Set request body - requestPayload := request.CreateWorkspaceRequest{ - Name: wsName, - BackendID: 1, - Owners: []string{"hua.li", "xiaoming.li"}, - } - reqBody, err := json.Marshal(requestPayload) - assert.NoError(t, err) - req.Body = io.NopCloser(bytes.NewReader(reqBody)) - req.Header.Add("Content-Type", "application/json") - - sqlMock.ExpectQuery("SELECT"). - WillReturnRows(sqlmock.NewRows([]string{"id"}). - AddRow(1)) - sqlMock.ExpectBegin() - sqlMock.ExpectExec("INSERT"). - WillReturnResult(sqlmock.NewResult(int64(1), int64(1))) - sqlMock.ExpectCommit() - - // Call the CreateWorkspace handler function - workspaceHandler.CreateWorkspace()(recorder, req) - assert.Equal(t, http.StatusOK, recorder.Code) - - // Unmarshal the response body - var resp handler.Response - err = json.Unmarshal(recorder.Body.Bytes(), &resp) - if err != nil { - t.Fatalf("Failed to unmarshal response: %v", err) - } - - // Assertion - assert.Equal(t, float64(1), resp.Data.(map[string]any)["id"]) - assert.Equal(t, wsName, resp.Data.(map[string]any)["name"]) + mockey.PatchConvey("mock creating initiated workspace config", t, func() { + sqlMock, fakeGDB, recorder, workspaceHandler := setupTest(t) + defer persistence.CloseDB(t, fakeGDB) + defer sqlMock.ExpectClose() + + // Mock creating an initiated workspace config. + mockCreateInitiatedWorkspaceConfig() + + // Create a new HTTP request + req, err := http.NewRequest("POST", "/workspaces", nil) + assert.NoError(t, err) + + rctx := chi.NewRouteContext() + rctx.URLParams.Add("workspaceID", "1") + req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx)) + + // Set request body + requestPayload := request.CreateWorkspaceRequest{ + Name: wsName, + BackendID: 1, + Owners: []string{"hua.li", "xiaoming.li"}, + } + reqBody, err := json.Marshal(requestPayload) + assert.NoError(t, err) + req.Body = io.NopCloser(bytes.NewReader(reqBody)) + req.Header.Add("Content-Type", "application/json") + + sqlMock.ExpectQuery("SELECT"). + WillReturnRows(sqlmock.NewRows([]string{"id"}). + AddRow(1)) + sqlMock.ExpectBegin() + sqlMock.ExpectExec("INSERT"). + WillReturnResult(sqlmock.NewResult(int64(1), int64(1))) + sqlMock.ExpectCommit() + + // Call the CreateWorkspace handler function + workspaceHandler.CreateWorkspace()(recorder, req) + assert.Equal(t, http.StatusOK, recorder.Code) + + // Unmarshal the response body + var resp handler.Response + err = json.Unmarshal(recorder.Body.Bytes(), &resp) + if err != nil { + t.Fatalf("Failed to unmarshal response: %v", err) + } + + // Assertion + assert.Equal(t, float64(1), resp.Data.(map[string]any)["id"]) + assert.Equal(t, wsName, resp.Data.(map[string]any)["name"]) + }) }) t.Run("UpdateExistingWorkspace", func(t *testing.T) { @@ -299,9 +310,68 @@ func setupTest(t *testing.T) (sqlmock.Sqlmock, *gorm.DB, *httptest.ResponseRecor require.NoError(t, err) workspaceRepo := persistence.NewWorkspaceRepository(fakeGDB) backendRepo := persistence.NewBackendRepository(fakeGDB) + moduleRepo := persistence.NewModuleRepository(fakeGDB) workspaceHandler := &Handler{ - workspaceManager: workspacemanager.NewWorkspaceManager(workspaceRepo, backendRepo, entity.Backend{}), + workspaceManager: workspacemanager.NewWorkspaceManager(workspaceRepo, backendRepo, moduleRepo, entity.Backend{}), } recorder := httptest.NewRecorder() return sqlMock, fakeGDB, recorder, workspaceHandler } + +func mockCreateInitiatedWorkspaceConfig() { + mockey.Mock(workspacemanager.NewBackendFromEntity).To(func(_ entity.Backend) (backend.Backend, error) { + return &mockBackend{}, nil + }).Build() +} + +type mockBackend struct{} + +func (m *mockBackend) WorkspaceStorage() (workspace.Storage, error) { + return &mockStorage{}, nil +} + +func (m *mockBackend) ReleaseStorage(string, string) (release.Storage, error) { + return nil, nil +} + +func (m *mockBackend) StateStorageWithPath(path string) (release.Storage, error) { + return nil, nil +} + +func (m *mockBackend) GraphStorage(project, workspace string) (graph.Storage, error) { + return nil, nil +} + +func (m *mockBackend) ProjectStorage() (map[string][]string, error) { + return nil, nil +} + +type mockStorage struct{} + +func (m *mockStorage) Get(name string) (*v1.Workspace, error) { + return nil, nil +} + +func (m *mockStorage) Create(ws *v1.Workspace) error { + return nil +} + +func (m *mockStorage) Update(ws *v1.Workspace) error { + return nil +} + +func (m *mockStorage) Delete(name string) error { + return nil +} + +func (m *mockStorage) GetNames() ([]string, error) { + return nil, nil +} + +func (m *mockStorage) GetCurrent() (string, error) { + return "", nil +} + +func (m *mockStorage) SetCurrent(name string) error { + return nil +} diff --git a/pkg/server/manager/workspace/configs.go b/pkg/server/manager/workspace/configs.go new file mode 100644 index 000000000..a0624823a --- /dev/null +++ b/pkg/server/manager/workspace/configs.go @@ -0,0 +1,255 @@ +package workspace + +import ( + "context" + "errors" + "fmt" + "net/url" + "strings" + + "gorm.io/gorm" + v1 "kusionstack.io/kusion/pkg/apis/api.kusion.io/v1" + "kusionstack.io/kusion/pkg/backend" + "kusionstack.io/kusion/pkg/domain/constant" + "kusionstack.io/kusion/pkg/domain/request" + logutil "kusionstack.io/kusion/pkg/server/util/logging" + + "github.com/elliotchance/orderedmap/v2" + kpmdownloader "kcl-lang.io/kpm/pkg/downloader" + kpmpkg "kcl-lang.io/kpm/pkg/package" +) + +func (m *WorkspaceManager) GetWorkspaceConfigs(ctx context.Context, id uint) (*request.WorkspaceConfigs, error) { + workspaceEntity, err := m.workspaceRepo.Get(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrGettingNonExistingWorkspace + } + return nil, err + } + + // Get backend from workspace name. + wsBackend, err := m.getBackendFromWorkspaceName(ctx, workspaceEntity.Name) + if err != nil { + return nil, err + } + + // Get workspace storage from backend. + wsStorage, err := wsBackend.WorkspaceStorage() + if err != nil { + return nil, err + } + + // Get workspace configurations from storage. + ws, err := wsStorage.Get(workspaceEntity.Name) + if err != nil { + return nil, err + } + + return &request.WorkspaceConfigs{ + Workspace: ws, + }, nil +} + +func (m *WorkspaceManager) ValidateWorkspaceConfigs(ctx context.Context, configs request.WorkspaceConfigs) (*request.WorkspaceConfigs, error) { + // Validate the workspace configs to be updated. + if err := m.validateWorkspaceConfigs(ctx, configs.Workspace); err != nil { + return nil, err + } + + return &configs, nil +} + +func (m *WorkspaceManager) UpdateWorkspaceConfigs(ctx context.Context, id uint, configs request.WorkspaceConfigs) (*request.WorkspaceConfigs, error) { + workspaceEntity, err := m.workspaceRepo.Get(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrGettingNonExistingWorkspace + } + return nil, err + } + + // Get backend from workspace name. + wsBackend, err := m.getBackendFromWorkspaceName(ctx, workspaceEntity.Name) + if err != nil { + return nil, err + } + + // Get workspace storage from backend. + wsStorage, err := wsBackend.WorkspaceStorage() + if err != nil { + return nil, err + } + + // Validate the workspace configs to be updated. + if configs.Workspace.Name != "" && configs.Workspace.Name != workspaceEntity.Name { + return nil, fmt.Errorf("inconsistent workspace name, want: %s, got: %s", workspaceEntity.Name, configs.Workspace.Name) + } else { + configs.Workspace.Name = workspaceEntity.Name + } + + if err = m.validateWorkspaceConfigs(ctx, configs.Workspace); err != nil { + return nil, err + } + + // Update workspace configs in the storage. + if err = wsStorage.Update(configs.Workspace); err != nil { + return nil, err + } + + return &configs, nil +} + +func (m *WorkspaceManager) CreateKCLModDependencies(ctx context.Context, id uint) (string, error) { + workspaceEntity, err := m.workspaceRepo.Get(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return "", ErrGettingNonExistingWorkspace + } + return "", err + } + + // Get backend from workspace name. + wsBackend, err := m.getBackendFromWorkspaceName(ctx, workspaceEntity.Name) + if err != nil { + return "", err + } + + // Get workspace storage from backend. + wsStorage, err := wsBackend.WorkspaceStorage() + if err != nil { + return "", err + } + + // Get workspace configurations from storage. + ws, err := wsStorage.Get(workspaceEntity.Name) + if err != nil { + return "", err + } + + // Generate the dependencies in `kcl.mod`. + deps := &kpmpkg.Dependencies{ + Deps: orderedmap.NewOrderedMap[string, kpmpkg.Dependency](), + } + + // Traverse the modules in the workspace. + for modName, modConfig := range ws.Modules { + // Parse the source url of the module. + src, err := kpmdownloader.NewSourceFromStr(modConfig.Path) + if err != nil { + return "", err + } + + // Prepare the dependency object. + dep := kpmpkg.Dependency{ + Name: modName, + Version: modConfig.Version, + } + + if src.Git != nil { + dep.Source = kpmdownloader.Source{ + Git: &kpmdownloader.Git{ + Url: modConfig.Path, + Tag: modConfig.Version, + }, + } + } else if src.Oci != nil { + u, _ := url.Parse(modConfig.Path) + dep.Source = kpmdownloader.Source{ + Oci: &kpmdownloader.Oci{ + Reg: u.Host, + Repo: strings.TrimPrefix(u.Path, "/"), + Tag: modConfig.Version, + }, + } + } else if src.Local != nil { + dep.Source = kpmdownloader.Source{ + Local: src.Local, + } + } + + deps.Deps.Set(modName, dep) + } + + return deps.MarshalTOML(), nil +} + +func (m *WorkspaceManager) getBackendFromWorkspaceName(ctx context.Context, workspaceName string) (backend.Backend, error) { + logger := logutil.GetLogger(ctx) + logger.Info("Getting backend based on workspace name...") + + var remoteBackend backend.Backend + if workspaceName == constant.DefaultWorkspace { + // Get the default backend. + return m.getDefaultBackend() + } else { + // Get workspace entity by name. + workspaceEntity, err := m.workspaceRepo.GetByName(ctx, workspaceName) + if err != nil && err == gorm.ErrRecordNotFound { + return nil, ErrGettingNonExistingWorkspace + } else if err != nil { + return nil, err + } + + // Generate backend from the workspace entity. + remoteBackend, err = NewBackendFromEntity(*workspaceEntity.Backend) + if err != nil { + return nil, err + } + } + + return remoteBackend, nil +} + +func (m *WorkspaceManager) getDefaultBackend() (backend.Backend, error) { + defaultBackendEntity := m.defaultBackend + remoteBackend, err := NewBackendFromEntity(defaultBackendEntity) + if err != nil { + return nil, err + } + + return remoteBackend, nil +} + +func (m *WorkspaceManager) validateWorkspaceConfigs(ctx context.Context, workspaceConfigs *v1.Workspace) error { + logger := logutil.GetLogger(ctx) + logger.Info("Validating workspace configs...") + + var modulesNotFound, modulesPathNotMatched []string + for moduleName, moduleConfigs := range workspaceConfigs.Modules { + // Get module entity by name. + moduleEntity, err := m.moduleRepo.Get(ctx, moduleName) + if err != nil { + // The modules declared in the workspace should be registered. + if errors.Is(err, gorm.ErrRecordNotFound) { + modulesNotFound = append(modulesNotFound, moduleName) + } else { + return err + } + } else { + // The oci path of the modules should match the registered information. + if moduleConfigs.Path != "" && moduleConfigs.Path != moduleEntity.URL.String() { + modulesPathNotMatched = append(modulesPathNotMatched, moduleName) + } else if moduleConfigs.Path == "" { + // Set the oci path with the registered information. + moduleConfigs.Path = moduleEntity.URL.String() + } + } + } + + // Prepare and return the errors according to the results. + errModulesNotFound := fmt.Errorf(ErrMsgModulesNotRegistered, + plural(len(modulesNotFound)), modulesNotFound, verb(len(modulesNotFound))) + errModulesPathNotMatched := fmt.Errorf(ErrMsgModulesPathNotMatched, + plural(len(modulesPathNotMatched)), modulesPathNotMatched, verb(len(modulesPathNotMatched))) + + if len(modulesNotFound) > 0 && len(modulesPathNotMatched) > 0 { + return errors.Join(errModulesNotFound, errModulesPathNotMatched) + } else if len(modulesNotFound) > 0 { + return errModulesNotFound + } else if len(modulesPathNotMatched) > 0 { + return errModulesPathNotMatched + } + + return nil +} diff --git a/pkg/server/manager/workspace/types.go b/pkg/server/manager/workspace/types.go index 40fb0e4a1..5806c2ac5 100644 --- a/pkg/server/manager/workspace/types.go +++ b/pkg/server/manager/workspace/types.go @@ -12,21 +12,42 @@ var ( ErrUpdatingNonExistingWorkspace = errors.New("the workspace to update does not exist") ErrInvalidWorkspaceID = errors.New("the workspace ID should be a uuid") ErrBackendNotFound = errors.New("the specified backend does not exist") + ErrMsgModulesNotRegistered = "the module%s %v %s not registered" + ErrMsgModulesPathNotMatched = "the oci path of the module%s %v %s not matched with the registered information" ) type WorkspaceManager struct { workspaceRepo repository.WorkspaceRepository backendRepo repository.BackendRepository + moduleRepo repository.ModuleRepository defaultBackend entity.Backend } func NewWorkspaceManager(workspaceRepo repository.WorkspaceRepository, backendRepo repository.BackendRepository, + moduleRepo repository.ModuleRepository, defaultBackend entity.Backend, ) *WorkspaceManager { return &WorkspaceManager{ workspaceRepo: workspaceRepo, backendRepo: backendRepo, + moduleRepo: moduleRepo, defaultBackend: defaultBackend, } } + +func plural(count int) string { + if count > 1 { + return "s" + } + + return "" +} + +func verb(count int) string { + if count > 1 { + return "are" + } + + return "is" +} diff --git a/pkg/server/manager/workspace/workspace_manager.go b/pkg/server/manager/workspace/workspace_manager.go index b8a600ba3..95c61f7cd 100644 --- a/pkg/server/manager/workspace/workspace_manager.go +++ b/pkg/server/manager/workspace/workspace_manager.go @@ -6,6 +6,7 @@ import ( "github.com/jinzhu/copier" "gorm.io/gorm" + v1 "kusionstack.io/kusion/pkg/apis/api.kusion.io/v1" "kusionstack.io/kusion/pkg/domain/entity" "kusionstack.io/kusion/pkg/domain/request" ) @@ -86,6 +87,23 @@ func (m *WorkspaceManager) CreateWorkspace(ctx context.Context, requestPayload r } createdEntity.Backend = backendEntity + // Generate backend from the backend entity. + remoteBackend, err := NewBackendFromEntity(*backendEntity) + if err != nil { + return nil, err + } + + // Get workspace storage from backend. + wsStorage, err := remoteBackend.WorkspaceStorage() + if err != nil { + return nil, err + } + + // Create an initiated workspace config. + if err = wsStorage.Create(&v1.Workspace{Name: createdEntity.Name}); err != nil { + return nil, err + } + // Create workspace with repository err = m.workspaceRepo.Create(ctx, &createdEntity) if err != nil { diff --git a/pkg/server/route/route.go b/pkg/server/route/route.go index a4a64af4e..040eaec5b 100644 --- a/pkg/server/route/route.go +++ b/pkg/server/route/route.go @@ -138,7 +138,7 @@ func setupRestAPIV1( sourceManager := sourcemanager.NewSourceManager(sourceRepo) organizationManager := organizationmanager.NewOrganizationManager(organizationRepo) backendManager := backendmanager.NewBackendManager(backendRepo) - workspaceManager := workspacemanager.NewWorkspaceManager(workspaceRepo, backendRepo, config.DefaultBackend) + workspaceManager := workspacemanager.NewWorkspaceManager(workspaceRepo, backendRepo, moduleRepo, config.DefaultBackend) projectManager := projectmanager.NewProjectManager(projectRepo, organizationRepo, sourceRepo, config.DefaultSource) resourceManager := resourcemanager.NewResourceManager(resourceRepo) moduleManager := modulemanager.NewModuleManager(moduleRepo) @@ -251,6 +251,18 @@ func setupRestAPIV1( r.Put("/", workspaceHandler.UpdateWorkspace()) r.Delete("/", workspaceHandler.DeleteWorkspace()) }) + r.Route("/configs", func(r chi.Router) { + r.Route("/{workspaceID}", func(r chi.Router) { + r.Get("/", workspaceHandler.GetWorkspaceConfigs()) + r.Put("/", workspaceHandler.UpdateWorkspaceConfigs()) + }) + r.Route("/validate", func(r chi.Router) { + r.Post("/", workspaceHandler.ValidateWorkspaceConfigs()) + }) + r.Route("/mod-deps", func(r chi.Router) { + r.Post("/{workspaceID}", workspaceHandler.CreateWorkspaceModDeps()) + }) + }) r.Post("/", workspaceHandler.CreateWorkspace()) r.Get("/", workspaceHandler.ListWorkspaces()) })