diff --git a/Wire.go b/Wire.go index d37157cb0b..0ae1b5d320 100644 --- a/Wire.go +++ b/Wire.go @@ -32,10 +32,12 @@ import ( "github.com/devtron-labs/devtron/api/deployment" "github.com/devtron-labs/devtron/api/externalLink" client "github.com/devtron-labs/devtron/api/helm-app" + "github.com/devtron-labs/devtron/api/module" "github.com/devtron-labs/devtron/api/restHandler" pipeline2 "github.com/devtron-labs/devtron/api/restHandler/app" "github.com/devtron-labs/devtron/api/router" "github.com/devtron-labs/devtron/api/router/pubsub" + "github.com/devtron-labs/devtron/api/server" "github.com/devtron-labs/devtron/api/sse" "github.com/devtron-labs/devtron/api/sso" "github.com/devtron-labs/devtron/api/team" @@ -114,6 +116,8 @@ func InitializeApp() (*App, error) { appStoreDiscover.AppStoreDiscoverWireSet, appStoreValues.AppStoreValuesWireSet, appStoreDeployment.AppStoreDeploymentWireSet, + server.ServerWireSet, + module.ModuleWireSet, // -------wireset end ---------- gitSensor.GetGitSensorConfig, gitSensor.NewGitSensorSession, diff --git a/api/helm-app/HelmAppService.go b/api/helm-app/HelmAppService.go index 66f7431a57..c2677bd20d 100644 --- a/api/helm-app/HelmAppService.go +++ b/api/helm-app/HelmAppService.go @@ -2,17 +2,28 @@ package client import ( "context" + "errors" "fmt" "github.com/devtron-labs/devtron/api/connector" openapi "github.com/devtron-labs/devtron/api/helm-app/openapiClient" "github.com/devtron-labs/devtron/client/k8s/application" "github.com/devtron-labs/devtron/pkg/cluster" + serverBean "github.com/devtron-labs/devtron/pkg/server/bean" + serverEnvConfig "github.com/devtron-labs/devtron/pkg/server/config" + serverDataStore "github.com/devtron-labs/devtron/pkg/server/store" + util2 "github.com/devtron-labs/devtron/util" "github.com/devtron-labs/devtron/util/rbac" + jsonpatch "github.com/evanphx/json-patch" + "github.com/ghodss/yaml" "github.com/gogo/protobuf/proto" + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" "go.uber.org/zap" "net/http" + "reflect" "strconv" "strings" + "time" ) const DEFAULT_CLUSTER = "default_cluster" @@ -34,26 +45,33 @@ type HelmAppService interface { IsReleaseInstalled(ctx context.Context, app *AppIdentifier) (bool, error) RollbackRelease(ctx context.Context, app *AppIdentifier, version int32) (bool, error) GetClusterConf(clusterId int) (*ClusterConfig, error) + GetDevtronHelmAppIdentifier() *AppIdentifier + UpdateApplicationWithChartInfoWithExtraValues(ctx context.Context, appIdentifier *AppIdentifier, chartRepository *ChartRepository, extraValues map[string]interface{}, extraValuesYamlUrl string, useLatestChartVersion bool) (*openapi.UpdateReleaseResponse, error) } type HelmAppServiceImpl struct { - logger *zap.SugaredLogger - clusterService cluster.ClusterService - helmAppClient HelmAppClient - pump connector.Pump - enforcerUtil rbac.EnforcerUtilHelm + logger *zap.SugaredLogger + clusterService cluster.ClusterService + helmAppClient HelmAppClient + pump connector.Pump + enforcerUtil rbac.EnforcerUtilHelm + serverDataStore *serverDataStore.ServerDataStore + serverEnvConfig *serverEnvConfig.ServerEnvConfig } func NewHelmAppServiceImpl(Logger *zap.SugaredLogger, clusterService cluster.ClusterService, helmAppClient HelmAppClient, - pump connector.Pump, enforcerUtil rbac.EnforcerUtilHelm) *HelmAppServiceImpl { + pump connector.Pump, enforcerUtil rbac.EnforcerUtilHelm, serverDataStore *serverDataStore.ServerDataStore, + serverEnvConfig *serverEnvConfig.ServerEnvConfig) *HelmAppServiceImpl { return &HelmAppServiceImpl{ - logger: Logger, - clusterService: clusterService, - helmAppClient: helmAppClient, - pump: pump, - enforcerUtil: enforcerUtil, + logger: Logger, + clusterService: clusterService, + helmAppClient: helmAppClient, + pump: pump, + enforcerUtil: enforcerUtil, + serverDataStore: serverDataStore, + serverEnvConfig: serverEnvConfig, } } @@ -179,6 +197,7 @@ func (impl *HelmAppServiceImpl) GetClusterConf(clusterId int) (*ClusterConfig, e } return config, nil } + func (impl *HelmAppServiceImpl) GetApplicationDetail(ctx context.Context, app *AppIdentifier) (*AppDetail, error) { config, err := impl.GetClusterConf(app.ClusterId) if err != nil { @@ -191,6 +210,26 @@ func (impl *HelmAppServiceImpl) GetApplicationDetail(ctx context.Context, app *A ReleaseName: app.ReleaseName, } appdetail, err := impl.helmAppClient.GetAppDetail(ctx, req) + if err != nil { + impl.logger.Errorw("error in fetching app detail", "err", err) + return nil, err + } + + // if application is devtron app helm release, + // then for FULL (installer object exists), then status is combination of helm app status and installer object status - + // if installer status is not applied then check for timeout and progressing + devtronHelmAppIdentifier := impl.GetDevtronHelmAppIdentifier() + if app.ClusterId == devtronHelmAppIdentifier.ClusterId && app.Namespace == devtronHelmAppIdentifier.Namespace && app.ReleaseName == devtronHelmAppIdentifier.ReleaseName && + impl.serverDataStore.InstallerCrdObjectExists { + if impl.serverDataStore.InstallerCrdObjectStatus != serverBean.InstallerCrdObjectStatusApplied { + // if timeout + if time.Now().After(appdetail.GetLastDeployed().AsTime().Add(1 * time.Hour)) { + appdetail.ApplicationStatus = serverBean.AppHealthStatusDegraded + } else { + appdetail.ApplicationStatus = serverBean.AppHealthStatusProgressing + } + } + } return appdetail, err } @@ -427,6 +466,115 @@ func (impl *HelmAppServiceImpl) RollbackRelease(ctx context.Context, app *AppIde return apiResponse.Result, nil } +func (impl *HelmAppServiceImpl) GetDevtronHelmAppIdentifier() *AppIdentifier { + return &AppIdentifier{ + ClusterId: 1, + Namespace: impl.serverEnvConfig.DevtronHelmReleaseNamespace, + ReleaseName: impl.serverEnvConfig.DevtronHelmReleaseName, + } +} + +func (impl *HelmAppServiceImpl) UpdateApplicationWithChartInfoWithExtraValues(ctx context.Context, appIdentifier *AppIdentifier, + chartRepository *ChartRepository, extraValues map[string]interface{}, extraValuesYamlUrl string, useLatestChartVersion bool) (*openapi.UpdateReleaseResponse, error) { + + // get release info + releaseInfo, err := impl.GetValuesYaml(context.Background(), appIdentifier) + if err != nil { + impl.logger.Errorw("error in fetching helm release info", "err", err) + return nil, err + } + + // initialise object with original values + jsonString := releaseInfo.MergedValues + + // handle extra values + // special handling for array + if len(extraValues) > 0 { + for k, v := range extraValues { + var valueI interface{} + if reflect.TypeOf(v).Kind() == reflect.Slice { + currentValue := gjson.Get(jsonString, k).Value() + value := make([]interface{}, 0) + if currentValue != nil { + value = currentValue.([]interface{}) + } + for _, singleNewVal := range v.([]interface{}) { + value = append(value, singleNewVal) + } + valueI = value + } else { + valueI = v + } + jsonString, err = sjson.Set(jsonString, k, valueI) + if err != nil { + impl.logger.Errorw("error in handing extra values", "err", err) + return nil, err + } + } + } + + // convert to byte array + mergedValuesJsonByteArr := []byte(jsonString) + + // handle extra values from url + if len(extraValuesYamlUrl) > 0 { + extraValuesUrlYamlByteArr, err := util2.ReadFromUrlWithRetry(extraValuesYamlUrl) + if err != nil { + impl.logger.Errorw("error in reading content", "extraValuesYamlUrl", extraValuesYamlUrl, "err", err) + return nil, err + } else if extraValuesUrlYamlByteArr == nil { + impl.logger.Errorw("response is empty from url", "extraValuesYamlUrl", extraValuesYamlUrl) + return nil, errors.New("response is empty from values url") + } + + extraValuesUrlJsonByteArr, err := yaml.YAMLToJSON(extraValuesUrlYamlByteArr) + if err != nil { + impl.logger.Errorw("error in converting json to yaml", "err", err) + return nil, err + } + + mergedValuesJsonByteArr, err = jsonpatch.MergePatch(mergedValuesJsonByteArr, extraValuesUrlJsonByteArr) + if err != nil { + impl.logger.Errorw("error in json patch of extra values from url", "err", err) + return nil, err + } + } + + // convert JSON to yaml byte array + mergedValuesYamlByteArr, err := yaml.JSONToYAML(mergedValuesJsonByteArr) + if err != nil { + impl.logger.Errorw("error in converting json to yaml", "err", err) + return nil, err + } + + // update in helm + updateReleaseRequest := &InstallReleaseRequest{ + ReleaseIdentifier: &ReleaseIdentifier{ + ReleaseName: appIdentifier.ReleaseName, + ReleaseNamespace: appIdentifier.Namespace, + }, + ChartName: releaseInfo.DeployedAppDetail.ChartName, + ValuesYaml: string(mergedValuesYamlByteArr), + ChartRepository: chartRepository, + } + if !useLatestChartVersion { + updateReleaseRequest.ChartVersion = releaseInfo.DeployedAppDetail.ChartVersion + } + + updateResponse, err := impl.UpdateApplicationWithChartInfo(ctx, appIdentifier.ClusterId, updateReleaseRequest) + if err != nil { + impl.logger.Errorw("error in upgrading release", "err", err) + return nil, err + } + // update in helm ends + + response := &openapi.UpdateReleaseResponse{ + Success: updateResponse.Success, + } + + return response, nil +} + type AppIdentifier struct { ClusterId int `json:"clusterId"` Namespace string `json:"namespace"` diff --git a/api/module/ModuleRestHandler.go b/api/module/ModuleRestHandler.go new file mode 100644 index 0000000000..1494faffcb --- /dev/null +++ b/api/module/ModuleRestHandler.go @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2020 Devtron Labs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package module + +import ( + "encoding/json" + "errors" + "github.com/devtron-labs/devtron/api/restHandler/common" + "github.com/devtron-labs/devtron/pkg/module" + "github.com/devtron-labs/devtron/pkg/user" + "github.com/devtron-labs/devtron/pkg/user/casbin" + "github.com/gorilla/mux" + "go.uber.org/zap" + "gopkg.in/go-playground/validator.v9" + "net/http" +) + +type ModuleRestHandler interface { + GetModuleInfo(w http.ResponseWriter, r *http.Request) + HandleModuleAction(w http.ResponseWriter, r *http.Request) +} + +type ModuleRestHandlerImpl struct { + logger *zap.SugaredLogger + moduleService module.ModuleService + userService user.UserService + enforcer casbin.Enforcer + validator *validator.Validate +} + +func NewModuleRestHandlerImpl(logger *zap.SugaredLogger, + moduleService module.ModuleService, + userService user.UserService, + enforcer casbin.Enforcer, + validator *validator.Validate, +) *ModuleRestHandlerImpl { + return &ModuleRestHandlerImpl{ + logger: logger, + moduleService: moduleService, + userService: userService, + enforcer: enforcer, + validator: validator, + } +} + +func (impl ModuleRestHandlerImpl) GetModuleInfo(w http.ResponseWriter, r *http.Request) { + // check if user is logged in or not + userId, err := impl.userService.GetLoggedInUser(r) + if userId == 0 || err != nil { + common.WriteJsonResp(w, err, "Unauthorized User", http.StatusUnauthorized) + return + } + + // check query param + params := mux.Vars(r) + moduleName := params["name"] + if len(moduleName) == 0 { + impl.logger.Error("module name is not supplied") + common.WriteJsonResp(w, errors.New("module name is not supplied"), nil, http.StatusBadRequest) + return + } + + // service call + res, err := impl.moduleService.GetModuleInfo(moduleName) + if err != nil { + impl.logger.Errorw("service err, GetModuleInfo", "err", err) + common.WriteJsonResp(w, err, nil, http.StatusInternalServerError) + return + } + common.WriteJsonResp(w, err, res, http.StatusOK) +} + +func (impl ModuleRestHandlerImpl) HandleModuleAction(w http.ResponseWriter, r *http.Request) { + // check if user is logged in or not + userId, err := impl.userService.GetLoggedInUser(r) + if userId == 0 || err != nil { + common.WriteJsonResp(w, err, "Unauthorized User", http.StatusUnauthorized) + return + } + + // check query param + params := mux.Vars(r) + moduleName := params["name"] + if len(moduleName) == 0 { + impl.logger.Error("module name is not supplied") + common.WriteJsonResp(w, errors.New("module name is not supplied"), nil, http.StatusBadRequest) + return + } + + // decode request + decoder := json.NewDecoder(r.Body) + var moduleActionRequestDto *module.ModuleActionRequestDto + err = decoder.Decode(&moduleActionRequestDto) + if err != nil { + impl.logger.Errorw("error in decoding request in HandleModuleAction", "err", err) + common.WriteJsonResp(w, err, nil, http.StatusBadRequest) + return + } + err = impl.validator.Struct(moduleActionRequestDto) + if err != nil { + impl.logger.Errorw("error in validating request in HandleModuleAction", "err", err) + common.WriteJsonResp(w, err, nil, http.StatusBadRequest) + return + } + + // handle super-admin RBAC + token := r.Header.Get("token") + if ok := impl.enforcer.Enforce(token, casbin.ResourceGlobal, casbin.ActionUpdate, "*"); !ok { + common.WriteJsonResp(w, errors.New("unauthorized"), nil, http.StatusForbidden) + return + } + + // service call + res, err := impl.moduleService.HandleModuleAction(userId, moduleName, moduleActionRequestDto) + if err != nil { + impl.logger.Errorw("service err, HandleModuleAction", "err", err) + common.WriteJsonResp(w, err, nil, http.StatusInternalServerError) + return + } + common.WriteJsonResp(w, err, res, http.StatusOK) +} diff --git a/api/module/ModuleRouter.go b/api/module/ModuleRouter.go new file mode 100644 index 0000000000..4ebd34eba2 --- /dev/null +++ b/api/module/ModuleRouter.go @@ -0,0 +1,22 @@ +package module + +import ( + "github.com/gorilla/mux" +) + +type ModuleRouter interface { + Init(configRouter *mux.Router) +} + +type ModuleRouterImpl struct { + moduleRestHandler ModuleRestHandler +} + +func NewModuleRouterImpl(moduleRestHandler ModuleRestHandler) *ModuleRouterImpl { + return &ModuleRouterImpl{moduleRestHandler: moduleRestHandler} +} + +func (impl ModuleRouterImpl) Init(configRouter *mux.Router) { + configRouter.Path("").HandlerFunc(impl.moduleRestHandler.GetModuleInfo).Queries("name", "{name}").Methods("GET") + configRouter.Path("").HandlerFunc(impl.moduleRestHandler.HandleModuleAction).Queries("name", "{name}").Methods("POST") +} diff --git a/api/module/wire_module.go b/api/module/wire_module.go new file mode 100644 index 0000000000..4128f70881 --- /dev/null +++ b/api/module/wire_module.go @@ -0,0 +1,24 @@ +package module + +import ( + "github.com/devtron-labs/devtron/pkg/module" + "github.com/google/wire" +) + +var ModuleWireSet = wire.NewSet( + module.NewModuleActionAuditLogRepositoryImpl, + wire.Bind(new(module.ModuleActionAuditLogRepository), new(*module.ModuleActionAuditLogRepositoryImpl)), + module.NewModuleRepositoryImpl, + wire.Bind(new(module.ModuleRepository), new(*module.ModuleRepositoryImpl)), + module.ParseModuleEnvConfig, + module.NewModuleServiceImpl, + wire.Bind(new(module.ModuleService), new(*module.ModuleServiceImpl)), + module.NewModuleCronServiceImpl, + wire.Bind(new(module.ModuleCronService), new(*module.ModuleCronServiceImpl)), + module.NewModuleCacheServiceImpl, + wire.Bind(new(module.ModuleCacheService), new(*module.ModuleCacheServiceImpl)), + NewModuleRestHandlerImpl, + wire.Bind(new(ModuleRestHandler), new(*ModuleRestHandlerImpl)), + NewModuleRouterImpl, + wire.Bind(new(ModuleRouter), new(*ModuleRouterImpl)), +) diff --git a/api/router/router.go b/api/router/router.go index 5150f198af..5a7ea81691 100644 --- a/api/router/router.go +++ b/api/router/router.go @@ -19,16 +19,18 @@ package router import ( "encoding/json" - appStore "github.com/devtron-labs/devtron/api/appStore" + "github.com/devtron-labs/devtron/api/appStore" appStoreDeployment "github.com/devtron-labs/devtron/api/appStore/deployment" - chartRepo "github.com/devtron-labs/devtron/api/chartRepo" + "github.com/devtron-labs/devtron/api/chartRepo" "github.com/devtron-labs/devtron/api/cluster" "github.com/devtron-labs/devtron/api/dashboardEvent" "github.com/devtron-labs/devtron/api/deployment" "github.com/devtron-labs/devtron/api/externalLink" client "github.com/devtron-labs/devtron/api/helm-app" + "github.com/devtron-labs/devtron/api/module" "github.com/devtron-labs/devtron/api/restHandler/common" "github.com/devtron-labs/devtron/api/router/pubsub" + "github.com/devtron-labs/devtron/api/server" "github.com/devtron-labs/devtron/api/sso" "github.com/devtron-labs/devtron/api/team" "github.com/devtron-labs/devtron/api/user" @@ -89,17 +91,19 @@ type MuxRouter struct { ssoLoginRouter sso.SsoLoginRouter telemetryRouter TelemetryRouter telemetryWatcher telemetry.TelemetryEventClient - bulkUpdateRouter BulkUpdateRouter - WebhookListenerRouter WebhookListenerRouter - appLabelsRouter AppLabelRouter - coreAppRouter CoreAppRouter - helmAppRouter client.HelmAppRouter - k8sApplicationRouter k8s.K8sApplicationRouter - pProfRouter PProfRouter - deploymentConfigRouter deployment.DeploymentConfigRouter - dashboardTelemetryRouter dashboardEvent.DashboardTelemetryRouter - commonDeploymentRouter appStoreDeployment.CommonDeploymentRouter - externalLinkRouter externalLink.ExternalLinkRouter + bulkUpdateRouter BulkUpdateRouter + WebhookListenerRouter WebhookListenerRouter + appLabelsRouter AppLabelRouter + coreAppRouter CoreAppRouter + helmAppRouter client.HelmAppRouter + k8sApplicationRouter k8s.K8sApplicationRouter + pProfRouter PProfRouter + deploymentConfigRouter deployment.DeploymentConfigRouter + dashboardTelemetryRouter dashboardEvent.DashboardTelemetryRouter + commonDeploymentRouter appStoreDeployment.CommonDeploymentRouter + externalLinkRouter externalLink.ExternalLinkRouter + moduleRouter module.ModuleRouter + serverRouter server.ServerRouter } func NewMuxRouter(logger *zap.SugaredLogger, HelmRouter HelmRouter, PipelineConfigRouter PipelineConfigRouter, @@ -122,7 +126,8 @@ func NewMuxRouter(logger *zap.SugaredLogger, HelmRouter HelmRouter, PipelineConf commonRouter CommonRouter, grafanaRouter GrafanaRouter, ssoLoginRouter sso.SsoLoginRouter, telemetryRouter TelemetryRouter, telemetryWatcher telemetry.TelemetryEventClient, bulkUpdateRouter BulkUpdateRouter, webhookListenerRouter WebhookListenerRouter, appLabelsRouter AppLabelRouter, coreAppRouter CoreAppRouter, helmAppRouter client.HelmAppRouter, k8sApplicationRouter k8s.K8sApplicationRouter, pProfRouter PProfRouter, deploymentConfigRouter deployment.DeploymentConfigRouter, dashboardTelemetryRouter dashboardEvent.DashboardTelemetryRouter, - commonDeploymentRouter appStoreDeployment.CommonDeploymentRouter, externalLinkRouter externalLink.ExternalLinkRouter) *MuxRouter { + commonDeploymentRouter appStoreDeployment.CommonDeploymentRouter, externalLinkRouter externalLink.ExternalLinkRouter, moduleRouter module.ModuleRouter, + serverRouter server.ServerRouter) *MuxRouter { r := &MuxRouter{ Router: mux.NewRouter(), HelmRouter: HelmRouter, @@ -179,6 +184,8 @@ func NewMuxRouter(logger *zap.SugaredLogger, HelmRouter HelmRouter, PipelineConf dashboardTelemetryRouter: dashboardTelemetryRouter, commonDeploymentRouter: commonDeploymentRouter, externalLinkRouter: externalLinkRouter, + moduleRouter: moduleRouter, + serverRouter: serverRouter, } return r } @@ -351,4 +358,12 @@ func (r MuxRouter) Init() { externalLinkRouter := r.Router.PathPrefix("/orchestrator/external-links").Subrouter() r.externalLinkRouter.InitExternalLinkRouter(externalLinkRouter) + + // module router + moduleRouter := r.Router.PathPrefix("/orchestrator/module").Subrouter() + r.moduleRouter.Init(moduleRouter) + + // server router + serverRouter := r.Router.PathPrefix("/orchestrator/server").Subrouter() + r.serverRouter.Init(serverRouter) } diff --git a/api/server/ServerRestHandler.go b/api/server/ServerRestHandler.go new file mode 100644 index 0000000000..b75544980a --- /dev/null +++ b/api/server/ServerRestHandler.go @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2020 Devtron Labs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package server + +import ( + "encoding/json" + "errors" + "github.com/devtron-labs/devtron/api/restHandler/common" + "github.com/devtron-labs/devtron/pkg/server" + serverBean "github.com/devtron-labs/devtron/pkg/server/bean" + "github.com/devtron-labs/devtron/pkg/user" + "github.com/devtron-labs/devtron/pkg/user/casbin" + "go.uber.org/zap" + "gopkg.in/go-playground/validator.v9" + "net/http" +) + +type ServerRestHandler interface { + GetServerInfo(w http.ResponseWriter, r *http.Request) + HandleServerAction(w http.ResponseWriter, r *http.Request) +} + +type ServerRestHandlerImpl struct { + logger *zap.SugaredLogger + serverService server.ServerService + userService user.UserService + enforcer casbin.Enforcer + validator *validator.Validate +} + +func NewServerRestHandlerImpl(logger *zap.SugaredLogger, + serverService server.ServerService, + userService user.UserService, + enforcer casbin.Enforcer, + validator *validator.Validate, +) *ServerRestHandlerImpl { + return &ServerRestHandlerImpl{ + logger: logger, + serverService: serverService, + userService: userService, + enforcer: enforcer, + validator: validator, + } +} + +func (impl ServerRestHandlerImpl) GetServerInfo(w http.ResponseWriter, r *http.Request) { + // check if user is logged in or not + userId, err := impl.userService.GetLoggedInUser(r) + if userId == 0 || err != nil { + common.WriteJsonResp(w, err, "Unauthorized User", http.StatusUnauthorized) + return + } + + // service call + res, err := impl.serverService.GetServerInfo() + if err != nil { + impl.logger.Errorw("service err, GetServerInfo", "err", err) + common.WriteJsonResp(w, err, nil, http.StatusInternalServerError) + return + } + common.WriteJsonResp(w, err, res, http.StatusOK) +} + +func (impl ServerRestHandlerImpl) HandleServerAction(w http.ResponseWriter, r *http.Request) { + // check if user is logged in or not + userId, err := impl.userService.GetLoggedInUser(r) + if userId == 0 || err != nil { + common.WriteJsonResp(w, err, "Unauthorized User", http.StatusUnauthorized) + return + } + + // decode request + decoder := json.NewDecoder(r.Body) + var serverActionRequestDto *serverBean.ServerActionRequestDto + err = decoder.Decode(&serverActionRequestDto) + if err != nil { + impl.logger.Errorw("error in decoding request in HandleServerAction", "err", err) + common.WriteJsonResp(w, err, nil, http.StatusBadRequest) + return + } + err = impl.validator.Struct(serverActionRequestDto) + if err != nil { + impl.logger.Errorw("error in validating request in HandleServerAction", "err", err) + common.WriteJsonResp(w, err, nil, http.StatusBadRequest) + return + } + + // handle super-admin RBAC + token := r.Header.Get("token") + if ok := impl.enforcer.Enforce(token, casbin.ResourceGlobal, casbin.ActionUpdate, "*"); !ok { + common.WriteJsonResp(w, errors.New("unauthorized"), nil, http.StatusForbidden) + return + } + + // service call + res, err := impl.serverService.HandleServerAction(userId, serverActionRequestDto) + if err != nil { + impl.logger.Errorw("service err, HandleServerAction", "err", err) + common.WriteJsonResp(w, err, nil, http.StatusInternalServerError) + return + } + common.WriteJsonResp(w, err, res, http.StatusOK) +} diff --git a/api/server/ServerRouter.go b/api/server/ServerRouter.go new file mode 100644 index 0000000000..53b5438ae4 --- /dev/null +++ b/api/server/ServerRouter.go @@ -0,0 +1,22 @@ +package server + +import ( + "github.com/gorilla/mux" +) + +type ServerRouter interface { + Init(configRouter *mux.Router) +} + +type ServerRouterImpl struct { + serverRestHandler ServerRestHandler +} + +func NewServerRouterImpl(serverRestHandler ServerRestHandler) *ServerRouterImpl { + return &ServerRouterImpl{serverRestHandler: serverRestHandler} +} + +func (impl ServerRouterImpl) Init(configRouter *mux.Router) { + configRouter.Path("").HandlerFunc(impl.serverRestHandler.GetServerInfo).Methods("GET") + configRouter.Path("").HandlerFunc(impl.serverRestHandler.HandleServerAction).Methods("POST") +} diff --git a/api/server/wire_server.go b/api/server/wire_server.go new file mode 100644 index 0000000000..06ff738b10 --- /dev/null +++ b/api/server/wire_server.go @@ -0,0 +1,23 @@ +package server + +import ( + "github.com/devtron-labs/devtron/pkg/server" + serverEnvConfig "github.com/devtron-labs/devtron/pkg/server/config" + serverDataStore "github.com/devtron-labs/devtron/pkg/server/store" + "github.com/google/wire" +) + +var ServerWireSet = wire.NewSet( + server.NewServerActionAuditLogRepositoryImpl, + wire.Bind(new(server.ServerActionAuditLogRepository), new(*server.ServerActionAuditLogRepositoryImpl)), + serverEnvConfig.ParseServerEnvConfig, + serverDataStore.InitServerDataStore, + server.NewServerServiceImpl, + wire.Bind(new(server.ServerService), new(*server.ServerServiceImpl)), + server.NewServerCacheServiceImpl, + wire.Bind(new(server.ServerCacheService), new(*server.ServerCacheServiceImpl)), + NewServerRestHandlerImpl, + wire.Bind(new(ServerRestHandler), new(*ServerRestHandlerImpl)), + NewServerRouterImpl, + wire.Bind(new(ServerRouter), new(*ServerRouterImpl)), +) diff --git a/cmd/external-app/router.go b/cmd/external-app/router.go index 36ecafc532..7ece4f1018 100644 --- a/cmd/external-app/router.go +++ b/cmd/external-app/router.go @@ -10,7 +10,9 @@ import ( "github.com/devtron-labs/devtron/api/dashboardEvent" "github.com/devtron-labs/devtron/api/externalLink" client "github.com/devtron-labs/devtron/api/helm-app" + "github.com/devtron-labs/devtron/api/module" "github.com/devtron-labs/devtron/api/restHandler/common" + "github.com/devtron-labs/devtron/api/server" "github.com/devtron-labs/devtron/api/sso" "github.com/devtron-labs/devtron/api/team" "github.com/devtron-labs/devtron/api/user" @@ -41,6 +43,8 @@ type MuxRouter struct { dashboardTelemetryRouter dashboardEvent.DashboardTelemetryRouter commonDeploymentRouter appStoreDeployment.CommonDeploymentRouter externalLinksRouter externalLink.ExternalLinkRouter + moduleRouter module.ModuleRouter + serverRouter server.ServerRouter } func NewMuxRouter( @@ -61,6 +65,8 @@ func NewMuxRouter( dashboardTelemetryRouter dashboardEvent.DashboardTelemetryRouter, commonDeploymentRouter appStoreDeployment.CommonDeploymentRouter, externalLinkRouter externalLink.ExternalLinkRouter, + moduleRouter module.ModuleRouter, + serverRouter server.ServerRouter, ) *MuxRouter { r := &MuxRouter{ Router: mux.NewRouter(), @@ -81,6 +87,8 @@ func NewMuxRouter( dashboardTelemetryRouter: dashboardTelemetryRouter, commonDeploymentRouter: commonDeploymentRouter, externalLinksRouter: externalLinkRouter, + moduleRouter: moduleRouter, + serverRouter: serverRouter, } return r } @@ -169,4 +177,12 @@ func (r *MuxRouter) Init() { externalLinkRouter := r.Router.PathPrefix("/orchestrator/external-links").Subrouter() r.externalLinksRouter.InitExternalLinkRouter(externalLinkRouter) + + // module router + moduleRouter := r.Router.PathPrefix("/orchestrator/module").Subrouter() + r.moduleRouter.Init(moduleRouter) + + // server router + serverRouter := r.Router.PathPrefix("/orchestrator/server").Subrouter() + r.serverRouter.Init(serverRouter) } diff --git a/cmd/external-app/wire.go b/cmd/external-app/wire.go index 20232effd5..cd1cfca2bc 100644 --- a/cmd/external-app/wire.go +++ b/cmd/external-app/wire.go @@ -14,6 +14,8 @@ import ( "github.com/devtron-labs/devtron/api/dashboardEvent" "github.com/devtron-labs/devtron/api/externalLink" client "github.com/devtron-labs/devtron/api/helm-app" + "github.com/devtron-labs/devtron/api/module" + "github.com/devtron-labs/devtron/api/server" "github.com/devtron-labs/devtron/api/sso" "github.com/devtron-labs/devtron/api/team" "github.com/devtron-labs/devtron/api/user" @@ -52,6 +54,8 @@ func InitializeApp() (*App, error) { appStoreDiscover.AppStoreDiscoverWireSet, appStoreValues.AppStoreValuesWireSet, appStoreDeployment.AppStoreDeploymentWireSet, + server.ServerWireSet, + module.ModuleWireSet, NewApp, NewMuxRouter, diff --git a/cmd/external-app/wire_gen.go b/cmd/external-app/wire_gen.go index fd05513fe9..8bfd86a46e 100644 --- a/cmd/external-app/wire_gen.go +++ b/cmd/external-app/wire_gen.go @@ -1,6 +1,6 @@ // Code generated by Wire. DO NOT EDIT. -//go:generate wire +//go:generate go run github.com/google/wire/cmd/wire //+build !wireinject package main @@ -17,6 +17,8 @@ import ( "github.com/devtron-labs/devtron/api/dashboardEvent" externalLink2 "github.com/devtron-labs/devtron/api/externalLink" client2 "github.com/devtron-labs/devtron/api/helm-app" + module2 "github.com/devtron-labs/devtron/api/module" + server2 "github.com/devtron-labs/devtron/api/server" sso2 "github.com/devtron-labs/devtron/api/sso" team2 "github.com/devtron-labs/devtron/api/team" user2 "github.com/devtron-labs/devtron/api/user" @@ -32,8 +34,6 @@ import ( service3 "github.com/devtron-labs/devtron/pkg/appStore/deployment/service" "github.com/devtron-labs/devtron/pkg/appStore/deployment/tool" "github.com/devtron-labs/devtron/pkg/appStore/discover/repository" - "github.com/devtron-labs/devtron/pkg/appStore/repository" - "github.com/devtron-labs/devtron/pkg/appStore/values" "github.com/devtron-labs/devtron/pkg/appStore/discover/service" "github.com/devtron-labs/devtron/pkg/appStore/values/repository" service2 "github.com/devtron-labs/devtron/pkg/appStore/values/service" @@ -43,7 +43,11 @@ import ( repository2 "github.com/devtron-labs/devtron/pkg/cluster/repository" delete2 "github.com/devtron-labs/devtron/pkg/delete" "github.com/devtron-labs/devtron/pkg/externalLink" + "github.com/devtron-labs/devtron/pkg/module" "github.com/devtron-labs/devtron/pkg/pipeline" + "github.com/devtron-labs/devtron/pkg/server" + "github.com/devtron-labs/devtron/pkg/server/config" + "github.com/devtron-labs/devtron/pkg/server/store" "github.com/devtron-labs/devtron/pkg/sql" "github.com/devtron-labs/devtron/pkg/sso" "github.com/devtron-labs/devtron/pkg/team" @@ -64,7 +68,10 @@ func InitializeApp() (*App, error) { if err != nil { return nil, err } - sugaredLogger := util.NewSugardLogger() + sugaredLogger, err := util.NewSugardLogger() + if err != nil { + return nil, err + } db, err := sql.NewDbConnection(config, sugaredLogger) if err != nil { return nil, err @@ -146,7 +153,12 @@ func InitializeApp() (*App, error) { helmAppClientImpl := client2.NewHelmAppClientImpl(sugaredLogger, helmClientConfig) pumpImpl := connector.NewPumpImpl(sugaredLogger) enforcerUtilHelmImpl := rbac.NewEnforcerUtilHelmImpl(sugaredLogger, clusterRepositoryImpl) - helmAppServiceImpl := client2.NewHelmAppServiceImpl(sugaredLogger, clusterServiceImpl, helmAppClientImpl, pumpImpl, enforcerUtilHelmImpl) + serverDataStoreServerDataStore := serverDataStore.InitServerDataStore() + serverEnvConfigServerEnvConfig, err := serverEnvConfig.ParseServerEnvConfig() + if err != nil { + return nil, err + } + helmAppServiceImpl := client2.NewHelmAppServiceImpl(sugaredLogger, clusterServiceImpl, helmAppClientImpl, pumpImpl, enforcerUtilHelmImpl, serverDataStoreServerDataStore, serverEnvConfigServerEnvConfig) installedAppRepositoryImpl := repository3.NewInstalledAppRepositoryImpl(sugaredLogger, db) appStoreDeploymentCommonServiceImpl := appStoreDeploymentCommon.NewAppStoreDeploymentCommonServiceImpl(sugaredLogger, installedAppRepositoryImpl) helmAppRestHandlerImpl := client2.NewHelmAppRestHandlerImpl(sugaredLogger, helmAppServiceImpl, enforcerImpl, clusterServiceImpl, enforcerUtilHelmImpl, appStoreDeploymentCommonServiceImpl) @@ -154,9 +166,9 @@ func InitializeApp() (*App, error) { environmentRestHandlerImpl := cluster2.NewEnvironmentRestHandlerImpl(environmentServiceImpl, sugaredLogger, userServiceImpl, validate, enforcerImpl, deleteServiceImpl) environmentRouterImpl := cluster2.NewEnvironmentRouterImpl(environmentRestHandlerImpl) k8sClientServiceImpl := application.NewK8sClientServiceImpl(sugaredLogger, clusterRepositoryImpl) - k8sApplicationServiceImpl := k8s.NewK8sApplicationServiceImpl(sugaredLogger, clusterServiceImpl, pumpImpl, k8sClientServiceImpl, helmAppServiceImpl) + k8sApplicationServiceImpl := k8s.NewK8sApplicationServiceImpl(sugaredLogger, clusterServiceImpl, pumpImpl, k8sClientServiceImpl, helmAppServiceImpl, k8sUtil, acdAuthConfig) terminalSessionHandlerImpl := terminal.NewTerminalSessionHandlerImpl(environmentServiceImpl, clusterServiceImpl, sugaredLogger) - k8sApplicationRestHandlerImpl := k8s.NewK8sApplicationRestHandlerImpl(sugaredLogger, k8sApplicationServiceImpl, pumpImpl, terminalSessionHandlerImpl, enforcerImpl, enforcerUtilHelmImpl, clusterServiceImpl, helmAppServiceImpl) + k8sApplicationRestHandlerImpl := k8s.NewK8sApplicationRestHandlerImpl(sugaredLogger, k8sApplicationServiceImpl, pumpImpl, terminalSessionHandlerImpl, enforcerImpl, enforcerUtilHelmImpl, clusterServiceImpl, helmAppServiceImpl, userServiceImpl) k8sApplicationRouterImpl := k8s.NewK8sApplicationRouterImpl(k8sApplicationRestHandlerImpl) chartRefRepositoryImpl := chartRepoRepository.NewChartRefRepositoryImpl(db) refChartDir := _wireRefChartDirValue @@ -202,7 +214,26 @@ func InitializeApp() (*App, error) { externalLinkServiceImpl := externalLink.NewExternalLinkServiceImpl(sugaredLogger, externalLinkMonitoringToolRepositoryImpl, externalLinkClusterMappingRepositoryImpl, externalLinkRepositoryImpl) externalLinkRestHandlerImpl := externalLink2.NewExternalLinkRestHandlerImpl(sugaredLogger, externalLinkServiceImpl, userServiceImpl, enforcerImpl) externalLinkRouterImpl := externalLink2.NewExternalLinkRouterImpl(externalLinkRestHandlerImpl) - muxRouter := NewMuxRouter(sugaredLogger, ssoLoginRouterImpl, teamRouterImpl, userAuthRouterImpl, userRouterImpl, clusterRouterImpl, dashboardRouterImpl, helmAppRouterImpl, environmentRouterImpl, k8sApplicationRouterImpl, chartRepositoryRouterImpl, appStoreDiscoverRouterImpl, appStoreValuesRouterImpl, appStoreDeploymentRouterImpl, dashboardTelemetryRouterImpl, commonDeploymentRouterImpl, externalLinkRouterImpl) + moduleRepositoryImpl := module.NewModuleRepositoryImpl(db) + moduleActionAuditLogRepositoryImpl := module.NewModuleActionAuditLogRepositoryImpl(db) + moduleEnvConfig, err := module.ParseModuleEnvConfig() + if err != nil { + return nil, err + } + moduleCacheServiceImpl := module.NewModuleCacheServiceImpl(sugaredLogger, k8sUtil, moduleEnvConfig, serverEnvConfigServerEnvConfig, serverDataStoreServerDataStore, moduleRepositoryImpl) + moduleCronServiceImpl, err := module.NewModuleCronServiceImpl(sugaredLogger, moduleEnvConfig, moduleRepositoryImpl) + if err != nil { + return nil, err + } + moduleServiceImpl := module.NewModuleServiceImpl(sugaredLogger, serverEnvConfigServerEnvConfig, moduleRepositoryImpl, moduleActionAuditLogRepositoryImpl, helmAppServiceImpl, moduleCacheServiceImpl, moduleCronServiceImpl) + moduleRestHandlerImpl := module2.NewModuleRestHandlerImpl(sugaredLogger, moduleServiceImpl, userServiceImpl, enforcerImpl, validate) + moduleRouterImpl := module2.NewModuleRouterImpl(moduleRestHandlerImpl) + serverActionAuditLogRepositoryImpl := server.NewServerActionAuditLogRepositoryImpl(db) + serverCacheServiceImpl := server.NewServerCacheServiceImpl(sugaredLogger, serverEnvConfigServerEnvConfig, serverDataStoreServerDataStore, helmAppServiceImpl) + serverServiceImpl := server.NewServerServiceImpl(sugaredLogger, serverActionAuditLogRepositoryImpl, serverDataStoreServerDataStore, serverEnvConfigServerEnvConfig, helmAppServiceImpl, serverCacheServiceImpl) + serverRestHandlerImpl := server2.NewServerRestHandlerImpl(sugaredLogger, serverServiceImpl, userServiceImpl, enforcerImpl, validate) + serverRouterImpl := server2.NewServerRouterImpl(serverRestHandlerImpl) + muxRouter := NewMuxRouter(sugaredLogger, ssoLoginRouterImpl, teamRouterImpl, userAuthRouterImpl, userRouterImpl, clusterRouterImpl, dashboardRouterImpl, helmAppRouterImpl, environmentRouterImpl, k8sApplicationRouterImpl, chartRepositoryRouterImpl, appStoreDiscoverRouterImpl, appStoreValuesRouterImpl, appStoreDeploymentRouterImpl, dashboardTelemetryRouterImpl, commonDeploymentRouterImpl, externalLinkRouterImpl, moduleRouterImpl, serverRouterImpl) mainApp := NewApp(db, sessionManager, muxRouter, telemetryEventClientImpl, sugaredLogger) return mainApp, nil } diff --git a/internal/util/BasicProviders.go b/internal/util/BasicProviders.go index 14d78f28c8..d220753e63 100644 --- a/internal/util/BasicProviders.go +++ b/internal/util/BasicProviders.go @@ -38,28 +38,30 @@ func GetLogger() *zap.SugaredLogger { } type LogConfig struct { - Level int `env:"LOG_LEVEL" envDefault:"0"` + Level int `env:"LOG_LEVEL" envDefault:"0"` // default info } -func init() { +func InitLogger() (*zap.SugaredLogger, error) { cfg := &LogConfig{} err := env.Parse(cfg) if err != nil { - fmt.Println(err) - return + fmt.Println("failed to parse logger env config: " + err.Error()) + return nil, err } config := zap.NewProductionConfig() config.Level = zap.NewAtomicLevelAt(zapcore.Level(cfg.Level)) l, err := config.Build() if err != nil { - panic("failed to create the default logger: " + err.Error()) + fmt.Println("failed to create the default logger: " + err.Error()) + return nil, err } logger = l.Sugar() + return logger, nil } -func NewSugardLogger() *zap.SugaredLogger { - return logger +func NewSugardLogger() (*zap.SugaredLogger, error) { + return InitLogger() } func NewHttpClient() *http.Client { diff --git a/internal/util/GitService_test.go b/internal/util/GitService_test.go index 8578fa6cd0..b683cddb8d 100644 --- a/internal/util/GitService_test.go +++ b/internal/util/GitService_test.go @@ -5,7 +5,7 @@ import ( ) func getTestGithubClient() GitHubClient { - logger := NewSugardLogger() + logger, err := NewSugardLogger() gitCliUtl := NewGitCliUtil(logger) gitService := NewGitServiceImpl(&GitConfig{GitToken: "", GitUserName: "nishant"}, logger, gitCliUtl) diff --git a/internal/util/K8sUtil.go b/internal/util/K8sUtil.go index 4fd7a6dfef..072c063331 100644 --- a/internal/util/K8sUtil.go +++ b/internal/util/K8sUtil.go @@ -463,3 +463,28 @@ func (impl K8sUtil) GetResourceInfoByLabelSelector(namespace string, labelSelect return &pods.Items[0], nil } } + +func (impl K8sUtil) GetK8sClusterRestConfig() (*rest.Config, error) { + impl.logger.Debug("getting k8s rest config") + if impl.runTimeConfig.LocalDevMode { + usr, err := user.Current() + if err != nil { + impl.logger.Errorw("Error while getting user current env details", "error", err) + } + kubeconfig := flag.String("read-kubeconfig", filepath.Join(usr.HomeDir, ".kube", "config"), "(optional) absolute path to the kubeconfig file") + flag.Parse() + restConfig, err := clientcmd.BuildConfigFromFlags("", *kubeconfig) + if err != nil { + impl.logger.Errorw("Error while building kubernetes cluster rest config", "error", err) + return nil, err + } + return restConfig, nil + } else { + clusterConfig, err := rest.InClusterConfig() + if err != nil { + impl.logger.Errorw("error in fetch default cluster config", "err", err) + return nil, err + } + return clusterConfig, nil + } +} \ No newline at end of file diff --git a/internal/util/K8sUtil_test.go b/internal/util/K8sUtil_test.go index a1b50ff9a7..e24cbce6a8 100644 --- a/internal/util/K8sUtil_test.go +++ b/internal/util/K8sUtil_test.go @@ -25,7 +25,8 @@ var k8sUtilClient *K8sUtil var clusterConfig *ClusterConfig func init() { - k8sUtilClient = NewK8sUtil(NewSugardLogger()) + logger, _ := NewSugardLogger() + k8sUtilClient = NewK8sUtil(logger, nil) clusterConfig = &ClusterConfig{ Host: "", BearerToken: "", diff --git a/pkg/chartRepo/ChartRepositoryService_test.go b/pkg/chartRepo/ChartRepositoryService_test.go index 3e6a07037a..871b53f835 100644 --- a/pkg/chartRepo/ChartRepositoryService_test.go +++ b/pkg/chartRepo/ChartRepositoryService_test.go @@ -11,7 +11,7 @@ type ChartRepositoryServiceMock struct { } func TestChartRepositoryServiceImpl_ValidateChartDetails(t *testing.T) { - sugaredLogger := util.NewSugardLogger() + sugaredLogger, _ := util.NewSugardLogger() impl := &ChartRepositoryServiceImpl{ logger: sugaredLogger, repoRepository: new(ChartRepoRepositoryImplMock), diff --git a/pkg/module/Bean.go b/pkg/module/Bean.go new file mode 100644 index 0000000000..6d1302ec43 --- /dev/null +++ b/pkg/module/Bean.go @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2020 Devtron Labs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package module + +import ( + "fmt" + "github.com/caarlos0/env" +) + +type ModuleInfoDto struct { + Name string `json:"name,notnull"` + Status string `json:"status,notnull" validate:"oneof=notInstalled installed installing installFailed timeout"` +} + +type ModuleActionRequestDto struct { + Action string `json:"action,notnull" validate:"oneof=install"` + Version string `json:"version,notnull"` +} + +type ActionResponse struct { + Success bool `json:"success"` +} + +type ModuleEnvConfig struct { + ModuleTimeoutStatusHandlingCronDurationInMin int `env:"MODULE_TIMEOUT_STATUS_HANDLING_CRON_DURATION_MIN" envDefault:"5"` // default 5 mins +} + +func ParseModuleEnvConfig() (*ModuleEnvConfig, error) { + cfg := &ModuleEnvConfig{} + err := env.Parse(cfg) + if err != nil { + fmt.Println("failed to parse module env config: " + err.Error()) + return nil, err + } + + return cfg, nil +} + +type ModuleStatus = string + +const ( + ModuleStatusNotInstalled ModuleStatus = "notInstalled" + ModuleStatusInstalled ModuleStatus = "installed" + ModuleStatusInstalling ModuleStatus = "installing" + ModuleStatusInstallFailed ModuleStatus = "installFailed" + ModuleStatusTimeout ModuleStatus = "timeout" +) \ No newline at end of file diff --git a/pkg/module/ModuleActionAuditLogRepository.go b/pkg/module/ModuleActionAuditLogRepository.go new file mode 100644 index 0000000000..ea0da41a49 --- /dev/null +++ b/pkg/module/ModuleActionAuditLogRepository.go @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2020 Devtron Labs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package module + +import ( + "github.com/go-pg/pg" + "time" +) + +type ModuleActionAuditLog struct { + tableName struct{} `sql:"module_action_audit_log"` + Id int `sql:"id,pk"` + ModuleName string `sql:"module_name, notnull"` + Action string `sql:"action,notnull"` + Version string `sql:"version,notnull"` + CreatedOn time.Time `sql:"created_on,notnull"` + CreatedBy int32 `sql:"created_by,notnull"` +} + +type ModuleActionAuditLogRepository interface { + Save(moduleActionAuditLog *ModuleActionAuditLog) error +} + +type ModuleActionAuditLogRepositoryImpl struct { + dbConnection *pg.DB +} + +func NewModuleActionAuditLogRepositoryImpl(dbConnection *pg.DB) *ModuleActionAuditLogRepositoryImpl { + return &ModuleActionAuditLogRepositoryImpl{dbConnection: dbConnection} +} + +func (impl ModuleActionAuditLogRepositoryImpl) Save(moduleActionAuditLog *ModuleActionAuditLog) error { + err := impl.dbConnection.Insert(moduleActionAuditLog) + return err +} diff --git a/pkg/module/ModuleCacheService.go b/pkg/module/ModuleCacheService.go new file mode 100644 index 0000000000..5bf4e1a012 --- /dev/null +++ b/pkg/module/ModuleCacheService.go @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2020 Devtron Labs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package module + +import ( + "context" + "github.com/devtron-labs/devtron/internal/util" + serverEnvConfig "github.com/devtron-labs/devtron/pkg/server/config" + serverDataStore "github.com/devtron-labs/devtron/pkg/server/store" + util2 "github.com/devtron-labs/devtron/util" + "go.uber.org/zap" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/dynamic/dynamicinformer" + "k8s.io/client-go/tools/cache" + "log" + "os" + "os/signal" + "sync" + "time" +) + +type ModuleCacheService interface { +} + +func NewModuleCacheServiceImpl(logger *zap.SugaredLogger, K8sUtil *util.K8sUtil, moduleEnvConfig *ModuleEnvConfig, serverEnvConfig *serverEnvConfig.ServerEnvConfig, + serverDataStore *serverDataStore.ServerDataStore, moduleRepository ModuleRepository) *ModuleCacheServiceImpl { + impl := &ModuleCacheServiceImpl{ + logger: logger, + K8sUtil: K8sUtil, + moduleEnvConfig: moduleEnvConfig, + serverEnvConfig: serverEnvConfig, + serverDataStore: serverDataStore, + moduleRepository: moduleRepository, + } + + // for hyperion mode, installer crd won't come in picture + // for full mode, need to update modules to installed in db in found as installing + if util2.GetDevtronVersion().ServerMode == util2.SERVER_MODE_FULL { + // handle cicd module status + impl.updateModuleStatusToInstalled() + } + + // listen in installer object to save status in-memory + // build informer to listen on installer object + go impl.buildInformerToListenOnInstallerObject() + + return impl +} + +type ModuleCacheServiceImpl struct { + logger *zap.SugaredLogger + mutex sync.Mutex + K8sUtil *util.K8sUtil + moduleEnvConfig *ModuleEnvConfig + serverEnvConfig *serverEnvConfig.ServerEnvConfig + serverDataStore *serverDataStore.ServerDataStore + moduleRepository ModuleRepository +} + +func (impl *ModuleCacheServiceImpl) updateModuleStatusToInstalled() { + impl.logger.Debug("updating module status to installed") + modules, err := impl.moduleRepository.FindAll() + if err != nil { + log.Fatalln("not able to get all the module from DB.", "error", err) + } + + for _, module := range modules { + if module.Status != ModuleStatusInstalling { + continue + } + module.Status = ModuleStatusInstalled + module.UpdatedOn = time.Now() + err = impl.moduleRepository.Update(&module) + if err != nil { + log.Fatalln("error in updating module status to installed", "name", module.Name, "err", err) + } + } +} + +func (impl *ModuleCacheServiceImpl) buildInformerToListenOnInstallerObject() { + impl.logger.Debug("building informer cache to listen on installer object") + clusterConfig, err := impl.K8sUtil.GetK8sClusterRestConfig() + if err != nil { + log.Fatalln("not able to get k8s cluster rest config.", "error", err) + } + + clusterClient, err := dynamic.NewForConfig(clusterConfig) + if err != nil { + log.Fatalln("not able to get config from rest config.", "error", err) + } + + installerResource := schema.GroupVersionResource{ + Group: impl.serverEnvConfig.InstallerCrdObjectGroupName, + Version: impl.serverEnvConfig.InstallerCrdObjectVersion, + Resource: impl.serverEnvConfig.InstallerCrdObjectResource, + } + factory := dynamicinformer.NewFilteredDynamicSharedInformerFactory( + clusterClient, time.Minute, impl.serverEnvConfig.InstallerCrdNamespace, nil) + informer := factory.ForResource(installerResource).Informer() + + informer.AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + impl.handleInstallerObjectChange(obj) + }, + UpdateFunc: func(oldObj, newObj interface{}) { + impl.handleInstallerObjectChange(newObj) + }, + DeleteFunc: func(obj interface{}) { + impl.serverDataStore.InstallerCrdObjectStatus = "" + impl.serverDataStore.InstallerCrdObjectExists = false + }, + }) + + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) + defer cancel() + + go informer.Run(ctx.Done()) + <-ctx.Done() +} + +func (impl *ModuleCacheServiceImpl) handleInstallerObjectChange(obj interface{}) { + u := obj.(*unstructured.Unstructured) + val, _, _ := unstructured.NestedString(u.Object, "status", "sync", "status") + impl.serverDataStore.InstallerCrdObjectStatus = val + impl.serverDataStore.InstallerCrdObjectExists = true +} diff --git a/pkg/module/ModuleCronService.go b/pkg/module/ModuleCronService.go new file mode 100644 index 0000000000..61ce5d083c --- /dev/null +++ b/pkg/module/ModuleCronService.go @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2020 Devtron Labs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package module + +import ( + "fmt" + "github.com/robfig/cron/v3" + "go.uber.org/zap" + "time" +) + +type ModuleCronService interface { +} + +type ModuleCronServiceImpl struct { + logger *zap.SugaredLogger + cron *cron.Cron + moduleEnvConfig *ModuleEnvConfig + moduleRepository ModuleRepository +} + +func NewModuleCronServiceImpl(logger *zap.SugaredLogger, moduleEnvConfig *ModuleEnvConfig, moduleRepository ModuleRepository) (*ModuleCronServiceImpl, error) { + + moduleCronServiceImpl := &ModuleCronServiceImpl{ + logger: logger, + moduleRepository: moduleRepository, + } + + // cron job to update status as timeout if installing state keeps in more than 1 hour + // initialise cron + cron := cron.New( + cron.WithChain()) + cron.Start() + + // add function into cron + _, err := cron.AddFunc(fmt.Sprintf("@every %dm", moduleEnvConfig.ModuleTimeoutStatusHandlingCronDurationInMin), moduleCronServiceImpl.HandleModuleTimeoutStatus) + if err != nil { + fmt.Println("error in adding cron function into module cron service") + return nil, err + } + + moduleCronServiceImpl.cron = cron + + return moduleCronServiceImpl, nil +} + +// check modules from DB. if status if installing for 1 hour, mark it as timeout +func (impl *ModuleCronServiceImpl) HandleModuleTimeoutStatus() { + impl.logger.Debug("starting module status check thread") + defer impl.logger.Debug("stopped module status check thread") + + // fetch all modules from DB + modules, err := impl.moduleRepository.FindAll() + if err != nil { + impl.logger.Errorw("error occurred while fetching all the modules from DB", "err", err) + return + } + + + // update status timeout if module status is installing for more than 1 hour + for _, module := range modules { + if module.Status != ModuleStatusInstalling || !time.Now().After(module.UpdatedOn.Add(1*time.Hour)) { + continue + } + + impl.logger.Debugw("updating module status as timeout", "name", module.Name) + module.Status = ModuleStatusTimeout + module.UpdatedOn = time.Now() + err = impl.moduleRepository.Update(&module) + if err != nil { + impl.logger.Errorw("error in updating module status to timeout", "name", module.Name, "err", err) + } + + } + +} diff --git a/pkg/module/ModuleRepository.go b/pkg/module/ModuleRepository.go new file mode 100644 index 0000000000..39113c2960 --- /dev/null +++ b/pkg/module/ModuleRepository.go @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2020 Devtron Labs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package module + +import ( + "github.com/go-pg/pg" + "time" +) + +type Module struct { + tableName struct{} `sql:"module"` + Id int `sql:"id,pk"` + Name string `sql:"name, notnull"` + Version string `sql:"version, notnull"` + Status string `sql:"status,notnull"` + UpdatedOn time.Time `sql:"updated_on"` +} + +type ModuleRepository interface { + Save(module *Module) error + FindOne(name string) (*Module, error) + Update(module *Module) error + FindAll() ([]Module, error) +} + +type ModuleRepositoryImpl struct { + dbConnection *pg.DB +} + +func NewModuleRepositoryImpl(dbConnection *pg.DB) *ModuleRepositoryImpl { + return &ModuleRepositoryImpl{dbConnection: dbConnection} +} + +func (impl ModuleRepositoryImpl) Save(module *Module) error { + return impl.dbConnection.Insert(module) +} + +func (impl ModuleRepositoryImpl) FindOne(name string) (*Module, error) { + module := &Module{} + err := impl.dbConnection.Model(module). + Where("name = ?", name).Select() + return module, err +} + +func (impl ModuleRepositoryImpl) Update(module *Module) error { + return impl.dbConnection.Update(module) +} + +func (impl ModuleRepositoryImpl) FindAll() ([]Module, error) { + var modules []Module + err := impl.dbConnection.Model(&modules). + Select() + return modules, err +} diff --git a/pkg/module/ModuleService.go b/pkg/module/ModuleService.go new file mode 100644 index 0000000000..0d52857fa5 --- /dev/null +++ b/pkg/module/ModuleService.go @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2020 Devtron Labs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package module + +import ( + "context" + "errors" + client "github.com/devtron-labs/devtron/api/helm-app" + serverEnvConfig "github.com/devtron-labs/devtron/pkg/server/config" + util2 "github.com/devtron-labs/devtron/util" + "github.com/go-pg/pg" + "go.uber.org/zap" + "time" +) + +type ModuleService interface { + GetModuleInfo(name string) (*ModuleInfoDto, error) + HandleModuleAction(userId int32, moduleName string, moduleActionRequest *ModuleActionRequestDto) (*ActionResponse, error) +} + +type ModuleServiceImpl struct { + logger *zap.SugaredLogger + serverEnvConfig *serverEnvConfig.ServerEnvConfig + moduleRepository ModuleRepository + moduleActionAuditLogRepository ModuleActionAuditLogRepository + helmAppService client.HelmAppService + // no need to inject moduleCacheService and cronService, but not generating in wire_gen (not triggering cache work in constructor) if not injecting. hence injecting + moduleCacheService ModuleCacheService + moduleCronService ModuleCronService +} + +func NewModuleServiceImpl(logger *zap.SugaredLogger, serverEnvConfig *serverEnvConfig.ServerEnvConfig, moduleRepository ModuleRepository, + moduleActionAuditLogRepository ModuleActionAuditLogRepository, helmAppService client.HelmAppService, moduleCacheService ModuleCacheService, moduleCronService ModuleCronService) *ModuleServiceImpl { + return &ModuleServiceImpl{ + logger: logger, + serverEnvConfig: serverEnvConfig, + moduleRepository: moduleRepository, + moduleActionAuditLogRepository: moduleActionAuditLogRepository, + helmAppService: helmAppService, + moduleCacheService: moduleCacheService, + moduleCronService: moduleCronService, + } +} + +func (impl ModuleServiceImpl) GetModuleInfo(name string) (*ModuleInfoDto, error) { + impl.logger.Debugw("getting module info", "name", name) + + moduleInfoDto := &ModuleInfoDto{ + Name: name, + } + + // fetch from DB + module, err := impl.moduleRepository.FindOne(name) + if err != nil { + if err == pg.ErrNoRows { + // if entry is not found in database, then treat it as "notInstalled" + moduleInfoDto.Status = ModuleStatusNotInstalled + return moduleInfoDto, nil + } + // otherwise some error case + impl.logger.Errorw("error in getting module from DB ", "err", err) + return nil, err + } + + // otherwise send DB status + moduleInfoDto.Status = module.Status + return moduleInfoDto, nil +} + +func (impl ModuleServiceImpl) HandleModuleAction(userId int32, moduleName string, moduleActionRequest *ModuleActionRequestDto) (*ActionResponse, error) { + impl.logger.Debugw("handling module action request", "moduleName", moduleName, "userId", userId, "payload", moduleActionRequest) + + // check if can update server + if !impl.serverEnvConfig.CanServerUpdate { + return nil, errors.New("module installation is not allowed") + } + + // insert into audit table + moduleActionAuditLog := &ModuleActionAuditLog{ + ModuleName: moduleName, + Version: moduleActionRequest.Version, + Action: moduleActionRequest.Action, + CreatedOn: time.Now(), + CreatedBy: userId, + } + err := impl.moduleActionAuditLogRepository.Save(moduleActionAuditLog) + if err != nil { + impl.logger.Errorw("error in saving into audit log for module action ", "err", err) + return nil, err + } + + // get module by name + // if error, throw error + // if module not found, then insert entry + // if module found, then update entry + module, err := impl.moduleRepository.FindOne(moduleName) + moduleFound := true + if err != nil { + // either error or no data found + if err == pg.ErrNoRows { + // in case of entry not found, update variable + moduleFound = false + // initialise module to save in DB + module = &Module{ + Name: moduleName, + } + } else { + // otherwise some error case + impl.logger.Errorw("error in getting module ", "moduleName", moduleName, "err", err) + return nil, err + } + } else { + // case of data found from DB + // check if module is already installed or installing + currentModuleStatus := module.Status + if currentModuleStatus == ModuleStatusInstalling || currentModuleStatus == ModuleStatusInstalled { + return nil, errors.New("module is already in installing/installed state") + } + + } + + // since the request can only come for install, hence update the DB with installing status + module.Status = ModuleStatusInstalling + module.Version = moduleActionRequest.Version + module.UpdatedOn = time.Now() + if moduleFound { + err = impl.moduleRepository.Update(module) + } else { + err = impl.moduleRepository.Save(module) + } + if err != nil { + impl.logger.Errorw("error in saving/updating module ", "moduleName", moduleName, "err", err) + return nil, err + } + + // HELM_OPERATION Starts + devtronHelmAppIdentifier := impl.helmAppService.GetDevtronHelmAppIdentifier() + chartRepository := &client.ChartRepository{ + Name: impl.serverEnvConfig.DevtronHelmRepoName, + Url: impl.serverEnvConfig.DevtronHelmRepoUrl, + } + + extraValues := make(map[string]interface{}) + extraValues["installer.release"] = moduleActionRequest.Version + extraValues["installer.modules"] = []interface{}{moduleName} + extraValuesYamlUrl := util2.BuildDevtronBomUrl(impl.serverEnvConfig.DevtronBomUrl, moduleActionRequest.Version) + + updateResponse, err := impl.helmAppService.UpdateApplicationWithChartInfoWithExtraValues(context.Background(), devtronHelmAppIdentifier, chartRepository, extraValues, extraValuesYamlUrl, true) + if err != nil { + impl.logger.Errorw("error in updating helm release ", "err", err) + module.Status = ModuleStatusInstallFailed + impl.moduleRepository.Update(module) + return nil, err + } + if !updateResponse.GetSuccess() { + module.Status = ModuleStatusInstallFailed + impl.moduleRepository.Update(module) + return nil, errors.New("success is false from helm") + } + // HELM_OPERATION Ends + + return &ActionResponse{ + Success: true, + }, nil +} diff --git a/pkg/server/ServerActionAuditLogRepository.go b/pkg/server/ServerActionAuditLogRepository.go new file mode 100644 index 0000000000..d4b40d657c --- /dev/null +++ b/pkg/server/ServerActionAuditLogRepository.go @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2020 Devtron Labs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package server + +import ( + "github.com/go-pg/pg" + "time" +) + +type ServerActionAuditLog struct { + tableName struct{} `sql:"server_action_audit_log"` + Id int `sql:"id,pk"` + Action string `sql:"action,notnull"` + Version string `sql:"version"` + CreatedOn time.Time `sql:"created_on,notnull"` + CreatedBy int32 `sql:"created_by,notnull"` +} + +type ServerActionAuditLogRepository interface { + Save(serverActionAuditLog *ServerActionAuditLog) error +} + +type ServerActionAuditLogRepositoryImpl struct { + dbConnection *pg.DB +} + +func NewServerActionAuditLogRepositoryImpl(dbConnection *pg.DB) *ServerActionAuditLogRepositoryImpl { + return &ServerActionAuditLogRepositoryImpl{dbConnection: dbConnection} +} + +func (impl ServerActionAuditLogRepositoryImpl) Save(serverActionAuditLog *ServerActionAuditLog) error { + err := impl.dbConnection.Insert(serverActionAuditLog) + return err +} diff --git a/pkg/server/ServerCacheService.go b/pkg/server/ServerCacheService.go new file mode 100644 index 0000000000..a250e0827c --- /dev/null +++ b/pkg/server/ServerCacheService.go @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2020 Devtron Labs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package server + +import ( + "context" + client "github.com/devtron-labs/devtron/api/helm-app" + serverEnvConfig "github.com/devtron-labs/devtron/pkg/server/config" + serverDataStore "github.com/devtron-labs/devtron/pkg/server/store" + "github.com/tidwall/gjson" + "go.uber.org/zap" + "log" +) + +type ServerCacheService interface { +} + +type ServerCacheServiceImpl struct { + logger *zap.SugaredLogger + serverEnvConfig *serverEnvConfig.ServerEnvConfig + serverDataStore *serverDataStore.ServerDataStore + helmAppService client.HelmAppService +} + +func NewServerCacheServiceImpl(logger *zap.SugaredLogger, serverEnvConfig *serverEnvConfig.ServerEnvConfig, serverDataStore *serverDataStore.ServerDataStore, helmAppService client.HelmAppService) *ServerCacheServiceImpl { + impl := &ServerCacheServiceImpl{ + logger: logger, + serverEnvConfig: serverEnvConfig, + serverDataStore: serverDataStore, + helmAppService: helmAppService, + } + + // fetch current version from helm release + appIdentifier := client.AppIdentifier{ + ClusterId: 1, + Namespace: impl.serverEnvConfig.DevtronHelmReleaseNamespace, + ReleaseName: impl.serverEnvConfig.DevtronHelmReleaseName, + } + releaseInfo, err := impl.helmAppService.GetValuesYaml(context.Background(), &appIdentifier) + if err != nil { + log.Fatalln("got error in fetching devtron helm release values.", "error", err) + } + currentVersion := gjson.Get(releaseInfo.GetMergedValues(), impl.serverEnvConfig.DevtronVersionIdentifierInHelmValues).String() + if len(currentVersion) == 0 { + log.Fatalln("current devtron version found empty") + } + + // store current version in-memory + impl.serverDataStore.CurrentVersion = currentVersion + + return impl +} diff --git a/pkg/server/ServerService.go b/pkg/server/ServerService.go new file mode 100644 index 0000000000..8af15de27c --- /dev/null +++ b/pkg/server/ServerService.go @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2020 Devtron Labs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package server + +import ( + "context" + "errors" + client "github.com/devtron-labs/devtron/api/helm-app" + serverBean "github.com/devtron-labs/devtron/pkg/server/bean" + serverEnvConfig "github.com/devtron-labs/devtron/pkg/server/config" + serverDataStore "github.com/devtron-labs/devtron/pkg/server/store" + util2 "github.com/devtron-labs/devtron/util" + "go.uber.org/zap" + "time" +) + +type ServerService interface { + GetServerInfo() (*serverBean.ServerInfoDto, error) + HandleServerAction(userId int32, serverActionRequest *serverBean.ServerActionRequestDto) (*serverBean.ActionResponse, error) +} + +type ServerServiceImpl struct { + logger *zap.SugaredLogger + serverActionAuditLogRepository ServerActionAuditLogRepository + serverDataStore *serverDataStore.ServerDataStore + serverEnvConfig *serverEnvConfig.ServerEnvConfig + helmAppService client.HelmAppService + // no need to inject serverCacheService, but not generating in wire_gen (not triggering cache work in constructor) if not injecting. hence injecting + serverCacheService ServerCacheService +} + +func NewServerServiceImpl(logger *zap.SugaredLogger, serverActionAuditLogRepository ServerActionAuditLogRepository, + serverDataStore *serverDataStore.ServerDataStore, serverEnvConfig *serverEnvConfig.ServerEnvConfig, helmAppService client.HelmAppService, serverCacheService ServerCacheService) *ServerServiceImpl { + return &ServerServiceImpl{ + logger: logger, + serverActionAuditLogRepository: serverActionAuditLogRepository, + serverDataStore: serverDataStore, + serverEnvConfig: serverEnvConfig, + helmAppService: helmAppService, + serverCacheService: serverCacheService, + } +} + +func (impl ServerServiceImpl) GetServerInfo() (*serverBean.ServerInfoDto, error) { + impl.logger.Debug("getting server info") + + // fetch status of devtron helm app + devtronHelmAppIdentifier := impl.helmAppService.GetDevtronHelmAppIdentifier() + devtronAppDetail, err := impl.helmAppService.GetApplicationDetail(context.Background(), devtronHelmAppIdentifier) + if err != nil { + impl.logger.Errorw("error in getting devtron helm app release status ", "err", err) + return nil, err + } + + helmReleaseStatus := devtronAppDetail.ReleaseStatus.Status + var serverStatus string + + // for hyperion mode mode i.e. installer object not found - use mapping + // for full mode - + // if installer object status is applied then use mapping + // if empty or downloaded, then check timeout + // else if deployed then upgrading + // else use mapping + if !impl.serverDataStore.InstallerCrdObjectExists { + serverStatus = mapServerStatusFromHelmReleaseStatus(helmReleaseStatus) + } else { + if impl.serverDataStore.InstallerCrdObjectStatus == serverBean.InstallerCrdObjectStatusApplied { + serverStatus = mapServerStatusFromHelmReleaseStatus(helmReleaseStatus) + } else if time.Now().After(devtronAppDetail.GetLastDeployed().AsTime().Add(1 * time.Hour)) { + serverStatus = serverBean.ServerStatusTimeout + } else if helmReleaseStatus == serverBean.HelmReleaseStatusDeployed { + serverStatus = serverBean.ServerStatusUpgrading + } else { + serverStatus = mapServerStatusFromHelmReleaseStatus(helmReleaseStatus) + } + } + + serverInfoDto := &serverBean.ServerInfoDto{ + CurrentVersion: impl.serverDataStore.CurrentVersion, + ReleaseName: impl.serverEnvConfig.DevtronHelmReleaseName, + Status: serverStatus, + CanUpdateServer: impl.serverEnvConfig.CanServerUpdate, + } + + return serverInfoDto, nil +} + +func (impl ServerServiceImpl) HandleServerAction(userId int32, serverActionRequest *serverBean.ServerActionRequestDto) (*serverBean.ActionResponse, error) { + impl.logger.Debugw("handling server action request", "userId", userId, "payload", serverActionRequest) + + // check if can update server + if !impl.serverEnvConfig.CanServerUpdate { + return nil, errors.New("server up-gradation is not allowed") + } + + // insert into audit table + serverActionAuditLog := &ServerActionAuditLog{ + Action: serverActionRequest.Action, + Version: serverActionRequest.Version, + CreatedOn: time.Now(), + CreatedBy: userId, + } + err := impl.serverActionAuditLogRepository.Save(serverActionAuditLog) + if err != nil { + impl.logger.Errorw("error in saving into audit log for server action ", "err", err) + return nil, err + } + + // HELM_OPERATION Starts + devtronHelmAppIdentifier := impl.helmAppService.GetDevtronHelmAppIdentifier() + chartRepository := &client.ChartRepository{ + Name: impl.serverEnvConfig.DevtronHelmRepoName, + Url: impl.serverEnvConfig.DevtronHelmRepoUrl, + } + + extraValues := make(map[string]interface{}) + extraValues["installer.release"] = serverActionRequest.Version + extraValuesYamlUrl := util2.BuildDevtronBomUrl(impl.serverEnvConfig.DevtronBomUrl, serverActionRequest.Version) + + updateResponse, err := impl.helmAppService.UpdateApplicationWithChartInfoWithExtraValues(context.Background(), devtronHelmAppIdentifier, chartRepository, extraValues, extraValuesYamlUrl, true) + if err != nil { + impl.logger.Errorw("error in updating helm release ", "err", err) + return nil, err + } + if !updateResponse.GetSuccess() { + return nil, errors.New("success is false from helm") + } + // HELM_OPERATION Ends + + return &serverBean.ActionResponse{ + Success: true, + }, nil +} + +func mapServerStatusFromHelmReleaseStatus(helmReleaseStatus string) string { + var serverStatus string + switch helmReleaseStatus { + case serverBean.HelmReleaseStatusDeployed: + serverStatus = serverBean.ServerStatusHealthy + case serverBean.HelmReleaseStatusFailed: + serverStatus = serverBean.ServerStatusUpgradeFailed + case serverBean.HelmReleaseStatusPendingUpgrade: + serverStatus = serverBean.ServerStatusUpgrading + default: + serverStatus = serverBean.ServerStatusUnknown + } + return serverStatus +} diff --git a/pkg/server/bean/Bean.go b/pkg/server/bean/Bean.go new file mode 100644 index 0000000000..4851aece35 --- /dev/null +++ b/pkg/server/bean/Bean.go @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2020 Devtron Labs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package serverBean + +type ServerInfoDto struct { + CurrentVersion string `json:"currentVersion,notnull"` + Status string `json:"status,notnull" validate:"oneof=healthy upgrading upgradeFailed unknown timeout"` + ReleaseName string `json:"releaseName,notnull"` + CanUpdateServer bool `json:"canUpdateServer,notnull"` +} + +type ServerActionRequestDto struct { + Action string `json:"action,notnull" validate:"oneof=upgrade"` + Version string `json:"version,notnull"` +} + +type ActionResponse struct { + Success bool `json:"success"` +} + +type ServerStatus = string +type InstallerCrdObjectStatus = string + +const ( + ServerStatusHealthy ServerStatus = "healthy" + ServerStatusUpgrading ServerStatus = "upgrading" + ServerStatusUpgradeFailed ServerStatus = "upgradeFailed" + ServerStatusUnknown ServerStatus = "unknown" + ServerStatusTimeout ServerStatus = "timeout" + + InstallerCrdObjectStatusBlank InstallerCrdObjectStatus = "" + InstallerCrdObjectStatusDownloaded InstallerCrdObjectStatus = "Downloaded" + InstallerCrdObjectStatusApplied InstallerCrdObjectStatus = "Applied" +) + +type HelmReleaseStatus = string + +// Describe the status of a release +// NOTE: Make sure to update cmd/helm/status.go when adding or modifying any of these statuses. +const ( + // HelmReleaseStatusUnknown indicates that a release is in an uncertain state. + HelmReleaseStatusUnknown HelmReleaseStatus = "unknown" + // HelmReleaseStatusDeployed indicates that the release has been pushed to Kubernetes. + HelmReleaseStatusDeployed HelmReleaseStatus = "deployed" + // HelmReleaseStatusUninstalled indicates that a release has been uninstalled from Kubernetes. + HelmReleaseStatusUninstalled HelmReleaseStatus = "uninstalled" + // HelmReleaseStatusSuperseded indicates that this release object is outdated and a newer one exists. + HelmReleaseStatusSuperseded HelmReleaseStatus = "superseded" + // HelmReleaseStatusFailed indicates that the release was not successfully deployed. + HelmReleaseStatusFailed HelmReleaseStatus = "failed" + // HelmReleaseStatusUninstalling indicates that a uninstall operation is underway. + HelmReleaseStatusUninstalling HelmReleaseStatus = "uninstalling" + // HelmReleaseStatusPendingInstall indicates that an install operation is underway. + HelmReleaseStatusPendingInstall HelmReleaseStatus = "pending-install" + // HelmReleaseStatusPendingUpgrade indicates that an upgrade operation is underway. + HelmReleaseStatusPendingUpgrade HelmReleaseStatus = "pending-upgrade" + // HelmReleaseStatusPendingRollback indicates that an rollback operation is underway. + HelmReleaseStatusPendingRollback HelmReleaseStatus = "pending-rollback" +) + +type AppHealthStatusCode = string + +const ( + AppHealthStatusProgressing AppHealthStatusCode = "Progressing" + AppHealthStatusDegraded AppHealthStatusCode = "Degraded" +) diff --git a/pkg/server/config/ServerEnvConfig.go b/pkg/server/config/ServerEnvConfig.go new file mode 100644 index 0000000000..76cc0f9762 --- /dev/null +++ b/pkg/server/config/ServerEnvConfig.go @@ -0,0 +1,32 @@ +package serverEnvConfig + +import ( + "fmt" + "github.com/caarlos0/env" +) + +type ServerEnvConfig struct { + CanServerUpdate bool `env:"CAN_SERVER_UPDATE" envDefault:"true"` // default true + InstallerCrdObjectGroupName string `env:"INSTALLER_CRD_OBJECT_GROUP_NAME" envDefault:"installer.devtron.ai"` + InstallerCrdObjectVersion string `env:"INSTALLER_CRD_OBJECT_VERSION" envDefault:"v1alpha1"` + InstallerCrdObjectResource string `env:"INSTALLER_CRD_OBJECT_RESOURCE" envDefault:"installers"` + InstallerCrdNamespace string `env:"INSTALLER_CRD_NAMESPACE" envDefault:"devtroncd"` + DevtronHelmRepoName string `env:"DEVTRON_HELM_REPO_NAME" envDefault:"devtron"` + DevtronHelmRepoUrl string `env:"DEVTRON_HELM_REPO_URL" envDefault:"https://helm.devtron.ai"` + DevtronHelmReleaseName string `env:"DEVTRON_HELM_RELEASE_NAME" envDefault:"devtron"` + DevtronHelmReleaseNamespace string `env:"DEVTRON_HELM_RELEASE_NAMESPACE" envDefault:"devtroncd"` + DevtronHelmReleaseChartName string `env:"DEVTRON_HELM_RELEASE_CHART_NAME" envDefault:"devtron-operator"` + DevtronVersionIdentifierInHelmValues string `env:"DEVTRON_VERSION_IDENTIFIER_IN_HELM_VALUES" envDefault:"installer.release"` + DevtronModulesIdentifierInHelmValues string `env:"DEVTRON_MODULES_IDENTIFIER_IN_HELM_VALUES" envDefault:"installer.modules"` + DevtronBomUrl string `env:"DEVTRON_BOM_URL" envDefault:"https://raw.githubusercontent.com/devtron-labs/devtron/%s/charts/devtron/devtron-bom.yaml"` +} + +func ParseServerEnvConfig() (*ServerEnvConfig, error) { + cfg := &ServerEnvConfig{} + err := env.Parse(cfg) + if err != nil { + fmt.Println("failed to parse server env config: " + err.Error()) + return nil, err + } + return cfg, nil +} diff --git a/pkg/server/store/ServerDataStore.go b/pkg/server/store/ServerDataStore.go new file mode 100644 index 0000000000..48c397283d --- /dev/null +++ b/pkg/server/store/ServerDataStore.go @@ -0,0 +1,11 @@ +package serverDataStore + +type ServerDataStore struct { + CurrentVersion string + InstallerCrdObjectStatus string + InstallerCrdObjectExists bool +} + +func InitServerDataStore() *ServerDataStore { + return &ServerDataStore{} +} diff --git a/scripts/sql/46_modularisation_v1.down.sql b/scripts/sql/46_modularisation_v1.down.sql new file mode 100644 index 0000000000..aa64c53fdc --- /dev/null +++ b/scripts/sql/46_modularisation_v1.down.sql @@ -0,0 +1,14 @@ +DROP TABLE "public"."server_action_audit_log" CASCADE; + +DROP TABLE "public"."module_action_audit_log" CASCADE; + +DROP TABLE "public"."module" CASCADE; + +---- DROP sequence +DROP SEQUENCE IF EXISTS public.id_seq_module; + +---- DROP sequence +DROP SEQUENCE IF EXISTS public.id_seq_module_action_audit_log; + +---- DROP sequence +DROP SEQUENCE IF EXISTS public.id_seq_server_action_audit_log; \ No newline at end of file diff --git a/scripts/sql/46_modularisation_v1.up.sql b/scripts/sql/46_modularisation_v1.up.sql new file mode 100644 index 0000000000..b5482b92f8 --- /dev/null +++ b/scripts/sql/46_modularisation_v1.up.sql @@ -0,0 +1,42 @@ +-- Sequence and defined type +CREATE SEQUENCE id_seq_module; + +-- Table Definition +CREATE TABLE "public"."module" +( + "id" int4 NOT NULL DEFAULT nextval('id_seq_module'::regclass), + "name" varchar(255) NOT NULL, + "version" varchar(255) NOT NULL, + "status" varchar(255) NOT NULL, + "updated_on" timestamptz, + PRIMARY KEY ("id") +); + +-- Sequence and defined type +CREATE SEQUENCE id_seq_module_action_audit_log; + +-- Table Definition +CREATE TABLE "public"."module_action_audit_log" +( + "id" int4 NOT NULL DEFAULT nextval('id_seq_module_action_audit_log'::regclass), + "module_name" varchar(255) NOT NULL, + "version" varchar(255) NOT NULL, + "action" varchar(255) NOT NULL, + "created_on" timestamptz NOT NULL, + "created_by" int4 NOT NULL, + PRIMARY KEY ("id") +); + +-- Sequence and defined type +CREATE SEQUENCE id_seq_server_action_audit_log; + +-- Table Definition +CREATE TABLE "public"."server_action_audit_log" +( + "id" int4 NOT NULL DEFAULT nextval('id_seq_server_action_audit_log'::regclass), + "action" varchar(255) NOT NULL, + "version" varchar(255), + "created_on" timestamptz NOT NULL, + "created_by" int4 NOT NULL, + PRIMARY KEY ("id") +); \ No newline at end of file diff --git a/specs/modularisation/v1.yaml b/specs/modularisation/v1.yaml new file mode 100644 index 0000000000..28c3aa79b1 --- /dev/null +++ b/specs/modularisation/v1.yaml @@ -0,0 +1,134 @@ +openapi: "3.0.0" +info: + version: 1.0.0 + title: Modularisation v1 APIs +paths: + /orchestrator/module: + get: + description: Get module info + parameters: + - name: name + in: query + description: module name + required: true + schema: + type: string + responses: + "200": + description: Successfully fetched modules + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/ModuleInfo" + post: + description: some action on module (for eg - install/upgrade/etc..) + parameters: + - name: name + in: query + description: module name + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ModuleActionRequest" + responses: + "200": + description: module action response + content: + application/json: + schema: + $ref: "#/components/schemas/ActionResponse" + /orchestrator/server: + get: + description: Get server info + responses: + "200": + description: Successfully fetched current server info + content: + application/json: + schema: + $ref: "#/components/schemas/ServerInfo" + post: + description: some action on server (for eg - install/upgrade/etc..) + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ServerActionRequest" + responses: + "200": + description: server action response + content: + application/json: + schema: + $ref: "#/components/schemas/ActionResponse" + +# Components +components: + schemas: + ModuleInfo: + type: object + properties: + name: + type: string + description: module name + example: "cicd" + status: + type: string + description: status of the module + example: "notInstalled|installed|installing|installFailed|timeout" + ModuleActionRequest: + type: object + properties: + action: + type: string + description: action on the module + example: "install" + version: + type: string + description: version on which module is to be installed + example: "v1.2.3" + ServerInfo: + type: object + properties: + currentVersion: + type: string + description: current version of the devtron server + example: "v1.2.3" + status: + type: string + description: status of the server + example: "healthy|upgrading|upgradeFailed|unknown|timeout" + releaseName: + type: string + description: helm release name of the devtron server + example: "devtron" + canUpdateServer: + type: boolean + description: if server update is allowed programatically + example: "true" + ServerActionRequest: + type: object + properties: + action: + type: string + description: action on the server + example: "upgrade" + version: + type: string + description: version to which server is to be upgradred + example: "v1.2.3" + ActionResponse: + type: object + properties: + success: + type: boolean + description: success or failure + example: true diff --git a/tests/pipeline/ChartService_test.go b/tests/pipeline/ChartService_test.go index ce59ee9545..272b3eada8 100644 --- a/tests/pipeline/ChartService_test.go +++ b/tests/pipeline/ChartService_test.go @@ -22,7 +22,7 @@ var bulkUpdateRepository bulkUpdate.BulkUpdateRepositoryImpl func setup() { config, _ := sql.GetConfig() - logger := util.NewSugardLogger() + logger, _ := util.NewSugardLogger() dbConnection, _ := sql.NewDbConnection(config, logger) bulkUpdateRepository := bulkUpdate.NewBulkUpdateRepository(dbConnection, logger) bulkUpdateService = pipeline.NewBulkUpdateServiceImpl(bulkUpdateRepository, nil, nil, nil, nil, "", diff --git a/util/HttpUtil.go b/util/HttpUtil.go new file mode 100644 index 0000000000..314efd4959 --- /dev/null +++ b/util/HttpUtil.go @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2020 Devtron Labs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package util + +import ( + "errors" + "io/ioutil" + "net/http" + "strconv" + "time" +) + +func ReadFromUrlWithRetry(url string) ([]byte, error) { + var ( + err error + response *http.Response + retries = 3 + ) + + for retries > 0 { + response, err = http.Get(url) + if err != nil { + retries -= 1 + time.Sleep(1 * time.Second) + } else { + break + } + } + if response != nil { + defer response.Body.Close() + statusCode := response.StatusCode + if statusCode != http.StatusOK { + return nil, errors.New("Error in downloading file. Status code : " + strconv.Itoa(statusCode)) + } + body, err := ioutil.ReadAll(response.Body) + if err != nil { + return nil, err + } + return body, nil + } + return nil, err +} diff --git a/util/helper.go b/util/helper.go index 46ae53e3f2..64788c57bf 100644 --- a/util/helper.go +++ b/util/helper.go @@ -210,3 +210,7 @@ func ExtractTarGz(gzipStream io.Reader, chartDir string) error { } return nil } + +func BuildDevtronBomUrl(bomUrl string, version string) string { + return fmt.Sprintf(bomUrl, version) +} diff --git a/vendor/k8s.io/client-go/dynamic/dynamicinformer/informer.go b/vendor/k8s.io/client-go/dynamic/dynamicinformer/informer.go new file mode 100644 index 0000000000..cd7c60ab22 --- /dev/null +++ b/vendor/k8s.io/client-go/dynamic/dynamicinformer/informer.go @@ -0,0 +1,157 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dynamicinformer + +import ( + "sync" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/dynamic/dynamiclister" + "k8s.io/client-go/informers" + "k8s.io/client-go/tools/cache" +) + +// NewDynamicSharedInformerFactory constructs a new instance of dynamicSharedInformerFactory for all namespaces. +func NewDynamicSharedInformerFactory(client dynamic.Interface, defaultResync time.Duration) DynamicSharedInformerFactory { + return NewFilteredDynamicSharedInformerFactory(client, defaultResync, metav1.NamespaceAll, nil) +} + +// NewFilteredDynamicSharedInformerFactory constructs a new instance of dynamicSharedInformerFactory. +// Listers obtained via this factory will be subject to the same filters as specified here. +func NewFilteredDynamicSharedInformerFactory(client dynamic.Interface, defaultResync time.Duration, namespace string, tweakListOptions TweakListOptionsFunc) DynamicSharedInformerFactory { + return &dynamicSharedInformerFactory{ + client: client, + defaultResync: defaultResync, + namespace: metav1.NamespaceAll, + informers: map[schema.GroupVersionResource]informers.GenericInformer{}, + startedInformers: make(map[schema.GroupVersionResource]bool), + tweakListOptions: tweakListOptions, + } +} + +type dynamicSharedInformerFactory struct { + client dynamic.Interface + defaultResync time.Duration + namespace string + + lock sync.Mutex + informers map[schema.GroupVersionResource]informers.GenericInformer + // startedInformers is used for tracking which informers have been started. + // This allows Start() to be called multiple times safely. + startedInformers map[schema.GroupVersionResource]bool + tweakListOptions TweakListOptionsFunc +} + +var _ DynamicSharedInformerFactory = &dynamicSharedInformerFactory{} + +func (f *dynamicSharedInformerFactory) ForResource(gvr schema.GroupVersionResource) informers.GenericInformer { + f.lock.Lock() + defer f.lock.Unlock() + + key := gvr + informer, exists := f.informers[key] + if exists { + return informer + } + + informer = NewFilteredDynamicInformer(f.client, gvr, f.namespace, f.defaultResync, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) + f.informers[key] = informer + + return informer +} + +// Start initializes all requested informers. +func (f *dynamicSharedInformerFactory) Start(stopCh <-chan struct{}) { + f.lock.Lock() + defer f.lock.Unlock() + + for informerType, informer := range f.informers { + if !f.startedInformers[informerType] { + go informer.Informer().Run(stopCh) + f.startedInformers[informerType] = true + } + } +} + +// WaitForCacheSync waits for all started informers' cache were synced. +func (f *dynamicSharedInformerFactory) WaitForCacheSync(stopCh <-chan struct{}) map[schema.GroupVersionResource]bool { + informers := func() map[schema.GroupVersionResource]cache.SharedIndexInformer { + f.lock.Lock() + defer f.lock.Unlock() + + informers := map[schema.GroupVersionResource]cache.SharedIndexInformer{} + for informerType, informer := range f.informers { + if f.startedInformers[informerType] { + informers[informerType] = informer.Informer() + } + } + return informers + }() + + res := map[schema.GroupVersionResource]bool{} + for informType, informer := range informers { + res[informType] = cache.WaitForCacheSync(stopCh, informer.HasSynced) + } + return res +} + +// NewFilteredDynamicInformer constructs a new informer for a dynamic type. +func NewFilteredDynamicInformer(client dynamic.Interface, gvr schema.GroupVersionResource, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions TweakListOptionsFunc) informers.GenericInformer { + return &dynamicInformer{ + gvr: gvr, + informer: cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options metav1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.Resource(gvr).Namespace(namespace).List(options) + }, + WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.Resource(gvr).Namespace(namespace).Watch(options) + }, + }, + &unstructured.Unstructured{}, + resyncPeriod, + indexers, + ), + } +} + +type dynamicInformer struct { + informer cache.SharedIndexInformer + gvr schema.GroupVersionResource +} + +var _ informers.GenericInformer = &dynamicInformer{} + +func (d *dynamicInformer) Informer() cache.SharedIndexInformer { + return d.informer +} + +func (d *dynamicInformer) Lister() cache.GenericLister { + return dynamiclister.NewRuntimeObjectShim(dynamiclister.New(d.informer.GetIndexer(), d.gvr)) +} diff --git a/vendor/k8s.io/client-go/dynamic/dynamicinformer/interface.go b/vendor/k8s.io/client-go/dynamic/dynamicinformer/interface.go new file mode 100644 index 0000000000..083977c301 --- /dev/null +++ b/vendor/k8s.io/client-go/dynamic/dynamicinformer/interface.go @@ -0,0 +1,34 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dynamicinformer + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/informers" +) + +// DynamicSharedInformerFactory provides access to a shared informer and lister for dynamic client +type DynamicSharedInformerFactory interface { + Start(stopCh <-chan struct{}) + ForResource(gvr schema.GroupVersionResource) informers.GenericInformer + WaitForCacheSync(stopCh <-chan struct{}) map[schema.GroupVersionResource]bool +} + +// TweakListOptionsFunc defines the signature of a helper function +// that wants to provide more listing options to API +type TweakListOptionsFunc func(*metav1.ListOptions) diff --git a/vendor/k8s.io/client-go/dynamic/dynamiclister/interface.go b/vendor/k8s.io/client-go/dynamic/dynamiclister/interface.go new file mode 100644 index 0000000000..c39cbee925 --- /dev/null +++ b/vendor/k8s.io/client-go/dynamic/dynamiclister/interface.go @@ -0,0 +1,40 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dynamiclister + +import ( + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" +) + +// Lister helps list resources. +type Lister interface { + // List lists all resources in the indexer. + List(selector labels.Selector) (ret []*unstructured.Unstructured, err error) + // Get retrieves a resource from the indexer with the given name + Get(name string) (*unstructured.Unstructured, error) + // Namespace returns an object that can list and get resources in a given namespace. + Namespace(namespace string) NamespaceLister +} + +// NamespaceLister helps list and get resources. +type NamespaceLister interface { + // List lists all resources in the indexer for a given namespace. + List(selector labels.Selector) (ret []*unstructured.Unstructured, err error) + // Get retrieves a resource from the indexer for a given namespace and name. + Get(name string) (*unstructured.Unstructured, error) +} diff --git a/vendor/k8s.io/client-go/dynamic/dynamiclister/lister.go b/vendor/k8s.io/client-go/dynamic/dynamiclister/lister.go new file mode 100644 index 0000000000..a50fc471e9 --- /dev/null +++ b/vendor/k8s.io/client-go/dynamic/dynamiclister/lister.go @@ -0,0 +1,91 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dynamiclister + +import ( + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/tools/cache" +) + +var _ Lister = &dynamicLister{} +var _ NamespaceLister = &dynamicNamespaceLister{} + +// dynamicLister implements the Lister interface. +type dynamicLister struct { + indexer cache.Indexer + gvr schema.GroupVersionResource +} + +// New returns a new Lister. +func New(indexer cache.Indexer, gvr schema.GroupVersionResource) Lister { + return &dynamicLister{indexer: indexer, gvr: gvr} +} + +// List lists all resources in the indexer. +func (l *dynamicLister) List(selector labels.Selector) (ret []*unstructured.Unstructured, err error) { + err = cache.ListAll(l.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*unstructured.Unstructured)) + }) + return ret, err +} + +// Get retrieves a resource from the indexer with the given name +func (l *dynamicLister) Get(name string) (*unstructured.Unstructured, error) { + obj, exists, err := l.indexer.GetByKey(name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(l.gvr.GroupResource(), name) + } + return obj.(*unstructured.Unstructured), nil +} + +// Namespace returns an object that can list and get resources from a given namespace. +func (l *dynamicLister) Namespace(namespace string) NamespaceLister { + return &dynamicNamespaceLister{indexer: l.indexer, namespace: namespace, gvr: l.gvr} +} + +// dynamicNamespaceLister implements the NamespaceLister interface. +type dynamicNamespaceLister struct { + indexer cache.Indexer + namespace string + gvr schema.GroupVersionResource +} + +// List lists all resources in the indexer for a given namespace. +func (l *dynamicNamespaceLister) List(selector labels.Selector) (ret []*unstructured.Unstructured, err error) { + err = cache.ListAllByNamespace(l.indexer, l.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*unstructured.Unstructured)) + }) + return ret, err +} + +// Get retrieves a resource from the indexer for a given namespace and name. +func (l *dynamicNamespaceLister) Get(name string) (*unstructured.Unstructured, error) { + obj, exists, err := l.indexer.GetByKey(l.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(l.gvr.GroupResource(), name) + } + return obj.(*unstructured.Unstructured), nil +} diff --git a/vendor/k8s.io/client-go/dynamic/dynamiclister/shim.go b/vendor/k8s.io/client-go/dynamic/dynamiclister/shim.go new file mode 100644 index 0000000000..92a5f54af9 --- /dev/null +++ b/vendor/k8s.io/client-go/dynamic/dynamiclister/shim.go @@ -0,0 +1,87 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package dynamiclister + +import ( + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/tools/cache" +) + +var _ cache.GenericLister = &dynamicListerShim{} +var _ cache.GenericNamespaceLister = &dynamicNamespaceListerShim{} + +// dynamicListerShim implements the cache.GenericLister interface. +type dynamicListerShim struct { + lister Lister +} + +// NewRuntimeObjectShim returns a new shim for Lister. +// It wraps Lister so that it implements cache.GenericLister interface +func NewRuntimeObjectShim(lister Lister) cache.GenericLister { + return &dynamicListerShim{lister: lister} +} + +// List will return all objects across namespaces +func (s *dynamicListerShim) List(selector labels.Selector) (ret []runtime.Object, err error) { + objs, err := s.lister.List(selector) + if err != nil { + return nil, err + } + + ret = make([]runtime.Object, len(objs)) + for index, obj := range objs { + ret[index] = obj + } + return ret, err +} + +// Get will attempt to retrieve assuming that name==key +func (s *dynamicListerShim) Get(name string) (runtime.Object, error) { + return s.lister.Get(name) +} + +func (s *dynamicListerShim) ByNamespace(namespace string) cache.GenericNamespaceLister { + return &dynamicNamespaceListerShim{ + namespaceLister: s.lister.Namespace(namespace), + } +} + +// dynamicNamespaceListerShim implements the NamespaceLister interface. +// It wraps NamespaceLister so that it implements cache.GenericNamespaceLister interface +type dynamicNamespaceListerShim struct { + namespaceLister NamespaceLister +} + +// List will return all objects in this namespace +func (ns *dynamicNamespaceListerShim) List(selector labels.Selector) (ret []runtime.Object, err error) { + objs, err := ns.namespaceLister.List(selector) + if err != nil { + return nil, err + } + + ret = make([]runtime.Object, len(objs)) + for index, obj := range objs { + ret[index] = obj + } + return ret, err +} + +// Get will attempt to retrieve by namespace and name +func (ns *dynamicNamespaceListerShim) Get(name string) (runtime.Object, error) { + return ns.namespaceLister.Get(name) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 730b2403e5..4f35581c1b 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -924,6 +924,8 @@ k8s.io/apimachinery/third_party/forked/golang/reflect ## explicit k8s.io/client-go/discovery k8s.io/client-go/dynamic +k8s.io/client-go/dynamic/dynamicinformer +k8s.io/client-go/dynamic/dynamiclister k8s.io/client-go/informers k8s.io/client-go/informers/admissionregistration k8s.io/client-go/informers/admissionregistration/v1beta1 diff --git a/wire_gen.go b/wire_gen.go index 16511e1e99..bffe5b451a 100644 --- a/wire_gen.go +++ b/wire_gen.go @@ -1,6 +1,6 @@ // Code generated by Wire. DO NOT EDIT. -//go:generate wire +//go:generate go run github.com/google/wire/cmd/wire //+build !wireinject package main @@ -19,10 +19,12 @@ import ( "github.com/devtron-labs/devtron/api/deployment" externalLink2 "github.com/devtron-labs/devtron/api/externalLink" client4 "github.com/devtron-labs/devtron/api/helm-app" + module2 "github.com/devtron-labs/devtron/api/module" "github.com/devtron-labs/devtron/api/restHandler" app3 "github.com/devtron-labs/devtron/api/restHandler/app" "github.com/devtron-labs/devtron/api/router" pubsub2 "github.com/devtron-labs/devtron/api/router/pubsub" + server2 "github.com/devtron-labs/devtron/api/server" "github.com/devtron-labs/devtron/api/sse" sso2 "github.com/devtron-labs/devtron/api/sso" team2 "github.com/devtron-labs/devtron/api/team" @@ -81,12 +83,16 @@ import ( "github.com/devtron-labs/devtron/pkg/git" "github.com/devtron-labs/devtron/pkg/gitops" jira2 "github.com/devtron-labs/devtron/pkg/jira" + "github.com/devtron-labs/devtron/pkg/module" "github.com/devtron-labs/devtron/pkg/notifier" "github.com/devtron-labs/devtron/pkg/pipeline" "github.com/devtron-labs/devtron/pkg/pipeline/history" repository4 "github.com/devtron-labs/devtron/pkg/pipeline/history/repository" "github.com/devtron-labs/devtron/pkg/projectManagementService/jira" security2 "github.com/devtron-labs/devtron/pkg/security" + "github.com/devtron-labs/devtron/pkg/server" + "github.com/devtron-labs/devtron/pkg/server/config" + "github.com/devtron-labs/devtron/pkg/server/store" "github.com/devtron-labs/devtron/pkg/sql" "github.com/devtron-labs/devtron/pkg/sso" "github.com/devtron-labs/devtron/pkg/team" @@ -108,7 +114,10 @@ import ( // Injectors from Wire.go: func InitializeApp() (*App, error) { - sugaredLogger := util.NewSugardLogger() + sugaredLogger, err := util.NewSugardLogger() + if err != nil { + return nil, err + } config, err := sql.GetConfig() if err != nil { return nil, err @@ -405,7 +414,12 @@ func InitializeApp() (*App, error) { } helmAppClientImpl := client4.NewHelmAppClientImpl(sugaredLogger, helmClientConfig) enforcerUtilHelmImpl := rbac.NewEnforcerUtilHelmImpl(sugaredLogger, clusterRepositoryImpl) - helmAppServiceImpl := client4.NewHelmAppServiceImpl(sugaredLogger, clusterServiceImplExtended, helmAppClientImpl, pumpImpl, enforcerUtilHelmImpl) + serverDataStoreServerDataStore := serverDataStore.InitServerDataStore() + serverEnvConfigServerEnvConfig, err := serverEnvConfig.ParseServerEnvConfig() + if err != nil { + return nil, err + } + helmAppServiceImpl := client4.NewHelmAppServiceImpl(sugaredLogger, clusterServiceImplExtended, helmAppClientImpl, pumpImpl, enforcerUtilHelmImpl, serverDataStoreServerDataStore, serverEnvConfigServerEnvConfig) appStoreDeploymentHelmServiceImpl := appStoreDeploymentTool.NewAppStoreDeploymentHelmServiceImpl(sugaredLogger, helmAppServiceImpl, appStoreApplicationVersionRepositoryImpl, environmentRepositoryImpl, helmAppClientImpl) installedAppVersionHistoryRepositoryImpl := repository6.NewInstalledAppVersionHistoryRepositoryImpl(sugaredLogger, db) appStoreDeploymentArgoCdServiceImpl := appStoreDeploymentGitopsTool.NewAppStoreDeploymentArgoCdServiceImpl(sugaredLogger, appStoreDeploymentFullModeServiceImpl, serviceClientImpl, chartGroupDeploymentRepositoryImpl, installedAppRepositoryImpl, installedAppVersionHistoryRepositoryImpl) @@ -533,7 +547,26 @@ func InitializeApp() (*App, error) { externalLinkServiceImpl := externalLink.NewExternalLinkServiceImpl(sugaredLogger, externalLinkMonitoringToolRepositoryImpl, externalLinkClusterMappingRepositoryImpl, externalLinkRepositoryImpl) externalLinkRestHandlerImpl := externalLink2.NewExternalLinkRestHandlerImpl(sugaredLogger, externalLinkServiceImpl, userServiceImpl, enforcerImpl) externalLinkRouterImpl := externalLink2.NewExternalLinkRouterImpl(externalLinkRestHandlerImpl) - muxRouter := router.NewMuxRouter(sugaredLogger, helmRouterImpl, pipelineConfigRouterImpl, migrateDbRouterImpl, appListingRouterImpl, environmentRouterImpl, clusterRouterImpl, webhookRouterImpl, userAuthRouterImpl, applicationRouterImpl, cdRouterImpl, projectManagementRouterImpl, gitProviderRouterImpl, gitHostRouterImpl, dockerRegRouterImpl, notificationRouterImpl, teamRouterImpl, gitWebhookHandlerImpl, workflowStatusUpdateHandlerImpl, applicationStatusUpdateHandlerImpl, ciEventHandlerImpl, pubSubClient, userRouterImpl, cronBasedEventReceiverImpl, chartRefRouterImpl, configMapRouterImpl, appStoreRouterImpl, chartRepositoryRouterImpl, releaseMetricsRouterImpl, deploymentGroupRouterImpl, batchOperationRouterImpl, chartGroupRouterImpl, testSuitRouterImpl, imageScanRouterImpl, policyRouterImpl, gitOpsConfigRouterImpl, dashboardRouterImpl, attributesRouterImpl, commonRouterImpl, grafanaRouterImpl, ssoLoginRouterImpl, telemetryRouterImpl, telemetryEventClientImplExtended, bulkUpdateRouterImpl, webhookListenerRouterImpl, appLabelRouterImpl, coreAppRouterImpl, helmAppRouterImpl, k8sApplicationRouterImpl, pProfRouterImpl, deploymentConfigRouterImpl, dashboardTelemetryRouterImpl, commonDeploymentRouterImpl, externalLinkRouterImpl) + moduleRepositoryImpl := module.NewModuleRepositoryImpl(db) + moduleActionAuditLogRepositoryImpl := module.NewModuleActionAuditLogRepositoryImpl(db) + moduleEnvConfig, err := module.ParseModuleEnvConfig() + if err != nil { + return nil, err + } + moduleCacheServiceImpl := module.NewModuleCacheServiceImpl(sugaredLogger, k8sUtil, moduleEnvConfig, serverEnvConfigServerEnvConfig, serverDataStoreServerDataStore, moduleRepositoryImpl) + moduleCronServiceImpl, err := module.NewModuleCronServiceImpl(sugaredLogger, moduleEnvConfig, moduleRepositoryImpl) + if err != nil { + return nil, err + } + moduleServiceImpl := module.NewModuleServiceImpl(sugaredLogger, serverEnvConfigServerEnvConfig, moduleRepositoryImpl, moduleActionAuditLogRepositoryImpl, helmAppServiceImpl, moduleCacheServiceImpl, moduleCronServiceImpl) + moduleRestHandlerImpl := module2.NewModuleRestHandlerImpl(sugaredLogger, moduleServiceImpl, userServiceImpl, enforcerImpl, validate) + moduleRouterImpl := module2.NewModuleRouterImpl(moduleRestHandlerImpl) + serverActionAuditLogRepositoryImpl := server.NewServerActionAuditLogRepositoryImpl(db) + serverCacheServiceImpl := server.NewServerCacheServiceImpl(sugaredLogger, serverEnvConfigServerEnvConfig, serverDataStoreServerDataStore, helmAppServiceImpl) + serverServiceImpl := server.NewServerServiceImpl(sugaredLogger, serverActionAuditLogRepositoryImpl, serverDataStoreServerDataStore, serverEnvConfigServerEnvConfig, helmAppServiceImpl, serverCacheServiceImpl) + serverRestHandlerImpl := server2.NewServerRestHandlerImpl(sugaredLogger, serverServiceImpl, userServiceImpl, enforcerImpl, validate) + serverRouterImpl := server2.NewServerRouterImpl(serverRestHandlerImpl) + muxRouter := router.NewMuxRouter(sugaredLogger, helmRouterImpl, pipelineConfigRouterImpl, migrateDbRouterImpl, appListingRouterImpl, environmentRouterImpl, clusterRouterImpl, webhookRouterImpl, userAuthRouterImpl, applicationRouterImpl, cdRouterImpl, projectManagementRouterImpl, gitProviderRouterImpl, gitHostRouterImpl, dockerRegRouterImpl, notificationRouterImpl, teamRouterImpl, gitWebhookHandlerImpl, workflowStatusUpdateHandlerImpl, applicationStatusUpdateHandlerImpl, ciEventHandlerImpl, pubSubClient, userRouterImpl, cronBasedEventReceiverImpl, chartRefRouterImpl, configMapRouterImpl, appStoreRouterImpl, chartRepositoryRouterImpl, releaseMetricsRouterImpl, deploymentGroupRouterImpl, batchOperationRouterImpl, chartGroupRouterImpl, testSuitRouterImpl, imageScanRouterImpl, policyRouterImpl, gitOpsConfigRouterImpl, dashboardRouterImpl, attributesRouterImpl, commonRouterImpl, grafanaRouterImpl, ssoLoginRouterImpl, telemetryRouterImpl, telemetryEventClientImplExtended, bulkUpdateRouterImpl, webhookListenerRouterImpl, appLabelRouterImpl, coreAppRouterImpl, helmAppRouterImpl, k8sApplicationRouterImpl, pProfRouterImpl, deploymentConfigRouterImpl, dashboardTelemetryRouterImpl, commonDeploymentRouterImpl, externalLinkRouterImpl, moduleRouterImpl, serverRouterImpl) mainApp := NewApp(muxRouter, sugaredLogger, sseSSE, versionServiceImpl, enforcer, db, pubSubClient, sessionManager) return mainApp, nil }