diff --git a/api/openapispec/docs.go b/api/openapispec/docs.go index d2c41075..8ddcea6b 100644 --- a/api/openapispec/docs.go +++ b/api/openapispec/docs.go @@ -278,6 +278,14 @@ const docTemplate = `{ ], "summary": "List module", "operationId": "listModule", + "parameters": [ + { + "type": "integer", + "description": "Workspace ID to filter module list by. Default to all workspaces.", + "name": "workspaceID", + "in": "query" + } + ], "responses": { "200": { "description": "Success", diff --git a/api/openapispec/swagger.json b/api/openapispec/swagger.json index 275f181f..42b61cab 100644 --- a/api/openapispec/swagger.json +++ b/api/openapispec/swagger.json @@ -267,6 +267,14 @@ ], "summary": "List module", "operationId": "listModule", + "parameters": [ + { + "type": "integer", + "description": "Workspace ID to filter module list by. Default to all workspaces.", + "name": "workspaceID", + "in": "query" + } + ], "responses": { "200": { "description": "Success", diff --git a/api/openapispec/swagger.yaml b/api/openapispec/swagger.yaml index 5d01c631..1536baab 100644 --- a/api/openapispec/swagger.yaml +++ b/api/openapispec/swagger.yaml @@ -1233,6 +1233,11 @@ paths: get: description: List module information operationId: listModule + parameters: + - description: Workspace ID to filter module list by. Default to all workspaces. + in: query + name: workspaceID + type: integer produces: - application/json responses: diff --git a/pkg/domain/entity/module.go b/pkg/domain/entity/module.go index edf2aa40..73f16c31 100644 --- a/pkg/domain/entity/module.go +++ b/pkg/domain/entity/module.go @@ -20,6 +20,23 @@ type Module struct { Doc *url.URL `yaml:"doc,omitempty" json:"doc,omitempty"` } +// ModuleWithVersion represents the specific configuration code module with version, +// which should be a specific instance of the Kusion Module provider. +type ModuleWithVersion struct { + // Name is the module name. + Name string `yaml:"name" json:"name"` + // URL is the module oci artifact registry URL. + URL *url.URL `yaml:"url" json:"url"` + // Version is the module oci artifact version. + Version string `yaml:"version" json:"version"` + // Description is a human-readable description of the module. + Description string `yaml:"description,omitempty" json:"description,omitempty"` + // Owners is a list of owners for the module. + Owners []string `yaml:"owners,omitempty" json:"owners,omitempty"` + // Doc is the documentation URL of the module. + Doc *url.URL `yaml:"doc,omitempty" json:"doc,omitempty"` +} + // Validate checks if the module is valid. // It returns an error if the module is not valid. func (m *Module) Validate() error { diff --git a/pkg/server/handler/module/handler.go b/pkg/server/handler/module/handler.go index fd45044b..68714d9b 100644 --- a/pkg/server/handler/module/handler.go +++ b/pkg/server/handler/module/handler.go @@ -3,6 +3,7 @@ package module import ( "context" "net/http" + "strconv" "github.com/go-chi/chi/v5" "github.com/go-chi/httplog/v2" @@ -89,7 +90,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 +126,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. @@ -146,12 +147,13 @@ func (h *Handler) GetModule() http.HandlerFunc { // @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" +// @Param workspaceID query uint false "Workspace ID to filter module list by. Default to all workspaces." +// @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) { @@ -160,6 +162,20 @@ func (h *Handler) ListModules() http.HandlerFunc { logger := logutil.GetLogger(ctx) logger.Info("Listing module...") + wsIDParam := r.URL.Query().Get("workspaceID") + if wsIDParam != "" { + wsID, err := strconv.Atoi(wsIDParam) + if err != nil { + render.Render(w, r, handler.FailureResponse(ctx, modulemanager.ErrInvalidWorkspaceID)) + return + } + + // List modules in the specified workspace. + moduleEntities, err := h.moduleManager.ListModulesByWorkspaceID(ctx, uint(wsID)) + handler.HandleResult(w, r, ctx, err, moduleEntities) + return + } + // List modules. moduleEntities, err := h.moduleManager.ListModules(ctx) handler.HandleResult(w, r, ctx, err, moduleEntities) diff --git a/pkg/server/handler/module/types.go b/pkg/server/handler/module/types.go index f6412266..53f85276 100644 --- a/pkg/server/handler/module/types.go +++ b/pkg/server/handler/module/types.go @@ -17,5 +17,6 @@ func NewHandler( } type ModuleRequestParams struct { - ModuleName string + ModuleName string + WorkspaceID uint } diff --git a/pkg/server/manager/module/module_manager.go b/pkg/server/manager/module/module_manager.go index eeb78d9c..76911f22 100644 --- a/pkg/server/manager/module/module_manager.go +++ b/pkg/server/manager/module/module_manager.go @@ -9,6 +9,7 @@ import ( "gorm.io/gorm" "kusionstack.io/kusion/pkg/domain/entity" "kusionstack.io/kusion/pkg/domain/request" + "kusionstack.io/kusion/pkg/server/manager/workspace" ) func (m *ModuleManager) CreateModule(ctx context.Context, requestPayload request.CreateModuleRequest) (*entity.Module, error) { @@ -116,3 +117,60 @@ func (m *ModuleManager) ListModules(ctx context.Context) ([]*entity.Module, erro return moduleEntities, nil } + +func (m *ModuleManager) ListModulesByWorkspaceID(ctx context.Context, workspaceID uint) ([]*entity.ModuleWithVersion, error) { + // Get workspace entity by ID. + existingEntity, err := m.workspaceRepo.Get(ctx, workspaceID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, workspace.ErrGettingNonExistingWorkspace + } + return nil, err + } + + // Get backend by backend ID. + backendEntity, err := m.backendRepo.Get(ctx, existingEntity.Backend.ID) + if err != nil && err == gorm.ErrRecordNotFound { + return nil, workspace.ErrBackendNotFound + } else if err != nil { + return nil, err + } + + // Generate backend from the backend entity. + remoteBackend, err := workspace.NewBackendFromEntity(*backendEntity) + if err != nil { + return nil, err + } + + // Get workspace storage from backend. + wsStorage, err := remoteBackend.WorkspaceStorage() + if err != nil { + return nil, err + } + + // Get workspace config from storage. + ws, err := wsStorage.Get(existingEntity.Name) + if err != nil { + return nil, err + } + + // Get the modules in the workspace. + moduleEntities := make([]*entity.ModuleWithVersion, 0, len(ws.Modules)) + for moduleName, moduleConfigs := range ws.Modules { + moduleEntity, err := m.moduleRepo.Get(ctx, moduleName) + if err != nil { + return nil, err + } + + moduleEntities = append(moduleEntities, &entity.ModuleWithVersion{ + Name: moduleEntity.Name, + URL: moduleEntity.URL, + Version: moduleConfigs.Version, + Description: moduleEntity.Description, + Owners: moduleEntity.Owners, + Doc: moduleEntity.Doc, + }) + } + + return moduleEntities, nil +} diff --git a/pkg/server/manager/module/types.go b/pkg/server/manager/module/types.go index 1301004f..d6e37936 100644 --- a/pkg/server/manager/module/types.go +++ b/pkg/server/manager/module/types.go @@ -10,14 +10,22 @@ var ( ErrGettingNonExistingModule = errors.New("the module does not exist") ErrUpdatingNonExistingModule = errors.New("the module to update does not exist") ErrEmptyModuleName = errors.New("the module name should not be empty") + ErrInvalidWorkspaceID = errors.New("the workspace id is invalid") ) type ModuleManager struct { - moduleRepo repository.ModuleRepository + moduleRepo repository.ModuleRepository + workspaceRepo repository.WorkspaceRepository + backendRepo repository.BackendRepository } -func NewModuleManager(moduleRepo repository.ModuleRepository) *ModuleManager { +func NewModuleManager(moduleRepo repository.ModuleRepository, + workspaceRepo repository.WorkspaceRepository, + backendRepo repository.BackendRepository, +) *ModuleManager { return &ModuleManager{ - moduleRepo: moduleRepo, + moduleRepo: moduleRepo, + workspaceRepo: workspaceRepo, + backendRepo: backendRepo, } } diff --git a/pkg/server/route/route.go b/pkg/server/route/route.go index a4a64af4..3c68b892 100644 --- a/pkg/server/route/route.go +++ b/pkg/server/route/route.go @@ -141,7 +141,7 @@ func setupRestAPIV1( workspaceManager := workspacemanager.NewWorkspaceManager(workspaceRepo, backendRepo, config.DefaultBackend) projectManager := projectmanager.NewProjectManager(projectRepo, organizationRepo, sourceRepo, config.DefaultSource) resourceManager := resourcemanager.NewResourceManager(resourceRepo) - moduleManager := modulemanager.NewModuleManager(moduleRepo) + moduleManager := modulemanager.NewModuleManager(moduleRepo, workspaceRepo, backendRepo) // Set up the handlers for the resources. sourceHandler, err := source.NewHandler(sourceManager)