diff --git a/api/cluster/ClusterRestHandler.go b/api/cluster/ClusterRestHandler.go index c6baecb85d..2d2f946cb9 100644 --- a/api/cluster/ClusterRestHandler.go +++ b/api/cluster/ClusterRestHandler.go @@ -20,14 +20,15 @@ import ( "context" "encoding/json" "errors" - bean2 "github.com/devtron-labs/devtron/pkg/cluster/bean" - "github.com/devtron-labs/devtron/pkg/cluster/environment" - "github.com/devtron-labs/devtron/pkg/cluster/rbac" "net/http" "strconv" "strings" "time" + bean2 "github.com/devtron-labs/devtron/pkg/cluster/bean" + "github.com/devtron-labs/devtron/pkg/cluster/environment" + "github.com/devtron-labs/devtron/pkg/cluster/rbac" + "github.com/devtron-labs/devtron/pkg/auth/authorisation/casbin" "github.com/devtron-labs/devtron/pkg/auth/user" "github.com/devtron-labs/devtron/pkg/genericNotes" @@ -60,6 +61,7 @@ type ClusterRestHandler interface { GetClusterNamespaces(w http.ResponseWriter, r *http.Request) GetAllClusterNamespaces(w http.ResponseWriter, r *http.Request) FindAllForClusterPermission(w http.ResponseWriter, r *http.Request) + FindByIds(w http.ResponseWriter, r *http.Request) } type ClusterRestHandlerImpl struct { @@ -296,6 +298,59 @@ func (impl ClusterRestHandlerImpl) FindAll(w http.ResponseWriter, r *http.Reques common.WriteJsonResp(w, err, result, http.StatusOK) } +func (impl ClusterRestHandlerImpl) FindByIds(w http.ResponseWriter, r *http.Request) { + token := r.Header.Get("token") + + // Parse clusterId query parameter + clusterIdsStr := r.URL.Query().Get("clusterId") + if clusterIdsStr == "" { + // If no clusterId parameter, return all clusters (same as FindAll) + impl.FindAll(w, r) + return + } + + // Parse comma-separated cluster IDs + var clusterIds []int + clusterIdStrs := strings.Split(clusterIdsStr, ",") + for _, idStr := range clusterIdStrs { + idStr = strings.TrimSpace(idStr) + if idStr == "" { + continue + } + id, err := strconv.Atoi(idStr) + if err != nil { + impl.logger.Errorw("request err, FindByIds", "error", err, "clusterId", idStr) + common.WriteJsonResp(w, err, nil, http.StatusBadRequest) + return + } + clusterIds = append(clusterIds, id) + } + + if len(clusterIds) == 0 { + // If no valid cluster IDs, return empty result + common.WriteJsonResp(w, nil, []*bean2.ClusterBean{}, http.StatusOK) + return + } + + clusterList, err := impl.clusterService.FindByIdsWithoutConfig(clusterIds) + if err != nil { + impl.logger.Errorw("service err, FindByIds", "err", err) + common.WriteJsonResp(w, err, nil, http.StatusInternalServerError) + return + } + + // RBAC enforcer applying + var result []*bean2.ClusterBean + for _, item := range clusterList { + if ok := impl.enforcer.Enforce(token, casbin.ResourceCluster, casbin.ActionGet, item.ClusterName); ok { + result = append(result, item) + } + } + //RBAC enforcer Ends + + common.WriteJsonResp(w, nil, result, http.StatusOK) +} + func (impl ClusterRestHandlerImpl) FindById(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) id := vars["id"] @@ -671,7 +726,14 @@ func (impl ClusterRestHandlerImpl) GetClusterNamespaces(w http.ResponseWriter, r allClusterNamespaces, err := impl.clusterService.FindAllNamespacesByUserIdAndClusterId(userId, clusterId, isActionUserSuperAdmin) if err != nil { - common.WriteJsonResp(w, err, nil, http.StatusInternalServerError) + // Check if it's a cluster connectivity error and return appropriate status code + if err.Error() == cluster.ErrClusterNotReachable { + impl.logger.Errorw("cluster connectivity error in GetClusterNamespaces", "error", err, "clusterId", clusterId) + common.WriteJsonResp(w, err, nil, http.StatusBadRequest) + } else { + impl.logger.Errorw("error in GetClusterNamespaces", "error", err, "clusterId", clusterId) + common.WriteJsonResp(w, err, nil, http.StatusInternalServerError) + } return } common.WriteJsonResp(w, nil, allClusterNamespaces, http.StatusOK) diff --git a/api/cluster/ClusterRouter.go b/api/cluster/ClusterRouter.go index abd233e953..07fb4f80ba 100644 --- a/api/cluster/ClusterRouter.go +++ b/api/cluster/ClusterRouter.go @@ -57,6 +57,11 @@ func (impl ClusterRouterImpl) InitClusterRouter(clusterRouter *mux.Router) { Queries("id", "{id}"). HandlerFunc(impl.clusterRestHandler.FindNoteByClusterId) + clusterRouter.Path(""). + Methods("GET"). + Queries("clusterId", "{clusterId}"). + HandlerFunc(impl.clusterRestHandler.FindByIds) + clusterRouter.Path(""). Methods("GET"). HandlerFunc(impl.clusterRestHandler.FindAll) diff --git a/pkg/cluster/ClusterService.go b/pkg/cluster/ClusterService.go index 6a1741e9c2..669487d541 100644 --- a/pkg/cluster/ClusterService.go +++ b/pkg/cluster/ClusterService.go @@ -20,6 +20,11 @@ import ( "context" "encoding/json" "fmt" + "log" + "net/url" + "sync" + "time" + "github.com/devtron-labs/common-lib/async" informerBean "github.com/devtron-labs/common-lib/informer" "github.com/devtron-labs/common-lib/utils/k8s/commonBean" @@ -32,10 +37,6 @@ import ( "github.com/devtron-labs/devtron/pkg/cluster/read" cronUtil "github.com/devtron-labs/devtron/util/cron" "github.com/robfig/cron/v3" - "log" - "net/url" - "sync" - "time" "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" "github.com/devtron-labs/common-lib/utils/k8s" @@ -75,6 +76,7 @@ type ClusterService interface { FindById(id int) (*bean.ClusterBean, error) FindByIdWithoutConfig(id int) (*bean.ClusterBean, error) FindByIds(id []int) ([]bean.ClusterBean, error) + FindByIdsWithoutConfig(ids []int) ([]*bean.ClusterBean, error) Update(ctx context.Context, bean *bean.ClusterBean, userId int32) (*bean.ClusterBean, error) Delete(bean *bean.ClusterBean, userId int32) error @@ -355,6 +357,21 @@ func (impl *ClusterServiceImpl) FindByIds(ids []int) ([]bean.ClusterBean, error) return beans, nil } +func (impl *ClusterServiceImpl) FindByIdsWithoutConfig(ids []int) ([]*bean.ClusterBean, error) { + models, err := impl.clusterRepository.FindByIds(ids) + if err != nil { + return nil, err + } + var beans []*bean.ClusterBean + for _, model := range models { + bean := adapter.GetClusterBean(model) + //empty bearer token as it will be hidden for user + bean.Config = map[string]string{commonBean.BearerToken: ""} + beans = append(beans, &bean) + } + return beans, nil +} + func (impl *ClusterServiceImpl) Update(ctx context.Context, bean *bean.ClusterBean, userId int32) (*bean.ClusterBean, error) { model, err := impl.clusterRepository.FindById(bean.Id) if err != nil { @@ -640,6 +657,11 @@ func (impl *ClusterServiceImpl) GetAllClusterNamespaces() map[string][]string { return result } +const ( + // Cluster connectivity error constants + ErrClusterNotReachable = "cluster is not reachable" +) + func (impl *ClusterServiceImpl) FindAllNamespacesByUserIdAndClusterId(userId int32, clusterId int, isActionUserSuperAdmin bool) ([]string, error) { result := make([]string, 0) clusterBean, err := impl.clusterReadService.FindById(clusterId) @@ -647,6 +669,13 @@ func (impl *ClusterServiceImpl) FindAllNamespacesByUserIdAndClusterId(userId int impl.logger.Errorw("failed to find cluster for id", "error", err, "clusterId", clusterId) return nil, err } + + // Check if cluster has connection errors + if len(clusterBean.ErrorInConnecting) > 0 { + impl.logger.Errorw("cluster is not reachable", "clusterId", clusterId, "clusterName", clusterBean.ClusterName, "error", clusterBean.ErrorInConnecting) + return nil, fmt.Errorf("%s: %s", ErrClusterNotReachable, clusterBean.ErrorInConnecting) + } + namespaceListGroupByCLuster := impl.K8sInformerFactory.GetLatestNamespaceListGroupByCLuster() namespaces := namespaceListGroupByCLuster[clusterBean.ClusterName] if len(namespaces) == 0 { diff --git a/pkg/cluster/ClusterServiceExtended.go b/pkg/cluster/ClusterServiceExtended.go index e4465c07cb..8fe04bf571 100644 --- a/pkg/cluster/ClusterServiceExtended.go +++ b/pkg/cluster/ClusterServiceExtended.go @@ -19,14 +19,15 @@ package cluster import ( "context" "fmt" + "net/http" + "strings" + "time" + "github.com/devtron-labs/common-lib/utils/k8s/commonBean" "github.com/devtron-labs/devtron/client/argocdServer" "github.com/devtron-labs/devtron/pkg/cluster/bean" "github.com/devtron-labs/devtron/pkg/cluster/environment/repository" "github.com/devtron-labs/devtron/pkg/deployment/gitOps/config" - "net/http" - "strings" - "time" cluster3 "github.com/argoproj/argo-cd/v2/pkg/apiclient/cluster" "github.com/devtron-labs/devtron/client/grafana" @@ -75,6 +76,14 @@ func (impl *ClusterServiceImplExtended) FindAllWithoutConfig() ([]*bean.ClusterB return beans, nil } +func (impl *ClusterServiceImplExtended) FindByIdsWithoutConfig(ids []int) ([]*bean.ClusterBean, error) { + beans, err := impl.ClusterServiceImpl.FindByIdsWithoutConfig(ids) + if err != nil { + return nil, err + } + return impl.GetClusterFullModeDTO(beans) +} + func (impl *ClusterServiceImplExtended) GetClusterFullModeDTO(beans []*bean.ClusterBean) ([]*bean.ClusterBean, error) { //devtron full mode logic var clusterIds []int diff --git a/pkg/pipeline/workflowStatus/WorkflowStageStatusService.go b/pkg/pipeline/workflowStatus/WorkflowStageStatusService.go index d583ef9261..df576a806f 100644 --- a/pkg/pipeline/workflowStatus/WorkflowStageStatusService.go +++ b/pkg/pipeline/workflowStatus/WorkflowStageStatusService.go @@ -2,11 +2,17 @@ package workflowStatus import ( "encoding/json" + "slices" + "strconv" + "strings" + "time" + "github.com/argoproj/argo-workflows/v3/pkg/apis/workflow/v1alpha1" "github.com/devtron-labs/devtron/api/bean" "github.com/devtron-labs/devtron/internal/sql/repository/pipelineConfig" "github.com/devtron-labs/devtron/internal/sql/repository/pipelineConfig/bean/workflow/cdWorkflow" bean3 "github.com/devtron-labs/devtron/pkg/bean" + envRepository "github.com/devtron-labs/devtron/pkg/cluster/environment/repository" "github.com/devtron-labs/devtron/pkg/pipeline/constants" "github.com/devtron-labs/devtron/pkg/pipeline/types" "github.com/devtron-labs/devtron/pkg/pipeline/workflowStatus/adapter" @@ -16,9 +22,6 @@ import ( "github.com/devtron-labs/devtron/pkg/sql" "github.com/go-pg/pg" "go.uber.org/zap" - "slices" - "strings" - "time" ) type WorkFlowStageStatusService interface { @@ -35,6 +38,7 @@ type WorkFlowStageStatusServiceImpl struct { workflowStatusRepository repository.WorkflowStageRepository ciWorkflowRepository pipelineConfig.CiWorkflowRepository cdWorkflowRepository pipelineConfig.CdWorkflowRepository + envRepository envRepository.EnvironmentRepository transactionManager sql.TransactionWrapper config *types.CiConfig } @@ -43,6 +47,7 @@ func NewWorkflowStageFlowStatusServiceImpl(logger *zap.SugaredLogger, workflowStatusRepository repository.WorkflowStageRepository, ciWorkflowRepository pipelineConfig.CiWorkflowRepository, cdWorkflowRepository pipelineConfig.CdWorkflowRepository, + envRepository envRepository.EnvironmentRepository, transactionManager sql.TransactionWrapper, ) *WorkFlowStageStatusServiceImpl { wfStageServiceImpl := &WorkFlowStageStatusServiceImpl{ @@ -50,6 +55,7 @@ func NewWorkflowStageFlowStatusServiceImpl(logger *zap.SugaredLogger, workflowStatusRepository: workflowStatusRepository, ciWorkflowRepository: ciWorkflowRepository, cdWorkflowRepository: cdWorkflowRepository, + envRepository: envRepository, transactionManager: transactionManager, } ciConfig, err := types.GetCiConfig() @@ -109,9 +115,32 @@ func (impl *WorkFlowStageStatusServiceImpl) updatePodStages(currentWorkflowStage //update pod stage status by using convertPodStatusToDevtronStatus for _, stage := range currentWorkflowStages { if stage.StatusFor == bean2.WORKFLOW_STAGE_STATUS_TYPE_POD { - // add pod name in stage metadata if not empty + // add pod name and clusterId in stage metadata if not empty if len(podName) > 0 { - marshalledMetadata, _ := json.Marshal(map[string]string{"podName": podName}) + metadata := map[string]string{"podName": podName} + + // Try to get clusterId from the workflow + if stage.WorkflowType == bean.CI_WORKFLOW_TYPE.String() { + // For CI workflows, get clusterId from environment + ciWorkflow, err := impl.ciWorkflowRepository.FindById(stage.WorkflowId) + if err == nil && ciWorkflow.EnvironmentId != 0 { + env, err := impl.envRepository.FindById(ciWorkflow.EnvironmentId) + if err == nil && env != nil && env.Cluster != nil { + metadata["clusterId"] = strconv.Itoa(env.Cluster.Id) + } + } + } else if stage.WorkflowType == bean.CD_WORKFLOW_TYPE_PRE.String() || stage.WorkflowType == bean.CD_WORKFLOW_TYPE_POST.String() || stage.WorkflowType == bean.CD_WORKFLOW_TYPE_DEPLOY.String() { + // For CD workflows, get clusterId from environment + cdWorkflowRunner, err := impl.cdWorkflowRepository.FindWorkflowRunnerById(stage.WorkflowId) + if err == nil && cdWorkflowRunner != nil && cdWorkflowRunner.CdWorkflow != nil && cdWorkflowRunner.CdWorkflow.Pipeline != nil { + env, err := impl.envRepository.FindById(cdWorkflowRunner.CdWorkflow.Pipeline.EnvironmentId) + if err == nil && env != nil && env.Cluster != nil { + metadata["clusterId"] = strconv.Itoa(env.Cluster.Id) + } + } + } + + marshalledMetadata, _ := json.Marshal(metadata) stage.Metadata = string(marshalledMetadata) } switch podStatus { diff --git a/specs/cluster_api_spec.yaml b/specs/cluster_api_spec.yaml index d1fa91e613..78cd252547 100644 --- a/specs/cluster_api_spec.yaml +++ b/specs/cluster_api_spec.yaml @@ -49,13 +49,24 @@ paths: required: true schema: type: integer + - name: clusterId + in: query + description: comma-separated list of cluster IDs to filter clusters. If not provided, returns all clusters. + required: false + schema: + type: string + example: "1,2,3" responses: '200': - description: Successfully get cluster + description: Successfully get cluster(s) content: application/json: schema: - $ref: '#/components/schemas/ClusterBean' + oneOf: + - $ref: '#/components/schemas/ClusterBean' + - type: array + items: + $ref: '#/components/schemas/ClusterBean' '400': description: Bad Request. Input Validation(decode) error/wrong request body. content: diff --git a/specs/swagger/openapi.yaml b/specs/swagger/openapi.yaml index da87266b2e..96bf7a84e7 100644 --- a/specs/swagger/openapi.yaml +++ b/specs/swagger/openapi.yaml @@ -1832,13 +1832,24 @@ paths: required: true schema: type: integer + - name: clusterId + in: query + description: comma-separated list of cluster IDs to filter clusters. If not provided, returns all clusters. + required: false + schema: + type: string + example: "1,2,3" responses: '200': - description: Successfully get cluster + description: Successfully get cluster(s) content: application/json: schema: - $ref: '#/components/schemas/ClusterBean' + oneOf: + - $ref: '#/components/schemas/ClusterBean' + - type: array + items: + $ref: '#/components/schemas/ClusterBean' '400': description: Bad Request. Input Validation(decode) error/wrong request body. content: diff --git a/specs/workflow/workflow-stage-status.internal.yaml b/specs/workflow/workflow-stage-status.internal.yaml index 549951a0f9..999e0af05a 100644 --- a/specs/workflow/workflow-stage-status.internal.yaml +++ b/specs/workflow/workflow-stage-status.internal.yaml @@ -13,7 +13,7 @@ paths: application/json: schema: $ref: '#/components/schemas/GetWorkflowStatusResponse' - example: '{"status":"In progress","startTime":"1","endTime":"","message":"e-message","podStatus":"Running","podName":"pod-name","workflowExecutionStages":{"workflow":[{"stageName":"Preparation","status":"SUCCESS","startTime":"1","endTime":"2","message":"p-message","metadata":{}},{"stageName":"Execution","status":"STARTED","startTime":"2","endTime":"","message":"e-message","metadata":{}}],"pod":[{"stageName":"Execution","status":"STARTED","startTime":"2","endTime":"","message":"e-message","metadata":{"ClusterID":"?? (possible?)","podName":"pod-name"}}]}}' + example: '{"status":"In progress","startTime":"1","endTime":"","message":"e-message","podStatus":"Running","podName":"pod-name","workflowExecutionStages":{"workflow":[{"stageName":"Preparation","status":"SUCCESS","startTime":"1","endTime":"2","message":"p-message","metadata":{}},{"stageName":"Execution","status":"STARTED","startTime":"2","endTime":"","message":"e-message","metadata":{}}],"pod":[{"stageName":"Execution","status":"STARTED","startTime":"2","endTime":"","message":"e-message","metadata":{"clusterId":"123","podName":"pod-name"}}]}}' components: schemas: @@ -89,7 +89,7 @@ components: metadata: type: object properties: - ClusterID: + clusterId: type: string description: Cluster ID podName: diff --git a/wire_gen.go b/wire_gen.go index 9a899a2eab..6127386c26 100644 --- a/wire_gen.go +++ b/wire_gen.go @@ -1,6 +1,6 @@ // Code generated by Wire. DO NOT EDIT. -//go:generate go run github.com/google/wire/cmd/wire +//go:generate go run -mod=mod github.com/google/wire/cmd/wire //go:build !wireinject // +build !wireinject @@ -622,7 +622,7 @@ func InitializeApp() (*App, error) { deployedAppMetricsServiceImpl := deployedAppMetrics.NewDeployedAppMetricsServiceImpl(sugaredLogger, appLevelMetricsRepositoryImpl, envLevelAppMetricsRepositoryImpl, chartRefServiceImpl) appListingServiceImpl := app2.NewAppListingServiceImpl(sugaredLogger, appListingRepositoryImpl, appDetailsReadServiceImpl, appRepositoryImpl, appListingViewBuilderImpl, pipelineRepositoryImpl, linkoutsRepositoryImpl, cdWorkflowRepositoryImpl, pipelineOverrideRepositoryImpl, environmentRepositoryImpl, chartRepositoryImpl, ciPipelineRepositoryImpl, dockerRegistryIpsConfigServiceImpl, userRepositoryImpl, deployedAppMetricsServiceImpl, ciArtifactRepositoryImpl, envConfigOverrideReadServiceImpl, ciPipelineConfigReadServiceImpl) workflowStageRepositoryImpl := repository18.NewWorkflowStageRepositoryImpl(sugaredLogger, db) - workFlowStageStatusServiceImpl := workflowStatus.NewWorkflowStageFlowStatusServiceImpl(sugaredLogger, workflowStageRepositoryImpl, ciWorkflowRepositoryImpl, cdWorkflowRepositoryImpl, transactionUtilImpl) + workFlowStageStatusServiceImpl := workflowStatus.NewWorkflowStageFlowStatusServiceImpl(sugaredLogger, workflowStageRepositoryImpl, ciWorkflowRepositoryImpl, cdWorkflowRepositoryImpl, environmentRepositoryImpl, transactionUtilImpl) cdWorkflowRunnerServiceImpl := cd.NewCdWorkflowRunnerServiceImpl(sugaredLogger, cdWorkflowRepositoryImpl, workFlowStageStatusServiceImpl, transactionUtilImpl) deploymentEventHandlerImpl := app2.NewDeploymentEventHandlerImpl(sugaredLogger, eventRESTClientImpl, eventSimpleFactoryImpl, runnable) appServiceImpl := app2.NewAppService(pipelineOverrideRepositoryImpl, utilMergeUtil, sugaredLogger, pipelineRepositoryImpl, eventRESTClientImpl, eventSimpleFactoryImpl, appRepositoryImpl, configMapRepositoryImpl, chartRepositoryImpl, cdWorkflowRepositoryImpl, commonServiceImpl, chartTemplateServiceImpl, pipelineStatusTimelineRepositoryImpl, pipelineStatusTimelineResourcesServiceImpl, pipelineStatusSyncDetailServiceImpl, pipelineStatusTimelineServiceImpl, appServiceConfig, appStatusServiceImpl, installedAppReadServiceImpl, installedAppVersionHistoryRepositoryImpl, scopedVariableCMCSManagerImpl, acdConfig, gitOpsConfigReadServiceImpl, gitOperationServiceImpl, deploymentTemplateServiceImpl, appListingServiceImpl, deploymentConfigServiceImpl, envConfigOverrideReadServiceImpl, cdWorkflowRunnerServiceImpl, deploymentEventHandlerImpl)