github.com/koderover/helm@v2.17.0+incompatible/pkg/tiller/release_server.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 tiller 18 19 import ( 20 "bytes" 21 "errors" 22 "fmt" 23 "path" 24 "regexp" 25 "strings" 26 "time" 27 28 "github.com/technosophos/moniker" 29 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 "k8s.io/client-go/discovery" 31 "k8s.io/client-go/kubernetes" 32 33 "k8s.io/helm/pkg/chartutil" 34 "k8s.io/helm/pkg/hooks" 35 "k8s.io/helm/pkg/proto/hapi/chart" 36 "k8s.io/helm/pkg/proto/hapi/release" 37 "k8s.io/helm/pkg/proto/hapi/services" 38 relutil "k8s.io/helm/pkg/releaseutil" 39 "k8s.io/helm/pkg/tiller/environment" 40 "k8s.io/helm/pkg/timeconv" 41 "k8s.io/helm/pkg/version" 42 ) 43 44 const ( 45 // releaseNameMaxLen is the maximum length of a release name. 46 // 47 // As of Kubernetes 1.4, the max limit on a name is 63 chars. We reserve 10 for 48 // charts to add data. Effectively, that gives us 53 chars. 49 // See https://github.com/kubernetes/helm/issues/1528 50 releaseNameMaxLen = 53 51 52 // NOTESFILE_SUFFIX that we want to treat special. It goes through the templating engine 53 // but it's not a yaml file (resource) hence can't have hooks, etc. And the user actually 54 // wants to see this file after rendering in the status command. However, it must be a suffix 55 // since there can be filepath in front of it. 56 notesFileSuffix = "NOTES.txt" 57 ) 58 59 var ( 60 // errMissingChart indicates that a chart was not provided. 61 errMissingChart = errors.New("no chart provided") 62 // errMissingRelease indicates that a release (name) was not provided. 63 errMissingRelease = errors.New("no release provided") 64 // errInvalidRevision indicates that an invalid release revision number was provided. 65 errInvalidRevision = errors.New("invalid release revision") 66 //errInvalidName indicates that an invalid release name was provided 67 errInvalidName = errors.New("invalid release name, must match regex ^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])+$ and the length must not be longer than 53") 68 // errPending indicates that Tiller is already applying an operation on a release 69 errPending = errors.New("another operation (install/upgrade/rollback) is in progress") 70 ) 71 72 // ListDefaultLimit is the default limit for number of items returned in a list. 73 var ListDefaultLimit int64 = 512 74 75 // ValidName is a regular expression for names. 76 // 77 // According to the Kubernetes help text, the regular expression it uses is: 78 // 79 // (([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])? 80 // 81 // We modified that. First, we added start and end delimiters. Second, we changed 82 // the final ? to + to require that the pattern match at least once. This modification 83 // prevents an empty string from matching. 84 var ValidName = regexp.MustCompile("^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])+$") 85 86 // ReleaseServer implements the server-side gRPC endpoint for the HAPI services. 87 type ReleaseServer struct { 88 ReleaseModule 89 env *environment.Environment 90 clientset kubernetes.Interface 91 Log func(string, ...interface{}) 92 } 93 94 // NewReleaseServer creates a new release server. 95 func NewReleaseServer(env *environment.Environment, clientset kubernetes.Interface, useRemote bool) *ReleaseServer { 96 var releaseModule ReleaseModule 97 if useRemote { 98 releaseModule = &RemoteReleaseModule{} 99 } else { 100 releaseModule = &LocalReleaseModule{ 101 clientset: clientset, 102 } 103 } 104 105 return &ReleaseServer{ 106 env: env, 107 clientset: clientset, 108 ReleaseModule: releaseModule, 109 Log: func(_ string, _ ...interface{}) {}, 110 } 111 } 112 113 // reuseValues copies values from the current release to a new release if the 114 // new release does not have any values. 115 // 116 // If the request already has values, or if there are no values in the current 117 // release, this does nothing. 118 // 119 // This is skipped if the req.ResetValues flag is set, in which case the 120 // request values are not altered. 121 func (s *ReleaseServer) reuseValues(req *services.UpdateReleaseRequest, current *release.Release) error { 122 if req.ResetValues { 123 // If ResetValues is set, we completely ignore current.Config. 124 s.Log("resetting values to the chart's original version") 125 return nil 126 } 127 128 // If the ReuseValues flag is set, we always copy the old values over the new config's values. 129 if req.ReuseValues { 130 s.Log("reusing the old release's values") 131 132 // We have to regenerate the old coalesced values: 133 oldVals, err := chartutil.CoalesceValues(current.Chart, current.Config) 134 if err != nil { 135 err := fmt.Errorf("failed to rebuild old values: %s", err) 136 s.Log("%s", err) 137 return err 138 } 139 nv, err := oldVals.YAML() 140 if err != nil { 141 return err 142 } 143 req.Chart.Values = &chart.Config{Raw: nv} 144 145 reqValues, err := chartutil.ReadValues([]byte(req.Values.Raw)) 146 if err != nil { 147 return err 148 } 149 150 currentConfig := chartutil.Values{} 151 if current.Config != nil && current.Config.Raw != "" && current.Config.Raw != "{}\n" { 152 currentConfig, err = chartutil.ReadValues([]byte(current.Config.Raw)) 153 if err != nil { 154 return err 155 } 156 } 157 158 currentConfig.MergeInto(reqValues) 159 data, err := currentConfig.YAML() 160 if err != nil { 161 return err 162 } 163 164 req.Values.Raw = data 165 return nil 166 } 167 168 // If req.Values is empty, but current.Config is not, copy current into the 169 // request. 170 if (req.Values == nil || req.Values.Raw == "" || req.Values.Raw == "{}\n") && 171 current.Config != nil && 172 current.Config.Raw != "" && 173 current.Config.Raw != "{}\n" { 174 s.Log("copying values from %s (v%d) to new release.", current.Name, current.Version) 175 req.Values = current.Config 176 } 177 return nil 178 } 179 180 func (s *ReleaseServer) uniqName(start string, reuse bool) (string, error) { 181 182 // If a name is supplied, we check to see if that name is taken. If not, it 183 // is granted. If reuse is true and a deleted release with that name exists, 184 // we re-grant it. Otherwise, an error is returned. 185 if start != "" { 186 187 if len(start) > releaseNameMaxLen { 188 return "", fmt.Errorf("release name %q exceeds max length of %d", start, releaseNameMaxLen) 189 } 190 191 h, err := s.env.Releases.History(start) 192 if err != nil || len(h) < 1 { 193 return start, nil 194 } 195 relutil.Reverse(h, relutil.SortByRevision) 196 rel := h[0] 197 198 if st := rel.Info.Status.Code; reuse && (st == release.Status_DELETED || st == release.Status_FAILED) { 199 // Allow re-use of names if the previous release is marked deleted. 200 s.Log("name %s exists but is not in use, reusing name", start) 201 return start, nil 202 } else if reuse { 203 return "", fmt.Errorf("a release named %s is in use, cannot re-use a name that is still in use", start) 204 } 205 206 return "", fmt.Errorf("a release named %s already exists.\nRun: helm ls --all %s; to check the status of the release\nOr run: helm del --purge %s; to delete it", start, start, start) 207 } 208 209 moniker := moniker.New() 210 newname, err := s.createUniqName(moniker) 211 if err != nil { 212 return "ERROR", err 213 } 214 215 s.Log("info: Created new release name %s", newname) 216 return newname, nil 217 218 } 219 220 func (s *ReleaseServer) createUniqName(m moniker.Namer) (string, error) { 221 maxTries := 5 222 for i := 0; i < maxTries; i++ { 223 name := m.NameSep("-") 224 if len(name) > releaseNameMaxLen { 225 name = name[:releaseNameMaxLen] 226 } 227 if _, err := s.env.Releases.Get(name, 1); err != nil { 228 if strings.Contains(err.Error(), "not found") { 229 return name, nil 230 } 231 } 232 s.Log("info: generated name %s is taken. Searching again.", name) 233 } 234 s.Log("warning: No available release names found after %d tries", maxTries) 235 return "ERROR", errors.New("no available release name found") 236 } 237 238 func (s *ReleaseServer) engine(ch *chart.Chart) environment.Engine { 239 renderer := s.env.EngineYard.Default() 240 if ch.Metadata.Engine != "" { 241 if r, ok := s.env.EngineYard.Get(ch.Metadata.Engine); ok { 242 renderer = r 243 } else { 244 s.Log("warning: %s requested non-existent template engine %s", ch.Metadata.Name, ch.Metadata.Engine) 245 } 246 } 247 return renderer 248 } 249 250 // capabilities builds a Capabilities from discovery information. 251 func capabilities(disc discovery.DiscoveryInterface) (*chartutil.Capabilities, error) { 252 sv, err := disc.ServerVersion() 253 if err != nil { 254 return nil, err 255 } 256 vs, err := GetAllVersionSet(disc) 257 if err != nil { 258 return nil, fmt.Errorf("Could not get apiVersions from Kubernetes: %s", err) 259 } 260 return &chartutil.Capabilities{ 261 APIVersions: vs, 262 KubeVersion: sv, 263 TillerVersion: version.GetVersionProto(), 264 }, nil 265 } 266 267 // GetAllVersionSet retrieves a set of available k8s API versions and objects 268 // 269 // This is a different function from GetVersionSet because the signature changed. 270 // To keep compatibility through the public functions this needed to be a new 271 // function.GetAllVersionSet 272 // TODO(mattfarina): In Helm v3 merge with GetVersionSet 273 func GetAllVersionSet(client discovery.ServerResourcesInterface) (chartutil.VersionSet, error) { 274 groups, resources, err := client.ServerGroupsAndResources() 275 // It is okay to silently swallow a GroupDiscoveryFailedError, which is actually just 276 // a warning. The 'groups' will still have all of the valid data. 277 if err != nil && !discovery.IsGroupDiscoveryFailedError(err) { 278 return chartutil.DefaultVersionSet, err 279 } 280 281 // FIXME: The Kubernetes test fixture for cli appears to always return nil 282 // for calls to Discovery().ServerGroupsAndResources(). So in this case, we 283 // return the default API list. This is also a safe value to return in any 284 // other odd-ball case. 285 if len(groups) == 0 && len(resources) == 0 { 286 return chartutil.DefaultVersionSet, nil 287 } 288 289 versionMap := make(map[string]interface{}) 290 versions := []string{} 291 292 // Extract the groups 293 for _, g := range groups { 294 for _, gv := range g.Versions { 295 versionMap[gv.GroupVersion] = struct{}{} 296 } 297 } 298 299 // Extract the resources 300 var id string 301 var ok bool 302 for _, r := range resources { 303 for _, rl := range r.APIResources { 304 305 // A Kind at a GroupVersion can show up more than once. We only want 306 // it displayed once in the final output. 307 id = path.Join(r.GroupVersion, rl.Kind) 308 if _, ok = versionMap[id]; !ok { 309 versionMap[id] = struct{}{} 310 } 311 } 312 } 313 314 // Convert to a form that NewVersionSet can use 315 for k := range versionMap { 316 versions = append(versions, k) 317 } 318 319 return chartutil.NewVersionSet(versions...), nil 320 } 321 322 // GetVersionSet retrieves a set of available k8s API versions 323 func GetVersionSet(client discovery.ServerGroupsInterface) (chartutil.VersionSet, error) { 324 groups, err := client.ServerGroups() 325 // It is okay to silently swallow a GroupDiscoveryFailedError, which is actually just 326 // a warning. The 'groups' will still have all of the valid data. 327 if err != nil && !discovery.IsGroupDiscoveryFailedError(err) { 328 return chartutil.DefaultVersionSet, err 329 } 330 331 // FIXME: The Kubernetes test fixture for cli appears to always return nil 332 // for calls to Discovery().ServerGroups(). So in this case, we return 333 // the default API list. This is also a safe value to return in any other 334 // odd-ball case. 335 if groups.Size() == 0 { 336 return chartutil.DefaultVersionSet, nil 337 } 338 339 versions := metav1.ExtractGroupVersions(groups) 340 return chartutil.NewVersionSet(versions...), nil 341 } 342 343 func (s *ReleaseServer) renderResources(ch *chart.Chart, values chartutil.Values, subNotes bool, vs chartutil.VersionSet) ([]*release.Hook, *bytes.Buffer, string, error) { 344 // Guard to make sure Tiller is at the right version to handle this chart. 345 sver := version.GetVersion() 346 if ch.Metadata.TillerVersion != "" && 347 !version.IsCompatibleRange(ch.Metadata.TillerVersion, sver) { 348 return nil, nil, "", fmt.Errorf("Chart incompatible with Tiller %s", sver) 349 } 350 351 if ch.Metadata.KubeVersion != "" { 352 cap, _ := values["Capabilities"].(*chartutil.Capabilities) 353 gitVersion := cap.KubeVersion.String() 354 k8sVersion := strings.Split(gitVersion, "+")[0] 355 if !version.IsCompatibleRange(ch.Metadata.KubeVersion, k8sVersion) { 356 return nil, nil, "", fmt.Errorf("Chart requires kubernetesVersion: %s which is incompatible with Kubernetes %s", ch.Metadata.KubeVersion, k8sVersion) 357 } 358 } 359 360 s.Log("rendering %s chart using values", ch.GetMetadata().Name) 361 renderer := s.engine(ch) 362 files, err := renderer.Render(ch, values) 363 if err != nil { 364 return nil, nil, "", err 365 } 366 367 // NOTES.txt gets rendered like all the other files, but because it's not a hook nor a resource, 368 // pull it out of here into a separate file so that we can actually use the output of the rendered 369 // text file. We have to spin through this map because the file contains path information, so we 370 // look for terminating NOTES.txt. We also remove it from the files so that we don't have to skip 371 // it in the sortHooks. 372 var notesBuffer bytes.Buffer 373 for k, v := range files { 374 if strings.HasSuffix(k, notesFileSuffix) { 375 if subNotes || (k == path.Join(ch.Metadata.Name, "templates", notesFileSuffix)) { 376 377 // If buffer contains data, add newline before adding more 378 if notesBuffer.Len() > 0 { 379 notesBuffer.WriteString("\n") 380 } 381 notesBuffer.WriteString(v) 382 } 383 delete(files, k) 384 } 385 } 386 387 notes := notesBuffer.String() 388 389 // Sort hooks, manifests, and partials. Only hooks and manifests are returned, 390 // as partials are not used after renderer.Render. Empty manifests are also 391 // removed here. 392 hooks, manifests, err := sortManifests(files, vs, InstallOrder) 393 if err != nil { 394 // By catching parse errors here, we can prevent bogus releases from going 395 // to Kubernetes. 396 // 397 // We return the files as a big blob of data to help the user debug parser 398 // errors. 399 b := bytes.NewBuffer(nil) 400 for name, content := range files { 401 if len(strings.TrimSpace(content)) == 0 { 402 continue 403 } 404 b.WriteString("\n---\n# Source: " + name + "\n") 405 b.WriteString(content) 406 } 407 return nil, b, "", err 408 } 409 410 // Aggregate all valid manifests into one big doc. 411 b := bytes.NewBuffer(nil) 412 for _, m := range manifests { 413 b.WriteString("\n---\n# Source: " + m.Name + "\n") 414 b.WriteString(m.Content) 415 } 416 417 return hooks, b, notes, nil 418 } 419 420 // recordRelease with an update operation in case reuse has been set. 421 func (s *ReleaseServer) recordRelease(r *release.Release, reuse bool) { 422 if reuse { 423 if err := s.env.Releases.Update(r); err != nil { 424 s.Log("warning: Failed to update release %s: %s", r.Name, err) 425 } 426 } else if err := s.env.Releases.Create(r); err != nil { 427 s.Log("warning: Failed to record release %s: %s", r.Name, err) 428 } 429 } 430 431 func (s *ReleaseServer) execHook(hs []*release.Hook, name, namespace, hook string, timeout int64) error { 432 kubeCli := s.env.KubeClient 433 code, ok := events[hook] 434 if !ok { 435 return fmt.Errorf("unknown hook %s", hook) 436 } 437 438 s.Log("executing %d %s hooks for %s", len(hs), hook, name) 439 executingHooks := []*release.Hook{} 440 for _, h := range hs { 441 for _, e := range h.Events { 442 if e == code { 443 executingHooks = append(executingHooks, h) 444 } 445 } 446 } 447 448 executingHooks = sortByHookWeight(executingHooks) 449 450 for _, h := range executingHooks { 451 if err := s.deleteHookByPolicy(h, hooks.BeforeHookCreation, name, namespace, hook, kubeCli); err != nil { 452 return err 453 } 454 455 b := bytes.NewBufferString(h.Manifest) 456 if err := kubeCli.Create(namespace, b, timeout, false); err != nil { 457 s.Log("warning: Release %s %s %s failed: %s", name, hook, h.Path, err) 458 return err 459 } 460 // No way to rewind a bytes.Buffer()? 461 b.Reset() 462 b.WriteString(h.Manifest) 463 464 // We can't watch CRDs, but need to wait until they reach the established state before continuing 465 if hook != hooks.CRDInstall { 466 if err := kubeCli.WatchUntilReady(namespace, b, timeout, false); err != nil { 467 s.Log("warning: Release %s %s %s could not complete: %s", name, hook, h.Path, err) 468 // If a hook is failed, checkout the annotation of the hook to determine whether the hook should be deleted 469 // under failed condition. If so, then clear the corresponding resource object in the hook 470 if err := s.deleteHookByPolicy(h, hooks.HookFailed, name, namespace, hook, kubeCli); err != nil { 471 return err 472 } 473 return err 474 } 475 } else { 476 if err := kubeCli.WaitUntilCRDEstablished(b, time.Duration(timeout)*time.Second); err != nil { 477 s.Log("warning: Release %s %s %s could not complete: %s", name, hook, h.Path, err) 478 return err 479 } 480 } 481 } 482 483 s.Log("hooks complete for %s %s", hook, name) 484 // If all hooks are succeeded, checkout the annotation of each hook to determine whether the hook should be deleted 485 // under succeeded condition. If so, then clear the corresponding resource object in each hook 486 for _, h := range executingHooks { 487 if err := s.deleteHookByPolicy(h, hooks.HookSucceeded, name, namespace, hook, kubeCli); err != nil { 488 return err 489 } 490 h.LastRun = timeconv.Now() 491 } 492 493 return nil 494 } 495 496 func validateManifest(c environment.KubeClient, ns string, manifest []byte) error { 497 r := bytes.NewReader(manifest) 498 return c.Validate(ns, r) 499 } 500 501 func validateReleaseName(releaseName string) error { 502 if releaseName == "" { 503 return errMissingRelease 504 } 505 506 if !ValidName.MatchString(releaseName) || (len(releaseName) > releaseNameMaxLen) { 507 return errInvalidName 508 } 509 510 return nil 511 } 512 513 func (s *ReleaseServer) deleteHookByPolicy(h *release.Hook, policy string, name, namespace, hook string, kubeCli environment.KubeClient) error { 514 b := bytes.NewBufferString(h.Manifest) 515 if hookHasDeletePolicy(h, policy) { 516 s.Log("deleting %s hook %s for release %s due to %q policy", hook, h.Name, name, policy) 517 waitForDelete := h.DeleteTimeout > 0 518 if errHookDelete := kubeCli.DeleteWithTimeout(namespace, b, h.DeleteTimeout, waitForDelete); errHookDelete != nil { 519 s.Log("warning: Release %s %s %S could not be deleted: %s", name, hook, h.Path, errHookDelete) 520 return errHookDelete 521 } 522 } 523 return nil 524 } 525 526 // hookHasDeletePolicy determines whether the defined hook deletion policy matches the hook deletion polices 527 // supported by helm. If so, mark the hook as one should be deleted. 528 func hookHasDeletePolicy(h *release.Hook, policy string) bool { 529 if dp, ok := deletePolices[policy]; ok { 530 for _, v := range h.DeletePolicies { 531 if dp == v { 532 return true 533 } 534 } 535 } 536 return false 537 }