Skip to content

Commit 8802287

Browse files
fix: handling side-effects for displaying external helm apps with same name across diff namespaces and clusters (#4951)
* store display name, along with unique appName(displayName-ns-clusterId) for external helm apps (in case same name ext helm app exists across diff ns or cluster) * comments * appName and display name handling at other places as well for external app * handling display name in GetHelmAppMetaInfo in AppCrudOperationService.go * FetchAppDetailsForInstalledAppV2 send display name if present * DeleteDBLinkedHelmApplication deletion handling for ext app with uniq app identifier and if not found by unique app identifier, then find by display name * applist method incorporated checks on display name if exists instead of appName * query change in GetAppAndEnvDetailsForDeploymentAppTypeInstalledApps to include also display_name * GenerateInstallAppVersionMinDTO if display name exists then send appname as display name * FindAppDetailsForAppstoreApplication, considering display name as well while preparing helm package name deploymentContainer.HelmPackageName * multiple fixes around corner cases around display names * minor fix * fixes for fetching resoirce trees for ext apps using display name instead of appName * bug fixes * linkHelmApplicationToChartStore, use displayName instead of appName for UpdateApplicationWithChartInfo * LinkHelmApplicationToChartStore: server mode full from hyperion when going to link a chart * revert:- LinkHelmApplicationToChartStore: server mode full from hyperion when going to link a chart * on update proj, update app offering mode to full and, getAppNameForInstalledApp changed param to installed app id from appId which was wrong * handled corner cases in DeleteDBLinkedHelmApplication * code refactoring * remove unwanted error check * removed logic to block update and deploy when not linked to devtron * code review incorporations and code refactoring * handling display name in migrate app type and trigger for migrated app type * minor fix * minor fix * code review incorporation and refactoring * wire fix * fixes for multiple installed apps with same app pointer linked to chart store * remove unused code * fix for showing unassigned proj to ext app that's already linked to devtron with same name * fix * fix * revert * fix
1 parent 18d0aaa commit 8802287

25 files changed

+603
-137
lines changed

api/appStore/InstalledAppRestHandler.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"github.com/devtron-labs/devtron/pkg/appStore/installedApp/service/FullMode"
2727
"github.com/devtron-labs/devtron/pkg/appStore/installedApp/service/FullMode/deploymentTypeChange"
2828
"github.com/devtron-labs/devtron/pkg/appStore/installedApp/service/FullMode/resource"
29+
util3 "github.com/devtron-labs/devtron/pkg/appStore/util"
2930
"github.com/devtron-labs/devtron/pkg/bean"
3031
"net/http"
3132
"strconv"
@@ -603,6 +604,10 @@ func (handler *InstalledAppRestHandlerImpl) FetchAppDetailsForInstalledApp(w htt
603604
common.WriteJsonResp(w, err, "App not found in database", http.StatusBadRequest)
604605
return
605606
}
607+
if util3.IsExternalChartStoreApp(installedApp.App.DisplayName) {
608+
//this is external app case where app_name is a unique identifier, and we want to fetch resource based on display_name
609+
handler.installedAppService.ChangeAppNameToDisplayNameForInstalledApp(installedApp)
610+
}
606611

607612
appDetail, err := handler.installedAppService.FindAppDetailsForAppstoreApplication(installedAppId, envId)
608613
if err != nil {
@@ -724,6 +729,10 @@ func (handler *InstalledAppRestHandlerImpl) FetchResourceTree(w http.ResponseWri
724729
common.WriteJsonResp(w, err, "App not found in database", http.StatusBadRequest)
725730
return
726731
}
732+
if util3.IsExternalChartStoreApp(installedApp.App.DisplayName) {
733+
//this is external app case where app_name is a unique identifier, and we want to fetch resource based on display_name
734+
handler.installedAppService.ChangeAppNameToDisplayNameForInstalledApp(installedApp)
735+
}
727736
if installedApp.Environment.IsVirtualEnvironment {
728737
common.WriteJsonResp(w, nil, nil, http.StatusOK)
729738
return

api/appStore/deployment/CommonDeploymentRestHandler.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,8 @@ func (handler *CommonDeploymentRestHandlerImpl) getAppOfferingMode(installedAppI
9393
err = &util.ApiError{HttpStatusCode: http.StatusBadRequest, UserMessage: "invalid app id"}
9494
return appOfferingMode, installedAppDto, err
9595
}
96-
installedAppDto, err = handler.installedAppService.GetInstalledAppByClusterNamespaceAndName(appIdentifier.ClusterId, appIdentifier.Namespace, appIdentifier.ReleaseName)
96+
uniqueAppName := appIdentifier.GetUniqueAppNameIdentifier()
97+
installedAppDto, err = handler.installedAppService.GetInstalledAppByClusterNamespaceAndName(appIdentifier.ClusterId, appIdentifier.Namespace, uniqueAppName)
9798
if err != nil {
9899
err = &util.ApiError{HttpStatusCode: http.StatusBadRequest, UserMessage: "unable to find app in database"}
99100
return appOfferingMode, installedAppDto, err

api/helm-app/HelmAppRestHandler.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -224,15 +224,15 @@ func (handler *HelmAppRestHandlerImpl) GetReleaseInfo(w http.ResponseWriter, r *
224224
common.WriteJsonResp(w, err, nil, http.StatusInternalServerError)
225225
return
226226
}
227-
installedApp, err := handler.installedAppService.GetInstalledAppByClusterNamespaceAndName(appIdentifier.ClusterId, appIdentifier.Namespace, appIdentifier.ReleaseName)
227+
installedAppVersionDto, err := handler.installedAppService.GetReleaseInfo(appIdentifier)
228228
if err != nil {
229229
common.WriteJsonResp(w, err, nil, http.StatusInternalServerError)
230230
return
231231
}
232232

233233
res := &bean.ReleaseAndInstalledAppInfo{
234234
ReleaseInfo: releaseInfo,
235-
InstalledAppInfo: bean.ConvertToInstalledAppInfo(installedApp),
235+
InstalledAppInfo: bean.ConvertToInstalledAppInfo(installedAppVersionDto),
236236
}
237237

238238
common.WriteJsonResp(w, err, res, http.StatusOK)

api/helm-app/HelmAppRouter.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ func (impl *HelmAppRouterImpl) InitAppListRouter(helmRouter *mux.Router) {
2929
helmRouter.Path("/hibernate").HandlerFunc(impl.helmAppRestHandler.Hibernate).Methods("POST")
3030
helmRouter.Path("/unhibernate").HandlerFunc(impl.helmAppRestHandler.UnHibernate).Methods("POST")
3131

32+
// GetReleaseInfo used only for external apps
3233
helmRouter.Path("/release-info").Queries("appId", "{appId}").
3334
HandlerFunc(impl.helmAppRestHandler.GetReleaseInfo).Methods("GET")
3435

api/helm-app/service/HelmAppService.go

Lines changed: 146 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import (
1414
"net/http"
1515
"reflect"
1616
"strconv"
17-
"strings"
1817
"time"
1918

2019
"github.com/caarlos0/env/v6"
@@ -56,7 +55,7 @@ type HelmAppService interface {
5655
GetValuesYaml(ctx context.Context, app *AppIdentifier) (*gRPC.ReleaseInfo, error)
5756
GetDesiredManifest(ctx context.Context, app *AppIdentifier, resource *openapi.ResourceIdentifier) (*openapi.DesiredManifestResponse, error)
5857
DeleteApplication(ctx context.Context, app *AppIdentifier) (*openapi.UninstallReleaseResponse, error)
59-
DeleteDBLinkedHelmApplication(ctx context.Context, app *AppIdentifier, useId int32) (*openapi.UninstallReleaseResponse, error)
58+
DeleteDBLinkedHelmApplication(ctx context.Context, appIdentifier *AppIdentifier, useId int32) (*openapi.UninstallReleaseResponse, error)
6059
// UpdateApplication is a wrapper over helmAppClient.UpdateApplication, sends update request to kubelink for external chart store apps
6160
UpdateApplication(ctx context.Context, app *AppIdentifier, request *bean.UpdateApplicationRequestDto) (*openapi.UpdateReleaseResponse, error)
6261
GetDeploymentDetail(ctx context.Context, app *AppIdentifier, version int32) (*openapi.HelmAppDeploymentManifestDetail, error)
@@ -448,7 +447,56 @@ func (impl *HelmAppServiceImpl) GetDesiredManifest(ctx context.Context, app *App
448447
return response, nil
449448
}
450449

451-
func (impl *HelmAppServiceImpl) DeleteDBLinkedHelmApplication(ctx context.Context, app *AppIdentifier, userId int32) (*openapi.UninstallReleaseResponse, error) {
450+
// getInstalledAppForAppIdentifier return installed_apps for app unique identifier or releaseName/displayName whichever exists else return pg.ErrNoRows
451+
func (impl *HelmAppServiceImpl) getInstalledAppForAppIdentifier(appIdentifier *AppIdentifier) (*repository.InstalledApps, error) {
452+
model := &repository.InstalledApps{}
453+
var err error
454+
//for ext apps search app from unique identifier
455+
appUniqueIdentifier := appIdentifier.GetUniqueAppNameIdentifier()
456+
model, err = impl.installedAppRepository.GetInstalledAppByAppName(appUniqueIdentifier)
457+
if err != nil {
458+
if util.IsErrNoRows(err) {
459+
//if error is pg no rows, then find installed app via app.DisplayName because this can also happen that
460+
//an ext-app is already linked to devtron, and it's entry in app_name col in app table will not be a unique
461+
//identifier but the display name.
462+
displayName := appIdentifier.ReleaseName
463+
model, err = impl.installedAppRepository.GetInstalledAppByAppName(displayName)
464+
if err != nil {
465+
impl.logger.Errorw("error in fetching installed app from display name", "appDisplayName", displayName, "err", err)
466+
return model, err
467+
}
468+
} else {
469+
impl.logger.Errorw("error in fetching installed app by app unique identifier", "appUniqueIdentifier", appUniqueIdentifier, "err", err)
470+
return model, err
471+
}
472+
}
473+
return model, nil
474+
}
475+
476+
func (impl *HelmAppServiceImpl) getAppForAppIdentifier(appIdentifier *AppIdentifier) (*app.App, error) {
477+
//for ext apps search app from unique identifier
478+
appUniqueIdentifier := appIdentifier.GetUniqueAppNameIdentifier()
479+
model, err := impl.appRepository.FindActiveByName(appUniqueIdentifier)
480+
if err != nil {
481+
if util.IsErrNoRows(err) {
482+
//if error is pg no rows, then find app via release name because this can also happen that a project is
483+
//already assigned a project, and it's entry in app_name col in app table will not be a unique
484+
//identifier but the display name i.e. release name.
485+
displayName := appIdentifier.ReleaseName
486+
model, err = impl.appRepository.FindActiveByName(displayName)
487+
if err != nil {
488+
impl.logger.Errorw("error in fetching app from display name", "appDisplayName", displayName, "err", err)
489+
return nil, err
490+
}
491+
} else {
492+
impl.logger.Errorw("error in fetching app by app unique identifier", "appUniqueIdentifier", appUniqueIdentifier, "err", err)
493+
return nil, err
494+
}
495+
}
496+
return model, nil
497+
}
498+
499+
func (impl *HelmAppServiceImpl) DeleteDBLinkedHelmApplication(ctx context.Context, appIdentifier *AppIdentifier, userId int32) (*openapi.UninstallReleaseResponse, error) {
452500
dbConnection := impl.appRepository.GetConnection()
453501
tx, err := dbConnection.Begin()
454502
if err != nil {
@@ -457,67 +505,94 @@ func (impl *HelmAppServiceImpl) DeleteDBLinkedHelmApplication(ctx context.Contex
457505
}
458506
// Rollback tx on error.
459507
defer tx.Rollback()
508+
var isAppLinkedToChartStore bool // if true, entry present in both app and installed_app table
460509

461-
// For Helm deployments ReleaseName is App.Name
462-
model, err := impl.installedAppRepository.GetInstalledAppByAppName(app.ReleaseName)
463-
if err != nil {
464-
impl.logger.Errorw("error in fetching installed app", "appName", app.ReleaseName, "err", err)
510+
installedAppModel, err := impl.getInstalledAppForAppIdentifier(appIdentifier)
511+
if err != nil && !util.IsErrNoRows(err) {
512+
impl.logger.Errorw("DeleteDBLinkedHelmApplication, error in fetching installed app for app identifier", "appIdentifier", appIdentifier, "err", err)
465513
return nil, err
466514
}
467-
468-
// If there are two releases with same name but in different namespace (eg: test -n demo-1 {Hyperion App}, test -n demo-2 {Externally Installed});
469-
// And if the request is received for the externally installed app, the below condition will handle
470-
if model.Environment.Namespace != app.Namespace {
471-
return nil, pg.ErrNoRows
515+
if installedAppModel.Id > 0 {
516+
isAppLinkedToChartStore = true
472517
}
473518

474-
// App Delete --> Start
475-
//soft delete app
476-
appModel := &model.App
477-
appModel.Active = false
478-
appModel.UpdatedBy = userId
479-
appModel.UpdatedOn = time.Now()
480-
err = impl.appRepository.UpdateWithTxn(appModel, tx)
481-
if err != nil {
482-
impl.logger.Errorw("error in deleting appModel", "app", appModel)
483-
return nil, err
484-
}
485-
// App Delete --> End
519+
if isAppLinkedToChartStore {
520+
// If there are two releases with same name but in different namespace (eg: test -n demo-1 {Hyperion App}, test -n demo-2 {Externally Installed});
521+
// And if the request is received for the externally installed app, the below condition will handle
522+
if installedAppModel.Environment.Namespace != appIdentifier.Namespace {
523+
return nil, pg.ErrNoRows
524+
}
486525

487-
// InstalledApp Delete --> Start
488-
// soft delete install app
489-
model.Active = false
490-
model.UpdatedBy = userId
491-
model.UpdatedOn = time.Now()
492-
_, err = impl.installedAppRepository.UpdateInstalledApp(model, tx)
493-
if err != nil {
494-
impl.logger.Errorw("error while deleting install app", "error", err)
495-
return nil, err
496-
}
497-
// InstalledApp Delete --> End
526+
// App Delete --> Start
527+
//soft delete app
528+
appModel := &installedAppModel.App
529+
appModel.Active = false
530+
appModel.UpdatedBy = userId
531+
appModel.UpdatedOn = time.Now()
532+
err = impl.appRepository.UpdateWithTxn(appModel, tx)
533+
if err != nil {
534+
impl.logger.Errorw("error in deleting appModel", "app", appModel)
535+
return nil, err
536+
}
537+
// App Delete --> End
538+
539+
// InstalledApp Delete --> Start
540+
// soft delete install app
541+
installedAppModel.Active = false
542+
installedAppModel.UpdatedBy = userId
543+
installedAppModel.UpdatedOn = time.Now()
544+
_, err = impl.installedAppRepository.UpdateInstalledApp(installedAppModel, tx)
545+
if err != nil {
546+
impl.logger.Errorw("error while deleting install app", "error", err)
547+
return nil, err
548+
}
549+
// InstalledApp Delete --> End
498550

499-
// InstalledAppVersions Delete --> Start
500-
models, err := impl.installedAppRepository.GetInstalledAppVersionByInstalledAppId(model.Id)
501-
if err != nil {
502-
impl.logger.Errorw("error while fetching install app versions", "error", err)
503-
return nil, err
504-
}
551+
// InstalledAppVersions Delete --> Start
552+
models, err := impl.installedAppRepository.GetInstalledAppVersionByInstalledAppId(installedAppModel.Id)
553+
if err != nil {
554+
impl.logger.Errorw("error while fetching install app versions", "error", err)
555+
return nil, err
556+
}
505557

506-
// soft delete install app versions
507-
for _, item := range models {
508-
item.Active = false
509-
item.UpdatedBy = userId
510-
item.UpdatedOn = time.Now()
511-
_, err = impl.installedAppRepository.UpdateInstalledAppVersion(item, tx)
558+
// soft delete install app versions
559+
for _, item := range models {
560+
item.Active = false
561+
item.UpdatedBy = userId
562+
item.UpdatedOn = time.Now()
563+
_, err = impl.installedAppRepository.UpdateInstalledAppVersion(item, tx)
564+
if err != nil {
565+
impl.logger.Errorw("error while fetching from db", "error", err)
566+
return nil, err
567+
}
568+
}
569+
// InstalledAppVersions Delete --> End
570+
} else {
571+
//this means app not found in installed_apps, but a scenario where an external app is only
572+
//assigned project and not linked to devtron, in that case only entry in app will be found.
573+
appModel, err := impl.getAppForAppIdentifier(appIdentifier)
512574
if err != nil {
513-
impl.logger.Errorw("error while fetching from db", "error", err)
575+
impl.logger.Errorw("DeleteDBLinkedHelmApplication, error in fetching app from appIdentifier", "appIdentifier", appIdentifier, "err", err)
514576
return nil, err
515577
}
578+
if appModel != nil && appModel.Id > 0 {
579+
// App Delete --> Start
580+
//soft delete app
581+
appModel.Active = false
582+
appModel.UpdatedBy = userId
583+
appModel.UpdatedOn = time.Now()
584+
err = impl.appRepository.UpdateWithTxn(appModel, tx)
585+
if err != nil {
586+
impl.logger.Errorw("error in deleting appModel", "app", appModel)
587+
return nil, err
588+
}
589+
// App Delete --> End
590+
}
516591
}
517-
// InstalledAppVersions Delete --> End
518-
res, err := impl.DeleteApplication(ctx, app)
592+
593+
res, err := impl.DeleteApplication(ctx, appIdentifier)
519594
if err != nil {
520-
impl.logger.Errorw("error in deleting helm application", "error", err, "appIdentifier", app)
595+
impl.logger.Errorw("error in deleting helm application", "error", err, "appIdentifier", appIdentifier)
521596
return nil, err
522597
}
523598

@@ -1005,29 +1080,32 @@ type AppIdentifier struct {
10051080
ReleaseName string `json:"releaseName"`
10061081
}
10071082

1083+
// GetUniqueAppNameIdentifier returns unique app name identifier, we store all helm releases in kubelink cache with key
1084+
// as what is returned from this func, this is the case where an app across diff namespace or cluster can have same name,
1085+
// so to identify then uniquely below implementation would serve as good unique identifier for an external app.
1086+
func (r *AppIdentifier) GetUniqueAppNameIdentifier() string {
1087+
return fmt.Sprintf("%s-%s-%s", r.ReleaseName, r.Namespace, strconv.Itoa(r.ClusterId))
1088+
}
1089+
1090+
func (r *AppIdentifier) GetUniqueAppIdentifierForGivenNamespaceAndCluster(namespace, clusterId string) string {
1091+
return fmt.Sprintf("%s-%s-%s", r.ReleaseName, namespace, clusterId)
1092+
}
1093+
10081094
func (impl *HelmAppServiceImpl) DecodeAppId(appId string) (*AppIdentifier, error) {
1009-
component := strings.Split(appId, "|")
1010-
if len(component) != 3 {
1011-
return nil, fmt.Errorf("malformed app id %s", appId)
1012-
}
1013-
clusterId, err := strconv.Atoi(component[0])
1014-
if err != nil {
1015-
return nil, err
1016-
}
1017-
if clusterId <= 0 {
1018-
return nil, fmt.Errorf("target cluster is not provided")
1019-
}
1020-
return &AppIdentifier{
1021-
ClusterId: clusterId,
1022-
Namespace: component[1],
1023-
ReleaseName: component[2],
1024-
}, nil
1095+
return DecodeExternalAppAppId(appId)
10251096
}
10261097

10271098
func (impl *HelmAppServiceImpl) EncodeAppId(appIdentifier *AppIdentifier) string {
10281099
return fmt.Sprintf("%d|%s|%s", appIdentifier.ClusterId, appIdentifier.Namespace, appIdentifier.ReleaseName)
10291100
}
10301101

1102+
func isSameAppName(deployedAppName string, appDto app.App) bool {
1103+
if len(appDto.DisplayName) > 0 {
1104+
return deployedAppName == appDto.DisplayName
1105+
}
1106+
return deployedAppName == appDto.AppName
1107+
}
1108+
10311109
func (impl *HelmAppServiceImpl) appListRespProtoTransformer(deployedApps *gRPC.DeployedAppList, token string, helmAuth func(token string, object string) bool, helmCdPipelines []*pipelineConfig.Pipeline, installedHelmApps []*repository.InstalledApps) openapi.AppList {
10321110
applicationType := "HELM-APP"
10331111
appList := openapi.AppList{ClusterIds: &[]int32{deployedApps.ClusterId}, ApplicationType: &applicationType}
@@ -1055,7 +1133,7 @@ func (impl *HelmAppServiceImpl) appListRespProtoTransformer(deployedApps *gRPC.D
10551133

10561134
// do not add helm apps in the list which are created using app_store
10571135
for _, installedHelmApp := range installedHelmApps {
1058-
if deployedapp.AppName == installedHelmApp.App.AppName && int(deployedapp.EnvironmentDetail.ClusterId) == installedHelmApp.Environment.ClusterId && deployedapp.EnvironmentDetail.Namespace == installedHelmApp.Environment.Namespace {
1136+
if isSameAppName(deployedapp.AppName, installedHelmApp.App) && int(deployedapp.EnvironmentDetail.ClusterId) == installedHelmApp.Environment.ClusterId && deployedapp.EnvironmentDetail.Namespace == installedHelmApp.Environment.Namespace {
10591137
toExcludeFromList = true
10601138
break
10611139
}

api/helm-app/service/helper.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package service
2+
3+
import (
4+
"fmt"
5+
"strconv"
6+
"strings"
7+
)
8+
9+
func DecodeExternalAppAppId(appId string) (*AppIdentifier, error) {
10+
component := strings.Split(appId, "|")
11+
if len(component) != 3 {
12+
return nil, fmt.Errorf("malformed app id %s", appId)
13+
}
14+
clusterId, err := strconv.Atoi(component[0])
15+
if err != nil {
16+
return nil, err
17+
}
18+
if clusterId <= 0 {
19+
return nil, fmt.Errorf("target cluster is not provided")
20+
}
21+
return &AppIdentifier{
22+
ClusterId: clusterId,
23+
Namespace: component[1],
24+
ReleaseName: component[2],
25+
}, nil
26+
}

api/restHandler/app/appList/AppListingRestHandler.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
argoApplication "github.com/devtron-labs/devtron/client/argocdServer/bean"
2929
"github.com/devtron-labs/devtron/pkg/appStore/installedApp/service/FullMode"
3030
"github.com/devtron-labs/devtron/pkg/appStore/installedApp/service/FullMode/resource"
31+
util4 "github.com/devtron-labs/devtron/pkg/appStore/util"
3132
bean2 "github.com/devtron-labs/devtron/pkg/cluster/repository/bean"
3233
"net/http"
3334
"strconv"
@@ -909,6 +910,10 @@ func (handler AppListingRestHandlerImpl) GetHostUrlsByBatch(w http.ResponseWrite
909910
common.WriteJsonResp(w, err, "App not found in database", http.StatusBadRequest)
910911
return
911912
}
913+
if util4.IsExternalChartStoreApp(installedApp.App.DisplayName) {
914+
//this is external app case where app_name is a unique identifier, and we want to fetch resource based on display_name
915+
handler.installedAppService.ChangeAppNameToDisplayNameForInstalledApp(installedApp)
916+
}
912917
resourceTreeAndNotesContainer := bean.AppDetailsContainer{}
913918
resourceTreeAndNotesContainer, err = handler.fetchResourceTreeFromInstallAppService(w, r, resourceTreeAndNotesContainer, *installedApp)
914919
if err != nil {

0 commit comments

Comments
 (0)