Repository: devtron-labs/devtron
Pull request #3725 (2023-08-03 10:48:52)

feat:sql script file added for Approval node notification 
<!--
Type of change: Title of the PR should clearly mention which type of PR is this, you can select any of the below mentioned types:

- docs - The PR contains Documentation ONLY changes. 
- feat - The PR contains new feature/enhancements.
- fix - The PR contains a bug fix.
- chore - Development changes related to the build system (involving scripts, configurations or tools) and package dependencies.
- test - Development changes related to tests.
- perf - Changes related to performance improvements.

Example Title: 
docs: Webhook CI documentation changes
-->

# Description
sql script file added for Approval node notification 

Fixes #

## How Has This Been Tested?
It has been tested locally

## Checklist:

* [x] The title of the PR states what changed and the related issues number (used for the release note).
* [ ] Does this PR requires documentation updates?
* [ ] I've updated documentation as required by this PR.
* [ ] I have performed a self-review of my own code.
* [x] I have commented my code, particularly in hard-to-understand areas.
* [ ] I have tested it for all user roles.
* [x] I have added all the required unit/api test cases.

## Does this PR introduce a user-facing change?
<!--
If NO, leave the release-note block blank.
If YES, a release note is required:
Enter your extended release note in the block below. If the PR requires additional manual action from users switching to the new version, include the string "action-required".

-->
```release-note

```




Patch:
diff --git a/api/restHandler/app/BuildPipelineRestHandler.go b/api/restHandler/app/BuildPipelineRestHandler.go
index c42740e42..38e1e94bc 100644
--- a/api/restHandler/app/BuildPipelineRestHandler.go
+++ b/api/restHandler/app/BuildPipelineRestHandler.go
@@ -280,11 +280,18 @@ func (handler PipelineConfigRestHandlerImpl) PatchCiMaterialSourceWithAppIdAndEn
 	patchRequest, userId, err := handler.parseSourceChangeRequest(w, r)
 	if err != nil {
 		handler.Logger.Errorw("Parse error, PatchCiMaterialSource", "err", err, "PatchCiMaterialSource", patchRequest)
+		common.WriteJsonResp(w, err, nil, http.StatusBadRequest)
+		return
+	}
+	if !(patchRequest.Source.Type == pipelineConfig.SOURCE_TYPE_BRANCH_FIXED || patchRequest.Source.Type == pipelineConfig.SOURCE_TYPE_BRANCH_REGEX) {
+		handler.Logger.Errorw("Unsupported source type, PatchCiMaterialSource", "err", err, "PatchCiMaterialSource", patchRequest)
+		common.WriteJsonResp(w, err, "source.type not supported", http.StatusBadRequest)
 		return
 	}
 	token := r.Header.Get("token")
 	if err = handler.authorizeCiSourceChangeRequest(w, patchRequest, token); err != nil {
 		handler.Logger.Errorw("Authorization error, PatchCiMaterialSource", "err", err, "PatchCiMaterialSource", patchRequest)
+		common.WriteJsonResp(w, err, nil, http.StatusUnauthorized)
 		return
 	}
 
diff --git a/api/restHandler/app/DeploymentPipelineRestHandler.go b/api/restHandler/app/DeploymentPipelineRestHandler.go
index 558a82b25..6aacd7410 100644
--- a/api/restHandler/app/DeploymentPipelineRestHandler.go
+++ b/api/restHandler/app/DeploymentPipelineRestHandler.go
@@ -50,6 +50,8 @@ type DevtronAppDeploymentRestHandler interface {
 	FetchCdWorkflowDetails(w http.ResponseWriter, r *http.Request)
 	GetCdPipelinesByEnvironment(w http.ResponseWriter, r *http.Request)
 	GetCdPipelinesByEnvironmentMin(w http.ResponseWriter, r *http.Request)
+
+	ChangeChartRef(w http.ResponseWriter, r *http.Request)
 }
 
 type DevtronAppDeploymentConfigRestHandler interface {
@@ -497,6 +499,150 @@ func (handler PipelineConfigRestHandlerImpl) HandleTriggerDeploymentAfterTypeCha
 	return
 }
 
