427 lines
11 KiB
Go
427 lines
11 KiB
Go
/*
|
|
Copyright 2020 The KubeSphere 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 helmwrapper
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"helm.sh/helm/v3/pkg/action"
|
|
"helm.sh/helm/v3/pkg/chart"
|
|
"helm.sh/helm/v3/pkg/chart/loader"
|
|
|
|
"k8s.io/apimachinery/pkg/util/wait"
|
|
|
|
"helm.sh/helm/v3/pkg/chartutil"
|
|
helmrelease "helm.sh/helm/v3/pkg/release"
|
|
"k8s.io/klog"
|
|
kpath "k8s.io/utils/path"
|
|
|
|
"kubesphere.io/kubesphere/pkg/server/errors"
|
|
"kubesphere.io/kubesphere/pkg/utils/idutils"
|
|
)
|
|
|
|
const (
|
|
workspaceBase = "/tmp/helm-operator"
|
|
)
|
|
|
|
var (
|
|
ErrorTimedOutToWaitResource = errors.New("timed out waiting for resources to be ready")
|
|
|
|
UninstallNotFoundFormat = "uninstall: Release not loaded: %s: release: not found"
|
|
StatusNotFoundFormat = "release: not found"
|
|
releaseExists = "release exists"
|
|
)
|
|
|
|
type HelmRes struct {
|
|
Message string
|
|
}
|
|
|
|
var _ HelmWrapper = &helmWrapper{}
|
|
|
|
type HelmWrapper interface {
|
|
Install(chartName, chartData, values string) error
|
|
// upgrade a release
|
|
Upgrade(chartName, chartData, values string) error
|
|
Uninstall() error
|
|
// Get manifests
|
|
Manifest() (string, error)
|
|
|
|
// IsReleaseReady check helm release is ready or not
|
|
IsReleaseReady(timeout time.Duration) (bool, error)
|
|
}
|
|
|
|
// IsReleaseReady check helm releases is ready or not
|
|
// If the return values is (true, nil), then the resources are ready
|
|
func (c *helmWrapper) IsReleaseReady(waitTime time.Duration) (bool, error) {
|
|
|
|
// Get the manifest to build resources
|
|
manifest, err := c.Manifest()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
client := c.helmConf.KubeClient
|
|
resources, _ := client.Build(bytes.NewBufferString(manifest), true)
|
|
|
|
err = client.Wait(resources, waitTime)
|
|
|
|
if err == nil {
|
|
return true, nil
|
|
}
|
|
|
|
if err == wait.ErrWaitTimeout {
|
|
return false, ErrorTimedOutToWaitResource
|
|
}
|
|
|
|
return false, err
|
|
}
|
|
|
|
func (c *helmWrapper) Status() (*helmrelease.Release, error) {
|
|
helmStatus := action.NewStatus(c.helmConf)
|
|
rel, err := helmStatus.Run(c.ReleaseName)
|
|
if err != nil {
|
|
if err.Error() == StatusNotFoundFormat {
|
|
klog.V(2).Infof("namespace: %s, name: %s, run command failed, error: %v", c.Namespace, c.ReleaseName, err)
|
|
return nil, err
|
|
}
|
|
klog.Errorf("namespace: %s, name: %s, run command failed, error: %v", c.Namespace, c.ReleaseName, err)
|
|
return nil, err
|
|
}
|
|
|
|
klog.V(2).Infof("namespace: %s, name: %s, run command success", c.Namespace, c.ReleaseName)
|
|
klog.V(8).Infof("namespace: %s, name: %s, run command success, manifest: %s", c.Namespace, c.ReleaseName, rel.Manifest)
|
|
return rel, nil
|
|
}
|
|
|
|
func (c *helmWrapper) Workspace() string {
|
|
if c.workspaceSuffix == "" {
|
|
return filepath.Join(c.base, fmt.Sprintf("%s_%s", c.Namespace, c.ReleaseName))
|
|
} else {
|
|
return filepath.Join(c.base, fmt.Sprintf("%s_%s_%s", c.Namespace, c.ReleaseName, c.workspaceSuffix))
|
|
}
|
|
}
|
|
|
|
type helmWrapper struct {
|
|
// KubeConfig string
|
|
Kubeconfig string
|
|
Namespace string
|
|
// helm release name
|
|
ReleaseName string
|
|
ChartName string
|
|
|
|
// helm action Config
|
|
helmConf *action.Configuration
|
|
|
|
// add labels to helm chart
|
|
labels map[string]string
|
|
// add annotations to helm chart
|
|
annotations map[string]string
|
|
|
|
base string
|
|
workspaceSuffix string
|
|
dryRun bool
|
|
mock bool
|
|
}
|
|
|
|
// The dir where chart saved
|
|
func (c *helmWrapper) chartDir() string {
|
|
return filepath.Join(c.Workspace(), "chart")
|
|
}
|
|
|
|
func (c *helmWrapper) chartPath() string {
|
|
return filepath.Join(c.chartDir(), fmt.Sprintf("%s.tgz", c.ChartName))
|
|
}
|
|
|
|
func (c *helmWrapper) cleanup() {
|
|
if err := os.RemoveAll(c.Workspace()); err != nil {
|
|
klog.Errorf("remove dir %s failed, error: %s", c.Workspace(), err)
|
|
}
|
|
}
|
|
|
|
func (c *helmWrapper) Set(options ...Option) {
|
|
for _, option := range options {
|
|
option(c)
|
|
}
|
|
}
|
|
|
|
type Option func(*helmWrapper)
|
|
|
|
func SetDryRun(dryRun bool) Option {
|
|
return func(wrapper *helmWrapper) {
|
|
wrapper.dryRun = dryRun
|
|
}
|
|
}
|
|
|
|
// extra annotations added to all resources in chart
|
|
func SetAnnotations(annotations map[string]string) Option {
|
|
return func(wrapper *helmWrapper) {
|
|
wrapper.annotations = annotations
|
|
}
|
|
}
|
|
|
|
// extra labels added to all resources in chart
|
|
func SetLabels(labels map[string]string) Option {
|
|
return func(wrapper *helmWrapper) {
|
|
wrapper.labels = labels
|
|
}
|
|
}
|
|
|
|
func SetMock(mock bool) Option {
|
|
return func(wrapper *helmWrapper) {
|
|
wrapper.mock = mock
|
|
}
|
|
}
|
|
|
|
func NewHelmWrapper(kubeconfig, ns, rls string, options ...Option) *helmWrapper {
|
|
c := &helmWrapper{
|
|
Kubeconfig: kubeconfig,
|
|
Namespace: ns,
|
|
ReleaseName: rls,
|
|
base: workspaceBase,
|
|
workspaceSuffix: idutils.GetUuid36(""),
|
|
}
|
|
|
|
klog.V(8).Infof("namespace: %s, name: %s, release: %s, kubeconfig:%s", c.Namespace, c.ReleaseName, rls, kubeconfig)
|
|
getter := NewClusterRESTClientGetter(kubeconfig, ns)
|
|
c.helmConf = new(action.Configuration)
|
|
c.helmConf.Init(getter, ns, "", klog.Infof)
|
|
|
|
for _, option := range options {
|
|
option(c)
|
|
}
|
|
|
|
return c
|
|
}
|
|
|
|
// ensureWorkspace check whether workspace exists or not.
|
|
// If not exists, create workspace dir.
|
|
func (c *helmWrapper) ensureWorkspace() error {
|
|
if exists, err := kpath.Exists(kpath.CheckFollowSymlink, c.Workspace()); err != nil {
|
|
klog.Errorf("check dir %s failed, error: %s", c.Workspace(), err)
|
|
return err
|
|
} else if !exists {
|
|
err = os.MkdirAll(c.Workspace(), os.ModeDir|os.ModePerm)
|
|
if err != nil {
|
|
klog.Errorf("mkdir %s failed, error: %s", c.Workspace(), err)
|
|
return err
|
|
}
|
|
}
|
|
|
|
err := os.MkdirAll(c.chartDir(), os.ModeDir|os.ModePerm)
|
|
if err != nil {
|
|
klog.Errorf("mkdir %s failed, error: %s", c.chartDir(), err)
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// create chart dir in workspace
|
|
// write values.yaml into workspace
|
|
func (c *helmWrapper) createChart(chartName, chartData, values string) error {
|
|
c.ChartName = chartName
|
|
|
|
// write chart
|
|
f, err := os.Create(c.chartPath())
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = f.Write([]byte(chartData))
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
f.Close()
|
|
|
|
// write values
|
|
f, err = os.Create(filepath.Join(c.Workspace(), "values.yaml"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = f.WriteString(values)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
f.Close()
|
|
return nil
|
|
}
|
|
|
|
// helm uninstall
|
|
func (c *helmWrapper) Uninstall() error {
|
|
start := time.Now()
|
|
defer func() {
|
|
klog.V(2).Infof("run command end, namespace: %s, name: %s elapsed: %v", c.Namespace, c.ReleaseName, time.Since(start))
|
|
}()
|
|
|
|
uninstall := action.NewUninstall(c.helmConf)
|
|
if c.dryRun {
|
|
uninstall.DryRun = true
|
|
}
|
|
|
|
_, err := uninstall.Run(c.ReleaseName)
|
|
if err != nil {
|
|
// release does not exist. It's ok.
|
|
if fmt.Sprintf(UninstallNotFoundFormat, c.ReleaseName) == err.Error() {
|
|
return nil
|
|
}
|
|
klog.Errorf("run command failed, error: %v", err)
|
|
return err
|
|
} else {
|
|
klog.V(2).Infof("namespace: %s, name: %s, run command success", c.Namespace, c.ReleaseName)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// helm upgrade
|
|
func (c *helmWrapper) Upgrade(chartName, chartData, values string) error {
|
|
sts, err := c.Status()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if sts.Info.Status == "deployed" {
|
|
return c.writeAction(chartName, chartData, values, true)
|
|
} else {
|
|
err = errors.New("cannot upgrade release %s/%s, current state is %s", c.Namespace, c.ReleaseName, sts.Info.Status)
|
|
return err
|
|
}
|
|
}
|
|
|
|
// helm install
|
|
func (c *helmWrapper) Install(chartName, chartData, values string) error {
|
|
sts, err := c.Status()
|
|
if err == nil {
|
|
// helm release has been installed
|
|
if sts.Info != nil && sts.Info.Status == "deployed" {
|
|
return nil
|
|
}
|
|
return errors.New(releaseExists)
|
|
} else {
|
|
if err.Error() == StatusNotFoundFormat {
|
|
// continue to install
|
|
return c.writeAction(chartName, chartData, values, false)
|
|
}
|
|
return err
|
|
}
|
|
}
|
|
|
|
func (c *helmWrapper) mockRelease() (*helmrelease.Release, error) {
|
|
return helmrelease.Mock(&helmrelease.MockReleaseOptions{Name: c.ReleaseName, Namespace: c.Namespace}), nil
|
|
}
|
|
|
|
func (c *helmWrapper) helmUpgrade(chart *chart.Chart, values map[string]interface{}) (*helmrelease.Release, error) {
|
|
upgrade := action.NewUpgrade(c.helmConf)
|
|
upgrade.Namespace = c.Namespace
|
|
|
|
if c.dryRun {
|
|
upgrade.DryRun = true
|
|
}
|
|
if len(c.labels) > 0 || len(c.annotations) > 0 {
|
|
postRenderer := newPostRendererKustomize(c.labels, c.annotations)
|
|
upgrade.PostRenderer = postRenderer
|
|
}
|
|
|
|
return upgrade.Run(c.ReleaseName, chart, values)
|
|
}
|
|
|
|
func (c *helmWrapper) helmInstall(chart *chart.Chart, values map[string]interface{}) (*helmrelease.Release, error) {
|
|
install := action.NewInstall(c.helmConf)
|
|
install.ReleaseName = c.ReleaseName
|
|
install.Namespace = c.Namespace
|
|
|
|
if c.dryRun {
|
|
install.DryRun = true
|
|
}
|
|
if len(c.labels) > 0 || len(c.annotations) > 0 {
|
|
postRenderer := newPostRendererKustomize(c.labels, c.annotations)
|
|
install.PostRenderer = postRenderer
|
|
}
|
|
|
|
return install.Run(chart, values)
|
|
}
|
|
|
|
func (c *helmWrapper) writeAction(chartName, chartData, values string, upgrade bool) error {
|
|
if klog.V(2) {
|
|
start := time.Now()
|
|
defer func() {
|
|
klog.V(2).Infof("run command end, namespace: %s, name: %s, upgrade: %t, elapsed: %v", c.Namespace, c.ReleaseName, upgrade, time.Since(start))
|
|
}()
|
|
}
|
|
|
|
if err := c.ensureWorkspace(); err != nil {
|
|
return err
|
|
}
|
|
defer c.cleanup()
|
|
|
|
if err := c.createChart(chartName, chartData, values); err != nil {
|
|
return err
|
|
}
|
|
klog.V(8).Infof("namespace: %s, name: %s, chart values: %s", c.Namespace, c.ReleaseName, values)
|
|
|
|
chartRequested, err := loader.Load(c.chartPath())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
valuePath := filepath.Join(c.Workspace(), "values.yaml")
|
|
helmValues, err := chartutil.ReadValuesFile(valuePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var rel *helmrelease.Release
|
|
if c.mock {
|
|
rel, err = c.mockRelease()
|
|
} else {
|
|
if upgrade {
|
|
rel, err = c.helmUpgrade(chartRequested, helmValues.AsMap())
|
|
} else {
|
|
rel, err = c.helmInstall(chartRequested, helmValues.AsMap())
|
|
}
|
|
}
|
|
|
|
if err != nil {
|
|
klog.Errorf("namespace: %s, name: %s, error: %v", c.Namespace, c.ReleaseName, err)
|
|
return err
|
|
}
|
|
|
|
klog.V(2).Infof("namespace: %s, name: %s, run command success", c.Namespace, c.ReleaseName)
|
|
klog.V(8).Infof("namespace: %s, name: %s, run command success, manifest: %s", c.Namespace, c.ReleaseName, rel.Manifest)
|
|
return nil
|
|
}
|
|
|
|
func (c *helmWrapper) Manifest() (string, error) {
|
|
get := action.NewGet(c.helmConf)
|
|
|
|
rel, err := get.Run(c.ReleaseName)
|
|
|
|
if err != nil {
|
|
klog.Errorf("namespace: %s, name: %s, run command failed, error: %v", c.Namespace, c.ReleaseName, err)
|
|
return "", err
|
|
}
|
|
klog.V(2).Infof("namespace: %s, name: %s, run command success", c.Namespace, c.ReleaseName)
|
|
klog.V(8).Infof("namespace: %s, name: %s, run command success, manifest: %s", c.Namespace, c.ReleaseName, rel.Manifest)
|
|
return rel.Manifest, nil
|
|
}
|