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 }