+func (handler PipelineConfigRestHandlerImpl) ChangeChartRef(w http.ResponseWriter, r *http.Request) {
+	userId, err := handler.userAuthService.GetLoggedInUser(r)
+	if userId == 0 || err != nil {
+		common.WriteJsonResp(w, err, "Unauthorized User", http.StatusUnauthorized)
+		return
+	}
+	decoder := json.NewDecoder(r.Body)
+	var request chart.ChartRefChangeRequest
+	err = decoder.Decode(&request)
+	if err != nil || request.EnvId == 0 || request.TargetChartRefId == 0 || request.AppId == 0 {
+		handler.Logger.Errorw("request err, ChangeChartRef", "err", err, "payload", request)
+		common.WriteJsonResp(w, err, request, http.StatusBadRequest)
+		return
+	}
+	envConfigProperties, err := handler.propertiesConfigService.GetLatestEnvironmentProperties(request.AppId, request.EnvId)
+	if err != nil || envConfigProperties == nil {
+		handler.Logger.Errorw("env properties not found, ChangeChartRef", "err", err, "payload", request)
+		common.WriteJsonResp(w, err, "env properties not found", http.StatusNotFound)
+		return
+	}
+	if !envConfigProperties.IsOverride {
+		handler.Logger.Errorw("isOverride is not true, ChangeChartRef", "err", err, "payload", request)
+		common.WriteJsonResp(w, err, "specific environment is not overriden", http.StatusUnprocessableEntity)
+		return
+	}
+	compatible, oldChartType, newChartType := handler.chartService.ChartRefIdsCompatible(envConfigProperties.ChartRefId, request.TargetChartRefId)
+	if !compatible {
+		common.WriteJsonResp(w, fmt.Errorf("charts not compatible"), "chart not compatible", http.StatusUnprocessableEntity)
+		return
+	}
+
+	envConfigProperties.EnvOverrideValues, err = handler.chartService.PatchEnvOverrides(envConfigProperties.EnvOverrideValues, oldChartType, newChartType)
+	if err != nil {
+		common.WriteJsonResp(w, err, "error in patching env override", http.StatusInternalServerError)
+		return
+	}
+
+	if newChartType == chart.RolloutChartType {
+		enabled, err := handler.chartService.FlaggerCanaryEnabled(envConfigProperties.EnvOverrideValues)
+		if err != nil || enabled {
+			handler.Logger.Errorw("rollout charts do not support flaggerCanary, ChangeChartRef", "err", err, "payload", request)
+			common.WriteJsonResp(w, err, "rollout charts do not support flaggerCanary, ChangeChartRef", http.StatusBadRequest)
+			return
+		}
+	}
+
+	envMetrics, err := handler.propertiesConfigService.FindEnvLevelAppMetricsByAppIdAndEnvId(request.AppId, request.EnvId)
+	if err != nil {
+		handler.Logger.Errorw("could not find envMetrics for, ChangeChartRef", "err", err, "payload", request)
+		common.WriteJsonResp(w, err, "env metric could not be fetched", http.StatusBadRequest)
+		return
+	}
+	envConfigProperties.ChartRefId = request.TargetChartRefId
+	envConfigProperties.UserId = userId
+	envConfigProperties.EnvironmentId = request.EnvId
+	envConfigProperties.AppMetrics = envMetrics.AppMetrics
+
+	token := r.Header.Get("token")
+	handler.Logger.Infow("request payload, EnvConfigOverrideCreate", "payload", request)
+	resourceName := handler.enforcerUtil.GetAppRBACNameByAppId(request.AppId)
+	if ok := handler.enforcer.Enforce(token, casbin.ResourceApplications, casbin.ActionCreate, resourceName); !ok {
+		common.WriteJsonResp(w, fmt.Errorf("unauthorized user"), "Unauthorized User", http.StatusForbidden)
+		return
+	}
+	object := handler.enforcerUtil.GetEnvRBACNameByAppId(request.AppId, request.EnvId)
+	if ok := handler.enforcer.Enforce(token, casbin.ResourceEnvironment, casbin.ActionUpdate, object); !ok {
+		common.WriteJsonResp(w, fmt.Errorf("unauthorized user"), "Unauthorized User", http.StatusForbidden)
+		return
+	}
+
+	validate, err2 := handler.chartService.DeploymentTemplateValidate(r.Context(), envConfigProperties.EnvOverrideValues, envConfigProperties.ChartRefId)
+	if !validate {
+		handler.Logger.Errorw("validation err, UpdateAppOverride", "err", err2, "payload", request)
+		common.WriteJsonResp(w, err2, "validation err, UpdateAppOverrid", http.StatusBadRequest)
+		return
+	}
+	envConfigPropertiesOld, err := handler.propertiesConfigService.FetchEnvProperties(request.AppId, request.EnvId, request.TargetChartRefId)
+	if err == nil {
+		envConfigProperties.Id = envConfigPropertiesOld.Id
+		createResp, err := handler.propertiesConfigService.UpdateEnvironmentProperties(request.AppId, envConfigProperties, userId)
+		if err != nil {
+			handler.Logger.Errorw("service err, EnvConfigOverrideUpdate", "err", err, "payload", envConfigProperties)
+			common.WriteJsonResp(w, err, createResp, http.StatusInternalServerError)
+			return
+		}
+		common.WriteJsonResp(w, err, createResp, http.StatusOK)
+		return
+	}
+	createResp, err := handler.propertiesConfigService.CreateEnvironmentProperties(request.AppId, envConfigProperties)
+
+	if err != nil {
+		if err.Error() == bean2.NOCHARTEXIST {
+			ctx, cancel := context.WithCancel(r.Context())
+			if cn, ok := w.(http.CloseNotifier); ok {
+				go func(done <-chan struct{}, closed <-chan bool) {
+					select {
+					case <-done:
+					case <-closed:
+						cancel()
+					}
+				}(ctx.Done(), cn.CloseNotify())
+			}
+			acdToken, err := handler.argoUserService.GetLatestDevtronArgoCdUserToken()
+			if err != nil {
+				handler.Logger.Errorw("error in getting acd token", "err", err)
+				common.WriteJsonResp(w, err, "error in getting acd token", http.StatusInternalServerError)
+				return
+			}
+			ctx = context.WithValue(r.Context(), "token", acdToken)
+			appMetrics := false
+			if envConfigProperties.AppMetrics != nil {
+				appMetrics = *envMetrics.AppMetrics
+			}
+			templateRequest := chart.TemplateRequest{
+				AppId:               request.AppId,
+				ChartRefId:          request.TargetChartRefId,
+				ValuesOverride:      []byte("{}"),
+				UserId:              userId,
+				IsAppMetricsEnabled: appMetrics,
+			}
+
+			_, err = handler.chartService.CreateChartFromEnvOverride(templateRequest, ctx)
+			if err != nil {
+				handler.Logger.Errorw("service err, CreateChartFromEnvOverride", "err", err, "payload", request)
+				common.WriteJsonResp(w, err, "could not create chart from env override", http.StatusInternalServerError)
+				return
+			}
+			createResp, err = handler.propertiesConfigService.CreateEnvironmentProperties(request.AppId, envConfigProperties)
+			if err != nil {
+				handler.Logger.Errorw("service err, CreateEnvironmentProperties", "err", err, "payload", request)
+				common.WriteJsonResp(w, err, "could not create env properties", http.StatusInternalServerError)
+				return
+			}
+			common.WriteJsonResp(w, err, createResp, http.StatusOK)
+			return
+		} else {
+			handler.Logger.Errorw("service err, EnvConfigOverrideCreate", "err", err, "payload", request)
+			common.WriteJsonResp(w, err, "service err, EnvConfigOverrideCreate", http.StatusInternalServerError)
+			return
+		}
+	}
+	common.WriteJsonResp(w, err, createResp, http.StatusOK)
+}
+
 func (handler PipelineConfigRestHandlerImpl) EnvConfigOverrideCreate(w http.ResponseWriter, r *http.Request) {
 	userId, err := handler.userAuthService.GetLoggedInUser(r)
 	if userId == 0 || err != nil {
diff --git a/api/router/PipelineConfigRouter.go b/api/router/PipelineConfigRouter.go
index c5ea04951..c5e85655b 100644
--- a/api/router/PipelineConfigRouter.go
+++ b/api/router/PipelineConfigRouter.go
@@ -76,6 +76,7 @@ func (router PipelineConfigRouterImpl) initPipelineConfigRouter(configRouter *mu
 	configRouter.Path("/cd-pipeline/{appId}/env/{envId}").HandlerFunc(router.restHandler.GetCdPipelinesForAppAndEnv).Methods("GET")
 	//save environment specific override
 	configRouter.Path("/env/{appId}/{environmentId}").HandlerFunc(router.restHandler.EnvConfigOverrideCreate).Methods("POST")
+	configRouter.Path("/env/patch").HandlerFunc(router.restHandler.ChangeChartRef).Methods("PATCH")
 	configRouter.Path("/env").HandlerFunc(router.restHandler.EnvConfigOverrideUpdate).Methods("PUT")
 	configRouter.Path("/env/{appId}/{environmentId}/{chartRefId}").HandlerFunc(router.restHandler.GetEnvConfigOverride).Methods("GET")
 
diff --git a/pkg/chart/ChartCompatibility.go b/pkg/chart/ChartCompatibility.go
new file mode 100644
index 000000000..c6461bd00
--- /dev/null
+++ b/pkg/chart/ChartCompatibility.go
@@ -0,0 +1,37 @@
+package chart
+
+type stringSet map[string]struct{}
+
+const (
+	DeploymentChartType = "Deployment"
+	RolloutChartType    = "Rollout Deployment"
+)
+
+var chartCompatibilityMatrix = map[string]stringSet{
+	DeploymentChartType: {RolloutChartType: {}, DeploymentChartType: {}},
+	RolloutChartType:    {DeploymentChartType: {}, RolloutChartType: {}},
+}
+
+func CheckCompatibility(oldChartType, newChartType string) bool {
+	compatibilityOfOld, found := chartCompatibilityMatrix[oldChartType]
+	if !found {
+		return false
+	}
+	_, found = compatibilityOfOld[newChartType]
+	if !found {
+		return false
+	}
+	return true
+}
+
+func CompatibleChartsWith(chartType string) []string {
+	resultSet, found := chartCompatibilityMatrix[chartType]
+	if !found {
+		return []string{}
+	}
+	var result []string
+	for k, _ := range resultSet {
+		result = append(result, k)
+	}
+	return result
+}
diff --git a/pkg/chart/ChartService.go b/pkg/chart/ChartService.go
index d85e1d830..2d3646e23 100644
--- a/pkg/chart/ChartService.go
+++ b/pkg/chart/ChartService.go
@@ -96,6 +96,12 @@ type ChartUpgradeRequest struct {
 	UserId     int32 `json:"-"`
 }
 
+type ChartRefChangeRequest struct {
+	AppId            int `json:"appId" validate:"required"`
+	EnvId            int `json:"envId" validate:"required"`
+	TargetChartRefId int `json:"targetChartRefId" validate:"required"`
+}
+
 type PipelineConfigRequest struct {
 	Id                   int             `json:"id"  validate:"number"`
 	AppId                int             `json:"appId,omitempty"  validate:"number,required"`
@@ -148,6 +154,9 @@ type ChartService interface {
 	FetchChartInfoByFlag(userUploaded bool) ([]*ChartDto, error)
 	CheckCustomChartByAppId(id int) (bool, error)
 	CheckCustomChartByChartId(id int) (bool, error)
+	ChartRefIdsCompatible(oldChartRefId int, newChartRefId int) (bool, string, string)
+	PatchEnvOverrides(values json.RawMessage, oldChartType string, newChartType string) (json.RawMessage, error)
+	FlaggerCanaryEnabled(values json.RawMessage) (bool, error)
 }
 type ChartServiceImpl struct {
 	chartRepository                  chartRepoRepository.ChartRepository
@@ -213,6 +222,48 @@ func NewChartServiceImpl(chartRepository chartRepoRepository.ChartRepository,
 	}
 }
 
+func (impl ChartServiceImpl) ChartRefIdsCompatible(oldChartRefId int, newChartRefId int) (bool, string, string) {
+	oldChart, err := impl.chartRefRepository.FindById(oldChartRefId)
+	if err != nil {
+		return false, "", ""
+	}
+	newChart, err := impl.chartRefRepository.FindById(newChartRefId)
+	if err != nil {
+		return false, "", ""
+	}
+	if len(oldChart.Name) == 0 {
+		oldChart.Name = RolloutChartType
+	}
+	if len(newChart.Name) == 0 {
+		newChart.Name = RolloutChartType
+	}
+	return CheckCompatibility(oldChart.Name, newChart.Name), oldChart.Name, newChart.Name
+}
+
+func (impl ChartServiceImpl) FlaggerCanaryEnabled(values json.RawMessage) (bool, error) {
+	var jsonMap map[string]json.RawMessage
+	if err := json.Unmarshal([]byte(values), &jsonMap); err != nil {
+		return false, err
+	}
+
+	flaggerCanary, found := jsonMap["flaggerCanary"]
+	if !found {
+		return false, nil
+	}
+	var flaggerCanaryUnmarshalled map[string]json.RawMessage
+	if err := json.Unmarshal([]byte(flaggerCanary), &flaggerCanaryUnmarshalled); err != nil {
+		return false, err
+	}
+	enabled, found := flaggerCanaryUnmarshalled["enabled"]
+	if !found {
+		return true, fmt.Errorf("flagger canary enabled field must be set and be equal to false")
+	}
+	return string(enabled) == "true", nil
+}
+func (impl ChartServiceImpl) PatchEnvOverrides(values json.RawMessage, oldChartType string, newChartType string) (json.RawMessage, error) {
+	return PatchWinterSoldierConfig(values, newChartType)
+}
+
 func (impl ChartServiceImpl) GetSchemaAndReadmeForTemplateByChartRefId(chartRefId int) ([]byte, []byte, error) {
 	refChart, _, err, _, _ := impl.getRefChart(TemplateRequest{ChartRefId: chartRefId})
 	if err != nil {
@@ -888,6 +939,31 @@ func (impl ChartServiceImpl) UpdateAppOverride(ctx context.Context, templateRequ
 	return templateRequest, nil
 }
 
+func (impl ChartServiceImpl) handleChartTypeChange(currentLatestChart *chartRepoRepository.Chart, templateRequest *TemplateRequest) (json.RawMessage, error) {
+	var oldChartRef, newChartRef *chartRepoRepository.ChartRef
+	var err error
+	if oldChartRef, err = impl.chartRefRepository.FindById(currentLatestChart.ChartRefId); err != nil {
+		return nil, fmt.Errorf("chartRef not found for %v", currentLatestChart.ChartRefId)
+	}
+	if newChartRef, err = impl.chartRefRepository.FindById(templateRequest.ChartRefId); err != nil {
+		return nil, fmt.Errorf("chartRef not found for %v", templateRequest.ChartRefId)
+	}
+	if len(oldChartRef.Name) == 0 {
+		oldChartRef.Name = RolloutChartType
+	}
+	if len(newChartRef.Name) == 0 {
+		oldChartRef.Name = RolloutChartType
+	}
+	if !CheckCompatibility(oldChartRef.Name, newChartRef.Name) {
+		return nil, fmt.Errorf("charts are not compatible")
+	}
+	updatedOverride, err := PatchWinterSoldierConfig(templateRequest.ValuesOverride, newChartRef.Name)
+	if err != nil {
+		return nil, err
+	}
+	return updatedOverride, nil
+}
+
 func (impl ChartServiceImpl) updateAppLevelMetrics(appMetricRequest *AppMetricEnableDisableRequest) (*repository3.AppLevelMetrics, error) {
 	existingAppLevelMetrics, err := impl.appLevelMetricsRepository.FindByAppId(appMetricRequest.AppId)
 	if err != nil && err != pg.ErrNoRows {
@@ -973,11 +1049,12 @@ type ChartRefMetaData struct {
 }
 
 type chartRefResponse struct {
-	ChartRefs         []chartRef                  `json:"chartRefs"`
-	LatestChartRef    int                         `json:"latestChartRef"`
-	LatestAppChartRef int                         `json:"latestAppChartRef"`
-	LatestEnvChartRef int                         `json:"latestEnvChartRef,omitempty"`
-	ChartsMetadata    map[string]ChartRefMetaData `json:"chartMetadata"` // chartName vs Metadata
+	ChartRefs            []chartRef                  `json:"chartRefs"`
+	LatestChartRef       int                         `json:"latestChartRef"`
+	LatestAppChartRef    int                         `json:"latestAppChartRef"`
+	LatestEnvChartRef    int                         `json:"latestEnvChartRef,omitempty"`
+	ChartsMetadata       map[string]ChartRefMetaData `json:"chartMetadata"` // chartName vs Metadata
+	CompatibleChartTypes []string                    `json:"compatibleChartTypes,omitempty"`
 }
 
 type ChartYamlStruct struct {
diff --git a/pkg/chart/ChartUtils.go b/pkg/chart/ChartUtils.go
new file mode 100644
index 000000000..af6837d80
--- /dev/null
+++ b/pkg/chart/ChartUtils.go
@@ -0,0 +1,55 @@
+package chart
+
+import (
+	"encoding/json"
+)
+
+func PatchWinterSoldierConfig(override json.RawMessage, newChartType string) (json.RawMessage, error) {
+	var jsonMap map[string]json.RawMessage
+	if err := json.Unmarshal([]byte(override), &jsonMap); err != nil {
+		return override, err
+	}
+	updatedJson, err := PatchWinterSoldierIfExists(newChartType, jsonMap)
+	if err != nil {
+		return override, err
+	}
+	updatedOverride, err := json.Marshal(updatedJson)
+
+	if err != nil {
+		return override, err
+	}
+	return updatedOverride, nil
+}
+
+func PatchWinterSoldierIfExists(newChartType string, jsonMap map[string]json.RawMessage) (map[string]json.RawMessage, error) {
+	winterSoldierConfig, found := jsonMap["winterSoldier"]
+	if !found {
+		return jsonMap, nil
+	}
+	var winterSoldierUnmarshalled map[string]json.RawMessage
+	if err := json.Unmarshal([]byte(winterSoldierConfig), &winterSoldierUnmarshalled); err != nil {
+		return jsonMap, err
+	}
+
+	_, found = winterSoldierUnmarshalled["type"]
+	if !found {
+		return jsonMap, nil
+	}
+	switch newChartType {
+	case DeploymentChartType:
+		winterSoldierUnmarshalled["type"] = json.RawMessage("\"Deployment\"")
+	case RolloutChartType:
+		winterSoldierUnmarshalled["type"] = json.RawMessage("\"Rollout\"")
+	}
+
+	winterSoldierMarshalled, err := json.Marshal(winterSoldierUnmarshalled)
+	if err != nil {
+		return jsonMap, err
+	}
+	jsonMap["winterSoldier"] = winterSoldierMarshalled
+	return jsonMap, nil
+}
+
+//func IsFlaggerCanaryEnabled(override json.RawMessage) (bool, error) {
+//
+//}
diff --git a/pkg/pipeline/PropertiesConfig.go b/pkg/pipeline/PropertiesConfig.go
index e1b522a13..57de2c2d8 100644
--- a/pkg/pipeline/PropertiesConfig.go
+++ b/pkg/pipeline/PropertiesConfig.go
@@ -79,10 +79,12 @@ type PropertiesConfigService interface {
 
 	GetAppIdByChartEnvId(chartEnvId int) (*chartConfig.EnvConfigOverride, error)
 	GetLatestEnvironmentProperties(appId, environmentId int) (*EnvironmentProperties, error)
+	FindEnvLevelAppMetricsByAppIdAndEnvId(appId int, envId int) (*repository.EnvLevelAppMetrics, error)
 	ResetEnvironmentProperties(id int) (bool, error)
 	CreateEnvironmentPropertiesWithNamespace(appId int, propertiesRequest *EnvironmentProperties) (*EnvironmentProperties, error)
 
 	EnvMetricsEnableDisable(appMetricRequest *chartService.AppMetricEnableDisableRequest) (*chartService.AppMetricEnableDisableRequest, error)
+	FetchEnvProperties(appId, envId, chartRefId int) (*chartConfig.EnvConfigOverride, error)
 }
 type PropertiesConfigServiceImpl struct {
 	logger                           *zap.SugaredLogger
@@ -237,6 +239,10 @@ func (impl PropertiesConfigServiceImpl) GetEnvironmentProperties(appId, environm
 	return environmentPropertiesResponse, nil
 }
 
+func (impl PropertiesConfigServiceImpl) FetchEnvProperties(appId, envId, chartRefId int) (*chartConfig.EnvConfigOverride, error) {
+	return impl.envConfigRepo.GetByAppIdEnvIdAndChartRefId(appId, envId, chartRefId)
+}
+
 func (impl PropertiesConfigServiceImpl) CreateEnvironmentProperties(appId int, environmentProperties *EnvironmentProperties) (*EnvironmentProperties, error) {
 	chart, err := impl.chartRepo.FindChartByAppIdAndRefId(appId, environmentProperties.ChartRefId)
 	if err != nil && pg.ErrNoRows != err {
@@ -530,12 +536,15 @@ func (impl PropertiesConfigServiceImpl) GetAppIdByChartEnvId(chartEnvId int) (*c
 	return envOverride, nil
 }
 
+func (impl PropertiesConfigServiceImpl) FindEnvLevelAppMetricsByAppIdAndEnvId(appId int, envId int) (*repository.EnvLevelAppMetrics, error) {
+	return impl.envLevelAppMetricsRepository.FindByAppIdAndEnvId(appId, envId)
+}
+
 func (impl PropertiesConfigServiceImpl) GetLatestEnvironmentProperties(appId, environmentId int) (environmentProperties *EnvironmentProperties, err error) {
 	env, err := impl.environmentRepository.FindById(environmentId)
 	if err != nil {
 		return nil, err
 	}
-
 	// step 1
 	envOverride, err := impl.envConfigRepo.ActiveEnvConfigOverride(appId, environmentId)
 	if err != nil {
@@ -553,14 +562,18 @@ func (impl PropertiesConfigServiceImpl) GetLatestEnvironmentProperties(appId, en
 
 		environmentProperties = &EnvironmentProperties{
 			Id:                envOverride.Id,
-			Status:            envOverride.Status,
 			EnvOverrideValues: r,
+			Status:            envOverride.Status,
 			ManualReviewed:    envOverride.ManualReviewed,
 			Active:            envOverride.Active,
 			Namespace:         env.Namespace,
 			EnvironmentId:     environmentId,
 			EnvironmentName:   env.Name,
 			Latest:            envOverride.Latest,
+			IsOverride:        envOverride.IsOverride,
+			IsBasicViewLocked: envOverride.IsBasicViewLocked,
+			CurrentViewEditor: envOverride.CurrentViewEditor,
+			ChartRefId:        envOverride.Chart.ChartRefId,
 		}
 	}
 
diff --git a/scripts/sql/162_notification_template_for_approval.down.sql b/scripts/sql/162_notification_template_for_approval.down.sql
new file mode 100644
index 000000000..c8e65bc54
--- /dev/null
+++ b/scripts/sql/162_notification_template_for_approval.down.sql
@@ -0,0 +1,5 @@
+ALTER TABLE ONLY public.notifier_event_log DROP CONSTRAINT notifier_event_log_event_type_id_fkey;
+ALTER TABLE ONLY public.notifier_event_log
+    ADD CONSTRAINT notifier_event_log_event_type_id_fkey FOREIGN KEY (event_type_id) REFERENCES public.event(id) ON DELETE CASCADE;
+delete from "public"."notification_templates" where event_type_id=4;
+delete from public.event where event_type='APPROVAL';
\ No newline at end of file
diff --git a/scripts/sql/162_notification_template_for_approval.up.sql b/scripts/sql/162_notification_template_for_approval.up.sql
new file mode 100644
index 000000000..f2cba64f5
--- /dev/null
+++ b/scripts/sql/162_notification_template_for_approval.up.sql
@@ -0,0 +1,12 @@
+ALTER TABLE ONLY public.notifier_event_log DROP CONSTRAINT notifier_event_log_event_type_id_fkey;
+ALTER TABLE ONLY public.notifier_event_log
+    ADD CONSTRAINT notifier_event_log_event_type_id_fkey FOREIGN KEY (event_type_id) REFERENCES public.event(id) ;
+INSERT INTO public.event (id, event_type, description) VALUES (4, 'APPROVAL', '');
+INSERT INTO "public"."notification_templates" (channel_type, node_type, event_type_id, template_name, template_payload)
+VALUES ('smtp', 'CD', 4, 'CD approval smtp template', '{"from": "{{fromEmail}}",
+"to": "{{toEmail}}","subject": "🛎️ Image approval requested | Application > {{appName}} | Environment > {{envName}}","html": "<table style=\"width: 600px; height: 485px;  border-collapse: collapse; padding: 20px;\"><tr style=\"background-color:#E5F2FF;\"><td colspan=\"2\" style=\"padding-left:16px;\"><h2 style=\"color:#000A14;\">Image approval request</h2><span>{{eventTime}}</span><br><span>by <strong style=\"color:#0066CC;\">{{triggeredBy}}</strong></span><br><br>{{#imageApprovalLink}}<a href=\"{{&imageApprovalLink}}\" style=\" height: 32px; padding: 7px 12px; line-height: 32px; font-size: 12px; font-weight: 600; border-radius: 4px; text-decoration: none; outline: none; min-width: 64px; text-transform: capitalize; text-align: center; background: #0066CC; color: #fff; border: 1px solid transparent; cursor: pointer;\">View request</a><br><br>{{/imageApprovalLink}}</td><td style=\"text-align: right;\"><img src=\"https://cdn.devtron.ai/images/img_build_notification.png\" style=\"height: 72px; width: 72px;\"></td></tr><tr><td colspan=\"3\"><hr><br><span>Application: <strong>{{appName}}</strong></span>&nbsp;&nbsp;|&nbsp;&nbsp;<span>Environment: <strong>{{envName}}</strong></span><br></span><br><br><hr><h3>Image Details</h3><span>Image tag <br><strong>{{imageTag}}</strong></span></td></tr><tr><td colspan=\"3\"><br><br><span style=\"display: {{commentDisplayStyle}}\">Comment<br><strong>{{comment}}</strong></span></td></tr><tr><td colspan=\"3\"><br><br><span style=\"display: {{tagDisplayStyle}}\">Tags<br><strong>{{tags}}</strong></span><br></td></tr></table>"}');
+INSERT INTO "public"."notification_templates" (channel_type, node_type, event_type_id, template_name, template_payload)
+VALUES ('ses', 'CD', 4, 'CD approval ses template', '{"from": "{{fromEmail}}",
+"to": "{{toEmail}}","subject": "🛎️ Image approval requested | Application > {{appName}} | Environment > {{envName}}","html": "<table style=\"width: 600px; height: 485px;  border-collapse: collapse; padding: 20px;\"><tr style=\"background-color:#E5F2FF;\"><td colspan=\"2\" style=\"padding-left:16px;\"><h2 style=\"color:#000A14;\">Image approval request</h2><span>{{eventTime}}</span><br><span>by <strong style=\"color:#0066CC;\">{{triggeredBy}}</strong></span><br><br>{{#imageApprovalLink}}<a href=\"{{&imageApprovalLink}}\" style=\" height: 32px; padding: 7px 12px; line-height: 32px; font-size: 12px; font-weight: 600; border-radius: 4px; text-decoration: none; outline: none; min-width: 64px; text-transform: capitalize; text-align: center; background: #0066CC; color: #fff; border: 1px solid transparent; cursor: pointer;\">View request</a><br><br>{{/imageApprovalLink}}</td><td style=\"text-align: right;\"><img src=\"https://cdn.devtron.ai/images/img_build_notification.png\" style=\"height: 72px; width: 72px;\"></td></tr><tr><td colspan=\"3\"><hr><br><span>Application: <strong>{{appName}}</strong></span>&nbsp;&nbsp;|&nbsp;&nbsp;<span>Environment: <strong>{{envName}}</strong></span><br></span><br><br><hr><h3>Image Details</h3><span>Image tag <br><strong>{{imageTag}}</strong></span></td></tr><tr><td colspan=\"3\"><br><br><span style=\"display: {{commentDisplayStyle}}\">Comment<br><strong>{{comment}}</strong></span></td></tr><tr><td colspan=\"3\"><br><br><span style=\"display: {{tagDisplayStyle}}\">Tags<br><strong>{{tags}}</strong></span><br></td></tr></table>"}');
+
+
diff --git a/specs/change-deployment-chart-type.yaml b/specs/change-deployment-chart-type.yaml
new file mode 100644
index 000000000..496352d04
--- /dev/null
+++ b/specs/change-deployment-chart-type.yaml
@@ -0,0 +1,34 @@
+openapi: "3.0.0"
+info:
+  version: 1.0.0
+  title: Devtron Labs
+paths:
+  /orchestrator/app/env/patch:
+    patch:
+      description: change the deployment template for an app and environment
+      requestBody:
+        content:
+          application/json:
+            schema:
+              properties:
+                envId:
+                  type: integer
+                appId:
+                  type: integer
+                targetChartRefId:
+                  type: integer
+
+      responses:
+        '200':
+          description: patched data
+          content:
+            application/json:
+              schema:
+                properties:
+        '422':
+          description: bad request
+          content:
+            application/json:
+              schema:
+                userDetailedMessage:
+                  type: string
\ No newline at end of file
diff --git a/specs/ci-pipeline/ci-pipeline-change-source.yaml b/specs/ci-pipeline/ci-pipeline-change-source.yaml
new file mode 100644
index 000000000..b1a7c7d5b
--- /dev/null
+++ b/specs/ci-pipeline/ci-pipeline-change-source.yaml
@@ -0,0 +1,44 @@
+openapi: "3.0.0"
+info:
+  version: 1.0.0
+  title: CiPipeline material change source
+paths:
+  /orchestrator/app/ci-pipeline/patch-source:
+    patch:
+      description: update source of a ci-matrial
+      requestBody:
+        content:
+          application/json:
+            schema:
+              $ref: "#/components/schemas/RequestBody"
+      responses:
+        "200":
+          description: Successfully fetched commit info. if CommitInfo is null, then commit is not found.
+          content:
+            application/json:
+              schema:
+                type: object
+
+#{"appId": 16, "environmentId": 1, "source": {"type":"SOURCE_TYPE_BRANCH_FIXED", "value": "main1", "regex":""}}
+# Components
+components:
+  schemas:
+    RequestBody:
+      type: object
+      properties:
+        appId:
+          type: integer
+        environmentId:
+          type: integer
+        source:
+          type: object
+          properties:
+            type:
+              type: string
+              description: "SOURCE_TYPE_BRANCH_FIXED / SOURCE_TYPE_BRANCH_REGEX"
+            value:
+              type: string
+              description: "name of the branch"
+            regex:
+              type: string
+              description: "regular expression when type is SOURCE_TYPE_BRANCH_REGEX"
\ No newline at end of file


Test patch:
diff --git a/api/restHandler/app/BuildPipelineRestHandler_test.go b/api/restHandler/app/BuildPipelineRestHandler_test.go
index d2befef30..8425b73f0 100644
--- a/api/restHandler/app/BuildPipelineRestHandler_test.go
+++ b/api/restHandler/app/BuildPipelineRestHandler_test.go
@@ -79,7 +79,7 @@ func TestPipelineConfigRestHandlerImpl_PatchCiMaterialSource(t *testing.T) {
 			fields: fields{
 				validator: validator.New(),
 			},
-			body: "{\"appId\":4, \"id\": 5 ,\"ciMaterial\":[{\"gitMaterialId\":4,\"id\":5,\"source\":{\"type\":\"SOURCE_TYPE_BRANCH_FIXED\",\"value\":\"main3\",\"regex\":\"\"}}]}",
+			body: "{\"appId\":4, \"id\": 5 ,\"source\":{\"type\":\"SOURCE_TYPE_BRANCH_FIXED\",\"value\":\"main3\",\"regex\":\"\"}}",
 			setup: func(fields2 *fields) {
 				ctrl := gomock.NewController(t)
 				fields2.pipelineBuilder = mock_pipeline.NewMockPipelineBuilder(ctrl)
diff --git a/pkg/chart/ChartCompatibility_test.go b/pkg/chart/ChartCompatibility_test.go
new file mode 100644
index 000000000..95005d191
--- /dev/null
+++ b/pkg/chart/ChartCompatibility_test.go
@@ -0,0 +1,83 @@
+package chart
+
+import (
+	"reflect"
+	"testing"
+)
+
+func Test_checkCompatibility(t *testing.T) {
+	type args struct {
+		oldChartType string
+		newChartType string
+	}
+	tests := []struct {
+		name string
+		args args
+		want bool
+	}{
+		{
+			name: "when charts are compatible, it should return true",
+			args: args{
+				oldChartType: DeploymentChartType,
+				newChartType: RolloutChartType,
+			},
+			want: true,
+		},
+		{
+			name: "when oldChart is not found, it should return false",
+			args: args{
+				oldChartType: "Sdfasdf",
+				newChartType: RolloutChartType,
+			},
+			want: false,
+		},
+		{
+			name: "when newChart is not found, it should return false",
+			args: args{
+				oldChartType: DeploymentChartType,
+				newChartType: "random sdfasdf saldfj",
+			},
+			want: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := CheckCompatibility(tt.args.oldChartType, tt.args.newChartType); got != tt.want {
+				t.Errorf("checkCompatibility() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestCompatibleChartsWith(t *testing.T) {
+	type args struct {
+		chartType string
+	}
+	tests := []struct {
+		name string
+		args args
+		want []string
+	}{
+		{
+			name: "when chart is not found, it should return empty slice",
+			args: args{
+				chartType: "sdfsdf",
+			},
+			want: []string{},
+		},
+		{
+			name: "when chart is found, it should return a slice of all cpmpatible chartIds",
+			args: args{
+				chartType: DeploymentChartType,
+			},
+			want: []string{RolloutChartType},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := CompatibleChartsWith(tt.args.chartType); !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("CompatibleChartsWith() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
diff --git a/pkg/chart/ChartUtils_test.go b/pkg/chart/ChartUtils_test.go
new file mode 100644
index 000000000..58a733f99
--- /dev/null
+++ b/pkg/chart/ChartUtils_test.go
@@ -0,0 +1,1 @@
+package chart