From 13258c6e5ddff0d7fc7c8ec5dc54b03ae83df8fb Mon Sep 17 00:00:00 2001 From: ffforest Date: Wed, 17 Apr 2024 18:26:37 +0800 Subject: [PATCH] refactor: offload heavy logic from handler to manager --- pkg/engine/api/source/source.go | 2 +- pkg/server/handler/backend/handler.go | 129 +--- pkg/server/handler/backend/types.go | 20 +- pkg/server/handler/organization/handler.go | 130 +--- pkg/server/handler/organization/types.go | 20 +- pkg/server/handler/source/handler.go | 143 +---- pkg/server/handler/source/handler_test.go | 7 +- pkg/server/handler/source/types.go | 20 +- pkg/server/handler/stack/execute.go | 588 ++---------------- pkg/server/handler/stack/handler.go | 169 +---- pkg/server/handler/stack/types.go | 42 +- pkg/server/handler/workspace/handler.go | 138 +--- pkg/server/handler/workspace/types.go | 28 +- pkg/server/manager/backend/backend_manager.go | 86 +++ pkg/server/manager/backend/types.go | 23 + .../organization/organization_manager.go | 91 +++ pkg/server/manager/organization/types.go | 23 + pkg/server/manager/source/source_manager.go | 101 +++ pkg/server/manager/source/types.go | 23 + pkg/server/manager/stack/stack_manager.go | 335 ++++++++++ pkg/server/manager/stack/types.go | 30 + pkg/server/{handler => manager}/stack/util.go | 167 +++-- pkg/server/manager/workspace/types.go | 26 + .../manager/workspace/workspace_manager.go | 95 +++ pkg/server/route/route.go | 43 +- 25 files changed, 1224 insertions(+), 1255 deletions(-) create mode 100644 pkg/server/manager/backend/backend_manager.go create mode 100644 pkg/server/manager/backend/types.go create mode 100644 pkg/server/manager/organization/organization_manager.go create mode 100644 pkg/server/manager/organization/types.go create mode 100644 pkg/server/manager/source/source_manager.go create mode 100644 pkg/server/manager/source/types.go create mode 100644 pkg/server/manager/stack/stack_manager.go create mode 100644 pkg/server/manager/stack/types.go rename pkg/server/{handler => manager}/stack/util.go (51%) create mode 100644 pkg/server/manager/workspace/types.go create mode 100644 pkg/server/manager/workspace/workspace_manager.go diff --git a/pkg/engine/api/source/source.go b/pkg/engine/api/source/source.go index 44abb9f8..2f0be4d0 100644 --- a/pkg/engine/api/source/source.go +++ b/pkg/engine/api/source/source.go @@ -31,7 +31,7 @@ func Pull(ctx context.Context, source *entity.Source) (string, error) { return directory, nil } -// Cleanup() is a method that cleans up tje temporary source code from the source provider. +// Cleanup() is a method that cleans up the temporary source code from the source provider. func Cleanup(ctx context.Context, localDirectory string) { logger := util.GetLogger(ctx) logger.Info("Cleaning up temp directory...") diff --git a/pkg/server/handler/backend/handler.go b/pkg/server/handler/backend/handler.go index 2e310c8c..9c23e934 100644 --- a/pkg/server/handler/backend/handler.go +++ b/pkg/server/handler/backend/handler.go @@ -1,18 +1,16 @@ package backend import ( - "errors" + "context" "net/http" "strconv" - "time" "github.com/go-chi/chi/v5" "github.com/go-chi/render" - "github.com/jinzhu/copier" - "gorm.io/gorm" - "kusionstack.io/kusion/pkg/domain/entity" + "github.com/go-logr/logr" "kusionstack.io/kusion/pkg/domain/request" "kusionstack.io/kusion/pkg/server/handler" + backendmanager "kusionstack.io/kusion/pkg/server/manager/backend" "kusionstack.io/kusion/pkg/server/util" ) @@ -42,22 +40,7 @@ func (h *Handler) CreateBackend() http.HandlerFunc { return } - // Convert request payload to domain model - var createdEntity entity.Backend - if err := copier.Copy(&createdEntity, &requestPayload); err != nil { - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - // The default state is UnSynced - createdEntity.CreationTimestamp = time.Now() - createdEntity.UpdateTimestamp = time.Now() - - // Create backend with repository - err := h.backendRepo.Create(ctx, &createdEntity) - if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } + createdEntity, err := h.backendManager.CreateBackend(ctx, requestPayload) handler.HandleResult(w, r, ctx, err, createdEntity) } } @@ -77,22 +60,14 @@ func (h *Handler) CreateBackend() http.HandlerFunc { func (h *Handler) DeleteBackend() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Getting stuff from context - ctx := r.Context() - logger := util.GetLogger(ctx) - logger.Info("Deleting source...") - backendID := chi.URLParam(r, "backendID") - - // Delete backend with repository - id, err := strconv.Atoi(backendID) - if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, ErrInvalidBackendID)) - return - } - err = h.backendRepo.Delete(ctx, uint(id)) + ctx, logger, params, err := requestHelper(r) if err != nil { render.Render(w, r, handler.FailureResponse(ctx, err)) return } + logger.Info("Deleting backend...", "backendID", params.BackendID) + + err = h.backendManager.DeleteBackendByID(ctx, params.BackendID) handler.HandleResult(w, r, ctx, err, "Deletion Success") } } @@ -112,17 +87,12 @@ func (h *Handler) DeleteBackend() http.HandlerFunc { func (h *Handler) UpdateBackend() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Getting stuff from context - ctx := r.Context() - logger := util.GetLogger(ctx) - logger.Info("Updating backend...") - backendID := chi.URLParam(r, "backendID") - - // convert backend ID to int - id, err := strconv.Atoi(backendID) + ctx, logger, params, err := requestHelper(r) if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, ErrInvalidBackendID)) + render.Render(w, r, handler.FailureResponse(ctx, err)) return } + logger.Info("Updating backend..., backendID", params.BackendID) // Decode the request body into the payload. var requestPayload request.UpdateBackendRequest @@ -131,35 +101,7 @@ func (h *Handler) UpdateBackend() http.HandlerFunc { return } - // Convert request payload to domain model - var requestEntity entity.Backend - if err := copier.Copy(&requestEntity, &requestPayload); err != nil { - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - - // Get the existing backend by id - updatedEntity, err := h.backendRepo.Get(ctx, uint(id)) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - render.Render(w, r, handler.FailureResponse(ctx, ErrUpdatingNonExistingBackend)) - return - } - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - - // Overwrite non-zero values in request entity to existing entity - copier.CopyWithOption(updatedEntity, requestEntity, copier.Option{IgnoreEmpty: true}) - - // Update backend with repository - err = h.backendRepo.Update(ctx, updatedEntity) - if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - - // Return updated backend + updatedEntity, err := h.backendManager.UpdateBackendByID(ctx, params.BackendID, requestPayload) handler.HandleResult(w, r, ctx, err, updatedEntity) } } @@ -178,28 +120,14 @@ func (h *Handler) UpdateBackend() http.HandlerFunc { func (h *Handler) GetBackend() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Getting stuff from context - ctx := r.Context() - logger := util.GetLogger(ctx) - logger.Info("Getting backend...") - backendID := chi.URLParam(r, "backendID") - - // Get backend with repository - id, err := strconv.Atoi(backendID) + ctx, logger, params, err := requestHelper(r) if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, ErrInvalidBackendID)) - return - } - existingEntity, err := h.backendRepo.Get(ctx, uint(id)) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - render.Render(w, r, handler.FailureResponse(ctx, ErrGettingNonExistingBackend)) - return - } render.Render(w, r, handler.FailureResponse(ctx, err)) return } + logger.Info("Getting backend...", "backendID", params.BackendID) - // Return found backend + existingEntity, err := h.backendManager.GetBackendByID(ctx, params.BackendID) handler.HandleResult(w, r, ctx, err, existingEntity) } } @@ -221,17 +149,22 @@ func (h *Handler) ListBackends() http.HandlerFunc { logger := util.GetLogger(ctx) logger.Info("Listing backend...") - backendEntities, err := h.backendRepo.List(ctx) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - render.Render(w, r, handler.FailureResponse(ctx, ErrGettingNonExistingBackend)) - return - } - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - - // Return found backends + backendEntities, err := h.backendManager.ListBackends(ctx) handler.HandleResult(w, r, ctx, err, backendEntities) } } + +func requestHelper(r *http.Request) (context.Context, *logr.Logger, *BackendRequestParams, error) { + ctx := r.Context() + backendID := chi.URLParam(r, "backendID") + // Get stack with repository + id, err := strconv.Atoi(backendID) + if err != nil { + return nil, nil, nil, backendmanager.ErrInvalidBackendID + } + logger := util.GetLogger(ctx) + params := BackendRequestParams{ + BackendID: uint(id), + } + return ctx, &logger, ¶ms, nil +} diff --git a/pkg/server/handler/backend/types.go b/pkg/server/handler/backend/types.go index ab360477..effccdba 100644 --- a/pkg/server/handler/backend/types.go +++ b/pkg/server/handler/backend/types.go @@ -1,25 +1,21 @@ package backend import ( - "errors" - - "kusionstack.io/kusion/pkg/domain/repository" -) - -var ( - ErrGettingNonExistingBackend = errors.New("the backend does not exist") - ErrUpdatingNonExistingBackend = errors.New("the backend to update does not exist") - ErrInvalidBackendID = errors.New("the backend ID should be a uuid") + backendmanager "kusionstack.io/kusion/pkg/server/manager/backend" ) func NewHandler( - backendRepo repository.BackendRepository, + backendManager *backendmanager.BackendManager, ) (*Handler, error) { return &Handler{ - backendRepo: backendRepo, + backendManager: backendManager, }, nil } type Handler struct { - backendRepo repository.BackendRepository + backendManager *backendmanager.BackendManager +} + +type BackendRequestParams struct { + BackendID uint } diff --git a/pkg/server/handler/organization/handler.go b/pkg/server/handler/organization/handler.go index 25e5190b..d787c4bc 100644 --- a/pkg/server/handler/organization/handler.go +++ b/pkg/server/handler/organization/handler.go @@ -1,18 +1,16 @@ package organization import ( - "errors" + "context" "net/http" "strconv" - "time" "github.com/go-chi/chi/v5" "github.com/go-chi/render" - "github.com/jinzhu/copier" - "gorm.io/gorm" - "kusionstack.io/kusion/pkg/domain/entity" + "github.com/go-logr/logr" "kusionstack.io/kusion/pkg/domain/request" "kusionstack.io/kusion/pkg/server/handler" + organizationmanager "kusionstack.io/kusion/pkg/server/manager/organization" "kusionstack.io/kusion/pkg/server/util" ) @@ -42,22 +40,7 @@ func (h *Handler) CreateOrganization() http.HandlerFunc { return } - // Convert request payload to domain model - var createdEntity entity.Organization - if err := copier.Copy(&createdEntity, &requestPayload); err != nil { - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - // The default state is UnSynced - createdEntity.CreationTimestamp = time.Now() - createdEntity.UpdateTimestamp = time.Now() - - // Create organization with repository - err := h.organizationRepo.Create(ctx, &createdEntity) - if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } + createdEntity, err := h.organizationManager.CreateOrganization(ctx, requestPayload) handler.HandleResult(w, r, ctx, err, createdEntity) } } @@ -72,27 +55,18 @@ func (h *Handler) CreateOrganization() http.HandlerFunc { // @Failure 429 {object} errors.DetailError "Too Many Requests" // @Failure 404 {object} errors.DetailError "Not Found" // @Failure 500 {object} errors.DetailError "Internal Server Error" -// @Router /api/v1/organization/{organizationName} [delete] // @Router /api/v1/organization/{organizationID} [delete] func (h *Handler) DeleteOrganization() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Getting stuff from context - ctx := r.Context() - logger := util.GetLogger(ctx) - logger.Info("Deleting source...") - organizationID := chi.URLParam(r, "organizationID") - - // Delete organization with repository - id, err := strconv.Atoi(organizationID) - if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, ErrInvalidOrganizationID)) - return - } - err = h.organizationRepo.Delete(ctx, uint(id)) + ctx, logger, params, err := requestHelper(r) if err != nil { render.Render(w, r, handler.FailureResponse(ctx, err)) return } + logger.Info("Deleting source...") + + err = h.organizationManager.DeleteOrganizationByID(ctx, params.OrganizationID) handler.HandleResult(w, r, ctx, err, "Deletion Success") } } @@ -112,17 +86,12 @@ func (h *Handler) DeleteOrganization() http.HandlerFunc { func (h *Handler) UpdateOrganization() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Getting stuff from context - ctx := r.Context() - logger := util.GetLogger(ctx) - logger.Info("Updating organization...") - organizationID := chi.URLParam(r, "organizationID") - - // convert organization ID to int - id, err := strconv.Atoi(organizationID) + ctx, logger, params, err := requestHelper(r) if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, ErrInvalidOrganizationID)) + render.Render(w, r, handler.FailureResponse(ctx, err)) return } + logger.Info("Updating organization...") // Decode the request body into the payload. var requestPayload request.UpdateOrganizationRequest @@ -131,35 +100,7 @@ func (h *Handler) UpdateOrganization() http.HandlerFunc { return } - // Convert request payload to domain model - var requestEntity entity.Organization - if err := copier.Copy(&requestEntity, &requestPayload); err != nil { - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - - // Get the existing organization by id - updatedEntity, err := h.organizationRepo.Get(ctx, uint(id)) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - render.Render(w, r, handler.FailureResponse(ctx, ErrUpdatingNonExistingOrganization)) - return - } - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - - // Overwrite non-zero values in request entity to existing entity - copier.CopyWithOption(updatedEntity, requestEntity, copier.Option{IgnoreEmpty: true}) - - // Update organization with repository - err = h.organizationRepo.Update(ctx, updatedEntity) - if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - - // Return updated organization + updatedEntity, err := h.organizationManager.UpdateOrganizationByID(ctx, params.OrganizationID, requestPayload) handler.HandleResult(w, r, ctx, err, updatedEntity) } } @@ -178,28 +119,14 @@ func (h *Handler) UpdateOrganization() http.HandlerFunc { func (h *Handler) GetOrganization() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Getting stuff from context - ctx := r.Context() - logger := util.GetLogger(ctx) - logger.Info("Getting organization...") - organizationID := chi.URLParam(r, "organizationID") - - // Get organization with repository - id, err := strconv.Atoi(organizationID) + ctx, logger, params, err := requestHelper(r) if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, ErrInvalidOrganizationID)) - return - } - existingEntity, err := h.organizationRepo.Get(ctx, uint(id)) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - render.Render(w, r, handler.FailureResponse(ctx, ErrGettingNonExistingOrganization)) - return - } render.Render(w, r, handler.FailureResponse(ctx, err)) return } + logger.Info("Getting organization...") - // Return found organization + existingEntity, err := h.organizationManager.GetOrganizationByID(ctx, params.OrganizationID) handler.HandleResult(w, r, ctx, err, existingEntity) } } @@ -221,17 +148,22 @@ func (h *Handler) ListOrganizations() http.HandlerFunc { logger := util.GetLogger(ctx) logger.Info("Listing organization...") - organizationEntities, err := h.organizationRepo.List(ctx) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - render.Render(w, r, handler.FailureResponse(ctx, ErrGettingNonExistingOrganization)) - return - } - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - - // Return found organizations + organizationEntities, err := h.organizationManager.ListOrganizations(ctx) handler.HandleResult(w, r, ctx, err, organizationEntities) } } + +func requestHelper(r *http.Request) (context.Context, *logr.Logger, *OrganizationRequestParams, error) { + ctx := r.Context() + sourceID := chi.URLParam(r, "sourceID") + // Get stack with repository + id, err := strconv.Atoi(sourceID) + if err != nil { + return nil, nil, nil, organizationmanager.ErrInvalidOrganizationID + } + logger := util.GetLogger(ctx) + params := OrganizationRequestParams{ + OrganizationID: uint(id), + } + return ctx, &logger, ¶ms, nil +} diff --git a/pkg/server/handler/organization/types.go b/pkg/server/handler/organization/types.go index 3ee4e557..bdc1c8f0 100644 --- a/pkg/server/handler/organization/types.go +++ b/pkg/server/handler/organization/types.go @@ -1,25 +1,21 @@ package organization import ( - "errors" - - "kusionstack.io/kusion/pkg/domain/repository" -) - -var ( - ErrGettingNonExistingOrganization = errors.New("the organization does not exist") - ErrUpdatingNonExistingOrganization = errors.New("the organization to update does not exist") - ErrInvalidOrganizationID = errors.New("the organization ID should be a uuid") + organizationmanager "kusionstack.io/kusion/pkg/server/manager/organization" ) func NewHandler( - organizationRepo repository.OrganizationRepository, + organizationManager *organizationmanager.OrganizationManager, ) (*Handler, error) { return &Handler{ - organizationRepo: organizationRepo, + organizationManager: organizationManager, }, nil } type Handler struct { - organizationRepo repository.OrganizationRepository + organizationManager *organizationmanager.OrganizationManager +} + +type OrganizationRequestParams struct { + OrganizationID uint } diff --git a/pkg/server/handler/source/handler.go b/pkg/server/handler/source/handler.go index 184c47a1..e223a986 100644 --- a/pkg/server/handler/source/handler.go +++ b/pkg/server/handler/source/handler.go @@ -1,18 +1,16 @@ package source import ( + "context" "net/http" - "net/url" "strconv" "github.com/go-chi/chi/v5" "github.com/go-chi/render" - "github.com/jinzhu/copier" - "github.com/pkg/errors" - "gorm.io/gorm" - "kusionstack.io/kusion/pkg/domain/entity" + "github.com/go-logr/logr" "kusionstack.io/kusion/pkg/domain/request" "kusionstack.io/kusion/pkg/server/handler" + sourcemanager "kusionstack.io/kusion/pkg/server/manager/source" "kusionstack.io/kusion/pkg/server/util" ) @@ -42,29 +40,8 @@ func (h *Handler) CreateSource() http.HandlerFunc { return } - // Convert request payload to domain model - var createdEntity entity.Source - if err := copier.Copy(&createdEntity, &requestPayload); err != nil { - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - - // Convert Remote string to URL - remote, err := url.Parse(requestPayload.Remote) - if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - createdEntity.Remote = remote - - // Create source with repository - err = h.sourceRepo.Create(ctx, &createdEntity) - if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - // Return created entity + createdEntity, err := h.sourceManager.CreateSource(ctx, requestPayload) handler.HandleResult(w, r, ctx, err, createdEntity) } } @@ -83,22 +60,14 @@ func (h *Handler) CreateSource() http.HandlerFunc { func (h *Handler) DeleteSource() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Getting stuff from context - ctx := r.Context() - logger := util.GetLogger(ctx) - logger.Info("Deleting source...") - sourceID := chi.URLParam(r, "sourceID") - - // Delete source with repository - id, err := strconv.Atoi(sourceID) - if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, ErrInvalidSourceID)) - return - } - err = h.sourceRepo.Delete(ctx, uint(id)) + ctx, logger, params, err := requestHelper(r) if err != nil { render.Render(w, r, handler.FailureResponse(ctx, err)) return } + logger.Info("Deleting source...") + + err = h.sourceManager.DeleteSourceByID(ctx, params.SourceID) handler.HandleResult(w, r, ctx, err, "Deletion Success") } } @@ -118,17 +87,12 @@ func (h *Handler) DeleteSource() http.HandlerFunc { func (h *Handler) UpdateSource() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Getting stuff from context - ctx := r.Context() - logger := util.GetLogger(ctx) - logger.Info("Updating source...") - sourceID := chi.URLParam(r, "sourceID") - - // Convert sourceID to int - id, err := strconv.Atoi(sourceID) + ctx, logger, params, err := requestHelper(r) if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, ErrInvalidSourceID)) + render.Render(w, r, handler.FailureResponse(ctx, err)) return } + logger.Info("Updating source...") // Decode the request body into the payload. var requestPayload request.UpdateSourceRequest @@ -137,43 +101,8 @@ func (h *Handler) UpdateSource() http.HandlerFunc { return } - // Convert request payload to domain model - var requestEntity entity.Source - if err := copier.Copy(&requestEntity, &requestPayload); err != nil { - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - - // Convert Remote string to URL - remote, err := url.Parse(requestPayload.Remote) - if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - requestEntity.Remote = remote - - // Get the existing source by id - updatedEntity, err := h.sourceRepo.Get(ctx, uint(id)) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - render.Render(w, r, handler.FailureResponse(ctx, ErrUpdatingNonExistingSource)) - return - } - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - - // Overwrite non-zero values in request entity to existing entity - copier.CopyWithOption(updatedEntity, requestEntity, copier.Option{IgnoreEmpty: true}) - - // Update source with repository - err = h.sourceRepo.Update(ctx, updatedEntity) - if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - // Return updated source + updatedEntity, err := h.sourceManager.UpdateSourceByID(ctx, params.SourceID, requestPayload) handler.HandleResult(w, r, ctx, err, updatedEntity) } } @@ -192,28 +121,14 @@ func (h *Handler) UpdateSource() http.HandlerFunc { func (h *Handler) GetSource() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Getting stuff from context - ctx := r.Context() - logger := util.GetLogger(ctx) - logger.Info("Getting source...") - sourceID := chi.URLParam(r, "sourceID") - - // Get source with repository - id, err := strconv.Atoi(sourceID) + ctx, logger, params, err := requestHelper(r) if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, ErrInvalidSourceID)) - return - } - existingEntity, err := h.sourceRepo.Get(ctx, uint(id)) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - render.Render(w, r, handler.FailureResponse(ctx, ErrGettingNonExistingSource)) - return - } render.Render(w, r, handler.FailureResponse(ctx, err)) return } + logger.Info("Getting source...") - // Return found source + existingEntity, err := h.sourceManager.GetSourceByID(ctx, params.SourceID) handler.HandleResult(w, r, ctx, err, existingEntity) } } @@ -235,17 +150,23 @@ func (h *Handler) ListSources() http.HandlerFunc { logger := util.GetLogger(ctx) logger.Info("Listing source...") - existingEntity, err := h.sourceRepo.List(ctx) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - render.Render(w, r, handler.FailureResponse(ctx, ErrGettingNonExistingSource)) - return - } - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } + // List sources + sourceEntities, err := h.sourceManager.ListSources(ctx) + handler.HandleResult(w, r, ctx, err, sourceEntities) + } +} - // Return found source - handler.HandleResult(w, r, ctx, err, existingEntity) +func requestHelper(r *http.Request) (context.Context, *logr.Logger, *SourceRequestParams, error) { + ctx := r.Context() + sourceID := chi.URLParam(r, "sourceID") + // Get stack with repository + id, err := strconv.Atoi(sourceID) + if err != nil { + return nil, nil, nil, sourcemanager.ErrInvalidSourceID + } + logger := util.GetLogger(ctx) + params := SourceRequestParams{ + SourceID: uint(id), } + return ctx, &logger, ¶ms, nil } diff --git a/pkg/server/handler/source/handler_test.go b/pkg/server/handler/source/handler_test.go index 3ad3d886..23376a60 100644 --- a/pkg/server/handler/source/handler_test.go +++ b/pkg/server/handler/source/handler_test.go @@ -18,6 +18,7 @@ import ( "kusionstack.io/kusion/pkg/domain/request" "kusionstack.io/kusion/pkg/infra/persistence" "kusionstack.io/kusion/pkg/server/handler" + sourcemanager "kusionstack.io/kusion/pkg/server/manager/source" ) func TestSourceHandler(t *testing.T) { @@ -242,7 +243,7 @@ func TestSourceHandler(t *testing.T) { assert.Equal(t, http.StatusOK, recorder.Code) assert.Equal(t, resp.Success, false) - assert.Equal(t, resp.Message, gorm.ErrRecordNotFound.Error()) + assert.Equal(t, resp.Message, sourcemanager.ErrGettingNonExistingSource.Error()) }) t.Run("Update Nonexisting Source", func(t *testing.T) { @@ -287,7 +288,7 @@ func TestSourceHandler(t *testing.T) { // Assertion assert.Equal(t, http.StatusOK, recorder.Code) assert.Equal(t, resp.Success, false) - assert.Equal(t, resp.Message, ErrUpdatingNonExistingSource.Error()) + assert.Equal(t, resp.Message, sourcemanager.ErrUpdatingNonExistingSource.Error()) }) } @@ -296,7 +297,7 @@ func setupTest(t *testing.T) (sqlmock.Sqlmock, *gorm.DB, *httptest.ResponseRecor require.NoError(t, err) repo := persistence.NewSourceRepository(fakeGDB) sourceHandler := &Handler{ - sourceRepo: repo, + sourceManager: sourcemanager.NewSourceManager(repo), } recorder := httptest.NewRecorder() return sqlMock, fakeGDB, recorder, sourceHandler diff --git a/pkg/server/handler/source/types.go b/pkg/server/handler/source/types.go index 784532ba..1f58b1b5 100644 --- a/pkg/server/handler/source/types.go +++ b/pkg/server/handler/source/types.go @@ -1,25 +1,21 @@ package source import ( - "errors" - - "kusionstack.io/kusion/pkg/domain/repository" -) - -var ( - ErrGettingNonExistingSource = errors.New("the source does not exist") - ErrUpdatingNonExistingSource = errors.New("the source to update does not exist") - ErrInvalidSourceID = errors.New("the source ID should be a uuid") + sourcemanager "kusionstack.io/kusion/pkg/server/manager/source" ) func NewHandler( - sourceRepo repository.SourceRepository, + sourceManager *sourcemanager.SourceManager, ) (*Handler, error) { return &Handler{ - sourceRepo: sourceRepo, + sourceManager: sourceManager, }, nil } type Handler struct { - sourceRepo repository.SourceRepository + sourceManager *sourcemanager.SourceManager +} + +type SourceRequestParams struct { + SourceID uint } diff --git a/pkg/server/handler/stack/execute.go b/pkg/server/handler/stack/execute.go index d86a4a86..304ca44e 100644 --- a/pkg/server/handler/stack/execute.go +++ b/pkg/server/handler/stack/execute.go @@ -1,24 +1,16 @@ package stack import ( - "encoding/json" - "errors" - "fmt" + "context" "net/http" - "os" "strconv" - "time" "github.com/go-chi/chi/v5" "github.com/go-chi/render" + "github.com/go-logr/logr" yamlv2 "gopkg.in/yaml.v2" - "gorm.io/gorm" - apiv1 "kusionstack.io/kusion/pkg/apis/api.kusion.io/v1" - "kusionstack.io/kusion/pkg/backend" - "kusionstack.io/kusion/pkg/domain/constant" - engineapi "kusionstack.io/kusion/pkg/engine/api" - sourceapi "kusionstack.io/kusion/pkg/engine/api/source" "kusionstack.io/kusion/pkg/server/handler" + stackmanager "kusionstack.io/kusion/pkg/server/manager/stack" "kusionstack.io/kusion/pkg/server/util" ) @@ -36,148 +28,31 @@ import ( func (h *Handler) PreviewStack() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Getting stuff from context - ctx := r.Context() - logger := util.GetLogger(ctx) - logger.Info("Previewing stack...") - // Get params from URL parameter - stackID := chi.URLParam(r, "stackID") - - // Get params from query parameter - formatParam := r.URL.Query().Get("output") - // TODO: Define default behaviors - detailParam, _ := strconv.ParseBool(r.URL.Query().Get("detail")) - // kpmParam, _ := strconv.ParseBool(r.URL.Query().Get("kpm")) - // TODO: Should match automatically eventually - workspaceParam := r.URL.Query().Get("workspace") - - // Get stack with repository - id, err := strconv.Atoi(stackID) - if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, ErrInvalidStacktID)) - return - } - stackEntity, err := h.stackRepo.Get(ctx, uint(id)) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - render.Render(w, r, handler.FailureResponse(ctx, ErrGettingNonExistingStack)) - return - } - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - - // Get project by id - project, err := stackEntity.Project.ConvertToCore() - if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - - // Get stack by id - stack, err := stackEntity.ConvertToCore() - if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - - // get workspace configurations - bk, err := backend.NewBackend("") - if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - wsStorage, err := bk.WorkspaceStorage() - if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - ws, err := wsStorage.Get(workspaceParam) + ctx, logger, params, err := requestHelper(r) if err != nil { render.Render(w, r, handler.FailureResponse(ctx, err)) return } + logger.Info("Previewing stack...", "stackID", params.StackID) - // Build API inputs - // get project to get source and workdir - projectEntity, err := handler.GetProjectByID(ctx, h.projectRepo, stackEntity.Project.ID) + // Call preview stack + changes, err := h.stackManager.PreviewStack(ctx, params.StackID, params.Workspace) if err != nil { render.Render(w, r, handler.FailureResponse(ctx, err)) return } - directory, workDir, err := getWorkDirFromSource(ctx, stackEntity, projectEntity) + previewChanges, err := stackmanager.ProcessChanges(ctx, w, changes, params.Format, params.Detail) if err != nil { render.Render(w, r, handler.FailureResponse(ctx, err)) return } - previewOptions := buildOptions(false) - stack.Path = workDir - - // Cleanup - defer sourceapi.Cleanup(ctx, directory) - - // Generate spec - sp, err := engineapi.GenerateSpecWithSpinner(project, stack, ws, true) - if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - - // return immediately if no resource found in stack - // todo: if there is no resource, should still do diff job; for now, if output is json format, there is no hint - if sp == nil || len(sp.Resources) == 0 { - if formatParam != engineapi.JSONOutput { - logger.Info("No resource change found in this stack...") - render.Render(w, r, handler.SuccessResponse(ctx, "No resource change found in this stack.")) - return - } - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - - // Compute state storage - // TODO: this local storage is temporary, will support remote later - stateStorage := bk.StateStorage(project.Name, stack.Name, ws.Name) - logger.Info("Local state storage found", "Path", stateStorage) - - // Compute changes for preview - changes, err := engineapi.Preview(previewOptions, stateStorage, sp, project, stack) - if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - - // If output format is json, return details without any summary or formatting - if formatParam == engineapi.JSONOutput { - var previewChanges []byte - previewChanges, err = json.Marshal(changes) - if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - logger.Info(string(previewChanges)) - render.Render(w, r, handler.SuccessResponse(ctx, string(previewChanges))) - return - } - - if changes.AllUnChange() { - logger.Info("All resources are reconciled. No diff found") - render.Render(w, r, handler.SuccessResponse(ctx, "All resources are reconciled. No diff found")) - return - } - - // Summary preview table - changes.Summary(w, true) - - // Detail detection - if detailParam { - render.Render(w, r, handler.SuccessResponse(ctx, changes.Diffs(true))) - } + render.Render(w, r, handler.SuccessResponse(ctx, previewChanges)) } } -// @Summary Build stack -// @Description Build stack information by stack ID +// @Summary Generate stack +// @Description Generate stack information by stack ID // @Produce json // @Param id path int true "Stack ID" // @Success 200 {object} entity.Stack "Success" @@ -186,100 +61,25 @@ func (h *Handler) PreviewStack() http.HandlerFunc { // @Failure 429 {object} errors.DetailError "Too Many Requests" // @Failure 404 {object} errors.DetailError "Not Found" // @Failure 500 {object} errors.DetailError "Internal Server Error" -// @Router /api/v1/stack/{stackID}/build [post] -func (h *Handler) BuildStack() http.HandlerFunc { +// @Router /api/v1/stack/{stackID}/generate [post] +func (h *Handler) GenerateStack() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Getting stuff from context - ctx := r.Context() - logger := util.GetLogger(ctx) - logger.Info("Building stack...") - // Get params from URL parameter - stackID := chi.URLParam(r, "stackID") - // TODO: Define default behaviors - // kpmParam, _ := strconv.ParseBool(r.URL.Query().Get("kpm")) - // TODO: Should match automatically eventually - workspaceParam := r.URL.Query().Get("workspace") - - // Get stack with repository - id, err := strconv.Atoi(stackID) - if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, ErrInvalidStacktID)) - return - } - stackEntity, err := h.stackRepo.Get(ctx, uint(id)) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - render.Render(w, r, handler.FailureResponse(ctx, ErrGettingNonExistingStack)) - return - } - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - - // Get project by id - project, err := stackEntity.Project.ConvertToCore() - if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - - // Get stack by id - stack, err := stackEntity.ConvertToCore() - if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - - // get workspace configurations - bk, err := backend.NewBackend("") - if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - wsStorage, err := bk.WorkspaceStorage() - if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - ws, err := wsStorage.Get(workspaceParam) - if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - - // Build API inputs - // get project to get source and workdir - projectEntity, err := handler.GetProjectByID(ctx, h.projectRepo, stackEntity.Project.ID) + ctx, logger, params, err := requestHelper(r) if err != nil { render.Render(w, r, handler.FailureResponse(ctx, err)) return } + logger.Info("Generating stack...", "stackID", params.StackID) - directory, workDir, err := getWorkDirFromSource(ctx, stackEntity, projectEntity) - logger.Info("workDir derived", "workDir", workDir) - logger.Info("directory derived", "directory", directory) - - stack.Path = workDir - if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - // intentOptions, _ := buildOptions(workDir, kpmParam, false) - // Cleanup - defer sourceapi.Cleanup(ctx, directory) - - // Generate spec - sp, err := engineapi.GenerateSpecWithSpinner(project, stack, ws, true) + // Call generate stack + sp, err := h.stackManager.GenerateStack(ctx, params.StackID, params.Workspace) if err != nil { render.Render(w, r, handler.FailureResponse(ctx, err)) return } yaml, err := yamlv2.Marshal(sp) - if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } handler.HandleResult(w, r, ctx, err, string(yaml)) } } @@ -298,205 +98,25 @@ func (h *Handler) BuildStack() http.HandlerFunc { func (h *Handler) ApplyStack() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Getting stuff from context - ctx := r.Context() - logger := util.GetLogger(ctx) - logger.Info("Applying stack...") - // Get params from URL parameter - stackID := chi.URLParam(r, "stackID") - - // Get params from query parameter - formatParam := r.URL.Query().Get("output") - dryRunParam, _ := strconv.ParseBool(r.URL.Query().Get("dryrun")) - // TODO: Define default behaviors - detailParam, _ := strconv.ParseBool(r.URL.Query().Get("detail")) - // kpmParam, _ := strconv.ParseBool(r.URL.Query().Get("kpm")) - // TODO: Should match automatically eventually - workspaceParam := r.URL.Query().Get("workspace") - - // Get stack with repository - id, err := strconv.Atoi(stackID) - if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, ErrInvalidStacktID)) - return - } - stackEntity, err := h.stackRepo.Get(ctx, uint(id)) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - render.Render(w, r, handler.FailureResponse(ctx, ErrGettingNonExistingStack)) - return - } - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - - // Get project by id - project, err := stackEntity.Project.ConvertToCore() - if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - - // Get stack by id - stack, err := stackEntity.ConvertToCore() - if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - - // get workspace configurations - localBackend, err := backend.NewBackend("") - if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - // wsStorage, err := bk.WorkspaceStorage() - // if err != nil { - // render.Render(w, r, handler.FailureResponse(ctx, err)) - // return - // } - // ws, err := wsStorage.Get(workspaceParam) - // if err != nil { - // render.Render(w, r, handler.FailureResponse(ctx, err)) - // return - // } - - // // Get backend by id - // workspaceEntity, err := h.workspaceRepo.GetByName(ctx, workspaceParam) - // if err != nil && err == gorm.ErrRecordNotFound { - // render.Render(w, r, handler.FailureResponse(ctx, ErrWorkspaceNotFound)) - // return - // } else if err != nil { - // render.Render(w, r, handler.FailureResponse(ctx, err)) - // return - // } - // // Generate backend from entity - // remoteBackend, err := NewBackendFromEntity(*workspaceEntity.Backend) - // if err != nil { - // render.Render(w, r, handler.FailureResponse(ctx, err)) - // return - // } - - remoteBackend, err := h.GetBackendFromWorkspaceName(ctx, workspaceParam) - if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - - // Get workspace configurations from backend - // TODO: temporarily local for now, should be replaced by variable sets - wsStorage, err := localBackend.WorkspaceStorage() - if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - ws, err := wsStorage.Get(workspaceParam) - if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - - // Build API inputs - // get project to get source and workdir - projectEntity, err := handler.GetProjectByID(ctx, h.projectRepo, stackEntity.Project.ID) + ctx, logger, params, err := requestHelper(r) if err != nil { render.Render(w, r, handler.FailureResponse(ctx, err)) return } + logger.Info("Applying stack...", "stackID", params.StackID) - directory, workDir, err := getWorkDirFromSource(ctx, stackEntity, projectEntity) + err = h.stackManager.ApplyStack(ctx, params.StackID, params.Workspace, params.Format, params.Detail, params.Dryrun, w) if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - // Cleanup - defer sourceapi.Cleanup(ctx, directory) - - executeOptions := buildOptions(dryRunParam) - stack.Path = workDir - - // Generate spec - sp, err := engineapi.GenerateSpecWithSpinner(project, stack, ws, true) - if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - - // return immediately if no resource found in stack - // todo: if there is no resource, should still do diff job; for now, if output is json format, there is no hint - if sp == nil || len(sp.Resources) == 0 { - if formatParam != engineapi.JSONOutput { - logger.Info("No resource change found in this stack...") - render.Render(w, r, handler.SuccessResponse(ctx, "No resource change found in this stack.")) + if err == stackmanager.ErrDryrunDestroy { + render.Render(w, r, handler.SuccessResponse(ctx, "Dry-run mode enabled, the above resources will be destroyed if dryrun is set to false")) return - } - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - - // Compute state storage - // TODO: this local storage is temporary, will support remote later - stateStorage := remoteBackend.StateStorage(project.Name, stack.Name, workspaceParam) - logger.Info("Remote state storage found", "Remote", stateStorage) - // logger.Info("Local state storage found", "Path", stateStorage) - - // Compute changes for preview - changes, err := engineapi.Preview(executeOptions, stateStorage, sp, project, stack) - if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - - // If output format is json, return details without any summary or formatting - if formatParam == engineapi.JSONOutput { - var previewChanges []byte - previewChanges, err = json.Marshal(changes) - if err != nil { + } else { render.Render(w, r, handler.FailureResponse(ctx, err)) return } - logger.Info(string(previewChanges)) - render.Render(w, r, handler.SuccessResponse(ctx, string(previewChanges))) - return - } - - if changes.AllUnChange() { - logger.Info("All resources are reconciled. No diff found") - render.Render(w, r, handler.SuccessResponse(ctx, "All resources are reconciled. No diff found")) - return - } - - // Summary preview table - changes.Summary(w, true) - // detail detection - if detailParam { - changes.OutputDiff("all") - } - - logger.Info("Start applying diffs ...") - if err = engineapi.Apply(executeOptions, stateStorage, sp, changes, os.Stdout); err != nil { - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - - // if dry run, print the hint - if dryRunParam { - fmt.Printf("NOTE: Currently running in the --dry-run mode, the above configuration does not really take effect") - render.Render(w, r, handler.SuccessResponse(ctx, "NOTE: Currently running in the --dry-run mode, the above configuration does not really take effect")) - return - } - - // Update LastSyncTimestamp to current time and set stack syncState to synced - stackEntity.LastSyncTimestamp = time.Now() - stackEntity.SyncState = constant.StackStateSynced - - // Update stack with repository - err = h.stackRepo.Update(ctx, stackEntity) - if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, err)) - return } - // Destroy completed + // Apply completed logger.Info("apply completed") render.Render(w, r, handler.SuccessResponse(ctx, "apply completed")) @@ -524,143 +144,51 @@ func (h *Handler) ApplyStack() http.HandlerFunc { func (h *Handler) DestroyStack() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Getting stuff from context - ctx := r.Context() - logger := util.GetLogger(ctx) - logger.Info("Destroying stack...") - // Get params from URL parameter - stackID := chi.URLParam(r, "stackID") - // TODO: Define default behaviors - // kpmParam, _ := strconv.ParseBool(r.URL.Query().Get("kpm")) - // TODO: Should match automatically eventually - workspaceParam := r.URL.Query().Get("workspace") - detailParam, _ := strconv.ParseBool(r.URL.Query().Get("detail")) - dryRunParam, _ := strconv.ParseBool(r.URL.Query().Get("dryrun")) - - // Get stack with repository - id, err := strconv.Atoi(stackID) - if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, ErrInvalidStacktID)) - return - } - stackEntity, err := h.stackRepo.Get(ctx, uint(id)) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - render.Render(w, r, handler.FailureResponse(ctx, ErrGettingNonExistingStack)) - return - } - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - - // Get project by id - project, err := stackEntity.Project.ConvertToCore() - if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - - // Get stack by id - stack, err := stackEntity.ConvertToCore() - if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - - // get workspace configurations - // localBackend, err := backend.NewBackend("") - // if err != nil { - // render.Render(w, r, handler.FailureResponse(ctx, err)) - // return - // } - // wsStorage, err := bk.WorkspaceStorage() - // if err != nil { - // render.Render(w, r, handler.FailureResponse(ctx, err)) - // return - // } - // ws, err := wsStorage.Get(workspaceParam) - // if err != nil { - // render.Render(w, r, handler.FailureResponse(ctx, err)) - // return - // } - - remoteBackend, err := h.GetBackendFromWorkspaceName(ctx, workspaceParam) - if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - - // Build API inputs - // get project to get source and workdir - projectEntity, err := handler.GetProjectByID(ctx, h.projectRepo, stackEntity.Project.ID) + ctx, logger, params, err := requestHelper(r) if err != nil { render.Render(w, r, handler.FailureResponse(ctx, err)) return } + logger.Info("Destroying stack...", "stackID", params.StackID) - directory, workDir, err := getWorkDirFromSource(ctx, stackEntity, projectEntity) + err = h.stackManager.DestroyStack(ctx, params.StackID, params.Workspace, params.Detail, params.Dryrun, w) if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - destroyOptions := buildOptions(dryRunParam) - stack.Path = workDir - - // Cleanup - defer sourceapi.Cleanup(ctx, directory) - - // Compute state storage - // TODO: this local storage is temporary, will support remote later - stateStorage := remoteBackend.StateStorage(project.Name, stack.Name, workspaceParam) - // logger.Info("Local state storage found", "Path", stateStorage) - logger.Info("Remote state storage found", "Remote", stateStorage) - - priorState, err := stateStorage.Get() - if err != nil || priorState == nil { - logger.Info("can't find state", "project", project.Name, "stack", stack.Name, "workspace", workspaceParam) - render.Render(w, r, handler.FailureResponse(ctx, ErrGettingNonExistingStateForStack)) - return - } - destroyResources := priorState.Resources - - if destroyResources == nil || len(priorState.Resources) == 0 { - render.Render(w, r, handler.SuccessResponse(ctx, "No managed resources to destroy")) - return - } - - // compute changes for preview - i := &apiv1.Spec{Resources: destroyResources} - changes, err := engineapi.DestroyPreview(destroyOptions, i, project, stack, stateStorage) - if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - - // Summary preview table - changes.Summary(w, true) - // detail detection - if detailParam { - changes.OutputDiff("all") - } - - // if dryrun, print the hint - if dryRunParam { - fmt.Printf("Dry-run mode enabled, the above resources will be destroyed if dryrun is set to false") - render.Render(w, r, handler.SuccessResponse(ctx, "Dry-run mode enabled, the above resources will be destroyed if dryrun is set to false")) - return - } - - // Destroy - logger.Info("Start destroying resources......") - if err = engineapi.Destroy(destroyOptions, i, changes, stateStorage); err != nil { - render.Render(w, r, handler.FailureResponse(ctx, err)) - return + if err == stackmanager.ErrDryrunDestroy { + render.Render(w, r, handler.SuccessResponse(ctx, "Dry-run mode enabled, the above resources will be destroyed if dryrun is set to false")) + return + } else { + render.Render(w, r, handler.FailureResponse(ctx, err)) + return + } } // Destroy completed logger.Info("destroy completed") render.Render(w, r, handler.SuccessResponse(ctx, "destroy completed")) + } +} - // Cleanup - sourceapi.Cleanup(ctx, directory) +func requestHelper(r *http.Request) (context.Context, *logr.Logger, *StackRequestParams, error) { + ctx := r.Context() + stackID := chi.URLParam(r, "stackID") + // Get stack with repository + id, err := strconv.Atoi(stackID) + if err != nil { + return nil, nil, nil, stackmanager.ErrInvalidStackID + } + logger := util.GetLogger(ctx) + // Get Params + detailParam, _ := strconv.ParseBool(r.URL.Query().Get("detail")) + dryrunParam, _ := strconv.ParseBool(r.URL.Query().Get("dryrun")) + outputParam := r.URL.Query().Get("output") + // TODO: Should match automatically eventually??? + workspaceParam := r.URL.Query().Get("workspace") + params := StackRequestParams{ + StackID: uint(id), + Workspace: workspaceParam, + Detail: detailParam, + Dryrun: dryrunParam, + Format: outputParam, } + return ctx, &logger, ¶ms, nil } diff --git a/pkg/server/handler/stack/handler.go b/pkg/server/handler/stack/handler.go index f217c0c0..920e6521 100644 --- a/pkg/server/handler/stack/handler.go +++ b/pkg/server/handler/stack/handler.go @@ -1,17 +1,9 @@ package stack import ( - "errors" "net/http" - "strconv" - "time" - "github.com/go-chi/chi/v5" "github.com/go-chi/render" - "github.com/jinzhu/copier" - "gorm.io/gorm" - "kusionstack.io/kusion/pkg/domain/constant" - "kusionstack.io/kusion/pkg/domain/entity" "kusionstack.io/kusion/pkg/domain/request" "kusionstack.io/kusion/pkg/server/handler" "kusionstack.io/kusion/pkg/server/util" @@ -35,7 +27,6 @@ func (h *Handler) CreateStack() http.HandlerFunc { ctx := r.Context() logger := util.GetLogger(ctx) logger.Info("Creating stack...") - // workspaceParam := chi.URLParam(r, "workspaceName") // Decode the request body into the payload. var requestPayload request.CreateStackRequest @@ -44,50 +35,7 @@ func (h *Handler) CreateStack() http.HandlerFunc { return } - // Convert request payload to domain model - var createdEntity entity.Stack - if err := copier.Copy(&createdEntity, &requestPayload); err != nil { - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - // The default state is UnSynced - createdEntity.SyncState = constant.StackStateUnSynced - createdEntity.CreationTimestamp = time.Now() - createdEntity.UpdateTimestamp = time.Now() - createdEntity.LastSyncTimestamp = time.Unix(0, 0) // default to none - - // TODO: Only project ID should be needed here. Not source and org IDs. - // Get source by id - // sourceEntity, err := handler.GetSourceByID(ctx, h.sourceRepo, requestPayload.SourceID) - // if err != nil { - // render.Render(w, r, handler.FailureResponse(ctx, err)) - // return - // } - // createdEntity.Source = sourceEntity - - // Get project by id - projectEntity, err := handler.GetProjectByID(ctx, h.projectRepo, requestPayload.ProjectID) - if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - createdEntity.Project = projectEntity - - // // Get organization by id - // organizationEntity, err := handler.GetOrganizationByID(ctx, h.orgRepository, requestPayload.OrganizationID) - // if err != nil { - // render.Render(w, r, handler.FailureResponse(ctx, err)) - // return - // } - // createdEntity.Organization = organizationEntity - // TODO: Only project ID should be needed here. Not source and org IDs. - - // Create stack with repository - err = h.stackRepo.Create(ctx, &createdEntity) - if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } + createdEntity, err := h.stackManager.CreateStack(ctx, requestPayload) handler.HandleResult(w, r, ctx, err, createdEntity) } } @@ -107,22 +55,14 @@ func (h *Handler) CreateStack() http.HandlerFunc { func (h *Handler) DeleteStack() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Getting stuff from context - ctx := r.Context() - logger := util.GetLogger(ctx) - logger.Info("Deleting source...") - stackID := chi.URLParam(r, "stackID") - - // Delete stack with repository - id, err := strconv.Atoi(stackID) - if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, ErrInvalidStacktID)) - return - } - err = h.stackRepo.Delete(ctx, uint(id)) + ctx, logger, params, err := requestHelper(r) if err != nil { render.Render(w, r, handler.FailureResponse(ctx, err)) return } + logger.Info("Deleting source...", "stackID", params.StackID) + + err = h.stackManager.DeleteStackByID(ctx, params.StackID) handler.HandleResult(w, r, ctx, err, "Deletion Success") } } @@ -142,17 +82,12 @@ func (h *Handler) DeleteStack() http.HandlerFunc { func (h *Handler) UpdateStack() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Getting stuff from context - ctx := r.Context() - logger := util.GetLogger(ctx) - logger.Info("Updating stack...") - stackID := chi.URLParam(r, "stackID") - - // convert stack ID to int - id, err := strconv.Atoi(stackID) + ctx, logger, params, err := requestHelper(r) if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, ErrInvalidStacktID)) + render.Render(w, r, handler.FailureResponse(ctx, err)) return } + logger.Info("Updating stack...", "stackID", params.StackID) // Decode the request body into the payload. var requestPayload request.UpdateStackRequest @@ -161,61 +96,7 @@ func (h *Handler) UpdateStack() http.HandlerFunc { return } - // Convert request payload to domain model - var requestEntity entity.Stack - if err := copier.Copy(&requestEntity, &requestPayload); err != nil { - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - - // TODO: Only project ID should be needed here. Not source and org IDs. - // Get source by id - // sourceEntity, err := handler.GetSourceByID(ctx, h.sourceRepo, requestPayload.SourceID) - // if err != nil { - // render.Render(w, r, handler.FailureResponse(ctx, err)) - // return - // } - // requestEntity.Source = sourceEntity - - // Get project by id - projectEntity, err := handler.GetProjectByID(ctx, h.projectRepo, requestPayload.ProjectID) - if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - requestEntity.Project = projectEntity - - // // Get organization by id - // organizationEntity, err := handler.GetOrganizationByID(ctx, h.orgRepository, requestPayload.OrganizationID) - // if err != nil { - // render.Render(w, r, handler.FailureResponse(ctx, err)) - // return - // } - // requestEntity.Organization = organizationEntity - // TODO: Only project ID should be needed here. Not source and org IDs. - - // Get the existing stack by id - updatedEntity, err := h.stackRepo.Get(ctx, uint(id)) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - render.Render(w, r, handler.FailureResponse(ctx, ErrUpdatingNonExistingStack)) - return - } - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - - // Overwrite non-zero values in request entity to existing entity - copier.CopyWithOption(updatedEntity, requestEntity, copier.Option{IgnoreEmpty: true}) - - // Update stack with repository - err = h.stackRepo.Update(ctx, updatedEntity) - if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - - // Return updated stack + updatedEntity, err := h.stackManager.UpdateStackByID(ctx, params.StackID, requestPayload) handler.HandleResult(w, r, ctx, err, updatedEntity) } } @@ -234,28 +115,14 @@ func (h *Handler) UpdateStack() http.HandlerFunc { func (h *Handler) GetStack() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Getting stuff from context - ctx := r.Context() - logger := util.GetLogger(ctx) - logger.Info("Getting stack...") - stackID := chi.URLParam(r, "stackID") - - // Get stack with repository - id, err := strconv.Atoi(stackID) - if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, ErrInvalidStacktID)) - return - } - existingEntity, err := h.stackRepo.Get(ctx, uint(id)) + ctx, logger, params, err := requestHelper(r) if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - render.Render(w, r, handler.FailureResponse(ctx, ErrGettingNonExistingStack)) - return - } render.Render(w, r, handler.FailureResponse(ctx, err)) return } + logger.Info("Getting stack...", "stackID", params.StackID) - // Return found stack + existingEntity, err := h.stackManager.GetStackByID(ctx, params.StackID) handler.HandleResult(w, r, ctx, err, existingEntity) } } @@ -277,17 +144,7 @@ func (h *Handler) ListStacks() http.HandlerFunc { logger := util.GetLogger(ctx) logger.Info("Listing stack...") - stackEntities, err := h.stackRepo.List(ctx) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - render.Render(w, r, handler.FailureResponse(ctx, ErrGettingNonExistingStack)) - return - } - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - - // Return found stacks + stackEntities, err := h.stackManager.ListStacks(ctx) handler.HandleResult(w, r, ctx, err, stackEntities) } } diff --git a/pkg/server/handler/stack/types.go b/pkg/server/handler/stack/types.go index 549a3279..5f7c41f5 100644 --- a/pkg/server/handler/stack/types.go +++ b/pkg/server/handler/stack/types.go @@ -1,43 +1,25 @@ package stack import ( - "errors" - - "kusionstack.io/kusion/pkg/domain/repository" -) - -const Stdout = "stdout" - -var ( - ErrGettingNonExistingStack = errors.New("the stack does not exist") - ErrUpdatingNonExistingStack = errors.New("the stack to update does not exist") - ErrSourceNotFound = errors.New("the specified source does not exist") - ErrWorkspaceNotFound = errors.New("the specified workspace does not exist") - ErrProjectNotFound = errors.New("the specified project does not exist") - ErrInvalidStacktID = errors.New("the stack ID should be a uuid") - ErrGettingNonExistingStateForStack = errors.New("can not find State in this stack") + stackmanager "kusionstack.io/kusion/pkg/server/manager/stack" ) func NewHandler( - orgRepository repository.OrganizationRepository, - projectRepo repository.ProjectRepository, - stackRepo repository.StackRepository, - sourceRepo repository.SourceRepository, - workspaceRepo repository.WorkspaceRepository, + stackManager *stackmanager.StackManager, ) (*Handler, error) { return &Handler{ - orgRepository: orgRepository, - stackRepo: stackRepo, - projectRepo: projectRepo, - sourceRepo: sourceRepo, - workspaceRepo: workspaceRepo, + stackManager: stackManager, }, nil } type Handler struct { - orgRepository repository.OrganizationRepository - projectRepo repository.ProjectRepository - stackRepo repository.StackRepository - sourceRepo repository.SourceRepository - workspaceRepo repository.WorkspaceRepository + stackManager *stackmanager.StackManager +} + +type StackRequestParams struct { + StackID uint + Workspace string + Format string + Detail bool + Dryrun bool } diff --git a/pkg/server/handler/workspace/handler.go b/pkg/server/handler/workspace/handler.go index 61db7c43..4050ee39 100644 --- a/pkg/server/handler/workspace/handler.go +++ b/pkg/server/handler/workspace/handler.go @@ -1,18 +1,16 @@ package workspace import ( - "errors" + "context" "net/http" "strconv" - "time" "github.com/go-chi/chi/v5" "github.com/go-chi/render" - "github.com/jinzhu/copier" - "gorm.io/gorm" - "kusionstack.io/kusion/pkg/domain/entity" + "github.com/go-logr/logr" "kusionstack.io/kusion/pkg/domain/request" "kusionstack.io/kusion/pkg/server/handler" + workspacemanager "kusionstack.io/kusion/pkg/server/manager/workspace" "kusionstack.io/kusion/pkg/server/util" ) @@ -42,33 +40,7 @@ func (h *Handler) CreateWorkspace() http.HandlerFunc { return } - // Convert request payload to domain model - var createdEntity entity.Workspace - if err := copier.Copy(&createdEntity, &requestPayload); err != nil { - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - // The default state is UnSynced - createdEntity.CreationTimestamp = time.Now() - createdEntity.UpdateTimestamp = time.Now() - - // Get backend by id - backendEntity, err := h.backendRepo.Get(ctx, requestPayload.BackendID) - if err != nil && err == gorm.ErrRecordNotFound { - render.Render(w, r, handler.FailureResponse(ctx, ErrBackendNotFound)) - return - } else if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - createdEntity.Backend = backendEntity - - // Create workspace with repository - err = h.workspaceRepo.Create(ctx, &createdEntity) - if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } + createdEntity, err := h.workspaceManager.CreateWorkspace(ctx, requestPayload) handler.HandleResult(w, r, ctx, err, createdEntity) } } @@ -88,22 +60,14 @@ func (h *Handler) CreateWorkspace() http.HandlerFunc { func (h *Handler) DeleteWorkspace() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Getting stuff from context - ctx := r.Context() - logger := util.GetLogger(ctx) - logger.Info("Deleting source...") - workspaceID := chi.URLParam(r, "workspaceID") - - // Delete workspace with repository - id, err := strconv.Atoi(workspaceID) - if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, ErrInvalidWorkspaceID)) - return - } - err = h.workspaceRepo.Delete(ctx, uint(id)) + ctx, logger, params, err := requestHelper(r) if err != nil { render.Render(w, r, handler.FailureResponse(ctx, err)) return } + logger.Info("Deleting source...", "workspaceID", params.WorkspaceID) + + err = h.workspaceManager.DeleteWorkspaceByID(ctx, params.WorkspaceID) handler.HandleResult(w, r, ctx, err, "Deletion Success") } } @@ -123,17 +87,12 @@ func (h *Handler) DeleteWorkspace() http.HandlerFunc { func (h *Handler) UpdateWorkspace() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Getting stuff from context - ctx := r.Context() - logger := util.GetLogger(ctx) - logger.Info("Updating workspace...") - workspaceID := chi.URLParam(r, "workspaceID") - - // convert workspace ID to int - id, err := strconv.Atoi(workspaceID) + ctx, logger, params, err := requestHelper(r) if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, ErrInvalidWorkspaceID)) + render.Render(w, r, handler.FailureResponse(ctx, err)) return } + logger.Info("Updating workspace...", "workspaceID", params.WorkspaceID) // Decode the request body into the payload. var requestPayload request.UpdateWorkspaceRequest @@ -142,35 +101,7 @@ func (h *Handler) UpdateWorkspace() http.HandlerFunc { return } - // Convert request payload to domain model - var requestEntity entity.Workspace - if err := copier.Copy(&requestEntity, &requestPayload); err != nil { - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - - // Get the existing workspace by id - updatedEntity, err := h.workspaceRepo.Get(ctx, uint(id)) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - render.Render(w, r, handler.FailureResponse(ctx, ErrUpdatingNonExistingWorkspace)) - return - } - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - - // Overwrite non-zero values in request entity to existing entity - copier.CopyWithOption(updatedEntity, requestEntity, copier.Option{IgnoreEmpty: true}) - - // Update workspace with repository - err = h.workspaceRepo.Update(ctx, updatedEntity) - if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - - // Return updated workspace + updatedEntity, err := h.workspaceManager.UpdateWorkspaceByID(ctx, params.WorkspaceID, requestPayload) handler.HandleResult(w, r, ctx, err, updatedEntity) } } @@ -189,28 +120,15 @@ func (h *Handler) UpdateWorkspace() http.HandlerFunc { func (h *Handler) GetWorkspace() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Getting stuff from context - ctx := r.Context() - logger := util.GetLogger(ctx) - logger.Info("Getting workspace...") - workspaceID := chi.URLParam(r, "workspaceID") - - // Get workspace with repository - id, err := strconv.Atoi(workspaceID) - if err != nil { - render.Render(w, r, handler.FailureResponse(ctx, ErrInvalidWorkspaceID)) - return - } - existingEntity, err := h.workspaceRepo.Get(ctx, uint(id)) + ctx, logger, params, err := requestHelper(r) if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - render.Render(w, r, handler.FailureResponse(ctx, ErrGettingNonExistingWorkspace)) - return - } render.Render(w, r, handler.FailureResponse(ctx, err)) return } + logger.Info("Getting workspace...", "workspaceID", params.WorkspaceID) // Return found workspace + existingEntity, err := h.workspaceManager.GetWorkspaceByID(ctx, params.WorkspaceID) handler.HandleResult(w, r, ctx, err, existingEntity) } } @@ -232,17 +150,23 @@ func (h *Handler) ListWorkspaces() http.HandlerFunc { logger := util.GetLogger(ctx) logger.Info("Listing workspace...") - workspaceEntities, err := h.workspaceRepo.List(ctx) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - render.Render(w, r, handler.FailureResponse(ctx, ErrGettingNonExistingWorkspace)) - return - } - render.Render(w, r, handler.FailureResponse(ctx, err)) - return - } - // Return found workspaces + workspaceEntities, err := h.workspaceManager.ListWorkspaces(ctx) handler.HandleResult(w, r, ctx, err, workspaceEntities) } } + +func requestHelper(r *http.Request) (context.Context, *logr.Logger, *WorkspaceRequestParams, error) { + ctx := r.Context() + workspaceID := chi.URLParam(r, "workspaceID") + // Get stack with repository + id, err := strconv.Atoi(workspaceID) + if err != nil { + return nil, nil, nil, workspacemanager.ErrInvalidWorkspaceID + } + logger := util.GetLogger(ctx) + params := WorkspaceRequestParams{ + WorkspaceID: uint(id), + } + return ctx, &logger, ¶ms, nil +} diff --git a/pkg/server/handler/workspace/types.go b/pkg/server/handler/workspace/types.go index ae848580..b48e9eae 100644 --- a/pkg/server/handler/workspace/types.go +++ b/pkg/server/handler/workspace/types.go @@ -1,29 +1,25 @@ package workspace import ( - "errors" - - "kusionstack.io/kusion/pkg/domain/repository" -) - -var ( - ErrGettingNonExistingWorkspace = errors.New("the workspace does not exist") - 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") + backendmanager "kusionstack.io/kusion/pkg/server/manager/backend" + workspacemanager "kusionstack.io/kusion/pkg/server/manager/workspace" ) func NewHandler( - workspaceRepo repository.WorkspaceRepository, - backendRepo repository.BackendRepository, + workspaceManager *workspacemanager.WorkspaceManager, + backendManager *backendmanager.BackendManager, ) (*Handler, error) { return &Handler{ - workspaceRepo: workspaceRepo, - backendRepo: backendRepo, + workspaceManager: workspaceManager, + backendManager: backendManager, }, nil } type Handler struct { - workspaceRepo repository.WorkspaceRepository - backendRepo repository.BackendRepository + workspaceManager *workspacemanager.WorkspaceManager + backendManager *backendmanager.BackendManager +} + +type WorkspaceRequestParams struct { + WorkspaceID uint } diff --git a/pkg/server/manager/backend/backend_manager.go b/pkg/server/manager/backend/backend_manager.go new file mode 100644 index 00000000..cd6e6813 --- /dev/null +++ b/pkg/server/manager/backend/backend_manager.go @@ -0,0 +1,86 @@ +package backend + +import ( + "context" + "errors" + + "github.com/jinzhu/copier" + "gorm.io/gorm" + "kusionstack.io/kusion/pkg/domain/entity" + "kusionstack.io/kusion/pkg/domain/request" +) + +func (m *BackendManager) ListBackends(ctx context.Context) ([]*entity.Backend, error) { + backendEntities, err := m.backendRepo.List(ctx) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrGettingNonExistingBackend + } + return nil, err + } + return backendEntities, nil +} + +func (m *BackendManager) GetBackendByID(ctx context.Context, id uint) (*entity.Backend, error) { + existingEntity, err := m.backendRepo.Get(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrGettingNonExistingBackend + } + return nil, err + } + return existingEntity, nil +} + +func (m *BackendManager) DeleteBackendByID(ctx context.Context, id uint) error { + err := m.backendRepo.Delete(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrGettingNonExistingBackend + } + return err + } + return nil +} + +func (m *BackendManager) UpdateBackendByID(ctx context.Context, id uint, requestPayload request.UpdateBackendRequest) (*entity.Backend, error) { + // Convert request payload to domain model + var requestEntity entity.Backend + if err := copier.Copy(&requestEntity, &requestPayload); err != nil { + return nil, err + } + + // Get the existing backend by id + updatedEntity, err := m.backendRepo.Get(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrUpdatingNonExistingBackend + } + return nil, err + } + + // Overwrite non-zero values in request entity to existing entity + copier.CopyWithOption(updatedEntity, requestEntity, copier.Option{IgnoreEmpty: true}) + + // Update backend with repository + err = m.backendRepo.Update(ctx, updatedEntity) + if err != nil { + return nil, err + } + return updatedEntity, nil +} + +func (m *BackendManager) CreateBackend(ctx context.Context, requestPayload request.CreateBackendRequest) (*entity.Backend, error) { + // Convert request payload to domain model + var createdEntity entity.Backend + if err := copier.Copy(&createdEntity, &requestPayload); err != nil { + return nil, err + } + + // Create backend with repository + err := m.backendRepo.Create(ctx, &createdEntity) + if err != nil { + return nil, err + } + return &createdEntity, nil +} diff --git a/pkg/server/manager/backend/types.go b/pkg/server/manager/backend/types.go new file mode 100644 index 00000000..bf137f91 --- /dev/null +++ b/pkg/server/manager/backend/types.go @@ -0,0 +1,23 @@ +package backend + +import ( + "errors" + + "kusionstack.io/kusion/pkg/domain/repository" +) + +var ( + ErrGettingNonExistingBackend = errors.New("the backend does not exist") + ErrUpdatingNonExistingBackend = errors.New("the backend to update does not exist") + ErrInvalidBackendID = errors.New("the backend ID should be a uuid") +) + +type BackendManager struct { + backendRepo repository.BackendRepository +} + +func NewBackendManager(backendRepo repository.BackendRepository) *BackendManager { + return &BackendManager{ + backendRepo: backendRepo, + } +} diff --git a/pkg/server/manager/organization/organization_manager.go b/pkg/server/manager/organization/organization_manager.go new file mode 100644 index 00000000..8e7d62c0 --- /dev/null +++ b/pkg/server/manager/organization/organization_manager.go @@ -0,0 +1,91 @@ +package organization + +import ( + "context" + "errors" + "time" + + "github.com/jinzhu/copier" + "gorm.io/gorm" + "kusionstack.io/kusion/pkg/domain/entity" + "kusionstack.io/kusion/pkg/domain/request" +) + +func (m *OrganizationManager) ListOrganizations(ctx context.Context) ([]*entity.Organization, error) { + organizationEntities, err := m.organizationRepo.List(ctx) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrGettingNonExistingOrganization + } + return nil, err + } + return organizationEntities, nil +} + +func (m *OrganizationManager) GetOrganizationByID(ctx context.Context, id uint) (*entity.Organization, error) { + existingEntity, err := m.organizationRepo.Get(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrGettingNonExistingOrganization + } + return nil, err + } + return existingEntity, nil +} + +func (m *OrganizationManager) DeleteOrganizationByID(ctx context.Context, id uint) error { + err := m.organizationRepo.Delete(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrGettingNonExistingOrganization + } + return err + } + return nil +} + +func (m *OrganizationManager) UpdateOrganizationByID(ctx context.Context, id uint, requestPayload request.UpdateOrganizationRequest) (*entity.Organization, error) { + // Convert request payload to domain model + var requestEntity entity.Organization + if err := copier.Copy(&requestEntity, &requestPayload); err != nil { + return nil, err + } + + // Get the existing organization by id + updatedEntity, err := m.organizationRepo.Get(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrUpdatingNonExistingOrganization + } + return nil, err + } + + // Overwrite non-zero values in request entity to existing entity + copier.CopyWithOption(updatedEntity, requestEntity, copier.Option{IgnoreEmpty: true}) + + // Update organization with repository + err = m.organizationRepo.Update(ctx, updatedEntity) + if err != nil { + return nil, err + } + + return updatedEntity, nil +} + +func (m *OrganizationManager) CreateOrganization(ctx context.Context, requestPayload request.CreateOrganizationRequest) (*entity.Organization, error) { + // Convert request payload to domain model + var createdEntity entity.Organization + if err := copier.Copy(&createdEntity, &requestPayload); err != nil { + return nil, err + } + // The default state is UnSynced + createdEntity.CreationTimestamp = time.Now() + createdEntity.UpdateTimestamp = time.Now() + + // Create organization with repository + err := m.organizationRepo.Create(ctx, &createdEntity) + if err != nil { + return nil, err + } + return &createdEntity, nil +} diff --git a/pkg/server/manager/organization/types.go b/pkg/server/manager/organization/types.go new file mode 100644 index 00000000..bb72fbd5 --- /dev/null +++ b/pkg/server/manager/organization/types.go @@ -0,0 +1,23 @@ +package organization + +import ( + "errors" + + "kusionstack.io/kusion/pkg/domain/repository" +) + +var ( + ErrGettingNonExistingOrganization = errors.New("the organization does not exist") + ErrUpdatingNonExistingOrganization = errors.New("the organization to update does not exist") + ErrInvalidOrganizationID = errors.New("the organization ID should be a uuid") +) + +type OrganizationManager struct { + organizationRepo repository.OrganizationRepository +} + +func NewOrganizationManager(organizationRepo repository.OrganizationRepository) *OrganizationManager { + return &OrganizationManager{ + organizationRepo: organizationRepo, + } +} diff --git a/pkg/server/manager/source/source_manager.go b/pkg/server/manager/source/source_manager.go new file mode 100644 index 00000000..d38235ae --- /dev/null +++ b/pkg/server/manager/source/source_manager.go @@ -0,0 +1,101 @@ +package source + +import ( + "context" + "errors" + "net/url" + + "github.com/jinzhu/copier" + "gorm.io/gorm" + "kusionstack.io/kusion/pkg/domain/entity" + "kusionstack.io/kusion/pkg/domain/request" +) + +func (m *SourceManager) ListSources(ctx context.Context) ([]*entity.Source, error) { + sourceEntities, err := m.sourceRepo.List(ctx) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrGettingNonExistingSource + } + return nil, err + } + return sourceEntities, nil +} + +func (m *SourceManager) GetSourceByID(ctx context.Context, id uint) (*entity.Source, error) { + existingEntity, err := m.sourceRepo.Get(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrGettingNonExistingSource + } + return nil, err + } + return existingEntity, nil +} + +func (m *SourceManager) DeleteSourceByID(ctx context.Context, id uint) error { + err := m.sourceRepo.Delete(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrGettingNonExistingSource + } + return err + } + return nil +} + +func (m *SourceManager) UpdateSourceByID(ctx context.Context, id uint, requestPayload request.UpdateSourceRequest) (*entity.Source, error) { + // Convert request payload to domain model + var requestEntity entity.Source + if err := copier.Copy(&requestEntity, &requestPayload); err != nil { + return nil, err + } + + // Convert Remote string to URL + remote, err := url.Parse(requestPayload.Remote) + if err != nil { + return nil, err + } + requestEntity.Remote = remote + + // Get the existing source by id + updatedEntity, err := m.sourceRepo.Get(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrUpdatingNonExistingSource + } + return nil, err + } + + // Overwrite non-zero values in request entity to existing entity + copier.CopyWithOption(updatedEntity, requestEntity, copier.Option{IgnoreEmpty: true}) + + // Update source with repository + err = m.sourceRepo.Update(ctx, updatedEntity) + if err != nil { + return nil, err + } + return updatedEntity, nil +} + +func (m *SourceManager) CreateSource(ctx context.Context, requestPayload request.CreateSourceRequest) (*entity.Source, error) { + // Convert request payload to domain model + var createdEntity entity.Source + if err := copier.Copy(&createdEntity, &requestPayload); err != nil { + return nil, err + } + + // Convert Remote string to URL + remote, err := url.Parse(requestPayload.Remote) + if err != nil { + return nil, err + } + createdEntity.Remote = remote + + // Create source with repository + err = m.sourceRepo.Create(ctx, &createdEntity) + if err != nil { + return nil, err + } + return &createdEntity, nil +} diff --git a/pkg/server/manager/source/types.go b/pkg/server/manager/source/types.go new file mode 100644 index 00000000..a8e5741d --- /dev/null +++ b/pkg/server/manager/source/types.go @@ -0,0 +1,23 @@ +package source + +import ( + "errors" + + "kusionstack.io/kusion/pkg/domain/repository" +) + +var ( + ErrGettingNonExistingSource = errors.New("the source does not exist") + ErrUpdatingNonExistingSource = errors.New("the source to update does not exist") + ErrInvalidSourceID = errors.New("the source ID should be a uuid") +) + +type SourceManager struct { + sourceRepo repository.SourceRepository +} + +func NewSourceManager(sourceRepo repository.SourceRepository) *SourceManager { + return &SourceManager{ + sourceRepo: sourceRepo, + } +} diff --git a/pkg/server/manager/stack/stack_manager.go b/pkg/server/manager/stack/stack_manager.go new file mode 100644 index 00000000..71396b44 --- /dev/null +++ b/pkg/server/manager/stack/stack_manager.go @@ -0,0 +1,335 @@ +package stack + +import ( + "context" + "errors" + "net/http" + "os" + "time" + + "github.com/jinzhu/copier" + "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/entity" + "kusionstack.io/kusion/pkg/domain/repository" + "kusionstack.io/kusion/pkg/domain/request" + + engineapi "kusionstack.io/kusion/pkg/engine/api" + "kusionstack.io/kusion/pkg/engine/operation/models" + + sourceapi "kusionstack.io/kusion/pkg/engine/api/source" + "kusionstack.io/kusion/pkg/server/handler" + "kusionstack.io/kusion/pkg/server/util" +) + +func NewStackManager(stackRepo repository.StackRepository, projectRepo repository.ProjectRepository, workspaceRepo repository.WorkspaceRepository) *StackManager { + return &StackManager{ + stackRepo: stackRepo, + projectRepo: projectRepo, + workspaceRepo: workspaceRepo, + } +} + +func (m *StackManager) GenerateStack(ctx context.Context, id uint, workspaceName string) (*v1.Spec, error) { + logger := util.GetLogger(ctx) + logger.Info("Starting generating spec in StackManager ...") + + // Generate a stack + stackEntity, err := m.stackRepo.Get(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrGettingNonExistingStack + } + return nil, err + } + + // Get project by id + project, err := stackEntity.Project.ConvertToCore() + if err != nil { + return nil, err + } + + // Get stack by id + stack, err := stackEntity.ConvertToCore() + if err != nil { + return nil, err + } + + // get workspace configurations + bk, err := backend.NewBackend("") + if err != nil { + return nil, err + } + wsStorage, err := bk.WorkspaceStorage() + if err != nil { + return nil, err + } + ws, err := wsStorage.Get(workspaceName) + if err != nil { + return nil, err + } + + // Build API inputs + // get project to get source and workdir + projectEntity, err := handler.GetProjectByID(ctx, m.projectRepo, stackEntity.Project.ID) + if err != nil { + return nil, err + } + + directory, workDir, err := GetWorkDirFromSource(ctx, stackEntity, projectEntity) + logger.Info("workDir derived", "workDir", workDir) + logger.Info("directory derived", "directory", directory) + + stack.Path = workDir + if err != nil { + return nil, err + } + // intentOptions, _ := buildOptions(workDir, kpmParam, false) + // Cleanup + defer sourceapi.Cleanup(ctx, directory) + + // Generate spec + return engineapi.GenerateSpecWithSpinner(project, stack, ws, true) +} + +func (m *StackManager) PreviewStack(ctx context.Context, id uint, workspaceName string) (*models.Changes, error) { + logger := util.GetLogger(ctx) + logger.Info("Starting previewing stack in StackManager ...") + _, changes, _, err := m.previewHelper(ctx, id, workspaceName) + return changes, err +} + +func (m *StackManager) ApplyStack(ctx context.Context, id uint, workspaceName, format string, detail, dryrun bool, w http.ResponseWriter) error { + logger := util.GetLogger(ctx) + logger.Info("Starting applying stack in StackManager ...") + + // Get the stack entity by id + stackEntity, err := m.stackRepo.Get(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrGettingNonExistingStack + } + return err + } + + // Preview a stack + sp, changes, stateStorage, err := m.previewHelper(ctx, id, workspaceName) + if err != nil { + return err + } + + _, err = ProcessChanges(ctx, w, changes, format, detail) + if err != nil { + return err + } + + // if dry run, print the hint + if dryrun { + logger.Info("NOTE: Currently running in the --dry-run mode, the above configuration does not really take effect") + return ErrDryrunDestroy + } + + logger.Info("Dryrun set to false. Start applying diffs ...") + executeOptions := BuildOptions(dryrun) + if err = engineapi.Apply(executeOptions, stateStorage, sp, changes, os.Stdout); err != nil { + return err + } + + // Update LastSyncTimestamp to current time and set stack syncState to synced + stackEntity.LastSyncTimestamp = time.Now() + stackEntity.SyncState = constant.StackStateSynced + + // Update stack with repository + err = m.stackRepo.Update(ctx, stackEntity) + if err != nil { + return err + } + + return nil +} + +func (m *StackManager) DestroyStack(ctx context.Context, id uint, workspaceName string, detail, dryrun bool, w http.ResponseWriter) error { + logger := util.GetLogger(ctx) + logger.Info("Starting applying stack in StackManager ...") + + // Get the stack entity by id + stackEntity, err := m.stackRepo.Get(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrGettingNonExistingStack + } + return err + } + + // Get project by id + project, err := stackEntity.Project.ConvertToCore() + if err != nil { + return err + } + + // Get stack by id + stack, err := stackEntity.ConvertToCore() + if err != nil { + return err + } + + stateBackend, err := m.getBackendFromWorkspaceName(ctx, workspaceName) + if err != nil { + return err + } + + // Build API inputs + // get project to get source and workdir + projectEntity, err := handler.GetProjectByID(ctx, m.projectRepo, stackEntity.Project.ID) + if err != nil { + return err + } + + directory, workDir, err := GetWorkDirFromSource(ctx, stackEntity, projectEntity) + if err != nil { + return err + } + destroyOptions := BuildOptions(dryrun) + stack.Path = workDir + + // Cleanup + defer sourceapi.Cleanup(ctx, directory) + + // Compute state storage + stateStorage := stateBackend.StateStorage(project.Name, stack.Name, workspaceName) + logger.Info("Remote state storage found", "Remote", stateStorage) + + priorState, err := stateStorage.Get() + if err != nil || priorState == nil { + logger.Info("can't find state", "project", project.Name, "stack", stack.Name, "workspace", workspaceName) + return ErrGettingNonExistingStateForStack + } + destroyResources := priorState.Resources + + if destroyResources == nil || len(priorState.Resources) == 0 { + return ErrNoManagedResourceToDestroy + } + + // compute changes for preview + i := &v1.Spec{Resources: destroyResources} + changes, err := engineapi.DestroyPreview(destroyOptions, i, project, stack, stateStorage) + if err != nil { + return err + } + + // Summary preview table + changes.Summary(w, true) + // detail detection + if detail { + changes.OutputDiff("all") + } + + // if dryrun, print the hint + if dryrun { + logger.Info("Dry-run mode enabled, the above resources will be destroyed if dryrun is set to false") + return ErrDryrunDestroy + } + + // Destroy + logger.Info("Start destroying resources......") + if err = engineapi.Destroy(destroyOptions, i, changes, stateStorage); err != nil { + return err + } + return nil +} + +func (m *StackManager) ListStacks(ctx context.Context) ([]*entity.Stack, error) { + stackEntities, err := m.stackRepo.List(ctx) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrGettingNonExistingStack + } + return nil, err + } + return stackEntities, nil +} + +func (m *StackManager) GetStackByID(ctx context.Context, id uint) (*entity.Stack, error) { + existingEntity, err := m.stackRepo.Get(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrGettingNonExistingStack + } + return nil, err + } + return existingEntity, nil +} + +func (m *StackManager) DeleteStackByID(ctx context.Context, id uint) error { + err := m.stackRepo.Delete(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrGettingNonExistingStack + } + return err + } + return nil +} + +func (m *StackManager) UpdateStackByID(ctx context.Context, id uint, requestPayload request.UpdateStackRequest) (*entity.Stack, error) { + // Convert request payload to domain model + var requestEntity entity.Stack + if err := copier.Copy(&requestEntity, &requestPayload); err != nil { + return nil, err + } + + // Get project by id + projectEntity, err := handler.GetProjectByID(ctx, m.projectRepo, requestPayload.ProjectID) + if err != nil { + return nil, err + } + requestEntity.Project = projectEntity + + // Get the existing stack by id + updatedEntity, err := m.stackRepo.Get(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrUpdatingNonExistingStack + } + return nil, err + } + + // Overwrite non-zero values in request entity to existing entity + copier.CopyWithOption(updatedEntity, requestEntity, copier.Option{IgnoreEmpty: true}) + + // Update stack with repository + err = m.stackRepo.Update(ctx, updatedEntity) + if err != nil { + return nil, err + } + return updatedEntity, nil +} + +func (m *StackManager) CreateStack(ctx context.Context, requestPayload request.CreateStackRequest) (*entity.Stack, error) { + // Convert request payload to domain model + var createdEntity entity.Stack + if err := copier.Copy(&createdEntity, &requestPayload); err != nil { + return nil, err + } + // The default state is UnSynced + createdEntity.SyncState = constant.StackStateUnSynced + createdEntity.CreationTimestamp = time.Now() + createdEntity.UpdateTimestamp = time.Now() + createdEntity.LastSyncTimestamp = time.Unix(0, 0) // default to none + + // Get project by id + projectEntity, err := handler.GetProjectByID(ctx, m.projectRepo, requestPayload.ProjectID) + if err != nil { + return nil, err + } + createdEntity.Project = projectEntity + + // Create stack with repository + err = m.stackRepo.Create(ctx, &createdEntity) + if err != nil { + return nil, err + } + return &createdEntity, nil +} diff --git a/pkg/server/manager/stack/types.go b/pkg/server/manager/stack/types.go new file mode 100644 index 00000000..114f31b4 --- /dev/null +++ b/pkg/server/manager/stack/types.go @@ -0,0 +1,30 @@ +package stack + +import ( + "errors" + + "kusionstack.io/kusion/pkg/domain/repository" +) + +const ( + Stdout = "stdout" + NoDiffFound = "All resources are reconciled. No diff found" +) + +var ( + ErrGettingNonExistingStack = errors.New("the stack does not exist") + ErrUpdatingNonExistingStack = errors.New("the stack to update does not exist") + ErrSourceNotFound = errors.New("the specified source does not exist") + ErrWorkspaceNotFound = errors.New("the specified workspace does not exist") + ErrProjectNotFound = errors.New("the specified project does not exist") + ErrInvalidStackID = errors.New("the stack ID should be a uuid") + ErrGettingNonExistingStateForStack = errors.New("can not find State in this stack") + ErrNoManagedResourceToDestroy = errors.New("no managed resources to destroy") + ErrDryrunDestroy = errors.New("dryrun-mode is enabled, no resources will be destroyed") +) + +type StackManager struct { + stackRepo repository.StackRepository + projectRepo repository.ProjectRepository + workspaceRepo repository.WorkspaceRepository +} diff --git a/pkg/server/handler/stack/util.go b/pkg/server/manager/stack/util.go similarity index 51% rename from pkg/server/handler/stack/util.go rename to pkg/server/manager/stack/util.go index 26fc5027..71b6d762 100644 --- a/pkg/server/handler/stack/util.go +++ b/pkg/server/manager/stack/util.go @@ -2,10 +2,14 @@ package stack import ( "context" + "encoding/json" + "errors" "fmt" + "net/http" "path/filepath" "gorm.io/gorm" + apiv1 "kusionstack.io/kusion/pkg/apis/api.kusion.io/v1" v1 "kusionstack.io/kusion/pkg/apis/internal.kusion.io/v1" "kusionstack.io/kusion/pkg/backend" "kusionstack.io/kusion/pkg/backend/storages" @@ -13,35 +17,26 @@ import ( "kusionstack.io/kusion/pkg/domain/entity" engineapi "kusionstack.io/kusion/pkg/engine/api" sourceapi "kusionstack.io/kusion/pkg/engine/api/source" + "kusionstack.io/kusion/pkg/engine/operation/models" + "kusionstack.io/kusion/pkg/engine/state" + "kusionstack.io/kusion/pkg/server/handler" "kusionstack.io/kusion/pkg/server/util" ) -func buildOptions(dryrun bool) *engineapi.APIOptions { - // Construct intent options - // intentOptions := &buildersapi.Options{ - // IsKclPkg: kpmParam, - // WorkDir: workDir, - // Arguments: map[string]string{}, - // NoStyle: true, - // } - // Construct preview api option - // TODO: Complete preview options - // TODO: Operator should be derived from auth info - // TODO: Cluster should be derived from workspace config - previewOptions := &engineapi.APIOptions{ +func BuildOptions(dryrun bool) *engineapi.APIOptions { + executeOptions := &engineapi.APIOptions{ // Operator: "operator", // Cluster: "cluster", // IgnoreFields: []string{}, DryRun: dryrun, } - // return intentOptions, previewOptions - return previewOptions + return executeOptions } // getWorkDirFromSource returns the workdir based on the source // if the source type is local, it will return the path as an absolute path on the local filesystem // if the source type is remote (git for example), it will pull the source and return the path to the pulled source -func getWorkDirFromSource(ctx context.Context, stack *entity.Stack, project *entity.Project) (string, string, error) { +func GetWorkDirFromSource(ctx context.Context, stack *entity.Stack, project *entity.Project) (string, string, error) { logger := util.GetLogger(ctx) logger.Info("Getting workdir from stack source...") // TODO: Also copy the local workdir to /tmp directory? @@ -63,30 +58,6 @@ func getWorkDirFromSource(ctx context.Context, stack *entity.Stack, project *ent } func NewBackendFromEntity(backendEntity entity.Backend) (backend.Backend, error) { - // var emptyCfg bool - // cfg, err := config.GetConfig() - // if errors.Is(err, config.ErrEmptyConfig) { - // emptyCfg = true - // } else if err != nil { - // return nil, err - // } else if cfg.Backends == nil { - // emptyCfg = true - // } - - // var bkCfg *v1.BackendConfig - // if name == "" && (emptyCfg || cfg.Backends.Current == "") { - // // if empty backends config or empty current backend, use default local storage - // bkCfg = &v1.BackendConfig{Type: v1.BackendTypeLocal} - // } else { - // if name == "" { - // name = cfg.Backends.Current - // } - // bkCfg = cfg.Backends.Backends[name] - // if bkCfg == nil { - // return nil, fmt.Errorf("config of backend %s does not exist", name) - // } - // } - // TODO: refactor this so backend.NewBackend() share the same common logic var storage backend.Backend var err error @@ -133,11 +104,38 @@ func NewBackendFromEntity(backendEntity entity.Backend) (backend.Backend, error) return storage, nil } -func (h *Handler) GetBackendFromWorkspaceName(ctx context.Context, workspaceName string) (backend.Backend, error) { +func ProcessChanges(ctx context.Context, w http.ResponseWriter, changes *models.Changes, format string, detail bool) (string, error) { + logger := util.GetLogger(ctx) + logger.Info("Starting previewing stack in StackManager ...") + + if format == engineapi.JSONOutput { + previewChanges, err := json.Marshal(changes) + if err != nil { + return "", err + } + logger.Info(string(previewChanges)) + return string(previewChanges), nil + } + + if changes.AllUnChange() { + logger.Info(NoDiffFound) + return NoDiffFound, nil + } + + // Summary preview table + changes.Summary(w, true) + // detail detection + if detail { + return changes.Diffs(true), nil + } + return "", nil +} + +func (m *StackManager) getBackendFromWorkspaceName(ctx context.Context, workspaceName string) (backend.Backend, error) { logger := util.GetLogger(ctx) logger.Info("Getting backend based on workspace name...") // Get backend by id - workspaceEntity, err := h.workspaceRepo.GetByName(ctx, workspaceName) + workspaceEntity, err := m.workspaceRepo.GetByName(ctx, workspaceName) if err != nil && err == gorm.ErrRecordNotFound { return nil, err } else if err != nil { @@ -150,3 +148,88 @@ func (h *Handler) GetBackendFromWorkspaceName(ctx context.Context, workspaceName } return remoteBackend, nil } + +func (m *StackManager) previewHelper(ctx context.Context, id uint, workspaceName string) (*apiv1.Spec, *models.Changes, state.Storage, error) { + logger := util.GetLogger(ctx) + logger.Info("Starting previewing stack in StackManager ...") + + // Get the stack entity by id + stackEntity, err := m.stackRepo.Get(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil, nil, ErrGettingNonExistingStack + } + return nil, nil, nil, err + } + + // Get project by id + project, err := stackEntity.Project.ConvertToCore() + if err != nil { + return nil, nil, nil, err + } + + // Get stack by id + stack, err := stackEntity.ConvertToCore() + if err != nil { + return nil, nil, nil, err + } + + // Temp: LocalBackend for ws config and Remote Backend for state storage + // TODO: should use variable set eventually and remove this localBackend eventually + wsBackend, err := backend.NewBackend("") + if err != nil { + return nil, nil, nil, err + } + stateBackend, err := m.getBackendFromWorkspaceName(ctx, workspaceName) + if err != nil { + return nil, nil, nil, err + } + + // Get workspace configurations from backend + // TODO: temporarily local for now, should be replaced by variable sets + wsStorage, err := wsBackend.WorkspaceStorage() + if err != nil { + return nil, nil, nil, err + } + ws, err := wsStorage.Get(workspaceName) + if err != nil { + return nil, nil, nil, err + } + + // Build API inputs + // get project to get source and workdir + projectEntity, err := handler.GetProjectByID(ctx, m.projectRepo, stackEntity.Project.ID) + if err != nil { + return nil, nil, nil, err + } + + directory, workDir, err := GetWorkDirFromSource(ctx, stackEntity, projectEntity) + if err != nil { + return nil, nil, nil, err + } + executeOptions := BuildOptions(false) + stack.Path = workDir + + // Cleanup + defer sourceapi.Cleanup(ctx, directory) + + // Generate spec + sp, err := engineapi.GenerateSpecWithSpinner(project, stack, ws, true) + if err != nil { + return nil, nil, nil, err + } + + // return immediately if no resource found in stack + // todo: if there is no resource, should still do diff job; for now, if output is json format, there is no hint + if sp == nil || len(sp.Resources) == 0 { + logger.Info("No resource change found in this stack...") + return nil, nil, nil, nil + } + + // Compute state storage + stateStorage := stateBackend.StateStorage(project.Name, stack.Name, ws.Name) + logger.Info("Local state storage found", "Path", stateStorage) + + changes, err := engineapi.Preview(executeOptions, stateStorage, sp, project, stack) + return sp, changes, stateStorage, err +} diff --git a/pkg/server/manager/workspace/types.go b/pkg/server/manager/workspace/types.go new file mode 100644 index 00000000..7c78d256 --- /dev/null +++ b/pkg/server/manager/workspace/types.go @@ -0,0 +1,26 @@ +package workspace + +import ( + "errors" + + "kusionstack.io/kusion/pkg/domain/repository" +) + +var ( + ErrGettingNonExistingWorkspace = errors.New("the workspace does not exist") + 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") +) + +type WorkspaceManager struct { + workspaceRepo repository.WorkspaceRepository + backendRepo repository.BackendRepository +} + +func NewWorkspaceManager(workspaceRepo repository.WorkspaceRepository, backendRepo repository.BackendRepository) *WorkspaceManager { + return &WorkspaceManager{ + workspaceRepo: workspaceRepo, + backendRepo: backendRepo, + } +} diff --git a/pkg/server/manager/workspace/workspace_manager.go b/pkg/server/manager/workspace/workspace_manager.go new file mode 100644 index 00000000..1adafa28 --- /dev/null +++ b/pkg/server/manager/workspace/workspace_manager.go @@ -0,0 +1,95 @@ +package workspace + +import ( + "context" + "errors" + + "github.com/jinzhu/copier" + "gorm.io/gorm" + "kusionstack.io/kusion/pkg/domain/entity" + "kusionstack.io/kusion/pkg/domain/request" +) + +func (m *WorkspaceManager) ListWorkspaces(ctx context.Context) ([]*entity.Workspace, error) { + workspaceEntities, err := m.workspaceRepo.List(ctx) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrGettingNonExistingWorkspace + } + return nil, err + } + return workspaceEntities, nil +} + +func (m *WorkspaceManager) GetWorkspaceByID(ctx context.Context, id uint) (*entity.Workspace, error) { + existingEntity, err := m.workspaceRepo.Get(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrGettingNonExistingWorkspace + } + return nil, err + } + return existingEntity, nil +} + +func (m *WorkspaceManager) DeleteWorkspaceByID(ctx context.Context, id uint) error { + err := m.workspaceRepo.Delete(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return ErrGettingNonExistingWorkspace + } + return err + } + return nil +} + +func (m *WorkspaceManager) UpdateWorkspaceByID(ctx context.Context, id uint, requestPayload request.UpdateWorkspaceRequest) (*entity.Workspace, error) { + // Convert request payload to domain model + var requestEntity entity.Workspace + if err := copier.Copy(&requestEntity, &requestPayload); err != nil { + return nil, err + } + + // Get the existing workspace by id + updatedEntity, err := m.workspaceRepo.Get(ctx, id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, ErrUpdatingNonExistingWorkspace + } + return nil, err + } + + // Overwrite non-zero values in request entity to existing entity + copier.CopyWithOption(updatedEntity, requestEntity, copier.Option{IgnoreEmpty: true}) + + // Update workspace with repository + err = m.workspaceRepo.Update(ctx, updatedEntity) + if err != nil { + return nil, err + } + return updatedEntity, nil +} + +func (m *WorkspaceManager) CreateWorkspace(ctx context.Context, requestPayload request.CreateWorkspaceRequest) (*entity.Workspace, error) { + // Convert request payload to domain model + var createdEntity entity.Workspace + if err := copier.Copy(&createdEntity, &requestPayload); err != nil { + return nil, err + } + + // Get backend by id + backendEntity, err := m.backendRepo.Get(ctx, requestPayload.BackendID) + if err != nil && err == gorm.ErrRecordNotFound { + return nil, ErrBackendNotFound + } else if err != nil { + return nil, err + } + createdEntity.Backend = backendEntity + + // Create workspace with repository + err = m.workspaceRepo.Create(ctx, &createdEntity) + if err != nil { + return nil, err + } + return &createdEntity, nil +} diff --git a/pkg/server/route/route.go b/pkg/server/route/route.go index 71594efc..7b35147c 100644 --- a/pkg/server/route/route.go +++ b/pkg/server/route/route.go @@ -19,6 +19,11 @@ import ( "kusionstack.io/kusion/pkg/server/handler/source" "kusionstack.io/kusion/pkg/server/handler/stack" "kusionstack.io/kusion/pkg/server/handler/workspace" + backendmanager "kusionstack.io/kusion/pkg/server/manager/backend" + organizationmanager "kusionstack.io/kusion/pkg/server/manager/organization" + sourcemanager "kusionstack.io/kusion/pkg/server/manager/source" + stackmanager "kusionstack.io/kusion/pkg/server/manager/stack" + workspacemanager "kusionstack.io/kusion/pkg/server/manager/workspace" appmiddleware "kusionstack.io/kusion/pkg/server/middleware" "kusionstack.io/kusion/pkg/server/util" @@ -87,13 +92,19 @@ func setupRestAPIV1( workspaceRepo := persistence.NewWorkspaceRepository(config.DB) backendRepo := persistence.NewBackendRepository(config.DB) + stackManager := stackmanager.NewStackManager(stackRepo, projectRepo, workspaceRepo) + sourceManager := sourcemanager.NewSourceManager(sourceRepo) + organizationManager := organizationmanager.NewOrganizationManager(organizationRepo) + backendManager := backendmanager.NewBackendManager(backendRepo) + workspaceManager := workspacemanager.NewWorkspaceManager(workspaceRepo, backendRepo) + // Set up the handlers for the resources. - sourceHandler, err := source.NewHandler(sourceRepo) + sourceHandler, err := source.NewHandler(sourceManager) if err != nil { logger.Error(err, "Error creating source handler...", "error", err) return } - orgHandler, err := organization.NewHandler(organizationRepo) + orgHandler, err := organization.NewHandler(organizationManager) if err != nil { logger.Error(err, "Error creating org handler...", "error", err) return @@ -103,17 +114,17 @@ func setupRestAPIV1( logger.Error(err, "Error creating project handler...", "error", err) return } - stackHandler, err := stack.NewHandler(organizationRepo, projectRepo, stackRepo, sourceRepo, workspaceRepo) + stackHandler, err := stack.NewHandler(stackManager) if err != nil { logger.Error(err, "Error creating stack handler...", "error", err) return } - workspaceHandler, err := workspace.NewHandler(workspaceRepo, backendRepo) + workspaceHandler, err := workspace.NewHandler(workspaceManager, backendManager) if err != nil { logger.Error(err, "Error creating workspace handler...", "error", err) return } - backendHandler, err := backend.NewHandler(backendRepo) + backendHandler, err := backend.NewHandler(backendManager) if err != nil { logger.Error(err, "Error creating backend handler...", "error", err) return @@ -132,7 +143,7 @@ func setupRestAPIV1( r.Route("/stack", func(r chi.Router) { r.Route("/{stackID}", func(r chi.Router) { r.Post("/", stackHandler.CreateStack()) - r.Post("/build", stackHandler.BuildStack()) + r.Post("/generate", stackHandler.GenerateStack()) r.Post("/preview", stackHandler.PreviewStack()) r.Post("/apply", stackHandler.ApplyStack()) r.Post("/destroy", stackHandler.DestroyStack()) @@ -178,24 +189,4 @@ func setupRestAPIV1( }) r.Get("/", backendHandler.ListBackends()) }) - // r.Route("/project", func(r chi.Router) { - // //r.Get("/", projectHandler.ListProjects()) - // r.Route("/{projectName}", func(r chi.Router) { - // // r.Post("/", projectHandler.CreateProject()) - // // r.Get("/", projectHandler.GetProject()) - // // r.Put("/", projectHandler.UpdateProject()) - // // r.Delete("/", projectHandler.DeleteProject()) - // r.Route("/stack", func(r chi.Router) { - // //r.Get("/", stackHandler.ListStacks()) - // r.Route("/{stackName}", func(r chi.Router) { - // r.Post("/", stackHandler.CreateStack()) - // // r.Get("/", stackHandler.GetStack()) - // // r.Put("/", stackHandler.UpdateStack()) - // // r.Delete("/", stackHandler.DeleteStack()) - // r.Post("/preview", stack.ExecutePreview()) - // //r.Post("/apply", stack.ExecuteApply()) - // }) - // }) - // }) - // }) }