From 69f9b252ba1c9ede088a73011384e4533c5b0ddc Mon Sep 17 00:00:00 2001 From: liuhaoming Date: Mon, 11 Nov 2024 20:57:56 +0800 Subject: [PATCH] feat: add workspace apis for kusion server --- api/openapispec/docs.go | 249 ++++++++++++++++++ api/openapispec/swagger.json | 249 ++++++++++++++++++ api/openapispec/swagger.yaml | 167 ++++++++++++ pkg/domain/request/workspace_request.go | 14 +- pkg/server/handler/module/handler.go | 134 +++++----- pkg/server/handler/workspace/configs.go | 104 ++++++++ pkg/server/handler/workspace/handler_test.go | 3 +- pkg/server/manager/workspace/configs.go | 175 ++++++++++++ pkg/server/manager/workspace/types.go | 21 ++ .../manager/workspace/workspace_manager.go | 18 ++ pkg/server/route/route.go | 11 +- 11 files changed, 1075 insertions(+), 70 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..6896d4bb1 100644 --- a/api/openapispec/docs.go +++ b/api/openapispec/docs.go @@ -2054,6 +2054,174 @@ const docTemplate = `{ } } }, + "/api/v1/workspaces/configs/validate": { + "post": { + "description": "Validate the configurations in the specified workspace", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "workspace" + ], + "summary": "Validate workspace configurations", + "operationId": "validateWorkspaceConfigs", + "parameters": [ + { + "description": "Workspace configurations to be validated", + "name": "workspace", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WorkspaceConfigs" + } + } + ], + "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": {} + } + } + } + }, + "/api/v1/workspaces/configs/{id}": { + "get": { + "description": "Get configurations in the specified workspace", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "workspace" + ], + "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/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": {} + } + } + }, + "put": { + "description": "Update the configurations in the specified workspace", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "workspace" + ], + "summary": "Update workspace configurations", + "operationId": "updateWorkspaceConfigs", + "parameters": [ + { + "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.WorkspaceConfigs" + } + } + ], + "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": {} + } + } + } + }, "/api/v1/workspaces/{id}": { "get": { "description": "Get workspace information by workspace ID", @@ -3348,6 +3516,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 +3672,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 +3720,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..706991f43 100644 --- a/api/openapispec/swagger.json +++ b/api/openapispec/swagger.json @@ -2043,6 +2043,174 @@ } } }, + "/api/v1/workspaces/configs/validate": { + "post": { + "description": "Validate the configurations in the specified workspace", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "workspace" + ], + "summary": "Validate workspace configurations", + "operationId": "validateWorkspaceConfigs", + "parameters": [ + { + "description": "Workspace configurations to be validated", + "name": "workspace", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/request.WorkspaceConfigs" + } + } + ], + "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": {} + } + } + } + }, + "/api/v1/workspaces/configs/{id}": { + "get": { + "description": "Get configurations in the specified workspace", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "workspace" + ], + "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/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": {} + } + } + }, + "put": { + "description": "Update the configurations in the specified workspace", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "workspace" + ], + "summary": "Update workspace configurations", + "operationId": "updateWorkspaceConfigs", + "parameters": [ + { + "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.WorkspaceConfigs" + } + } + ], + "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": {} + } + } + } + }, "/api/v1/workspaces/{id}": { "get": { "description": "Get workspace information by workspace ID", @@ -3337,6 +3505,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 +3661,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 +3709,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..24d02f557 100644 --- a/api/openapispec/swagger.yaml +++ b/api/openapispec/swagger.yaml @@ -797,6 +797,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 +914,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 +944,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: @@ -2597,6 +2647,123 @@ 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/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..a807c9067 100644 --- a/pkg/server/handler/module/handler.go +++ b/pkg/server/handler/module/handler.go @@ -13,20 +13,20 @@ import ( logutil "kusionstack.io/kusion/pkg/server/util/logging" ) -// @Id createModule -// @Summary Create module -// @Description Create a new Kusion module -// @Tags module -// @Accept json -// @Produce json -// @Param module body request.CreateModuleRequest true "Created module" -// @Success 200 {object} entity.Module "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/modules [post] +// @Id createModule +// @Summary Create module +// @Description Create a new Kusion module +// @Tags module +// @Accept json +// @Produce json +// @Param module body request.CreateModuleRequest true "Created module" +// @Success 200 {object} entity.Module "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/modules [post] func (h *Handler) CreateModule() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Getting stuff from context. @@ -47,19 +47,19 @@ func (h *Handler) CreateModule() http.HandlerFunc { } } -// @Id deleteModule -// @Summary Delete module -// @Description Delete the specified module by name -// @Tags module -// @Produce json -// @Param name path string true "Module Name" -// @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/modules/{name} [delete] +// @Id deleteModule +// @Summary Delete module +// @Description Delete the specified module by name +// @Tags module +// @Produce json +// @Param name path string true "Module Name" +// @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/modules/{name} [delete] func (h *Handler) DeleteModule() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Getting stuff from context. @@ -75,21 +75,21 @@ func (h *Handler) DeleteModule() http.HandlerFunc { } } -// @Id updateModule -// @Summary Update module -// @Description Update the specified module -// @Tags module -// @Accept json -// @Produce json -// @Param name path string true "Module Name" -// @Param module body request.UpdateModuleRequest true "Updated module" -// @Success 200 {object} entity.Module "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/modules/{name} [put] +// @Id updateModule +// @Summary Update module +// @Description Update the specified module +// @Tags module +// @Accept json +// @Produce json +// @Param name path string true "Module Name" +// @Param module body request.UpdateModuleRequest true "Updated module" +// @Success 200 {object} entity.Module "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/modules/{name} [put] func (h *Handler) UpdateModule() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Getting stuff from context. @@ -113,19 +113,19 @@ func (h *Handler) UpdateModule() http.HandlerFunc { } } -// @Id getModule -// @Summary Get module -// @Description Get module information by module name -// @Tags module -// @Produce json -// @Param name path string true "Module Name" -// @Success 200 {object} entity.Module "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/modules/{name} [get] +// @Id getModule +// @Summary Get module +// @Description Get module information by module name +// @Tags module +// @Produce json +// @Param name path string true "Module Name" +// @Success 200 {object} entity.Module "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/modules/{name} [get] func (h *Handler) GetModule() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Getting stuff from context. @@ -141,18 +141,18 @@ func (h *Handler) GetModule() http.HandlerFunc { } } -// @Id listModule -// @Summary List module -// @Description List module information -// @Tags module -// @Produce json -// @Success 200 {object} []entity.Module "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/modules [get] +// @Id listModule +// @Summary List module +// @Description List module information +// @Tags module +// @Produce json +// @Success 200 {object} []entity.Module "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/modules [get] func (h *Handler) ListModules() 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..83bdec3a5 --- /dev/null +++ b/pkg/server/handler/workspace/configs.go @@ -0,0 +1,104 @@ +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) + } +} diff --git a/pkg/server/handler/workspace/handler_test.go b/pkg/server/handler/workspace/handler_test.go index 34480b69b..417dc6041 100644 --- a/pkg/server/handler/workspace/handler_test.go +++ b/pkg/server/handler/workspace/handler_test.go @@ -299,8 +299,9 @@ 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 diff --git a/pkg/server/manager/workspace/configs.go b/pkg/server/manager/workspace/configs.go new file mode 100644 index 000000000..ee360512c --- /dev/null +++ b/pkg/server/manager/workspace/configs.go @@ -0,0 +1,175 @@ +package workspace + +import ( + "context" + "errors" + "fmt" + + "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" +) + +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) 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 93c598535..4795ec793 100644 --- a/pkg/server/route/route.go +++ b/pkg/server/route/route.go @@ -137,7 +137,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) @@ -238,6 +238,15 @@ 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.Post("/", workspaceHandler.CreateWorkspace()) r.Get("/", workspaceHandler.ListWorkspaces()) })