github.com/oam-dev/kubevela@v1.9.11/pkg/utils/helm/helm_helper.go (about) 1 /* 2 Copyright 2021 The KubeVela Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package helm 18 19 import ( 20 "bytes" 21 "context" 22 "fmt" 23 "io" 24 "net/url" 25 "os" 26 "path" 27 "path/filepath" 28 "regexp" 29 "strings" 30 "time" 31 32 "github.com/pkg/errors" 33 "helm.sh/helm/v3/pkg/action" 34 "helm.sh/helm/v3/pkg/chart" 35 "helm.sh/helm/v3/pkg/chart/loader" 36 "helm.sh/helm/v3/pkg/chartutil" 37 "helm.sh/helm/v3/pkg/cli" 38 "helm.sh/helm/v3/pkg/downloader" 39 "helm.sh/helm/v3/pkg/getter" 40 "helm.sh/helm/v3/pkg/kube" 41 "helm.sh/helm/v3/pkg/release" 42 relutil "helm.sh/helm/v3/pkg/releaseutil" 43 "helm.sh/helm/v3/pkg/repo" 44 "helm.sh/helm/v3/pkg/storage" 45 "helm.sh/helm/v3/pkg/storage/driver" 46 appsv1 "k8s.io/api/apps/v1" 47 crdv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 48 kyaml "k8s.io/apimachinery/pkg/util/yaml" 49 "k8s.io/client-go/rest" 50 k8scmdutil "k8s.io/kubectl/pkg/cmd/util" 51 "sigs.k8s.io/yaml" 52 53 "github.com/oam-dev/kubevela/pkg/utils" 54 "github.com/oam-dev/kubevela/pkg/utils/common" 55 cmdutil "github.com/oam-dev/kubevela/pkg/utils/util" 56 ) 57 58 const ( 59 repoPatten = " repoUrl: %s" 60 valuesPatten = "repoUrl: %s, chart: %s, version: %s" 61 ) 62 63 // ChartValues contain all values files in chart and default chart values 64 type ChartValues struct { 65 Data map[string]string 66 Values map[string]interface{} 67 } 68 69 // Helper provides helper functions for common Helm operations 70 type Helper struct { 71 cache *utils.MemoryCacheStore 72 } 73 74 // NewHelper creates a Helper 75 func NewHelper() *Helper { 76 return &Helper{} 77 } 78 79 // NewHelperWithCache creates a Helper with cache usually used by apiserver 80 func NewHelperWithCache() *Helper { 81 return &Helper{ 82 cache: utils.NewMemoryCacheStore(context.Background()), 83 } 84 } 85 86 // LoadCharts load helm chart from local or remote 87 func (h *Helper) LoadCharts(chartRepoURL string, opts *common.HTTPOption) (*chart.Chart, error) { 88 var err error 89 var chart *chart.Chart 90 if utils.IsValidURL(chartRepoURL) { 91 chartBytes, err := common.HTTPGetWithOption(context.Background(), chartRepoURL, opts) 92 if err != nil { 93 return nil, errors.New("error retrieving Helm Chart at " + chartRepoURL + ": " + err.Error()) 94 } 95 ch, err := loader.LoadArchive(bytes.NewReader(chartBytes)) 96 if err != nil { 97 return nil, errors.New("error retrieving Helm Chart at " + chartRepoURL + ": " + err.Error()) 98 } 99 return ch, err 100 } 101 chart, err = loader.Load(chartRepoURL) 102 if err != nil { 103 return nil, err 104 } 105 return chart, nil 106 } 107 108 // UpgradeChartOptions options for upgrade chart 109 type UpgradeChartOptions struct { 110 Config *rest.Config 111 Detail bool 112 Logging cmdutil.IOStreams 113 Wait bool 114 ReuseValues bool 115 } 116 117 // UpgradeChart install or upgrade helm chart 118 func (h *Helper) UpgradeChart(ch *chart.Chart, releaseName, namespace string, values map[string]interface{}, config UpgradeChartOptions) (*release.Release, error) { 119 if ch == nil || len(ch.Templates) == 0 { 120 return nil, fmt.Errorf("empty chart provided for %s", releaseName) 121 } 122 config.Logging.Infof("Start upgrading Helm Chart %s in namespace %s\n", releaseName, namespace) 123 124 cfg, err := newActionConfig(config.Config, namespace, config.Detail, config.Logging) 125 if err != nil { 126 return nil, err 127 } 128 histClient := action.NewHistory(cfg) 129 var newRelease *release.Release 130 timeoutInMinutes := 18 131 releases, err := histClient.Run(releaseName) 132 if err != nil || len(releases) == 0 { 133 if errors.Is(err, driver.ErrReleaseNotFound) { 134 // fresh install 135 install := action.NewInstall(cfg) 136 install.Namespace = namespace 137 install.ReleaseName = releaseName 138 install.Wait = config.Wait 139 install.Timeout = time.Duration(timeoutInMinutes) * time.Minute 140 newRelease, err = install.Run(ch, values) 141 } else { 142 return nil, fmt.Errorf("could not retrieve history of releases associated to %s: %w", releaseName, err) 143 } 144 } else { 145 config.Logging.Infof("Found existing installation, overwriting...") 146 147 // check if the previous installation is still pending (e.g., waiting to complete) 148 for _, r := range releases { 149 if r.Info.Status == release.StatusPendingInstall || r.Info.Status == release.StatusPendingUpgrade || 150 r.Info.Status == release.StatusPendingRollback { 151 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) 152 } 153 } 154 155 // 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. 156 // the new default values <= the old custom values <= the new custom values 157 if config.ReuseValues { 158 // sort will sort the release by revision from old to new 159 relutil.SortByRevision(releases) 160 rel := releases[len(releases)-1] 161 // merge new values as the user input, follow the new user input for --set 162 values = chartutil.CoalesceTables(values, rel.Config) 163 } 164 165 // overwrite existing installation 166 install := action.NewUpgrade(cfg) 167 install.Namespace = namespace 168 install.Wait = config.Wait 169 install.Timeout = time.Duration(timeoutInMinutes) * time.Minute 170 // use the new default value set. 171 install.ReuseValues = false 172 newRelease, err = install.Run(releaseName, ch, values) 173 } 174 // check if install/upgrade worked 175 if err != nil { 176 return nil, fmt.Errorf("error when installing/upgrading Helm Chart %s in namespace %s: %w", 177 releaseName, namespace, err) 178 } 179 if newRelease == nil { 180 return nil, fmt.Errorf("failed to install release %s", releaseName) 181 } 182 return newRelease, nil 183 } 184 185 // UninstallRelease uninstalls the provided release 186 func (h *Helper) UninstallRelease(releaseName, namespace string, config *rest.Config, showDetail bool, logging cmdutil.IOStreams) error { 187 cfg, err := newActionConfig(config, namespace, showDetail, logging) 188 if err != nil { 189 return err 190 } 191 192 iCli := action.NewUninstall(cfg) 193 _, err = iCli.Run(releaseName) 194 195 if err != nil { 196 return fmt.Errorf("error when uninstalling Helm release %s in namespace %s: %w", 197 releaseName, namespace, err) 198 } 199 return nil 200 } 201 202 // ListVersions list available versions from repo 203 func (h *Helper) ListVersions(repoURL string, chartName string, skipCache bool, opts *common.HTTPOption) (repo.ChartVersions, error) { 204 i, err := h.GetIndexInfo(repoURL, skipCache, opts) 205 if err != nil { 206 return nil, err 207 } 208 return i.Entries[chartName], nil 209 } 210 211 // GetIndexInfo get index.yaml form given repo url 212 func (h *Helper) GetIndexInfo(repoURL string, skipCache bool, opts *common.HTTPOption) (*repo.IndexFile, error) { 213 repoURL = utils.Sanitize(repoURL) 214 if h.cache != nil && !skipCache { 215 if i := h.cache.Get(fmt.Sprintf(repoPatten, repoURL)); i != nil { 216 return i.(*repo.IndexFile), nil 217 } 218 } 219 var body []byte 220 if utils.IsValidURL(repoURL) { 221 indexURL, err := utils.JoinURL(repoURL, "index.yaml") 222 if err != nil { 223 return nil, err 224 } 225 body, err = common.HTTPGetWithOption(context.Background(), indexURL, opts) 226 if err != nil { 227 return nil, fmt.Errorf("download index file from %s failure %w", repoURL, err) 228 } 229 } else { 230 var err error 231 body, err = os.ReadFile(path.Join(filepath.Clean(repoURL), "index.yaml")) 232 if err != nil { 233 return nil, fmt.Errorf("read index file from %s failure %w", repoURL, err) 234 } 235 } 236 i := &repo.IndexFile{} 237 if err := yaml.UnmarshalStrict(body, i); err != nil { 238 return nil, fmt.Errorf("parse index file from %s failure", repoURL) 239 } 240 241 if h.cache != nil { 242 h.cache.Put(fmt.Sprintf(repoPatten, repoURL), i, calculateCacheTimeFromIndex(len(i.Entries))) 243 } 244 return i, nil 245 } 246 247 // GetDeploymentsFromManifest get deployment from helm manifest 248 func GetDeploymentsFromManifest(helmManifest string) []*appsv1.Deployment { 249 deployments := []*appsv1.Deployment{} 250 dec := kyaml.NewYAMLToJSONDecoder(strings.NewReader(helmManifest)) 251 for { 252 var deployment appsv1.Deployment 253 err := dec.Decode(&deployment) 254 if errors.Is(err, io.EOF) { 255 break 256 } 257 if err != nil { 258 continue 259 } 260 if strings.EqualFold(deployment.Kind, "deployment") { 261 deployments = append(deployments, &deployment) 262 } 263 } 264 return deployments 265 } 266 267 // GetCRDFromChart get crd from helm chart 268 func GetCRDFromChart(chart *chart.Chart) []*crdv1.CustomResourceDefinition { 269 crds := []*crdv1.CustomResourceDefinition{} 270 for _, crdFile := range chart.CRDs() { 271 var crd crdv1.CustomResourceDefinition 272 err := kyaml.Unmarshal(crdFile.Data, &crd) 273 if err != nil { 274 continue 275 } 276 crds = append(crds, &crd) 277 } 278 return crds 279 } 280 281 func newActionConfig(config *rest.Config, namespace string, showDetail bool, logging cmdutil.IOStreams) (*action.Configuration, error) { 282 restClientGetter := cmdutil.NewRestConfigGetterByConfig(config, namespace) 283 log := func(format string, a ...interface{}) { 284 if showDetail { 285 logging.Infof(format+"\n", a...) 286 } 287 } 288 kubeClient := &kube.Client{ 289 Factory: k8scmdutil.NewFactory(restClientGetter), 290 Log: log, 291 } 292 client, err := kubeClient.Factory.KubernetesClientSet() 293 if err != nil { 294 return nil, err 295 } 296 s := driver.NewSecrets(client.CoreV1().Secrets(namespace)) 297 s.Log = log 298 return &action.Configuration{ 299 RESTClientGetter: restClientGetter, 300 Releases: storage.Init(s), 301 KubeClient: kubeClient, 302 Log: log, 303 }, nil 304 } 305 306 // ListChartsFromRepo list available helm charts in a repo 307 func (h *Helper) ListChartsFromRepo(repoURL string, skipCache bool, opts *common.HTTPOption) ([]string, error) { 308 i, err := h.GetIndexInfo(repoURL, skipCache, opts) 309 if err != nil { 310 return nil, err 311 } 312 res := make([]string, len(i.Entries)) 313 j := 0 314 for s := range i.Entries { 315 res[j] = s 316 j++ 317 } 318 return res, nil 319 } 320 321 // GetValuesFromChart will extract the parameter from a helm chart 322 func (h *Helper) GetValuesFromChart(repoURL string, chartName string, version string, skipCache bool, repoType string, opts *common.HTTPOption) (*ChartValues, error) { 323 if h.cache != nil && !skipCache { 324 if v := h.cache.Get(fmt.Sprintf(valuesPatten, repoURL, chartName, version)); v != nil { 325 return v.(*ChartValues), nil 326 } 327 } 328 if repoType == "oci" { 329 v, err := fetchChartValuesFromOciRepo(repoURL, chartName, version, opts) 330 if err != nil { 331 return nil, err 332 } 333 if h.cache != nil { 334 h.cache.Put(fmt.Sprintf(valuesPatten, repoURL, chartName, version), v, 20*time.Minute) 335 } 336 return v, nil 337 } 338 i, err := h.GetIndexInfo(repoURL, skipCache, opts) 339 if err != nil { 340 return nil, err 341 } 342 chartVersions, ok := i.Entries[chartName] 343 if !ok { 344 return nil, fmt.Errorf("cannot find chart %s in this repo", chartName) 345 } 346 var urls []string 347 for _, chartVersion := range chartVersions { 348 if chartVersion.Version == version { 349 for _, url := range chartVersion.URLs { 350 if !(strings.HasPrefix(url, "https://") || strings.HasPrefix(url, "http://")) { 351 urls = append(urls, fmt.Sprintf("%s/%s", repoURL, url)) 352 } else { 353 urls = append(urls, url) 354 } 355 } 356 } 357 } 358 for _, u := range urls { 359 c, err := h.LoadCharts(u, opts) 360 if err != nil { 361 continue 362 } 363 v := &ChartValues{ 364 Data: loadValuesYamlFile(c), 365 Values: c.Values, 366 } 367 if err != nil { 368 return nil, err 369 } 370 if h.cache != nil { 371 h.cache.Put(fmt.Sprintf(valuesPatten, repoURL, chartName, version), v, calculateCacheTimeFromIndex(len(i.Entries))) 372 } 373 return v, nil 374 } 375 return nil, fmt.Errorf("cannot load chart from chart repo") 376 } 377 378 // ValidateRepo will validate the helm repository 379 func (h *Helper) ValidateRepo(ctx context.Context, repo *Repository) (bool, error) { 380 parsedURL, err := url.Parse(repo.URL) 381 if err != nil { 382 return false, err 383 } 384 userInfo := parsedURL.User 385 if len(repo.Username) > 0 && len(repo.Password) > 0 { 386 userInfo = url.UserPassword(repo.Username, repo.Password) 387 } 388 var cred = &RepoCredential{} 389 // TODO: support S3Config validation 390 if strings.HasPrefix(repo.URL, "https://") || strings.HasPrefix(repo.URL, "http://") { 391 if userInfo != nil { 392 cred.Username = userInfo.Username() 393 cred.Password, _ = userInfo.Password() 394 } 395 } 396 397 _, err = LoadRepoIndex(ctx, repo.URL, cred) 398 if err != nil { 399 return false, err 400 } 401 return true, nil 402 } 403 404 func calculateCacheTimeFromIndex(length int) time.Duration { 405 cacheTime := 3 * time.Minute 406 if length > 20 { 407 // huge helm repo like https://charts.bitnami.com/bitnami have too many(106) charts, generally user cannot modify it. 408 // need more cache time 409 cacheTime = 1 * time.Hour 410 } 411 return cacheTime 412 } 413 414 // nolint 415 func fetchChartValuesFromOciRepo(repoURL string, chartName string, version string, opts *common.HTTPOption) (*ChartValues, error) { 416 d := downloader.ChartDownloader{ 417 Verify: downloader.VerifyNever, 418 Getters: getter.All(cli.New()), 419 } 420 421 if opts != nil { 422 d.Options = append(d.Options, getter.WithInsecureSkipVerifyTLS(opts.InsecureSkipTLS), 423 getter.WithTLSClientConfig(opts.CertFile, opts.KeyFile, opts.CaFile), 424 getter.WithBasicAuth(opts.Username, opts.Password)) 425 } 426 427 var err error 428 dest, err := os.MkdirTemp("", "helm-") 429 if err != nil { 430 return nil, errors.Wrap(err, "failed to fetch values file") 431 } 432 defer os.RemoveAll(dest) 433 434 chartRef := fmt.Sprintf("%s/%s", repoURL, chartName) 435 saved, _, err := d.DownloadTo(chartRef, version, dest) 436 if err != nil { 437 return nil, err 438 } 439 c, err := loader.Load(saved) 440 if err != nil { 441 return nil, errors.Wrap(err, "failed to fetch values file") 442 } 443 return &ChartValues{ 444 Data: loadValuesYamlFile(c), 445 Values: c.Values, 446 }, nil 447 } 448 449 func loadValuesYamlFile(chart *chart.Chart) map[string]string { 450 result := map[string]string{} 451 re := regexp.MustCompile(`.*yaml$`) 452 for _, f := range chart.Raw { 453 if re.MatchString(f.Name) && !strings.Contains(f.Name, "/") && f.Name != "Chart.yaml" { 454 result[f.Name] = string(f.Data) 455 } 456 } 457 return result 458 }