github.com/telepresenceio/telepresence/v2@v2.20.0-pro.6.0.20240517030216-236ea954e789/pkg/client/cli/helm/install.go (about)

     1  package helm
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"strings"
     9  	"time"
    10  
    11  	"helm.sh/helm/v3/pkg/action"
    12  	"helm.sh/helm/v3/pkg/chart"
    13  	"helm.sh/helm/v3/pkg/chartutil"
    14  	"helm.sh/helm/v3/pkg/cli"
    15  	"helm.sh/helm/v3/pkg/cli/values"
    16  	"helm.sh/helm/v3/pkg/getter"
    17  	"helm.sh/helm/v3/pkg/release"
    18  	"k8s.io/cli-runtime/pkg/genericclioptions"
    19  
    20  	"github.com/datawire/dlib/dlog"
    21  	"github.com/datawire/dlib/dtime"
    22  	"github.com/datawire/k8sapi/pkg/k8sapi"
    23  	"github.com/telepresenceio/telepresence/rpc/v2/connector"
    24  	"github.com/telepresenceio/telepresence/v2/pkg/client"
    25  	"github.com/telepresenceio/telepresence/v2/pkg/client/userd/k8s"
    26  	"github.com/telepresenceio/telepresence/v2/pkg/dos"
    27  	"github.com/telepresenceio/telepresence/v2/pkg/errcat"
    28  	"github.com/telepresenceio/telepresence/v2/pkg/ioutil"
    29  )
    30  
    31  const (
    32  	helmDriver                = "secrets"
    33  	trafficManagerReleaseName = "traffic-manager"
    34  	crdReleaseName            = "telepresence-crds"
    35  )
    36  
    37  var GetValuesFunc = GetValues //nolint:gochecknoglobals // extension point
    38  
    39  type RequestType int32
    40  
    41  const (
    42  	Install RequestType = iota
    43  	Upgrade
    44  	Uninstall
    45  )
    46  
    47  type Request struct {
    48  	values.Options
    49  	Type        RequestType
    50  	ValuesJson  []byte
    51  	ReuseValues bool
    52  	ResetValues bool
    53  	Crds        bool
    54  	NoHooks     bool
    55  }
    56  
    57  func (hr *Request) Run(ctx context.Context, cr *connector.ConnectRequest) error {
    58  	if hr.ReuseValues && hr.ResetValues {
    59  		return errcat.User.New("--reset-values and --reuse-values are mutually exclusive")
    60  	}
    61  
    62  	if cr.ManagerNamespace == "" {
    63  		if ns, ok := cr.KubeFlags["namespace"]; ok {
    64  			cr.ManagerNamespace = ns
    65  		} else {
    66  			cr.ManagerNamespace = "ambassador"
    67  		}
    68  	}
    69  	dlog.Debugf(ctx, "using manager namespace %q", cr.ManagerNamespace)
    70  
    71  	allValues, err := hr.MergeValues(getter.All(cli.New()))
    72  	if err != nil {
    73  		return err
    74  	}
    75  
    76  	hr.ValuesJson, err = json.Marshal(allValues)
    77  	if err != nil {
    78  		return err
    79  	}
    80  
    81  	var config *client.Kubeconfig
    82  	config, err = client.DaemonKubeconfig(ctx, cr)
    83  	if err != nil {
    84  		return err
    85  	}
    86  
    87  	var cluster *k8s.Cluster
    88  	cluster, err = k8s.ConnectCluster(ctx, cr, config)
    89  	if err != nil {
    90  		return err
    91  	}
    92  
    93  	if hr.Type == Uninstall {
    94  		err = DeleteTrafficManager(ctx, cluster.Kubeconfig, cluster.GetManagerNamespace(), false, hr)
    95  	} else {
    96  		dlog.Debug(ctx, "ensuring that traffic-manager exists")
    97  		err = EnsureTrafficManager(cluster.WithK8sInterface(ctx), cluster.Kubeconfig, cluster.GetManagerNamespace(), hr)
    98  	}
    99  	if err != nil {
   100  		return err
   101  	}
   102  
   103  	var msg string
   104  	switch hr.Type {
   105  	case Install:
   106  		msg = "installed"
   107  	case Upgrade:
   108  		msg = "upgraded"
   109  	case Uninstall:
   110  		msg = "uninstalled"
   111  	}
   112  
   113  	updatedResource := "Traffic Manager"
   114  	if hr.Crds {
   115  		updatedResource = "Telepresence CRDs"
   116  	}
   117  
   118  	ioutil.Printf(dos.Stdout(ctx), "\n%s %s successfully\n", updatedResource, msg)
   119  	return nil
   120  }
   121  
   122  func getHelmConfig(ctx context.Context, clientGetter genericclioptions.RESTClientGetter, namespace string) (*action.Configuration, error) {
   123  	helmConfig := &action.Configuration{}
   124  	err := helmConfig.Init(clientGetter, namespace, helmDriver, func(format string, args ...any) {
   125  		ctx := dlog.WithField(ctx, "source", "helm")
   126  		dlog.Infof(ctx, format, args...)
   127  	})
   128  	if err != nil {
   129  		return nil, err
   130  	}
   131  	return helmConfig, nil
   132  }
   133  
   134  func GetValues(ctx context.Context) map[string]any {
   135  	clientConfig := client.GetConfig(ctx)
   136  	imgConfig := clientConfig.Images()
   137  	imageRegistry := imgConfig.Registry(ctx)
   138  	imageTag := strings.TrimPrefix(client.Version(), "v")
   139  	values := map[string]any{
   140  		"image": map[string]any{
   141  			"registry": imageRegistry,
   142  			"tag":      imageTag,
   143  		},
   144  	}
   145  	if !clientConfig.Grpc().MaxReceiveSizeV.IsZero() {
   146  		values["grpc"] = map[string]any{
   147  			"maxReceiveSize": clientConfig.Grpc().MaxReceiveSizeV.String(),
   148  		}
   149  	}
   150  	if wai, wr := imgConfig.AgentImage(ctx), imgConfig.WebhookRegistry(ctx); wai != "" || wr != "" {
   151  		image := make(map[string]any)
   152  		if wai != "" {
   153  			i := strings.LastIndexByte(wai, '/')
   154  			if i >= 0 {
   155  				if wr == "" {
   156  					wr = wai[:i]
   157  				}
   158  				wai = wai[i+1:]
   159  			}
   160  			parts := strings.Split(wai, ":")
   161  			name := wai
   162  			tag := ""
   163  			if len(parts) > 1 {
   164  				name = parts[0]
   165  				tag = parts[1]
   166  			}
   167  			image["name"] = name
   168  			image["tag"] = tag
   169  		}
   170  		if wr != "" {
   171  			image["registry"] = wr
   172  		}
   173  		values["agent"] = map[string]any{"image": image}
   174  	}
   175  
   176  	if apc := clientConfig.Intercept().AppProtocolStrategy; apc != k8sapi.Http2Probe {
   177  		values["agentInjector"] = map[string]any{"appProtocolStrategy": apc.String()}
   178  	}
   179  	if clientConfig.TelepresenceAPI().Port != 0 {
   180  		values["telepresenceAPI"] = map[string]any{
   181  			"port": clientConfig.TelepresenceAPI().Port,
   182  		}
   183  	}
   184  
   185  	return values
   186  }
   187  
   188  func timedRun(ctx context.Context, run func(time.Duration) error) error {
   189  	timeouts := client.GetConfig(ctx).Timeouts()
   190  	ctx, cancel := timeouts.TimeoutContext(ctx, client.TimeoutHelm)
   191  	defer cancel()
   192  
   193  	runResult := make(chan error)
   194  	go func() {
   195  		runResult <- run(timeouts.Get(client.TimeoutHelm))
   196  	}()
   197  
   198  	select {
   199  	case <-ctx.Done():
   200  		return client.CheckTimeout(ctx, ctx.Err())
   201  	case err := <-runResult:
   202  		if err != nil {
   203  			err = client.CheckTimeout(ctx, err)
   204  		}
   205  		return err
   206  	}
   207  }
   208  
   209  func installNew(
   210  	ctx context.Context,
   211  	chrt *chart.Chart,
   212  	helmConfig *action.Configuration,
   213  	releaseName, namespace string,
   214  	req *Request,
   215  	values map[string]any,
   216  ) error {
   217  	dlog.Infof(ctx, "No existing %s found in namespace %s, installing %s...", releaseName, namespace, getTrafficManagerVersion(values))
   218  	install := action.NewInstall(helmConfig)
   219  	install.ReleaseName = releaseName
   220  	install.Namespace = namespace
   221  	install.Atomic = true
   222  	install.CreateNamespace = true
   223  	install.DisableHooks = req.NoHooks
   224  	return timedRun(ctx, func(timeout time.Duration) error {
   225  		install.Timeout = timeout
   226  		_, err := install.Run(chrt, values)
   227  		return err
   228  	})
   229  }
   230  
   231  func upgradeExisting(
   232  	ctx context.Context,
   233  	existingVer string,
   234  	chrt *chart.Chart,
   235  	helmConfig *action.Configuration,
   236  	releaseName, ns string,
   237  	req *Request,
   238  	values map[string]any,
   239  ) error {
   240  	dlog.Infof(ctx, "Existing Traffic Manager %s found in namespace %s, upgrading to %s...", existingVer, ns, client.Version())
   241  	upgrade := action.NewUpgrade(helmConfig)
   242  	upgrade.Atomic = true
   243  	upgrade.Namespace = ns
   244  	upgrade.ResetValues = req.ResetValues
   245  	upgrade.ReuseValues = req.ReuseValues
   246  	upgrade.DisableHooks = req.NoHooks
   247  	return timedRun(ctx, func(timeout time.Duration) error {
   248  		upgrade.Timeout = timeout
   249  		_, err := upgrade.Run(releaseName, chrt, values)
   250  		return err
   251  	})
   252  }
   253  
   254  func uninstallExisting(ctx context.Context, helmConfig *action.Configuration, releaseName, namespace string, req *Request) error {
   255  	dlog.Infof(ctx, "Uninstalling %s in namespace %s", releaseName, namespace)
   256  	uninstall := action.NewUninstall(helmConfig)
   257  	uninstall.DisableHooks = req.NoHooks
   258  	return timedRun(ctx, func(timeout time.Duration) error {
   259  		uninstall.Timeout = timeout
   260  		_, err := uninstall.Run(releaseName)
   261  		return err
   262  	})
   263  }
   264  
   265  var errStuck = errors.New("stuck in pending state") //nolint:gochecknoglobals // constant
   266  
   267  func isInstalled(
   268  	ctx context.Context,
   269  	timeout time.Duration,
   270  	clientGetter genericclioptions.RESTClientGetter,
   271  	releaseName, namespace string,
   272  ) (*release.Release, *action.Configuration, error) {
   273  	dlog.Debug(ctx, "getHelmConfig")
   274  	helmConfig, err := getHelmConfig(ctx, clientGetter, namespace)
   275  	if err != nil {
   276  		err = fmt.Errorf("failed to initialize helm config: %w", err)
   277  		return nil, nil, err
   278  	}
   279  
   280  	var existing *release.Release
   281  	transitionStart := time.Now()
   282  	for time.Since(transitionStart) < timeout {
   283  		dlog.Debugf(ctx, "getHelmRelease")
   284  		if existing, err = getHelmRelease(ctx, releaseName, helmConfig); err != nil {
   285  			// If we weren't able to get the helm release at all, there's no hope for installing it
   286  			// This could have happened because the user doesn't have the requisite permissions, or because there was some
   287  			// kind of issue communicating with kubernetes. Let's hope it's the former and let's hope the traffic manager
   288  			// is already set up. If it's the latter case (or the traffic manager isn't there), we'll be alerted by
   289  			// a subsequent error anyway.
   290  			return nil, nil, err
   291  		}
   292  		if existing == nil {
   293  			dlog.Infof(ctx, "isInstalled(namespace=%q): current install: none", namespace)
   294  			return nil, helmConfig, nil
   295  		}
   296  		st := existing.Info.Status
   297  		if !(st.IsPending() || st == release.StatusUninstalling) {
   298  			owner := "unknown"
   299  			if ow, ok := existing.Config["createdBy"]; ok {
   300  				owner = ow.(string)
   301  			}
   302  			dlog.Infof(ctx, "isInstalled(namespace=%q): current install: version=%q, owner=%q, state.status=%q, state.desc=%q",
   303  				namespace, releaseVer(existing), owner, st, existing.Info.Description)
   304  			return existing, helmConfig, nil
   305  		}
   306  		dlog.Infof(ctx, "isInstalled(namespace=%q): current install is in a pending or uninstalling state, waiting for it to transition...",
   307  			namespace)
   308  		dtime.SleepWithContext(ctx, 1*time.Second)
   309  	}
   310  	return existing, helmConfig, errStuck
   311  }
   312  
   313  func EnsureTrafficManager(ctx context.Context, clientGetter genericclioptions.RESTClientGetter, namespace string, req *Request) (err error) {
   314  	if req.Crds {
   315  		dlog.Debug(ctx, "loading build-in helm chart")
   316  		err = ensureIsInstalled(ctx, clientGetter, true, crdReleaseName, namespace, req)
   317  	} else {
   318  		err = ensureIsInstalled(ctx, clientGetter, false, trafficManagerReleaseName, namespace, req)
   319  	}
   320  	return err
   321  }
   322  
   323  // EnsureTrafficManager ensures the traffic manager is installed.
   324  func ensureIsInstalled(
   325  	ctx context.Context, clientGetter genericclioptions.RESTClientGetter, crd bool,
   326  	releaseName, namespace string, req *Request,
   327  ) error {
   328  	cleanFailedState := func(helmConfig *action.Configuration) error {
   329  		urq := Request{
   330  			Type:    Uninstall,
   331  			NoHooks: true,
   332  		}
   333  		err := uninstallExisting(ctx, helmConfig, releaseName, namespace, &urq)
   334  		if err != nil {
   335  			err = fmt.Errorf("failed to clean up leftover release history: %w", err)
   336  		}
   337  		return err
   338  	}
   339  
   340  	timeout := client.GetConfig(ctx).Timeouts().Get(client.TimeoutHelm)
   341  	existing, helmConfig, err := isInstalled(ctx, timeout, clientGetter, releaseName, namespace)
   342  	if err != nil {
   343  		if !(errors.Is(err, errStuck) && req.Type == Install) {
   344  			return err
   345  		}
   346  		dlog.Infof(ctx, "ensureIsInstalled(namespace=%q): current install is has been in a pending state for longer than `timeouts.helm` (%v); "+
   347  			"assuming it's stuck and will attempt uninstall", namespace, timeout)
   348  		err = cleanFailedState(helmConfig)
   349  		if err != nil {
   350  			return err
   351  		}
   352  		existing = nil
   353  	}
   354  
   355  	// Under various conditions, helm can leave the release history hanging around after the release is gone.
   356  	// In those cases, an uninstall should clean everything up and leave us ready to install again
   357  	if existing != nil && (existing.Info.Status != release.StatusDeployed) {
   358  		dlog.Infof(ctx, "ensureIsInstalled(namespace=%q): current status (status=%q, desc=%q) is not %q, so assuming it's corrupt or stuck; removing it...",
   359  			namespace, existing.Info.Status, existing.Info.Description, release.StatusDeployed)
   360  		err = cleanFailedState(helmConfig)
   361  		if err != nil {
   362  			return err
   363  		}
   364  		existing = nil
   365  	}
   366  
   367  	// OK, now install things.
   368  	var providedVals map[string]any
   369  	if len(req.ValuesJson) > 0 {
   370  		if err := json.Unmarshal(req.ValuesJson, &providedVals); err != nil {
   371  			return errcat.User.Newf("unable to parse values JSON: %w", err)
   372  		}
   373  	}
   374  
   375  	var vals map[string]any
   376  	if len(providedVals) > 0 {
   377  		vals = chartutil.CoalesceTables(providedVals, GetValuesFunc(ctx))
   378  	} else {
   379  		// No values were provided. This means that an upgrade should retain existing values unless
   380  		// reset-values is true.
   381  		if req.Type == Upgrade && !req.ResetValues {
   382  			req.ReuseValues = true
   383  		}
   384  		vals = GetValuesFunc(ctx)
   385  	}
   386  
   387  	version := getTrafficManagerVersion(vals)
   388  
   389  	var chrt *chart.Chart
   390  	if crd {
   391  		chrt, err = loadCRDChart(version)
   392  	} else {
   393  		chrt, err = loadCoreChart(version)
   394  	}
   395  	if err != nil {
   396  		return fmt.Errorf("unable to load built-in helm chart: %w", err)
   397  	}
   398  
   399  	switch {
   400  	case existing == nil && req.Type == Upgrade: // fresh install
   401  		err = errcat.User.Newf("%s is not installed, use 'telepresence helm install' to install it", releaseName)
   402  	case existing == nil:
   403  		dlog.Infof(ctx, "ensureIsInstalled(namespace=%q): performing fresh install...", namespace)
   404  		err = installNew(ctx, chrt, helmConfig, releaseName, namespace, req, vals)
   405  	case req.Type == Upgrade: // replace existing install
   406  		dlog.Infof(ctx, "ensureIsInstalled(namespace=%q): replacing %s from %q to %q...",
   407  			namespace, releaseName, releaseVer(existing), version)
   408  		err = upgradeExisting(ctx, releaseVer(existing), chrt, helmConfig, releaseName, namespace, req, vals)
   409  	default:
   410  		err = errcat.User.Newf(
   411  			"%s version %q is already installed, use 'telepresence helm upgrade' instead to replace it",
   412  			releaseName, releaseVer(existing))
   413  	}
   414  	return err
   415  }
   416  
   417  // DeleteTrafficManager deletes the traffic manager.
   418  func DeleteTrafficManager(
   419  	ctx context.Context, clientGetter genericclioptions.RESTClientGetter, namespace string, errOnFail bool, req *Request,
   420  ) error {
   421  	if !req.Crds {
   422  		err := ensureIsDeleted(ctx, clientGetter, trafficManagerReleaseName, namespace, errOnFail, req)
   423  		if err != nil {
   424  			return err
   425  		}
   426  		return nil
   427  	}
   428  
   429  	err := ensureIsDeleted(ctx, clientGetter, crdReleaseName, namespace, errOnFail, req)
   430  	if err != nil {
   431  		return err
   432  	}
   433  
   434  	return nil
   435  }
   436  
   437  func ensureIsDeleted(
   438  	ctx context.Context,
   439  	clientGetter genericclioptions.RESTClientGetter,
   440  	releaseName, namespace string,
   441  	errOnFail bool,
   442  	req *Request,
   443  ) error {
   444  	helmConfig, err := getHelmConfig(ctx, clientGetter, namespace)
   445  	if err != nil {
   446  		return fmt.Errorf("failed to initialize helm config: %w", err)
   447  	}
   448  
   449  	existing, err := getHelmRelease(ctx, releaseName, helmConfig)
   450  	if err != nil {
   451  		err := fmt.Errorf("unable to look for existing helm release in namespace %s: %w", namespace, err)
   452  		if errOnFail {
   453  			return err
   454  		}
   455  		dlog.Infof(ctx, "%s. Assuming it's already gone...", err.Error())
   456  		return nil
   457  	}
   458  	if existing == nil {
   459  		err := fmt.Errorf("%s in namespace %s already deleted", releaseName, namespace)
   460  		if errOnFail {
   461  			return err
   462  		}
   463  		dlog.Info(ctx, err.Error())
   464  		return nil
   465  	}
   466  	return uninstallExisting(ctx, helmConfig, releaseName, namespace, req)
   467  }
   468  
   469  func getTrafficManagerVersion(values map[string]any) string {
   470  	if img, ok := values["image"].(map[string]any); ok {
   471  		if tag, ok := img["tag"].(string); ok {
   472  			return tag
   473  		}
   474  	}
   475  	return strings.TrimPrefix(client.Version(), "v")
   476  }