From 194c667292042de213d3500f12cc0398d880bd37 Mon Sep 17 00:00:00 2001 From: "taekyu.kang" Date: Tue, 24 Oct 2023 20:36:40 +0900 Subject: [PATCH] feature. implementation importing cluster --- api/swagger/docs.go | 84 +++++++++++++++++++++++++++++++ api/swagger/swagger.json | 84 +++++++++++++++++++++++++++++++ api/swagger/swagger.yaml | 54 ++++++++++++++++++++ internal/delivery/http/cluster.go | 44 ++++++++++++++++ internal/repository/cluster.go | 2 +- internal/route/route.go | 1 + internal/usecase/cluster.go | 65 ++++++++++++++++++++++++ pkg/domain/cluster.go | 14 ++++++ pkg/httpErrors/errorCode.go | 1 + 9 files changed, 348 insertions(+), 1 deletion(-) diff --git a/api/swagger/docs.go b/api/swagger/docs.go index 0849170b..aac81160 100644 --- a/api/swagger/docs.go +++ b/api/swagger/docs.go @@ -634,6 +634,45 @@ const docTemplate = `{ } } }, + "/clusters/import": { + "post": { + "security": [ + { + "JWT": [] + } + ], + "description": "Import cluster", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Clusters" + ], + "summary": "Import cluster", + "parameters": [ + { + "description": "import cluster request", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.ImportClusterRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.ImportClusterResponse" + } + } + } + } + }, "/clusters/{clusterId}": { "get": { "security": [ @@ -4398,6 +4437,12 @@ const docTemplate = `{ "isStack": { "type": "boolean" }, + "kubeconfig": { + "type": "array", + "items": { + "type": "integer" + } + }, "name": { "type": "string" }, @@ -5661,6 +5706,45 @@ const docTemplate = `{ } } }, + "domain.ImportClusterRequest": { + "type": "object", + "required": [ + "name", + "organizationId", + "stackTemplateId" + ], + "properties": { + "clusterType": { + "type": "string" + }, + "description": { + "type": "string" + }, + "kubeconfig": { + "type": "array", + "items": { + "type": "integer" + } + }, + "name": { + "type": "string" + }, + "organizationId": { + "type": "string" + }, + "stackTemplateId": { + "type": "string" + } + } + }, + "domain.ImportClusterResponse": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + } + }, "domain.ListOrganizationBody": { "type": "object", "properties": { diff --git a/api/swagger/swagger.json b/api/swagger/swagger.json index 77cf3a4a..508f52e8 100644 --- a/api/swagger/swagger.json +++ b/api/swagger/swagger.json @@ -627,6 +627,45 @@ } } }, + "/clusters/import": { + "post": { + "security": [ + { + "JWT": [] + } + ], + "description": "Import cluster", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Clusters" + ], + "summary": "Import cluster", + "parameters": [ + { + "description": "import cluster request", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.ImportClusterRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.ImportClusterResponse" + } + } + } + } + }, "/clusters/{clusterId}": { "get": { "security": [ @@ -4391,6 +4430,12 @@ "isStack": { "type": "boolean" }, + "kubeconfig": { + "type": "array", + "items": { + "type": "integer" + } + }, "name": { "type": "string" }, @@ -5654,6 +5699,45 @@ } } }, + "domain.ImportClusterRequest": { + "type": "object", + "required": [ + "name", + "organizationId", + "stackTemplateId" + ], + "properties": { + "clusterType": { + "type": "string" + }, + "description": { + "type": "string" + }, + "kubeconfig": { + "type": "array", + "items": { + "type": "integer" + } + }, + "name": { + "type": "string" + }, + "organizationId": { + "type": "string" + }, + "stackTemplateId": { + "type": "string" + } + } + }, + "domain.ImportClusterResponse": { + "type": "object", + "properties": { + "id": { + "type": "string" + } + } + }, "domain.ListOrganizationBody": { "type": "object", "properties": { diff --git a/api/swagger/swagger.yaml b/api/swagger/swagger.yaml index 04de5300..54406791 100644 --- a/api/swagger/swagger.yaml +++ b/api/swagger/swagger.yaml @@ -391,6 +391,10 @@ definitions: type: string isStack: type: boolean + kubeconfig: + items: + type: integer + type: array name: type: string organizationId: @@ -1232,6 +1236,32 @@ definitions: type: string type: object type: object + domain.ImportClusterRequest: + properties: + clusterType: + type: string + description: + type: string + kubeconfig: + items: + type: integer + type: array + name: + type: string + organizationId: + type: string + stackTemplateId: + type: string + required: + - name + - organizationId + - stackTemplateId + type: object + domain.ImportClusterResponse: + properties: + id: + type: string + type: object domain.ListOrganizationBody: properties: createdAt: @@ -2502,6 +2532,30 @@ paths: summary: Get cluster site values for creating tags: - Clusters + /clusters/import: + post: + consumes: + - application/json + description: Import cluster + parameters: + - description: import cluster request + in: body + name: body + required: true + schema: + $ref: '#/definitions/domain.ImportClusterRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.ImportClusterResponse' + security: + - JWT: [] + summary: Import cluster + tags: + - Clusters /organizations: get: consumes: diff --git a/internal/delivery/http/cluster.go b/internal/delivery/http/cluster.go index cb8b8f6e..28a850a8 100644 --- a/internal/delivery/http/cluster.go +++ b/internal/delivery/http/cluster.go @@ -4,6 +4,7 @@ import ( "fmt" "net/http" + "github.com/google/uuid" "github.com/gorilla/mux" "github.com/openinfradev/tks-api/internal/pagination" "github.com/openinfradev/tks-api/internal/serializer" @@ -190,6 +191,49 @@ func (h *ClusterHandler) CreateCluster(w http.ResponseWriter, r *http.Request) { ResponseJSON(w, r, http.StatusOK, out) } +// ImportCluster godoc +// @Tags Clusters +// @Summary Import cluster +// @Description Import cluster +// @Accept json +// @Produce json +// @Param body body domain.ImportClusterRequest true "import cluster request" +// @Success 200 {object} domain.ImportClusterResponse +// @Router /clusters/import [post] +// @Security JWT +func (h *ClusterHandler) ImportCluster(w http.ResponseWriter, r *http.Request) { + input := domain.ImportClusterRequest{} + err := UnmarshalRequestInput(r, &input) + if err != nil { + ErrorJSON(w, r, err) + return + } + + var dto domain.Cluster + if err = serializer.Map(input, &dto); err != nil { + log.InfoWithContext(r.Context(), err) + } + + if err = serializer.Map(input, &dto.Conf); err != nil { + log.InfoWithContext(r.Context(), err) + } + dto.Conf.SetDefault() + log.InfoWithContext(r.Context(), dto.Conf) + + dto.CloudService = "AWS" + dto.CloudAccountId = uuid.Nil + clusterId, err := h.usecase.Import(r.Context(), dto) + if err != nil { + ErrorJSON(w, r, err) + return + } + + var out domain.ImportClusterResponse + out.ID = clusterId.String() + + ResponseJSON(w, r, http.StatusOK, out) +} + // InstallCluster godoc // @Tags Clusters // @Summary Install cluster on tks cluster diff --git a/internal/repository/cluster.go b/internal/repository/cluster.go index 570bc795..a34b87af 100644 --- a/internal/repository/cluster.go +++ b/internal/repository/cluster.go @@ -215,7 +215,7 @@ func (r *ClusterRepository) GetByName(organizationId string, name string) (out d func (r *ClusterRepository) Create(dto domain.Cluster) (clusterId domain.ClusterId, err error) { var cloudAccountId *uuid.UUID cloudAccountId = &dto.CloudAccountId - if dto.CloudService == domain.CloudService_BYOH { + if dto.CloudService == domain.CloudService_BYOH || dto.CloudAccountId == uuid.Nil { cloudAccountId = nil } cluster := Cluster{ diff --git a/internal/route/route.go b/internal/route/route.go index 7edbd419..f5ecbd4a 100644 --- a/internal/route/route.go +++ b/internal/route/route.go @@ -111,6 +111,7 @@ func SetupRouter(db *gorm.DB, argoClient argowf.ArgoClient, kc keycloak.IKeycloa clusterHandler := delivery.NewClusterHandler(usecase.NewClusterUsecase(repoFactory, argoClient, cache)) r.Handle(API_PREFIX+API_VERSION+"/clusters", authMiddleware.Handle(http.HandlerFunc(clusterHandler.CreateCluster))).Methods(http.MethodPost) r.Handle(API_PREFIX+API_VERSION+"/clusters", authMiddleware.Handle(http.HandlerFunc(clusterHandler.GetClusters))).Methods(http.MethodGet) + r.Handle(API_PREFIX+API_VERSION+"/clusters/import", authMiddleware.Handle(http.HandlerFunc(clusterHandler.ImportCluster))).Methods(http.MethodPost) r.Handle(API_PREFIX+API_VERSION+"/clusters/{clusterId}", authMiddleware.Handle(http.HandlerFunc(clusterHandler.GetCluster))).Methods(http.MethodGet) r.Handle(API_PREFIX+API_VERSION+"/clusters/{clusterId}", authMiddleware.Handle(http.HandlerFunc(clusterHandler.DeleteCluster))).Methods(http.MethodDelete) r.Handle(API_PREFIX+API_VERSION+"/clusters/{clusterId}/site-values", authMiddleware.Handle(http.HandlerFunc(clusterHandler.GetClusterSiteValues))).Methods(http.MethodGet) diff --git a/internal/usecase/cluster.go b/internal/usecase/cluster.go index 13d2be0b..de929b82 100644 --- a/internal/usecase/cluster.go +++ b/internal/usecase/cluster.go @@ -2,6 +2,7 @@ package usecase import ( "context" + "encoding/base64" "encoding/json" "fmt" "strings" @@ -33,6 +34,7 @@ type IClusterUsecase interface { Fetch(ctx context.Context, organizationId string, pg *pagination.Pagination) ([]domain.Cluster, error) FetchByCloudAccountId(ctx context.Context, cloudAccountId uuid.UUID, pg *pagination.Pagination) (out []domain.Cluster, err error) Create(ctx context.Context, dto domain.Cluster) (clusterId domain.ClusterId, err error) + Import(ctx context.Context, dto domain.Cluster) (clusterId domain.ClusterId, err error) Bootstrap(ctx context.Context, dto domain.Cluster) (clusterId domain.ClusterId, err error) Install(ctx context.Context, clusterId domain.ClusterId) (err error) Get(ctx context.Context, clusterId domain.ClusterId) (out domain.Cluster, err error) @@ -48,6 +50,7 @@ type ClusterUsecase struct { appGroupRepo repository.IAppGroupRepository cloudAccountRepo repository.ICloudAccountRepository stackTemplateRepo repository.IStackTemplateRepository + organizationRepo repository.IOrganizationRepository argo argowf.ArgoClient cache *gcache.Cache } @@ -58,6 +61,7 @@ func NewClusterUsecase(r repository.Repository, argoClient argowf.ArgoClient, ca appGroupRepo: r.AppGroup, cloudAccountRepo: r.CloudAccount, stackTemplateRepo: r.StackTemplate, + organizationRepo: r.Organization, argo: argoClient, cache: cache, } @@ -209,6 +213,67 @@ func (u *ClusterUsecase) Create(ctx context.Context, dto domain.Cluster) (cluste return clusterId, nil } +func (u *ClusterUsecase) Import(ctx context.Context, dto domain.Cluster) (clusterId domain.ClusterId, err error) { + user, ok := request.UserFrom(ctx) + if !ok { + return "", httpErrors.NewBadRequestError(fmt.Errorf("Invalid token"), "", "") + } + + _, err = u.repo.GetByName(dto.OrganizationId, dto.Name) + if err == nil { + return "", httpErrors.NewBadRequestError(httpErrors.DuplicateResource, "", "") + } + + _, err = u.organizationRepo.Get(dto.OrganizationId) + if err != nil { + return "", httpErrors.NewBadRequestError(fmt.Errorf("Invalid organizationId"), "", "") + } + + // check stackTemplate + stackTemplate, err := u.stackTemplateRepo.Get(dto.StackTemplateId) + if err != nil { + return "", httpErrors.NewBadRequestError(errors.Wrap(err, "Invalid stackTemplateId"), "", "") + } + if stackTemplate.CloudService != dto.CloudService { + return "", httpErrors.NewBadRequestError(fmt.Errorf("Invalid cloudService for stackTemplate "), "", "") + } + + userId := user.GetUserId() + dto.CreatorId = &userId + clusterId, err = u.repo.Create(dto) + if err != nil { + return "", errors.Wrap(err, "Failed to create cluster") + } + + kubeconfigBase64 := base64.StdEncoding.EncodeToString([]byte(dto.Kubeconfig)) + + workflowId, err := u.argo.SumbitWorkflowFromWftpl( + "import-tks-usercluster", + argowf.SubmitOptions{ + Parameters: []string{ + fmt.Sprintf("tks_api_url=%s", viper.GetString("external-address")), + "contract_id=" + dto.OrganizationId, + "cluster_id=" + clusterId.String(), + "template_name=" + stackTemplate.Template, + "kubeconfig=" + kubeconfigBase64, + "git_account=" + viper.GetString("git-account"), + "keycloak_url=" + strings.TrimSuffix(viper.GetString("keycloak-address"), "/auth"), + "base_repo_branch=" + viper.GetString("revision"), + }, + }) + if err != nil { + log.ErrorWithContext(ctx, "failed to submit argo workflow template. err : ", err) + return "", err + } + log.InfoWithContext(ctx, "Successfully submited workflow: ", workflowId) + + if err := u.repo.InitWorkflow(clusterId, workflowId, domain.ClusterStatus_INSTALLING); err != nil { + return "", errors.Wrap(err, "Failed to initialize status") + } + + return clusterId, nil +} + func (u *ClusterUsecase) Bootstrap(ctx context.Context, dto domain.Cluster) (clusterId domain.ClusterId, err error) { user, ok := request.UserFrom(ctx) if !ok { diff --git a/pkg/domain/cluster.go b/pkg/domain/cluster.go index 6964a83a..2752eee0 100644 --- a/pkg/domain/cluster.go +++ b/pkg/domain/cluster.go @@ -107,6 +107,7 @@ type Cluster struct { ByoClusterEndpointHost string ByoClusterEndpointPort int IsStack bool + Kubeconfig []byte } type ClusterConf struct { @@ -188,10 +189,23 @@ type CreateClusterRequest struct { TksUserNodeType string `json:"tksUserNodeType,omitempty"` } +type ImportClusterRequest struct { + OrganizationId string `json:"organizationId" validate:"required"` + StackTemplateId string `json:"stackTemplateId" validate:"required"` + Name string `json:"name" validate:"required,name"` + Description string `json:"description"` + ClusterType string `json:"clusterType"` + Kubeconfig []byte `json:"kubeconfig"` +} + type CreateClusterResponse struct { ID string `json:"id"` } +type ImportClusterResponse struct { + ID string `json:"id"` +} + type ClusterConfResponse struct { TksCpNode int `json:"tksCpNode"` TksCpNodeMax int `json:"tksCpNodeMax,omitempty"` diff --git a/pkg/httpErrors/errorCode.go b/pkg/httpErrors/errorCode.go index ff6b05ae..0fa40986 100644 --- a/pkg/httpErrors/errorCode.go +++ b/pkg/httpErrors/errorCode.go @@ -46,6 +46,7 @@ var errorMap = map[ErrorCode]string{ // Cluster "CL_INVALID_BYOH_CLUSTER_ENDPOINT": "BYOH 타입의 클러스터 생성을 위한 cluster endpoint 가 유효하지 않습니다.", + "CL_INVALID_CLUSTER_TYPE_AWS": "클러스터 타입이 유효하지 않습니다.", // Stack "S_INVALID_STACK_TEMPLATE": "스택 템플릿을 가져올 수 없습니다.",