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