github.com/stefanmcshane/helm@v0.0.0-20221213002717-88a4a2c6e77d/pkg/action/action.go (about) 1 /* 2 Copyright The Helm 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 action 18 19 import ( 20 "bytes" 21 "fmt" 22 "os" 23 "path" 24 "path/filepath" 25 "regexp" 26 "strings" 27 28 "github.com/pkg/errors" 29 "k8s.io/apimachinery/pkg/api/meta" 30 "k8s.io/cli-runtime/pkg/genericclioptions" 31 "k8s.io/client-go/discovery" 32 "k8s.io/client-go/kubernetes" 33 "k8s.io/client-go/rest" 34 35 "github.com/stefanmcshane/helm/pkg/chart" 36 "github.com/stefanmcshane/helm/pkg/chartutil" 37 "github.com/stefanmcshane/helm/pkg/engine" 38 "github.com/stefanmcshane/helm/pkg/kube" 39 "github.com/stefanmcshane/helm/pkg/postrender" 40 "github.com/stefanmcshane/helm/pkg/registry" 41 "github.com/stefanmcshane/helm/pkg/release" 42 "github.com/stefanmcshane/helm/pkg/releaseutil" 43 "github.com/stefanmcshane/helm/pkg/storage" 44 "github.com/stefanmcshane/helm/pkg/storage/driver" 45 "github.com/stefanmcshane/helm/pkg/time" 46 ) 47 48 // Timestamper is a function capable of producing a timestamp.Timestamper. 49 // 50 // By default, this is a time.Time function from the Helm time package. This can 51 // be overridden for testing though, so that timestamps are predictable. 52 var Timestamper = time.Now 53 54 var ( 55 // errMissingChart indicates that a chart was not provided. 56 errMissingChart = errors.New("no chart provided") 57 // errMissingRelease indicates that a release (name) was not provided. 58 errMissingRelease = errors.New("no release provided") 59 // errInvalidRevision indicates that an invalid release revision number was provided. 60 errInvalidRevision = errors.New("invalid release revision") 61 // errPending indicates that another instance of Helm is already applying an operation on a release. 62 errPending = errors.New("another operation (install/upgrade/rollback) is in progress") 63 ) 64 65 // ValidName is a regular expression for resource names. 66 // 67 // DEPRECATED: This will be removed in Helm 4, and is no longer used here. See 68 // pkg/lint/rules.validateMetadataNameFunc for the replacement. 69 // 70 // According to the Kubernetes help text, the regular expression it uses is: 71 // 72 // [a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)* 73 // 74 // This follows the above regular expression (but requires a full string match, not partial). 75 // 76 // The Kubernetes documentation is here, though it is not entirely correct: 77 // https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names 78 var ValidName = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$`) 79 80 // Configuration injects the dependencies that all actions share. 81 type Configuration struct { 82 // RESTClientGetter is an interface that loads Kubernetes clients. 83 RESTClientGetter RESTClientGetter 84 85 // Releases stores records of releases. 86 Releases *storage.Storage 87 88 // KubeClient is a Kubernetes API client. 89 KubeClient kube.Interface 90 91 // RegistryClient is a client for working with registries 92 RegistryClient *registry.Client 93 94 // Capabilities describes the capabilities of the Kubernetes cluster. 95 Capabilities *chartutil.Capabilities 96 97 Log func(string, ...interface{}) 98 } 99 100 // renderResources renders the templates in a chart 101 // 102 // TODO: This function is badly in need of a refactor. 103 // TODO: As part of the refactor the duplicate code in cmd/helm/template.go should be removed 104 // This code has to do with writing files to disk. 105 func (cfg *Configuration) renderResources(ch *chart.Chart, values chartutil.Values, releaseName, outputDir string, subNotes, useReleaseName, includeCrds bool, pr postrender.PostRenderer, dryRun bool) ([]*release.Hook, *bytes.Buffer, string, error) { 106 hs := []*release.Hook{} 107 b := bytes.NewBuffer(nil) 108 109 caps, err := cfg.getCapabilities() 110 if err != nil { 111 return hs, b, "", err 112 } 113 114 if ch.Metadata.KubeVersion != "" { 115 if !chartutil.IsCompatibleRange(ch.Metadata.KubeVersion, caps.KubeVersion.String()) { 116 return hs, b, "", errors.Errorf("chart requires kubeVersion: %s which is incompatible with Kubernetes %s", ch.Metadata.KubeVersion, caps.KubeVersion.String()) 117 } 118 } 119 120 var files map[string]string 121 var err2 error 122 123 // A `helm template` or `helm install --dry-run` should not talk to the remote cluster. 124 // It will break in interesting and exotic ways because other data (e.g. discovery) 125 // is mocked. It is not up to the template author to decide when the user wants to 126 // connect to the cluster. So when the user says to dry run, respect the user's 127 // wishes and do not connect to the cluster. 128 if !dryRun && cfg.RESTClientGetter != nil { 129 restConfig, err := cfg.RESTClientGetter.ToRESTConfig() 130 if err != nil { 131 return hs, b, "", err 132 } 133 files, err2 = engine.RenderWithClient(ch, values, restConfig) 134 } else { 135 files, err2 = engine.Render(ch, values) 136 } 137 138 if err2 != nil { 139 return hs, b, "", err2 140 } 141 142 // NOTES.txt gets rendered like all the other files, but because it's not a hook nor a resource, 143 // pull it out of here into a separate file so that we can actually use the output of the rendered 144 // text file. We have to spin through this map because the file contains path information, so we 145 // look for terminating NOTES.txt. We also remove it from the files so that we don't have to skip 146 // it in the sortHooks. 147 var notesBuffer bytes.Buffer 148 for k, v := range files { 149 if strings.HasSuffix(k, notesFileSuffix) { 150 if subNotes || (k == path.Join(ch.Name(), "templates", notesFileSuffix)) { 151 // If buffer contains data, add newline before adding more 152 if notesBuffer.Len() > 0 { 153 notesBuffer.WriteString("\n") 154 } 155 notesBuffer.WriteString(v) 156 } 157 delete(files, k) 158 } 159 } 160 notes := notesBuffer.String() 161 162 // Sort hooks, manifests, and partials. Only hooks and manifests are returned, 163 // as partials are not used after renderer.Render. Empty manifests are also 164 // removed here. 165 hs, manifests, err := releaseutil.SortManifests(files, caps.APIVersions, releaseutil.InstallOrder) 166 if err != nil { 167 // By catching parse errors here, we can prevent bogus releases from going 168 // to Kubernetes. 169 // 170 // We return the files as a big blob of data to help the user debug parser 171 // errors. 172 for name, content := range files { 173 if strings.TrimSpace(content) == "" { 174 continue 175 } 176 fmt.Fprintf(b, "---\n# Source: %s\n%s\n", name, content) 177 } 178 return hs, b, "", err 179 } 180 181 // Aggregate all valid manifests into one big doc. 182 fileWritten := make(map[string]bool) 183 184 if includeCrds { 185 for _, crd := range ch.CRDObjects() { 186 if outputDir == "" { 187 fmt.Fprintf(b, "---\n# Source: %s\n%s\n", crd.Name, string(crd.File.Data[:])) 188 } else { 189 err = writeToFile(outputDir, crd.Filename, string(crd.File.Data[:]), fileWritten[crd.Name]) 190 if err != nil { 191 return hs, b, "", err 192 } 193 fileWritten[crd.Name] = true 194 } 195 } 196 } 197 198 for _, m := range manifests { 199 if outputDir == "" { 200 fmt.Fprintf(b, "---\n# Source: %s\n%s\n", m.Name, m.Content) 201 } else { 202 newDir := outputDir 203 if useReleaseName { 204 newDir = filepath.Join(outputDir, releaseName) 205 } 206 // NOTE: We do not have to worry about the post-renderer because 207 // output dir is only used by `helm template`. In the next major 208 // release, we should move this logic to template only as it is not 209 // used by install or upgrade 210 err = writeToFile(newDir, m.Name, m.Content, fileWritten[m.Name]) 211 if err != nil { 212 return hs, b, "", err 213 } 214 fileWritten[m.Name] = true 215 } 216 } 217 218 if pr != nil { 219 b, err = pr.Run(b) 220 if err != nil { 221 return hs, b, notes, errors.Wrap(err, "error while running post render on files") 222 } 223 } 224 225 return hs, b, notes, nil 226 } 227 228 // RESTClientGetter gets the rest client 229 type RESTClientGetter interface { 230 ToRESTConfig() (*rest.Config, error) 231 ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) 232 ToRESTMapper() (meta.RESTMapper, error) 233 } 234 235 // DebugLog sets the logger that writes debug strings 236 type DebugLog func(format string, v ...interface{}) 237 238 // capabilities builds a Capabilities from discovery information. 239 func (cfg *Configuration) getCapabilities() (*chartutil.Capabilities, error) { 240 if cfg.Capabilities != nil { 241 return cfg.Capabilities, nil 242 } 243 dc, err := cfg.RESTClientGetter.ToDiscoveryClient() 244 if err != nil { 245 return nil, errors.Wrap(err, "could not get Kubernetes discovery client") 246 } 247 // force a discovery cache invalidation to always fetch the latest server version/capabilities. 248 dc.Invalidate() 249 kubeVersion, err := dc.ServerVersion() 250 if err != nil { 251 return nil, errors.Wrap(err, "could not get server version from Kubernetes") 252 } 253 // Issue #6361: 254 // Client-Go emits an error when an API service is registered but unimplemented. 255 // We trap that error here and print a warning. But since the discovery client continues 256 // building the API object, it is correctly populated with all valid APIs. 257 // See https://github.com/kubernetes/kubernetes/issues/72051#issuecomment-521157642 258 apiVersions, err := GetVersionSet(dc) 259 if err != nil { 260 if discovery.IsGroupDiscoveryFailedError(err) { 261 cfg.Log("WARNING: The Kubernetes server has an orphaned API service. Server reports: %s", err) 262 cfg.Log("WARNING: To fix this, kubectl delete apiservice <service-name>") 263 } else { 264 return nil, errors.Wrap(err, "could not get apiVersions from Kubernetes") 265 } 266 } 267 268 cfg.Capabilities = &chartutil.Capabilities{ 269 APIVersions: apiVersions, 270 KubeVersion: chartutil.KubeVersion{ 271 Version: kubeVersion.GitVersion, 272 Major: kubeVersion.Major, 273 Minor: kubeVersion.Minor, 274 }, 275 HelmVersion: chartutil.DefaultCapabilities.HelmVersion, 276 } 277 return cfg.Capabilities, nil 278 } 279 280 // KubernetesClientSet creates a new kubernetes ClientSet based on the configuration 281 func (cfg *Configuration) KubernetesClientSet() (kubernetes.Interface, error) { 282 conf, err := cfg.RESTClientGetter.ToRESTConfig() 283 if err != nil { 284 return nil, errors.Wrap(err, "unable to generate config for kubernetes client") 285 } 286 287 return kubernetes.NewForConfig(conf) 288 } 289 290 // Now generates a timestamp 291 // 292 // If the configuration has a Timestamper on it, that will be used. 293 // Otherwise, this will use time.Now(). 294 func (cfg *Configuration) Now() time.Time { 295 return Timestamper() 296 } 297 298 func (cfg *Configuration) releaseContent(name string, version int) (*release.Release, error) { 299 if err := chartutil.ValidateReleaseName(name); err != nil { 300 return nil, errors.Errorf("releaseContent: Release name is invalid: %s", name) 301 } 302 303 if version <= 0 { 304 return cfg.Releases.Last(name) 305 } 306 307 return cfg.Releases.Get(name, version) 308 } 309 310 // GetVersionSet retrieves a set of available k8s API versions 311 func GetVersionSet(client discovery.ServerResourcesInterface) (chartutil.VersionSet, error) { 312 groups, resources, err := client.ServerGroupsAndResources() 313 if err != nil && !discovery.IsGroupDiscoveryFailedError(err) { 314 return chartutil.DefaultVersionSet, errors.Wrap(err, "could not get apiVersions from Kubernetes") 315 } 316 317 // FIXME: The Kubernetes test fixture for cli appears to always return nil 318 // for calls to Discovery().ServerGroupsAndResources(). So in this case, we 319 // return the default API list. This is also a safe value to return in any 320 // other odd-ball case. 321 if len(groups) == 0 && len(resources) == 0 { 322 return chartutil.DefaultVersionSet, nil 323 } 324 325 versionMap := make(map[string]interface{}) 326 versions := []string{} 327 328 // Extract the groups 329 for _, g := range groups { 330 for _, gv := range g.Versions { 331 versionMap[gv.GroupVersion] = struct{}{} 332 } 333 } 334 335 // Extract the resources 336 var id string 337 var ok bool 338 for _, r := range resources { 339 for _, rl := range r.APIResources { 340 341 // A Kind at a GroupVersion can show up more than once. We only want 342 // it displayed once in the final output. 343 id = path.Join(r.GroupVersion, rl.Kind) 344 if _, ok = versionMap[id]; !ok { 345 versionMap[id] = struct{}{} 346 } 347 } 348 } 349 350 // Convert to a form that NewVersionSet can use 351 for k := range versionMap { 352 versions = append(versions, k) 353 } 354 355 return chartutil.VersionSet(versions), nil 356 } 357 358 // recordRelease with an update operation in case reuse has been set. 359 func (cfg *Configuration) recordRelease(r *release.Release) { 360 if err := cfg.Releases.Update(r); err != nil { 361 cfg.Log("warning: Failed to update release %s: %s", r.Name, err) 362 } 363 } 364 365 // Init initializes the action configuration 366 func (cfg *Configuration) Init(getter genericclioptions.RESTClientGetter, namespace, helmDriver string, log DebugLog) error { 367 kc := kube.New(getter) 368 kc.Log = log 369 370 lazyClient := &lazyClient{ 371 namespace: namespace, 372 clientFn: kc.Factory.KubernetesClientSet, 373 } 374 375 var store *storage.Storage 376 switch helmDriver { 377 case "secret", "secrets", "": 378 d := driver.NewSecrets(newSecretClient(lazyClient)) 379 d.Log = log 380 store = storage.Init(d) 381 case "configmap", "configmaps": 382 d := driver.NewConfigMaps(newConfigMapClient(lazyClient)) 383 d.Log = log 384 store = storage.Init(d) 385 case "memory": 386 var d *driver.Memory 387 if cfg.Releases != nil { 388 if mem, ok := cfg.Releases.Driver.(*driver.Memory); ok { 389 // This function can be called more than once (e.g., helm list --all-namespaces). 390 // If a memory driver was already initialized, re-use it but set the possibly new namespace. 391 // We re-use it in case some releases where already created in the existing memory driver. 392 d = mem 393 } 394 } 395 if d == nil { 396 d = driver.NewMemory() 397 } 398 d.SetNamespace(namespace) 399 store = storage.Init(d) 400 case "sql": 401 d, err := driver.NewSQL( 402 os.Getenv("HELM_DRIVER_SQL_CONNECTION_STRING"), 403 log, 404 namespace, 405 ) 406 if err != nil { 407 panic(fmt.Sprintf("Unable to instantiate SQL driver: %v", err)) 408 } 409 store = storage.Init(d) 410 default: 411 // Not sure what to do here. 412 panic("Unknown driver in HELM_DRIVER: " + helmDriver) 413 } 414 415 cfg.RESTClientGetter = getter 416 cfg.KubeClient = kc 417 cfg.Releases = store 418 cfg.Log = log 419 420 return nil 421 }