Skip to content

Commit 6459d27

Browse files
authored
Merge pull request #6494 from devtron-labs/hibernated-fix
feat: only deploy unhibernated apps in app group
2 parents bd387d8 + 603d0c9 commit 6459d27

File tree

9 files changed

+493
-253
lines changed

9 files changed

+493
-253
lines changed

api/restHandler/app/pipeline/configure/BuildPipelineRestHandler.go

Lines changed: 247 additions & 237 deletions
Large diffs are not rendered by default.
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/*
2+
* Copyright (c) 2024. Devtron Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package configure
18+
19+
import (
20+
"encoding/json"
21+
"fmt"
22+
"github.com/devtron-labs/devtron/api/restHandler/common"
23+
"github.com/devtron-labs/devtron/internal/sql/repository/pipelineConfig"
24+
"github.com/devtron-labs/devtron/pkg/auth/authorisation/casbin"
25+
"github.com/devtron-labs/devtron/pkg/bean"
26+
"net/http"
27+
"strconv"
28+
)
29+
30+
// Helper functions for common operations in BuildPipelineRestHandler.go
31+
32+
// getUserIdOrUnauthorized gets the logged-in user ID or returns an unauthorized response
33+
func (handler *PipelineConfigRestHandlerImpl) getUserIdOrUnauthorized(w http.ResponseWriter, r *http.Request) (int32, bool) {
34+
userId, err := handler.userAuthService.GetLoggedInUser(r)
35+
if userId == 0 || err != nil {
36+
common.WriteJsonResp(w, err, "Unauthorized User", http.StatusUnauthorized)
37+
return 0, false
38+
}
39+
return userId, true
40+
}
41+
42+
// getIntPathParam gets an integer path parameter from the request
43+
func (handler *PipelineConfigRestHandlerImpl) getIntPathParam(w http.ResponseWriter, vars map[string]string, paramName string) (int, bool) {
44+
paramValue, err := strconv.Atoi(vars[paramName])
45+
if err != nil {
46+
handler.Logger.Errorw("request err, invalid path param", "err", err, "paramName", paramName)
47+
common.WriteJsonResp(w, err, nil, http.StatusBadRequest)
48+
return 0, false
49+
}
50+
return paramValue, true
51+
}
52+
53+
// decodeJsonBody decodes the request body into the provided struct
54+
func (handler *PipelineConfigRestHandlerImpl) decodeJsonBody(w http.ResponseWriter, r *http.Request, obj interface{}, logContext string) bool {
55+
decoder := json.NewDecoder(r.Body)
56+
err := decoder.Decode(obj)
57+
if err != nil {
58+
handler.Logger.Errorw("request err, decode json body", "err", err, "context", logContext, "payload", obj)
59+
common.WriteJsonResp(w, err, nil, http.StatusBadRequest)
60+
return false
61+
}
62+
return true
63+
}
64+
65+
// validateRequestBody validates the request body against struct validation tags
66+
func (handler *PipelineConfigRestHandlerImpl) validateRequestBody(w http.ResponseWriter, obj interface{}, logContext string) bool {
67+
err := handler.validator.Struct(obj)
68+
if err != nil {
69+
handler.Logger.Errorw("validation err", "err", err, "context", logContext, "payload", obj)
70+
common.WriteJsonResp(w, err, nil, http.StatusBadRequest)
71+
return false
72+
}
73+
return true
74+
}
75+
76+
// getAppAndCheckAuthForAction gets the app and checks if the user has the required permission
77+
func (handler *PipelineConfigRestHandlerImpl) getAppAndCheckAuthForAction(w http.ResponseWriter, appId int, token string, action string) (app *bean.CreateAppDTO, authorized bool) {
78+
app, err := handler.pipelineBuilder.GetApp(appId)
79+
if err != nil {
80+
handler.Logger.Errorw("service err, GetApp", "err", err, "appId", appId)
81+
common.WriteJsonResp(w, err, nil, http.StatusBadRequest)
82+
return nil, false
83+
}
84+
85+
resourceName := handler.enforcerUtil.GetAppRBACName(app.AppName)
86+
if ok := handler.enforcer.Enforce(token, casbin.ResourceApplications, action, resourceName); !ok {
87+
common.WriteJsonResp(w, fmt.Errorf("unauthorized user"), "Unauthorized User", http.StatusForbidden)
88+
return nil, false
89+
}
90+
91+
return app, true
92+
}
93+
94+
// checkAppRbacForAppOrJob checks if the user has the required permission for app or job
95+
func (handler *PipelineConfigRestHandlerImpl) checkAppRbacForAppOrJob(w http.ResponseWriter, token string, appId int, action string) bool {
96+
object := handler.enforcerUtil.GetAppRBACNameByAppId(appId)
97+
ok := handler.enforcerUtil.CheckAppRbacForAppOrJob(token, object, action)
98+
if !ok {
99+
common.WriteJsonResp(w, nil, "Unauthorized User", http.StatusForbidden)
100+
return false
101+
}
102+
return true
103+
}
104+
105+
// getCiPipelineWithAuth gets the CI pipeline and checks if the user has the required permission
106+
func (handler *PipelineConfigRestHandlerImpl) getCiPipelineWithAuth(w http.ResponseWriter, pipelineId int, token string, action string) (*pipelineConfig.CiPipeline, bool) {
107+
ciPipeline, err := handler.ciPipelineRepository.FindById(pipelineId)
108+
if err != nil {
109+
handler.Logger.Errorw("service err, FindById", "err", err, "pipelineId", pipelineId)
110+
common.WriteJsonResp(w, err, nil, http.StatusInternalServerError)
111+
return nil, false
112+
}
113+
114+
object := handler.enforcerUtil.GetAppRBACNameByAppId(ciPipeline.AppId)
115+
ok := handler.enforcerUtil.CheckAppRbacForAppOrJob(token, object, action)
116+
if !ok {
117+
common.WriteJsonResp(w, nil, "Unauthorized User", http.StatusForbidden)
118+
return nil, false
119+
}
120+
121+
return ciPipeline, true
122+
}
123+
124+
// getQueryParamBool gets a boolean query parameter from the request
125+
func (handler *PipelineConfigRestHandlerImpl) getQueryParamBool(r *http.Request, paramName string, defaultValue bool) bool {
126+
v := r.URL.Query()
127+
paramValue := defaultValue
128+
paramStr := v.Get(paramName)
129+
if len(paramStr) > 0 {
130+
var err error
131+
paramValue, err = strconv.ParseBool(paramStr)
132+
if err != nil {
133+
paramValue = defaultValue
134+
}
135+
}
136+
return paramValue
137+
}

api/router/ResourceGroupingRouter.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ func NewResourceGroupingRouterImpl(restHandler configure.PipelineConfigRestHandl
4545
func (router ResourceGroupingRouterImpl) InitResourceGroupingRouter(resourceGroupingRouter *mux.Router) {
4646
resourceGroupingRouter.Path("/{envId}/app-wf").
4747
HandlerFunc(router.appWorkflowRestHandler.FindAppWorkflowByEnvironment).Methods("GET")
48+
resourceGroupingRouter.Path("/{envId}/app-metadata").HandlerFunc(router.pipelineConfigRestHandler.GetAppMetadataListByEnvironment).Methods("GET")
4849
resourceGroupingRouter.Path("/{envId}/ci-pipeline").HandlerFunc(router.pipelineConfigRestHandler.GetCiPipelineByEnvironment).Methods("GET")
4950
resourceGroupingRouter.Path("/{envId}/cd-pipeline").HandlerFunc(router.pipelineConfigRestHandler.GetCdPipelinesByEnvironment).Methods("GET")
5051
resourceGroupingRouter.Path("/{envId}/external-ci").HandlerFunc(router.pipelineConfigRestHandler.GetExternalCiByEnvironment).Methods("GET")

cmd/external-app/wire_gen.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/sql/repository/AppListingRepository.go

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ type AppListingRepository interface {
5151
DeploymentDetailByArtifactId(ciArtifactId int, envId int) (AppView.DeploymentDetailContainer, error)
5252
FindAppCount(isProd bool) (int, error)
5353
FetchAppsByEnvironmentV2(appListingFilter helper.AppListingFilter) ([]*AppView.AppEnvironmentContainer, int, error)
54-
FetchOverviewAppsByEnvironment(envId, limit, offset int) ([]*AppView.AppEnvironmentContainer, error)
54+
FetchAppsEnvContainers(envId int, appIds []int, limit, offset int) ([]*AppView.AppEnvironmentContainer, error)
5555
FetchLastDeployedImage(appId, envId int) (*LastDeployed, error)
5656
}
5757

@@ -137,17 +137,26 @@ func (impl *AppListingRepositoryImpl) FetchOverviewCiPipelines(jobId int) ([]*Ap
137137
return jobContainers, nil
138138
}
139139

140-
func (impl *AppListingRepositoryImpl) FetchOverviewAppsByEnvironment(envId, limit, offset int) ([]*AppView.AppEnvironmentContainer, error) {
140+
func (impl *AppListingRepositoryImpl) FetchAppsEnvContainers(envId int, appIds []int, limit, offset int) ([]*AppView.AppEnvironmentContainer, error) {
141141
query := ` SELECT a.id as app_id,a.app_name,aps.status as app_status, ld.last_deployed_time, p.id as pipeline_id
142142
FROM app a
143143
INNER JOIN pipeline p ON p.app_id = a.id and p.deleted = false and p.environment_id = ?
144144
LEFT JOIN app_status aps ON aps.app_id = a.id and aps.env_id = ?
145145
LEFT JOIN
146146
(SELECT pco.pipeline_id,MAX(pco.created_on) as last_deployed_time from pipeline_config_override pco
147147
GROUP BY pco.pipeline_id) ld ON ld.pipeline_id = p.id
148-
WHERE a.active = true
149-
ORDER BY a.app_name `
148+
WHERE a.active = true `
149+
150150
queryParams := []interface{}{envId, envId}
151+
152+
// Add app IDs filter if provided
153+
if len(appIds) > 0 {
154+
query += " AND a.id IN (?)"
155+
queryParams = append(queryParams, pg.In(appIds))
156+
}
157+
158+
query += " ORDER BY a.app_name "
159+
151160
if limit > 0 {
152161
query += fmt.Sprintf("LIMIT ? ")
153162
queryParams = append(queryParams, limit)
@@ -156,6 +165,7 @@ func (impl *AppListingRepositoryImpl) FetchOverviewAppsByEnvironment(envId, limi
156165
query += fmt.Sprintf("OFFSET ? ")
157166
queryParams = append(queryParams, offset)
158167
}
168+
159169
var envContainers []*AppView.AppEnvironmentContainer
160170
_, err := impl.dbConnection.Query(&envContainers, query, queryParams...)
161171
return envContainers, err

pkg/app/AppListingService.go

Lines changed: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"context"
2121
"fmt"
2222
"github.com/devtron-labs/devtron/api/bean/AppView"
23+
bean2 "github.com/devtron-labs/devtron/client/argocdServer/bean"
2324
"github.com/devtron-labs/devtron/internal/middleware"
2425
"github.com/devtron-labs/devtron/internal/sql/repository/app"
2526
"github.com/devtron-labs/devtron/internal/sql/repository/pipelineConfig/bean/workflow/cdWorkflow"
@@ -72,6 +73,7 @@ type AppListingService interface {
7273

7374
FetchAppsByEnvironmentV2(fetchAppListingRequest FetchAppListingRequest, w http.ResponseWriter, r *http.Request, token string) ([]*AppView.AppEnvironmentContainer, int, error)
7475
FetchOverviewAppsByEnvironment(envId, limit, offset int) (*OverviewAppsByEnvironmentBean, error)
76+
FetchAppsEnvContainers(envId int, appIds []int, limit, offset int) ([]*AppView.AppEnvironmentContainer, error)
7577
}
7678

7779
const (
@@ -233,15 +235,10 @@ func (impl AppListingServiceImpl) FetchOverviewAppsByEnvironment(envId, limit, o
233235
resp.CreatedBy = fmt.Sprintf("%s (inactive)", createdBy.EmailId)
234236
}
235237
}
236-
envContainers, err := impl.appListingRepository.FetchOverviewAppsByEnvironment(envId, limit, offset)
237-
if err != nil {
238-
impl.Logger.Errorw("failed to fetch environment containers", "err", err, "envId", envId)
239-
return resp, err
240-
}
241-
242-
err = impl.updateAppStatusForHelmTypePipelines(envContainers)
238+
var appIds []int
239+
envContainers, err := impl.FetchAppsEnvContainers(envId, appIds, limit, offset)
243240
if err != nil {
244-
impl.Logger.Errorw("err, updateAppStatusForHelmTypePipelines", "envId", envId, "err", err)
241+
impl.Logger.Errorw("failed to fetch env containers", "err", err, "envId", envId)
245242
return resp, err
246243
}
247244

@@ -293,6 +290,21 @@ func getUniqueArtifacts(artifactIds []int) (uniqueArtifactIds []int) {
293290
return uniqueArtifactIds
294291
}
295292

293+
func (impl AppListingServiceImpl) FetchAppsEnvContainers(envId int, appIds []int, limit, offset int) ([]*AppView.AppEnvironmentContainer, error) {
294+
envContainers, err := impl.appListingRepository.FetchAppsEnvContainers(envId, appIds, limit, offset)
295+
if err != nil {
296+
impl.Logger.Errorw("failed to fetch environment containers", "err", err, "envId", envId)
297+
return nil, err
298+
}
299+
300+
err = impl.updateAppStatusForHelmTypePipelines(envContainers)
301+
if err != nil {
302+
impl.Logger.Errorw("err, updateAppStatusForHelmTypePipelines", "envId", envId, "err", err)
303+
return nil, err
304+
}
305+
return envContainers, nil
306+
}
307+
296308
func (impl AppListingServiceImpl) FetchAllDevtronManagedApps() ([]AppNameTypeIdContainer, error) {
297309
impl.Logger.Debug("reached at FetchAllDevtronManagedApps:")
298310
apps := make([]AppNameTypeIdContainer, 0)
@@ -368,6 +380,18 @@ func (impl AppListingServiceImpl) FetchAppsByEnvironmentV2(fetchAppListingReques
368380
if len(fetchAppListingRequest.Namespaces) != 0 && len(fetchAppListingRequest.Environments) == 0 {
369381
return []*AppView.AppEnvironmentContainer{}, 0, nil
370382
}
383+
384+
// Currently AppStatus is available in Db for only ArgoApps
385+
// We fetch AppStatus on the fly for Helm Apps from scoop, So AppStatus filter will be applied in last
386+
// fun to check if "HIBERNATING" exists in fetchAppListingRequest.AppStatuses
387+
isFilteredOnHibernatingStatus := impl.isFilteredOnHibernatingStatus(fetchAppListingRequest)
388+
// remove ""HIBERNATING" from fetchAppListingRequest.AppStatuses
389+
appStatusesFilter := make([]string, 0)
390+
if isFilteredOnHibernatingStatus {
391+
appStatusesFilter = fetchAppListingRequest.AppStatuses
392+
fetchAppListingRequest.AppStatuses = []string{}
393+
}
394+
371395
appListingFilter := helper.AppListingFilter{
372396
Environments: fetchAppListingRequest.Environments,
373397
Statuses: fetchAppListingRequest.Statuses,
@@ -421,9 +445,30 @@ func (impl AppListingServiceImpl) FetchAppsByEnvironmentV2(fetchAppListingReques
421445
if err != nil {
422446
impl.Logger.Errorw("error, UpdateAppStatusForHelmTypePipelines", "envIds", envIds, "err", err)
423447
}
448+
449+
// apply filter for "HIBERNATING" status
450+
if isFilteredOnHibernatingStatus {
451+
filteredContainers := make([]*AppView.AppEnvironmentContainer, 0)
452+
for _, container := range envContainers {
453+
if slices.Contains(appStatusesFilter, container.AppStatus) {
454+
filteredContainers = append(filteredContainers, container)
455+
}
456+
}
457+
envContainers = filteredContainers
458+
appSize = len(filteredContainers)
459+
}
424460
return envContainers, appSize, nil
425461
}
426462

463+
func (impl AppListingServiceImpl) isFilteredOnHibernatingStatus(fetchAppListingRequest FetchAppListingRequest) bool {
464+
if fetchAppListingRequest.AppStatuses != nil && len(fetchAppListingRequest.AppStatuses) > 0 {
465+
if slices.Contains(fetchAppListingRequest.AppStatuses, bean2.HIBERNATING) {
466+
return true
467+
}
468+
}
469+
return false
470+
}
471+
427472
func (impl AppListingServiceImpl) ISLastReleaseStopType(appId, envId int) (bool, error) {
428473
override, err := impl.pipelineOverrideRepository.GetLatestRelease(appId, envId)
429474
if err != nil && !util.IsErrNoRows(err) {

pkg/pipeline/BuildPipelineConfigService.go

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"github.com/devtron-labs/devtron/internal/sql/repository/helper"
2929
"github.com/devtron-labs/devtron/internal/sql/repository/pipelineConfig"
3030
"github.com/devtron-labs/devtron/internal/util"
31+
"github.com/devtron-labs/devtron/pkg/app"
3132
"github.com/devtron-labs/devtron/pkg/attributes"
3233
bean2 "github.com/devtron-labs/devtron/pkg/attributes/bean"
3334
"github.com/devtron-labs/devtron/pkg/bean"
@@ -118,6 +119,7 @@ type CiPipelineConfigService interface {
118119
GetExternalCiByEnvironment(request resourceGroup2.ResourceGroupingRequest, token string) (ciConfig []*bean.ExternalCiConfig, err error)
119120
DeleteCiPipeline(request *bean.CiPatchRequest) (*bean.CiPipeline, error)
120121
CreateExternalCiAndAppWorkflowMapping(appId, appWorkflowId int, userId int32, tx *pg.Tx) (int, *appWorkflow.AppWorkflowMapping, error)
122+
GetAppMetadataListByEnvironment(envId int, appIds []int) (appMetadataListBean pipelineConfigBean.AppMetadataListBean, err error)
121123
}
122124

123125
type CiPipelineConfigServiceImpl struct {
@@ -148,6 +150,7 @@ type CiPipelineConfigServiceImpl struct {
148150
buildPipelineSwitchService BuildPipelineSwitchService
149151
pipelineStageRepository repository.PipelineStageRepository
150152
globalPluginRepository repository2.GlobalPluginRepository
153+
appListingService app.AppListingService
151154
}
152155

153156
func NewCiPipelineConfigServiceImpl(logger *zap.SugaredLogger,
@@ -175,7 +178,8 @@ func NewCiPipelineConfigServiceImpl(logger *zap.SugaredLogger,
175178
cdWorkflowRepository pipelineConfig.CdWorkflowRepository,
176179
buildPipelineSwitchService BuildPipelineSwitchService,
177180
pipelineStageRepository repository.PipelineStageRepository,
178-
globalPluginRepository repository2.GlobalPluginRepository) *CiPipelineConfigServiceImpl {
181+
globalPluginRepository repository2.GlobalPluginRepository,
182+
appListingService app.AppListingService) *CiPipelineConfigServiceImpl {
179183
securityConfig := &SecurityConfig{}
180184
err := env.Parse(securityConfig)
181185
if err != nil {
@@ -209,6 +213,7 @@ func NewCiPipelineConfigServiceImpl(logger *zap.SugaredLogger,
209213
buildPipelineSwitchService: buildPipelineSwitchService,
210214
pipelineStageRepository: pipelineStageRepository,
211215
globalPluginRepository: globalPluginRepository,
216+
appListingService: appListingService,
212217
}
213218
}
214219

@@ -2157,3 +2162,24 @@ func (impl *CiPipelineConfigServiceImpl) CreateExternalCiAndAppWorkflowMapping(a
21572162
}
21582163
return externalCiPipeline.Id, appWorkflowMap, nil
21592164
}
2165+
2166+
func (impl *CiPipelineConfigServiceImpl) GetAppMetadataListByEnvironment(envId int, appIds []int) (appMetadataListBean pipelineConfigBean.AppMetadataListBean, err error) {
2167+
appMetadataListBean = pipelineConfigBean.AppMetadataListBean{}
2168+
envContainers, err := impl.appListingService.FetchAppsEnvContainers(envId, appIds, 0, 0)
2169+
if err != nil {
2170+
impl.logger.Errorw("failed to fetch env containers", "err", err, "envId", envId)
2171+
return appMetadataListBean, err
2172+
}
2173+
appMetadataList := make([]*pipelineConfigBean.AppMetaData, 0)
2174+
for _, envContainer := range envContainers {
2175+
appMetaData := &pipelineConfigBean.AppMetaData{
2176+
AppId: envContainer.AppId,
2177+
AppName: envContainer.AppName,
2178+
AppStatus: envContainer.AppStatus,
2179+
}
2180+
appMetadataList = append(appMetadataList, appMetaData)
2181+
}
2182+
appMetadataListBean.Apps = appMetadataList
2183+
appMetadataListBean.AppCount = len(envContainers)
2184+
return appMetadataListBean, nil
2185+
}

pkg/pipeline/bean/pipelineStage.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,3 +222,14 @@ func (v *VariableAndConditionDataForStep) GetTriggerSkipConditions() []*Conditio
222222
func (v *VariableAndConditionDataForStep) GetSuccessFailureConditions() []*ConditionObject {
223223
return v.successFailureConditions
224224
}
225+
226+
type AppMetadataListBean struct {
227+
AppCount int `json:"appCount"`
228+
Apps []*AppMetaData `json:"apps"`
229+
}
230+
231+
type AppMetaData struct {
232+
AppId int `json:"appId"`
233+
AppName string `json:"appName"`
234+
AppStatus string `json:"appStatus"`
235+
}

0 commit comments

Comments
 (0)