Skip to content
47 changes: 30 additions & 17 deletions kubelink/pkg/helmClient/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,9 @@ 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/"
)

// NewClientFromRestConf returns a new Helm client constructed with the provided REST config options
Expand All @@ -56,7 +57,7 @@ func NewClientFromRestConf(options *RestConfClientOptions) (Client, error) {

clientGetter := NewRESTClientGetter(options.Namespace, nil, options.RestConfig)

err := setEnvSettings(options.Options, settings)
err := SetEnvSettings(options.Options, settings)
if err != nil {
return nil, err
}
Expand All @@ -66,7 +67,7 @@ func NewClientFromRestConf(options *RestConfClientOptions) (Client, error) {

// newClient returns a new Helm client via the provided options and REST config
func newClient(options *Options, clientGetter genericclioptions.RESTClientGetter, settings *cli.EnvSettings) (Client, error) {
err := setEnvSettings(options, settings)
err := SetEnvSettings(options, settings)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -99,12 +100,12 @@ func newClient(options *Options, clientGetter genericclioptions.RESTClientGetter
}, nil
}

// setEnvSettings sets the client's environment settings based on the provided client configuration
func setEnvSettings(options *Options, settings *cli.EnvSettings) error {
// SetEnvSettings sets the client's environment settings based on the provided client configuration
func SetEnvSettings(options *Options, settings *cli.EnvSettings) error {
if options == nil {
options = &Options{
RepositoryConfig: defaultRepositoryConfigPath,
RepositoryCache: defaultCachePath,
RepositoryConfig: DefaultRepositoryConfigPath,
RepositoryCache: DefaultCachePath,
Linting: true,
}
}
Expand All @@ -121,11 +122,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
Expand Down Expand Up @@ -796,11 +797,24 @@ func updateDependencies(helmChart *chart.Chart, chartPathOptions *action.ChartPa
}

func GetChartBytes(helmChart *chart.Chart) ([]byte, error) {

absFilePath, err := GetChartSavedDir(helmChart)
if err != nil {
fmt.Println("error in getting saved chart data directory path", "err", err)
}
chartBytes, err := os.ReadFile(absFilePath)
if err != nil {
fmt.Println("error in reading chartdata from the file ", " filePath : ", absFilePath, " err : ", 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() {
Expand All @@ -812,13 +826,12 @@ func GetChartBytes(helmChart *chart.Chart) ([]byte, error) {
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
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
}
2 changes: 2 additions & 0 deletions kubelink/pkg/helmClient/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -40,4 +41,5 @@ 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
}
26 changes: 18 additions & 8 deletions kubelink/pkg/service/helmApplicationService/helmAppService.go
Original file line number Diff line number Diff line change
Expand Up @@ -563,8 +563,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)
Expand All @@ -573,13 +573,24 @@ func (impl *HelmAppServiceImpl) UpgradeRelease(ctx context.Context, request *cli
}
return nil, err
}
impl.logger.Debug("Updating the dependencies if required")

// perform Dependency Update in case we detect any dependency listed in chart.Metadata
if helmRelease.Chart.Metadata.Dependencies != nil || len(helmRelease.Chart.Metadata.Dependencies) > 0 {
impl.logger.Infow("Dependencies listed in Chart.yaml, performing dependency update before upgrading")
err = UpdateChartDependencies(helmClientObj.GetProviders(), helmRelease, registryClient)
if err != nil {
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")
Expand Down Expand Up @@ -614,7 +625,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)
Expand Down
167 changes: 167 additions & 0 deletions kubelink/pkg/service/helmApplicationService/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,24 @@
package helmApplicationService

import (
"bytes"
"errors"
"fmt"
client "github.com/devtron-labs/kubelink/grpc"
"github.com/devtron-labs/kubelink/pkg/helmClient"
"github.com/devtron-labs/kubelink/pkg/k8sInformer"
chart2 "helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chart/loader"
"helm.sh/helm/v3/pkg/chartutil"
downloader2 "helm.sh/helm/v3/pkg/downloader"
"helm.sh/helm/v3/pkg/getter"
"helm.sh/helm/v3/pkg/registry"
"helm.sh/helm/v3/pkg/release"
"os"
"path/filepath"
"sigs.k8s.io/yaml"
"strconv"
"strings"
)

func getUniqueReleaseIdentifierName(releaseIdentifier *client.ReleaseIdentifier) string {
Expand All @@ -33,3 +47,156 @@ const (
func IsReleaseNotFoundInCacheError(err error) bool {
return errors.Is(err, k8sInformer.ErrorCacheMissReleaseNotFound)
}

// UpdateChartLock updates the Chart.lock file based on its contents.
func UpdateChartLock(lockFilePath string, helmRelease *release.Release) error {
lockFileData, err := os.ReadFile(lockFilePath)
if err != nil {
return fmt.Errorf("error reading Chart.lock file at %s: %w", lockFilePath, err)
}
helmRelease.Chart.Lock = &chart2.Lock{}
if err := yaml.Unmarshal(lockFileData, helmRelease.Chart.Lock); err != nil {
return fmt.Errorf("error unmarshalling Chart.lock file at %s: %w", lockFilePath, err)
}
return nil
}

// ProcessTGZFiles locates and processes .tgz files in the charts directory.
func ProcessTGZFiles(chartsDir string, helmRelease *release.Release) error {
files, err := os.ReadDir(chartsDir)
if err != nil {
return fmt.Errorf("error reading charts directory in dir %s :%w", chartsDir, err)

}
for _, file := range files {
if strings.HasSuffix(file.Name(), ".tgz") {
tgzPath := filepath.Join(chartsDir, file.Name())

if err := expandAndSetDependency(tgzPath, helmRelease); err != nil {
return err
}
}
}
return nil
}

// 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 {
return fmt.Errorf("error creating temporary directory: %w", 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
}

func UpdateChartDependencies(providers getter.Providers, 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 {
fmt.Println("error in deleting dir", " dir: ", outputChartPathDir, " err: ", err)
}
}()
abpath, err := chartutil.Save(helmRelease.Chart, outputChartPathDir)
if err != nil {
fmt.Println("error in saving chartdata in the destination dir ", " dir : ", 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 {
fmt.Println("error unpacking 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: providers,
RepositoryConfig: helmClient.DefaultRepositoryConfigPath,
RepositoryCache: helmClient.DefaultCachePath,
RegistryClient: registry,
}
err = manager.Update()
if err != nil {
return fmt.Errorf("error updating chart dependencies: %w", err)
}

// Step 2: Check and process .tgz files in charts directory
chartsDir := filepath.Join(outputChartPathDir, "charts")
if err := ProcessTGZFiles(chartsDir, helmRelease); err != nil {
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) {
fmt.Sprintf("No Chart.lock file found, skipping lock file update, %s", lockFilePath)
return nil
}

if err := UpdateChartLock(lockFilePath, helmRelease); err != nil {
return err
}
return nil
}
2 changes: 1 addition & 1 deletion kubelink/wire_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading