diff --git a/kubelink/pkg/helmClient/client.go b/kubelink/pkg/helmClient/client.go index 9d27445e8..60d0d3b65 100644 --- a/kubelink/pkg/helmClient/client.go +++ b/kubelink/pkg/helmClient/client.go @@ -46,8 +46,10 @@ var storage = repo.File{} const ( CHART_WORKING_DIR_PATH = "/tmp/charts/" - defaultCachePath = "/home/devtron/devtroncd/.helmcache" - defaultRepositoryConfigPath = "/home/devtron/devtroncd/.helmrepo" + DefaultCachePath = "/home/devtron/devtroncd/.helmcache" + DefaultRepositoryConfigPath = "/home/devtron/devtroncd/.helmrepo" + DefaultTempDirectory = "/tmp/dir/" + TemHelmChartDirectory = "helmDependencyChart" ) // NewClientFromRestConf returns a new Helm client constructed with the provided REST config options @@ -103,9 +105,9 @@ func newClient(options *Options, clientGetter genericclioptions.RESTClientGetter func setEnvSettings(options *Options, settings *cli.EnvSettings) error { if options == nil { options = &Options{ - RepositoryConfig: defaultRepositoryConfigPath, - RepositoryCache: defaultCachePath, - Linting: true, + //RepositoryConfig: DefaultRepositoryConfigPath, + //RepositoryCache: DefaultCachePath, + Linting: true, } } @@ -121,11 +123,11 @@ func setEnvSettings(options *Options, settings *cli.EnvSettings) error { }*/ if options.RepositoryConfig == "" { - options.RepositoryConfig = defaultRepositoryConfigPath + options.RepositoryConfig = DefaultRepositoryConfigPath } if options.RepositoryCache == "" { - options.RepositoryCache = defaultCachePath + options.RepositoryCache = DefaultCachePath } settings.RepositoryCache = options.RepositoryCache @@ -796,29 +798,51 @@ func updateDependencies(helmChart *chart.Chart, chartPathOptions *action.ChartPa } func GetChartBytes(helmChart *chart.Chart) ([]byte, error) { + + absFilePath, err := GetChartSavedDir(helmChart) + if err != nil { + fmt.Errorf("error in getting saved chart data directory path %w", err) + return nil, err + } + chartBytes, err := os.ReadFile(absFilePath) + if err != nil { + fmt.Errorf("error in reading chartdata from the file filePath : %s err : %w", absFilePath, err) + return nil, err + } + + return chartBytes, nil +} +func GetChartSavedDir(helmChart *chart.Chart) (string, error) { dirPath := CHART_WORKING_DIR_PATH outputChartPathDir := fmt.Sprintf("%s/%s", dirPath, strconv.FormatInt(time.Now().UnixNano(), 16)) err := os.MkdirAll(outputChartPathDir, os.ModePerm) if err != nil { - return nil, err + return "", err } defer func() { err := os.RemoveAll(outputChartPathDir) if err != nil { - fmt.Println("error in deleting dir", " dir: ", outputChartPathDir, " err: ", err) + fmt.Errorf("error in deleting dir, %s, err: %w", outputChartPathDir, err) } }() absFilePath, err := chartutil.Save(helmChart, outputChartPathDir) if err != nil { - fmt.Println("error in saving chartdata in the destination dir ", " dir : ", outputChartPathDir, " err : ", err) - return nil, err + fmt.Errorf("error in saving chartdata in the destination dir %s, err: %w", outputChartPathDir, err) + return "", err } - chartBytes, err := os.ReadFile(absFilePath) - if err != nil { - fmt.Println("error in reading chartdata from the file ", " filePath : ", absFilePath, " err : ", err) - } + return absFilePath, nil +} - return chartBytes, nil +func (c *HelmClient) GetProviders() getter.Providers { + return c.Providers +} + +func (c *HelmClient) GetRepositoryConfig() string { + return c.Settings.RepositoryConfig +} + +func (c *HelmClient) GetRepositoryCache() string { + return c.Settings.RepositoryCache } diff --git a/kubelink/pkg/helmClient/interface.go b/kubelink/pkg/helmClient/interface.go index 957697d47..eaf1c93af 100644 --- a/kubelink/pkg/helmClient/interface.go +++ b/kubelink/pkg/helmClient/interface.go @@ -19,6 +19,7 @@ package helmClient import ( "context" "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/getter" "helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/repo" ) @@ -40,4 +41,7 @@ type Client interface { RollbackRelease(spec *ChartSpec, version int) error TemplateChart(spec *ChartSpec, options *HelmTemplateOptions, chartData []byte, returnChartBytes bool) ([]byte, []byte, error) GetNotes(spec *ChartSpec, options *HelmTemplateOptions) ([]byte, error) + GetProviders() getter.Providers + GetRepositoryConfig() string + GetRepositoryCache() string } diff --git a/kubelink/pkg/service/helmApplicationService/helmAppService.go b/kubelink/pkg/service/helmApplicationService/helmAppService.go index 1e7cdc736..b420201e8 100644 --- a/kubelink/pkg/service/helmApplicationService/helmAppService.go +++ b/kubelink/pkg/service/helmApplicationService/helmAppService.go @@ -29,11 +29,15 @@ import ( error2 "github.com/devtron-labs/kubelink/error" repository "github.com/devtron-labs/kubelink/pkg/cluster" "github.com/devtron-labs/kubelink/pkg/service/commonHelmService" + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chart/loader" + downloader2 "helm.sh/helm/v3/pkg/downloader" "helm.sh/helm/v3/pkg/registry" "helm.sh/helm/v3/pkg/storage/driver" "net/url" "path" + "sigs.k8s.io/yaml" "strings" "time" @@ -563,8 +567,8 @@ func (impl *HelmAppServiceImpl) UpgradeRelease(ctx context.Context, request *cli impl.logger.Errorw(HELM_CLIENT_ERROR, "err", err) return nil, err } - - helmRelease, err := impl.common.GetHelmRelease(releaseIdentifier.ClusterConfig, releaseIdentifier.ReleaseNamespace, releaseIdentifier.ReleaseName) + var helmRelease *release.Release + helmRelease, err = impl.common.GetHelmRelease(releaseIdentifier.ClusterConfig, releaseIdentifier.ReleaseNamespace, releaseIdentifier.ReleaseName) if err != nil { impl.logger.Errorw("Error in getting helm release ", "err", err) internalErr := error2.ConvertHelmErrorToInternalError(err) @@ -573,13 +577,28 @@ func (impl *HelmAppServiceImpl) UpgradeRelease(ctx context.Context, request *cli } return nil, err } + // perform Dependency Update in case we detect any dependency listed in chart.Metadata + if req := helmRelease.Chart.Metadata.Dependencies; req != nil { + if err := action.CheckDependencies(helmRelease.Chart, req); err != nil { + impl.logger.Errorw("An error occurred while checking for chart dependencies. You may need to run `helm dependency build` to fetch missing dependencies", "err", err) + if len(helmRelease.Chart.Metadata.Dependencies) != 0 { + impl.logger.Infow("Dependencies listed in Chart.yaml, performing dependency update before upgrading", "dependencies", helmRelease.Chart.Metadata.Dependencies) + err = impl.updateChartDependencies(helmClientObj, helmRelease, registryClient) + if err != nil { + impl.logger.Errorw("error in updating chart Dependencies", "err", err) + return nil, err + } + } + } + } updateChartSpec := &helmClient.ChartSpec{ - ReleaseName: releaseIdentifier.ReleaseName, - Namespace: releaseIdentifier.ReleaseNamespace, - ValuesYaml: request.ValuesYaml, - MaxHistory: int(request.HistoryMax), - RegistryClient: registryClient, + ReleaseName: releaseIdentifier.ReleaseName, + Namespace: releaseIdentifier.ReleaseNamespace, + ValuesYaml: request.ValuesYaml, + MaxHistory: int(request.HistoryMax), + RegistryClient: registryClient, + DependencyUpdate: true, } impl.logger.Debug("Upgrading release") @@ -614,7 +633,6 @@ func (impl *HelmAppServiceImpl) UpgradeRelease(ctx context.Context, request *cli } return upgradeReleaseResponse, nil } - func (impl *HelmAppServiceImpl) GetDeploymentDetail(request *client.DeploymentDetailRequest) (*client.DeploymentDetailResponse, error) { releaseIdentifier := request.ReleaseIdentifier helmReleases, err := impl.getHelmReleaseHistory(releaseIdentifier.ClusterConfig, releaseIdentifier.ReleaseNamespace, releaseIdentifier.ReleaseName, impl.helmReleaseConfig.MaxCountForHelmRelease) @@ -1920,3 +1938,167 @@ func podMetadataAdapter(podmetadatas []*commonBean.PodMetadata) []*client.PodMet } return podMetadatas } + +func (impl *HelmAppServiceImpl) updateChartDependencies(client helmClient.Client, helmRelease *release.Release, registry *registry.Client) error { + // Step 1: Update chart dependencies + outputChartPathDir := fmt.Sprintf("%s", helmClient.DefaultTempDirectory) + err := os.MkdirAll(outputChartPathDir, os.ModePerm) + if err != nil { + return err + } + defer func() { + err := os.RemoveAll(outputChartPathDir) + if err != nil { + impl.logger.Errorw("error in deleting dir", " dir: ", outputChartPathDir, " err: ", err) + } + }() + abpath, err := chartutil.Save(helmRelease.Chart, outputChartPathDir) + if err != nil { + impl.logger.Errorw("error in saving chartData in the destination dir", "outputChartPathDir", outputChartPathDir, " err: ", err) + return err + } + + // Unpack the .tgz file to a directory + h, err := os.Open(abpath) + if err != nil { + return err + } + if err := chartutil.Expand(helmClient.DefaultTempDirectory, h); err != nil { + impl.logger.Errorw("error in unpacking the chart", "dir", abpath, "err", err) + return err + } + outputChartPathDir = filepath.Join(helmClient.DefaultTempDirectory, helmRelease.Chart.Metadata.Name) + outputBuffer := bytes.NewBuffer(nil) + manager := &downloader2.Manager{ + ChartPath: outputChartPathDir, + Out: outputBuffer, + Getters: client.GetProviders(), + RepositoryConfig: client.GetRepositoryConfig(), + RepositoryCache: client.GetRepositoryCache(), + RegistryClient: registry, + } + err = manager.Update() + if err != nil { + impl.logger.Errorw("error updating chart dependencies", "err", err) + return err + } + + // Step 2: Check and process .tgz files in charts directory + chartsDir := filepath.Join(outputChartPathDir, "charts") + if err := impl.processTGZFiles(chartsDir, helmRelease); err != nil { + impl.logger.Errorw("error in processing TGZ files", "err", err) + return err + } + + // Step 3: Update the Chart.lock file if it exists + lockFilePath := filepath.Join(outputChartPathDir, "Chart.lock") + if _, err := os.Stat(lockFilePath); os.IsNotExist(err) { + impl.logger.Infow("No Chart.lock file found, skipping lock file update", "lockFilePath", lockFilePath) + return nil + } + + if err := impl.updateChartLock(lockFilePath, helmRelease); err != nil { + impl.logger.Errorw("error in updating the Chart Lock", "lockFilePath", lockFilePath, "err", err) + return err + } + return nil +} + +// UpdateChartLock updates the Chart.lock file based on its contents. +func (impl *HelmAppServiceImpl) updateChartLock(lockFilePath string, helmRelease *release.Release) error { + lockFileData, err := os.ReadFile(lockFilePath) + if err != nil { + impl.logger.Errorw("error reading Chart.lock file at", "lockFilePath", lockFilePath, "err", err) + return err + } + + helmRelease.Chart.Lock = &chart.Lock{} + err = yaml.Unmarshal(lockFileData, helmRelease.Chart.Lock) + if err != nil { + impl.logger.Errorw("error unmarshalling Chart.lock file at ", "lockFilePath", lockFilePath, "err", err) + return err + } + return nil +} + +// expandAndSetDependency expands a .tgz file and sets it as a dependency. +func (impl *HelmAppServiceImpl) expandAndSetDependency(tgzPath string, helmRelease *release.Release) error { + // Create a temporary directory for expanding the chart + tempDir, err := os.MkdirTemp("", helmClient.TemHelmChartDirectory) + if err != nil { + impl.logger.Errorw("error in creating temporary directory", "err", err) + return err + } + defer func() { + // Clean up the temporary directory + if removeErr := os.RemoveAll(tempDir); removeErr != nil { + impl.logger.Errorw("error in removing temp Directory", "tempDir", tempDir, "err", err) + } + }() + + // Open the .tgz file + tgzFile, err := os.Open(tgzPath) + if err != nil { + impl.logger.Errorw("error opening tgz file of tgzPath", "tgzPath", tgzPath, "err", err) + return err + } + defer func(tgzFile *os.File) { + err := tgzFile.Close() + if err != nil { + impl.logger.Errorw("error in closing tgz file of tgzPath ", "tgzPath", tgzPath, "err", err) + } + }(tgzFile) + + // Expand the .tgz file into the temporary directory + if err := chartutil.Expand(tempDir, tgzFile); err != nil { + impl.logger.Errorw("error expanding tgz file having filePath", "tgzPath", tgzPath, "err", err) + return err + } + + // Dynamically find the expanded chart directory + entries, err := os.ReadDir(tempDir) + if err != nil { + impl.logger.Errorw("error reading contents of temporary directory", "tempDir", tempDir, "err", err) + return err + } + + var expandedChartDir string + for _, entry := range entries { + if entry.IsDir() { + expandedChartDir = filepath.Join(tempDir, entry.Name()) + break + } + } + + if expandedChartDir == "" { + impl.logger.Errorw("error locating expanded chart directory found in", "tempDir", tempDir, "err", err) + return err + } + + // Load the expanded chart + expandedChart, err := loader.LoadDir(expandedChartDir) + if err != nil { + impl.logger.Errorw("error loading expanded chart", "err", err) + return err + } + helmRelease.Chart.SetDependencies(expandedChart) + return nil +} + +// ProcessTGZFiles locates and processes .tgz files in the charts directory. +func (impl *HelmAppServiceImpl) processTGZFiles(chartsDir string, helmRelease *release.Release) error { + files, err := os.ReadDir(chartsDir) + if err != nil { + impl.logger.Errorw("error reading charts directory in dir", "chartsDir", chartsDir, "err", err) + return err + } + for _, file := range files { + if strings.HasSuffix(file.Name(), ".tgz") { + tgzPath := filepath.Join(chartsDir, file.Name()) + if err := impl.expandAndSetDependency(tgzPath, helmRelease); err != nil { + return err + } + } + } + return nil +} diff --git a/kubelink/pkg/service/helmApplicationService/utils.go b/kubelink/pkg/service/helmApplicationService/utils.go index b2162c1d6..cc262d2f9 100644 --- a/kubelink/pkg/service/helmApplicationService/utils.go +++ b/kubelink/pkg/service/helmApplicationService/utils.go @@ -17,8 +17,14 @@ package helmApplicationService import ( "errors" + "fmt" client "github.com/devtron-labs/kubelink/grpc" "github.com/devtron-labs/kubelink/pkg/k8sInformer" + "helm.sh/helm/v3/pkg/chart/loader" + "helm.sh/helm/v3/pkg/chartutil" + "helm.sh/helm/v3/pkg/release" + "os" + "path/filepath" "strconv" ) @@ -33,3 +39,63 @@ const ( func IsReleaseNotFoundInCacheError(err error) bool { return errors.Is(err, k8sInformer.ErrorCacheMissReleaseNotFound) } + +// expandAndSetDependency expands a .tgz file and sets it as a dependency. +func expandAndSetDependency(tgzPath string, helmRelease *release.Release) error { + // Create a temporary directory for expanding the chart + tempDir, err := os.MkdirTemp("", "helmDependencyChart*") + if err != nil { + fmt.Errorf("error creating temporary directory: %w", err) + return err + } + defer func() { + // Clean up the temporary directory + if removeErr := os.RemoveAll(tempDir); removeErr != nil { + fmt.Sprintf("error in removing temporary directory %s: %s", tempDir, removeErr.Error()) + } + }() + + // Open the .tgz file + tgzFile, err := os.Open(tgzPath) + if err != nil { + return fmt.Errorf("error opening tgz file of tgzPath %s: %w", tgzPath, err) + } + defer func(tgzFile *os.File) { + err := tgzFile.Close() + if err != nil { + fmt.Errorf("error in closing tgz file of tgzPath %s: %w", tgzPath, err) + } + }(tgzFile) + + // Expand the .tgz file into the temporary directory + if err := chartutil.Expand(tempDir, tgzFile); err != nil { + return fmt.Errorf("error expanding tgz file having filePath %s : %w", tgzPath, err) + } + + // Dynamically find the expanded chart directory + entries, err := os.ReadDir(tempDir) + if err != nil { + return fmt.Errorf("error reading contents of temporary directory %s: %w", tempDir, err) + } + + var expandedChartDir string + for _, entry := range entries { + if entry.IsDir() { + expandedChartDir = filepath.Join(tempDir, entry.Name()) + break + } + } + + if expandedChartDir == "" { + err := fmt.Errorf("no expanded chart directory found in %s", tempDir) + return fmt.Errorf("error locating expanded chart directory found in %s: %w", tempDir, err) + } + + // Load the expanded chart + expandedChart, err := loader.LoadDir(expandedChartDir) + if err != nil { + return fmt.Errorf("error loading expanded chart : %w", err) + } + helmRelease.Chart.SetDependencies(expandedChart) + return nil +} diff --git a/kubelink/wire_gen.go b/kubelink/wire_gen.go index c8075adb9..2ded63c44 100644 --- a/kubelink/wire_gen.go +++ b/kubelink/wire_gen.go @@ -1,6 +1,6 @@ // Code generated by Wire. DO NOT EDIT. -//go:generate go run github.com/google/wire/cmd/wire +//go:generate go run -mod=mod github.com/google/wire/cmd/wire //go:build !wireinject // +build !wireinject