mirror of
https://github.com/kubevela/kubevela.git
synced 2026-02-14 18:10:21 +00:00
* Chore: optimize the package dependces Signed-off-by: barnettZQG <barnett.zqg@gmail.com> * Fix: the code style Signed-off-by: barnettZQG <barnett.zqg@gmail.com> * Fix: remove the repetitive context Signed-off-by: barnettZQG <barnett.zqg@gmail.com> * Fix: change the context key Signed-off-by: barnettZQG <barnett.zqg@gmail.com> * Fix: Optimize the e2e test case Signed-off-by: barnettZQG <barnett.zqg@gmail.com> --------- Signed-off-by: barnettZQG <barnett.zqg@gmail.com>
426 lines
13 KiB
Go
426 lines
13 KiB
Go
/*
|
|
Copyright 2021 The KubeVela 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 helm
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/pkg/errors"
|
|
"helm.sh/helm/v3/pkg/action"
|
|
"helm.sh/helm/v3/pkg/chart"
|
|
"helm.sh/helm/v3/pkg/chart/loader"
|
|
"helm.sh/helm/v3/pkg/chartutil"
|
|
"helm.sh/helm/v3/pkg/cli"
|
|
"helm.sh/helm/v3/pkg/downloader"
|
|
"helm.sh/helm/v3/pkg/getter"
|
|
"helm.sh/helm/v3/pkg/kube"
|
|
"helm.sh/helm/v3/pkg/release"
|
|
relutil "helm.sh/helm/v3/pkg/releaseutil"
|
|
"helm.sh/helm/v3/pkg/repo"
|
|
"helm.sh/helm/v3/pkg/storage"
|
|
"helm.sh/helm/v3/pkg/storage/driver"
|
|
appsv1 "k8s.io/api/apps/v1"
|
|
crdv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
|
kyaml "k8s.io/apimachinery/pkg/util/yaml"
|
|
"k8s.io/client-go/rest"
|
|
k8scmdutil "k8s.io/kubectl/pkg/cmd/util"
|
|
"sigs.k8s.io/yaml"
|
|
|
|
"github.com/oam-dev/kubevela/pkg/utils"
|
|
"github.com/oam-dev/kubevela/pkg/utils/common"
|
|
cmdutil "github.com/oam-dev/kubevela/pkg/utils/util"
|
|
)
|
|
|
|
const (
|
|
repoPatten = " repoUrl: %s"
|
|
valuesPatten = "repoUrl: %s, chart: %s, version: %s"
|
|
)
|
|
|
|
// ChartValues contain all values files in chart and default chart values
|
|
type ChartValues struct {
|
|
Data map[string]string
|
|
Values map[string]interface{}
|
|
}
|
|
|
|
// Helper provides helper functions for common Helm operations
|
|
type Helper struct {
|
|
cache *utils.MemoryCacheStore
|
|
}
|
|
|
|
// NewHelper creates a Helper
|
|
func NewHelper() *Helper {
|
|
return &Helper{}
|
|
}
|
|
|
|
// NewHelperWithCache creates a Helper with cache usually used by apiserver
|
|
func NewHelperWithCache() *Helper {
|
|
return &Helper{
|
|
cache: utils.NewMemoryCacheStore(context.Background()),
|
|
}
|
|
}
|
|
|
|
// LoadCharts load helm chart from local or remote
|
|
func (h *Helper) LoadCharts(chartRepoURL string, opts *common.HTTPOption) (*chart.Chart, error) {
|
|
var err error
|
|
var chart *chart.Chart
|
|
if utils.IsValidURL(chartRepoURL) {
|
|
chartBytes, err := common.HTTPGetWithOption(context.Background(), chartRepoURL, opts)
|
|
if err != nil {
|
|
return nil, errors.New("error retrieving Helm Chart at " + chartRepoURL + ": " + err.Error())
|
|
}
|
|
ch, err := loader.LoadArchive(bytes.NewReader(chartBytes))
|
|
if err != nil {
|
|
return nil, errors.New("error retrieving Helm Chart at " + chartRepoURL + ": " + err.Error())
|
|
}
|
|
return ch, err
|
|
}
|
|
chart, err = loader.Load(chartRepoURL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return chart, nil
|
|
}
|
|
|
|
// UpgradeChartOptions options for upgrade chart
|
|
type UpgradeChartOptions struct {
|
|
Config *rest.Config
|
|
Detail bool
|
|
Logging cmdutil.IOStreams
|
|
Wait bool
|
|
ReuseValues bool
|
|
}
|
|
|
|
// UpgradeChart install or upgrade helm chart
|
|
func (h *Helper) UpgradeChart(ch *chart.Chart, releaseName, namespace string, values map[string]interface{}, config UpgradeChartOptions) (*release.Release, error) {
|
|
if ch == nil || len(ch.Templates) == 0 {
|
|
return nil, fmt.Errorf("empty chart provided for %s", releaseName)
|
|
}
|
|
config.Logging.Infof("Start upgrading Helm Chart %s in namespace %s\n", releaseName, namespace)
|
|
|
|
cfg, err := newActionConfig(config.Config, namespace, config.Detail, config.Logging)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
histClient := action.NewHistory(cfg)
|
|
var newRelease *release.Release
|
|
timeoutInMinutes := 18
|
|
releases, err := histClient.Run(releaseName)
|
|
if err != nil || len(releases) == 0 {
|
|
if errors.Is(err, driver.ErrReleaseNotFound) {
|
|
// fresh install
|
|
install := action.NewInstall(cfg)
|
|
install.Namespace = namespace
|
|
install.ReleaseName = releaseName
|
|
install.Wait = config.Wait
|
|
install.Timeout = time.Duration(timeoutInMinutes) * time.Minute
|
|
newRelease, err = install.Run(ch, values)
|
|
} else {
|
|
return nil, fmt.Errorf("could not retrieve history of releases associated to %s: %w", releaseName, err)
|
|
}
|
|
} else {
|
|
config.Logging.Infof("Found existing installation, overwriting...")
|
|
|
|
// check if the previous installation is still pending (e.g., waiting to complete)
|
|
for _, r := range releases {
|
|
if r.Info.Status == release.StatusPendingInstall || r.Info.Status == release.StatusPendingUpgrade ||
|
|
r.Info.Status == release.StatusPendingRollback {
|
|
return nil, fmt.Errorf("previous installation (e.g., using vela install or helm upgrade) is still in progress. Please try again in %d minutes", timeoutInMinutes)
|
|
}
|
|
}
|
|
|
|
// merge un-existing values into the values as user-input, because the helm chart upgrade didn't handle the new default values in the chart.
|
|
// the new default values <= the old custom values <= the new custom values
|
|
if config.ReuseValues {
|
|
// sort will sort the release by revision from old to new
|
|
relutil.SortByRevision(releases)
|
|
rel := releases[len(releases)-1]
|
|
// merge new values as the user input, follow the new user input for --set
|
|
values = chartutil.CoalesceTables(values, rel.Config)
|
|
}
|
|
|
|
// overwrite existing installation
|
|
install := action.NewUpgrade(cfg)
|
|
install.Namespace = namespace
|
|
install.Wait = config.Wait
|
|
install.Timeout = time.Duration(timeoutInMinutes) * time.Minute
|
|
// use the new default value set.
|
|
install.ReuseValues = false
|
|
newRelease, err = install.Run(releaseName, ch, values)
|
|
}
|
|
// check if install/upgrade worked
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error when installing/upgrading Helm Chart %s in namespace %s: %w",
|
|
releaseName, namespace, err)
|
|
}
|
|
if newRelease == nil {
|
|
return nil, fmt.Errorf("failed to install release %s", releaseName)
|
|
}
|
|
return newRelease, nil
|
|
}
|
|
|
|
// UninstallRelease uninstalls the provided release
|
|
func (h *Helper) UninstallRelease(releaseName, namespace string, config *rest.Config, showDetail bool, logging cmdutil.IOStreams) error {
|
|
cfg, err := newActionConfig(config, namespace, showDetail, logging)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
iCli := action.NewUninstall(cfg)
|
|
_, err = iCli.Run(releaseName)
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("error when uninstalling Helm release %s in namespace %s: %w",
|
|
releaseName, namespace, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ListVersions list available versions from repo
|
|
func (h *Helper) ListVersions(repoURL string, chartName string, skipCache bool, opts *common.HTTPOption) (repo.ChartVersions, error) {
|
|
i, err := h.GetIndexInfo(repoURL, skipCache, opts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return i.Entries[chartName], nil
|
|
}
|
|
|
|
// GetIndexInfo get index.yaml form given repo url
|
|
func (h *Helper) GetIndexInfo(repoURL string, skipCache bool, opts *common.HTTPOption) (*repo.IndexFile, error) {
|
|
repoURL = utils.Sanitize(repoURL)
|
|
if h.cache != nil && !skipCache {
|
|
if i := h.cache.Get(fmt.Sprintf(repoPatten, repoURL)); i != nil {
|
|
return i.(*repo.IndexFile), nil
|
|
}
|
|
}
|
|
var body []byte
|
|
if utils.IsValidURL(repoURL) {
|
|
indexURL, err := utils.JoinURL(repoURL, "index.yaml")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
body, err = common.HTTPGetWithOption(context.Background(), indexURL, opts)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("download index file from %s failure %w", repoURL, err)
|
|
}
|
|
} else {
|
|
var err error
|
|
body, err = os.ReadFile(path.Join(filepath.Clean(repoURL), "index.yaml"))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read index file from %s failure %w", repoURL, err)
|
|
}
|
|
}
|
|
i := &repo.IndexFile{}
|
|
if err := yaml.UnmarshalStrict(body, i); err != nil {
|
|
return nil, fmt.Errorf("parse index file from %s failure", repoURL)
|
|
}
|
|
|
|
if h.cache != nil {
|
|
h.cache.Put(fmt.Sprintf(repoPatten, repoURL), i, calculateCacheTimeFromIndex(len(i.Entries)))
|
|
}
|
|
return i, nil
|
|
}
|
|
|
|
// GetDeploymentsFromManifest get deployment from helm manifest
|
|
func GetDeploymentsFromManifest(helmManifest string) []*appsv1.Deployment {
|
|
deployments := []*appsv1.Deployment{}
|
|
dec := kyaml.NewYAMLToJSONDecoder(strings.NewReader(helmManifest))
|
|
for {
|
|
var deployment appsv1.Deployment
|
|
err := dec.Decode(&deployment)
|
|
if errors.Is(err, io.EOF) {
|
|
break
|
|
}
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if strings.EqualFold(deployment.Kind, "deployment") {
|
|
deployments = append(deployments, &deployment)
|
|
}
|
|
}
|
|
return deployments
|
|
}
|
|
|
|
// GetCRDFromChart get crd from helm chart
|
|
func GetCRDFromChart(chart *chart.Chart) []*crdv1.CustomResourceDefinition {
|
|
crds := []*crdv1.CustomResourceDefinition{}
|
|
for _, crdFile := range chart.CRDs() {
|
|
var crd crdv1.CustomResourceDefinition
|
|
err := kyaml.Unmarshal(crdFile.Data, &crd)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
crds = append(crds, &crd)
|
|
}
|
|
return crds
|
|
}
|
|
|
|
func newActionConfig(config *rest.Config, namespace string, showDetail bool, logging cmdutil.IOStreams) (*action.Configuration, error) {
|
|
restClientGetter := cmdutil.NewRestConfigGetterByConfig(config, namespace)
|
|
log := func(format string, a ...interface{}) {
|
|
if showDetail {
|
|
logging.Infof(format+"\n", a...)
|
|
}
|
|
}
|
|
kubeClient := &kube.Client{
|
|
Factory: k8scmdutil.NewFactory(restClientGetter),
|
|
Log: log,
|
|
}
|
|
client, err := kubeClient.Factory.KubernetesClientSet()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
s := driver.NewSecrets(client.CoreV1().Secrets(namespace))
|
|
s.Log = log
|
|
return &action.Configuration{
|
|
RESTClientGetter: restClientGetter,
|
|
Releases: storage.Init(s),
|
|
KubeClient: kubeClient,
|
|
Log: log,
|
|
}, nil
|
|
}
|
|
|
|
// ListChartsFromRepo list available helm charts in a repo
|
|
func (h *Helper) ListChartsFromRepo(repoURL string, skipCache bool, opts *common.HTTPOption) ([]string, error) {
|
|
i, err := h.GetIndexInfo(repoURL, skipCache, opts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
res := make([]string, len(i.Entries))
|
|
j := 0
|
|
for s := range i.Entries {
|
|
res[j] = s
|
|
j++
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
// GetValuesFromChart will extract the parameter from a helm chart
|
|
func (h *Helper) GetValuesFromChart(repoURL string, chartName string, version string, skipCache bool, repoType string, opts *common.HTTPOption) (*ChartValues, error) {
|
|
if h.cache != nil && !skipCache {
|
|
if v := h.cache.Get(fmt.Sprintf(valuesPatten, repoURL, chartName, version)); v != nil {
|
|
return v.(*ChartValues), nil
|
|
}
|
|
}
|
|
if repoType == "oci" {
|
|
v, err := fetchChartValuesFromOciRepo(repoURL, chartName, version, opts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if h.cache != nil {
|
|
h.cache.Put(fmt.Sprintf(valuesPatten, repoURL, chartName, version), v, 20*time.Minute)
|
|
}
|
|
return v, nil
|
|
}
|
|
i, err := h.GetIndexInfo(repoURL, skipCache, opts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
chartVersions, ok := i.Entries[chartName]
|
|
if !ok {
|
|
return nil, fmt.Errorf("cannot find chart %s in this repo", chartName)
|
|
}
|
|
var urls []string
|
|
for _, chartVersion := range chartVersions {
|
|
if chartVersion.Version == version {
|
|
urls = chartVersion.URLs
|
|
}
|
|
}
|
|
for _, u := range urls {
|
|
c, err := h.LoadCharts(u, opts)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
v := &ChartValues{
|
|
Data: loadValuesYamlFile(c),
|
|
Values: c.Values,
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if h.cache != nil {
|
|
h.cache.Put(fmt.Sprintf(valuesPatten, repoURL, chartName, version), v, calculateCacheTimeFromIndex(len(i.Entries)))
|
|
}
|
|
return v, nil
|
|
}
|
|
return nil, fmt.Errorf("cannot load chart from chart repo")
|
|
}
|
|
|
|
func calculateCacheTimeFromIndex(length int) time.Duration {
|
|
cacheTime := 3 * time.Minute
|
|
if length > 20 {
|
|
// huge helm repo like https://charts.bitnami.com/bitnami have too many(106) charts, generally user cannot modify it.
|
|
// need more cache time
|
|
cacheTime = 1 * time.Hour
|
|
}
|
|
return cacheTime
|
|
}
|
|
|
|
// nolint
|
|
func fetchChartValuesFromOciRepo(repoURL string, chartName string, version string, opts *common.HTTPOption) (*ChartValues, error) {
|
|
d := downloader.ChartDownloader{
|
|
Verify: downloader.VerifyNever,
|
|
Getters: getter.All(cli.New()),
|
|
}
|
|
|
|
if opts != nil {
|
|
d.Options = append(d.Options, getter.WithInsecureSkipVerifyTLS(opts.InsecureSkipTLS),
|
|
getter.WithTLSClientConfig(opts.CertFile, opts.KeyFile, opts.CaFile),
|
|
getter.WithBasicAuth(opts.Username, opts.Password))
|
|
}
|
|
|
|
var err error
|
|
dest, err := os.MkdirTemp("", "helm-")
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to fetch values file")
|
|
}
|
|
defer os.RemoveAll(dest)
|
|
|
|
chartRef := fmt.Sprintf("%s/%s", repoURL, chartName)
|
|
saved, _, err := d.DownloadTo(chartRef, version, dest)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
c, err := loader.Load(saved)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to fetch values file")
|
|
}
|
|
return &ChartValues{
|
|
Data: loadValuesYamlFile(c),
|
|
Values: c.Values,
|
|
}, nil
|
|
}
|
|
|
|
func loadValuesYamlFile(chart *chart.Chart) map[string]string {
|
|
result := map[string]string{}
|
|
re := regexp.MustCompile(`.*yaml$`)
|
|
for _, f := range chart.Raw {
|
|
if re.MatchString(f.Name) && !strings.Contains(f.Name, "/") && f.Name != "Chart.yaml" {
|
|
result[f.Name] = string(f.Data)
|
|
}
|
|
}
|
|
return result
|
|
}
|