github.com/latiif/helm@v2.15.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 ) 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 if err != nil { 274 return chartutil.DefaultVersionSet, err 275 } 276 277 // FIXME: The Kubernetes test fixture for cli appears to always return nil 278 // for calls to Discovery().ServerGroupsAndResources(). So in this case, we 279 // return the default API list. This is also a safe value to return in any 280 // other odd-ball case. 281 if len(groups) == 0 && len(resources) == 0 { 282 return chartutil.DefaultVersionSet, nil 283 } 284 285 versionMap := make(map[string]interface{}) 286 versions := []string{} 287 288 // Extract the groups 289 for _, g := range groups { 290 for _, gv := range g.Versions { 291 versionMap[gv.GroupVersion] = struct{}{} 292 } 293 } 294 295 // Extract the resources 296 var id string 297 var ok bool 298 for _, r := range resources { 299 for _, rl := range r.APIResources { 300 301 // A Kind at a GroupVersion can show up more than once. We only want 302 // it displayed once in the final output. 303 id = path.Join(r.GroupVersion, rl.Kind) 304 if _, ok = versionMap[id]; !ok { 305 versionMap[id] = struct{}{} 306 } 307 } 308 } 309 310 // Convert to a form that NewVersionSet can use 311 for k := range versionMap { 312 versions = append(versions, k) 313 } 314 315 return chartutil.NewVersionSet(versions...), nil 316 } 317 318 // GetVersionSet retrieves a set of available k8s API versions 319 func GetVersionSet(client discovery.ServerGroupsInterface) (chartutil.VersionSet, error) { 320 groups, err := client.ServerGroups() 321 if err != nil { 322 return chartutil.DefaultVersionSet, err 323 } 324 325 // FIXME: The Kubernetes test fixture for cli appears to always return nil 326 // for calls to Discovery().ServerGroups(). So in this case, we return 327 // the default API list. This is also a safe value to return in any other 328 // odd-ball case. 329 if groups.Size() == 0 { 330 return chartutil.DefaultVersionSet, nil 331 } 332 333 versions := metav1.ExtractGroupVersions(groups) 334 return chartutil.NewVersionSet(versions...), nil 335 } 336 337 func (s *ReleaseServer) renderResources(ch *chart.Chart, values chartutil.Values, subNotes bool, vs chartutil.VersionSet) ([]*release.Hook, *bytes.Buffer, string, error) { 338 // Guard to make sure Tiller is at the right version to handle this chart. 339 sver := version.GetVersion() 340 if ch.Metadata.TillerVersion != "" && 341 !version.IsCompatibleRange(ch.Metadata.TillerVersion, sver) { 342 return nil, nil, "", fmt.Errorf("Chart incompatible with Tiller %s", sver) 343 } 344 345 if ch.Metadata.KubeVersion != "" { 346 cap, _ := values["Capabilities"].(*chartutil.Capabilities) 347 gitVersion := cap.KubeVersion.String() 348 k8sVersion := strings.Split(gitVersion, "+")[0] 349 if !version.IsCompatibleRange(ch.Metadata.KubeVersion, k8sVersion) { 350 return nil, nil, "", fmt.Errorf("Chart requires kubernetesVersion: %s which is incompatible with Kubernetes %s", ch.Metadata.KubeVersion, k8sVersion) 351 } 352 } 353 354 s.Log("rendering %s chart using values", ch.GetMetadata().Name) 355 renderer := s.engine(ch) 356 files, err := renderer.Render(ch, values) 357 if err != nil { 358 return nil, nil, "", err 359 } 360 361 // NOTES.txt gets rendered like all the other files, but because it's not a hook nor a resource, 362 // pull it out of here into a separate file so that we can actually use the output of the rendered 363 // text file. We have to spin through this map because the file contains path information, so we 364 // look for terminating NOTES.txt. We also remove it from the files so that we don't have to skip 365 // it in the sortHooks. 366 var notesBuffer bytes.Buffer 367 for k, v := range files { 368 if strings.HasSuffix(k, notesFileSuffix) { 369 if subNotes || (k == path.Join(ch.Metadata.Name, "templates", notesFileSuffix)) { 370 371 // If buffer contains data, add newline before adding more 372 if notesBuffer.Len() > 0 { 373 notesBuffer.WriteString("\n") 374 } 375 notesBuffer.WriteString(v) 376 } 377 delete(files, k) 378 } 379 } 380 381 notes := notesBuffer.String() 382 383 // Sort hooks, manifests, and partials. Only hooks and manifests are returned, 384 // as partials are not used after renderer.Render. Empty manifests are also 385 // removed here. 386 hooks, manifests, err := sortManifests(files, vs, InstallOrder) 387 if err != nil { 388 // By catching parse errors here, we can prevent bogus releases from going 389 // to Kubernetes. 390 // 391 // We return the files as a big blob of data to help the user debug parser 392 // errors. 393 b := bytes.NewBuffer(nil) 394 for name, content := range files { 395 if len(strings.TrimSpace(content)) == 0 { 396 continue 397 } 398 b.WriteString("\n---\n# Source: " + name + "\n") 399 b.WriteString(content) 400 } 401 return nil, b, "", err 402 } 403 404 // Aggregate all valid manifests into one big doc. 405 b := bytes.NewBuffer(nil) 406 for _, m := range manifests { 407 b.WriteString("\n---\n# Source: " + m.Name + "\n") 408 b.WriteString(m.Content) 409 } 410 411 return hooks, b, notes, nil 412 } 413 414 // recordRelease with an update operation in case reuse has been set. 415 func (s *ReleaseServer) recordRelease(r *release.Release, reuse bool) { 416 if reuse { 417 if err := s.env.Releases.Update(r); err != nil { 418 s.Log("warning: Failed to update release %s: %s", r.Name, err) 419 } 420 } else if err := s.env.Releases.Create(r); err != nil { 421 s.Log("warning: Failed to record release %s: %s", r.Name, err) 422 } 423 } 424 425 func (s *ReleaseServer) execHook(hs []*release.Hook, name, namespace, hook string, timeout int64) error { 426 kubeCli := s.env.KubeClient 427 code, ok := events[hook] 428 if !ok { 429 return fmt.Errorf("unknown hook %s", hook) 430 } 431 432 s.Log("executing %d %s hooks for %s", len(hs), hook, name) 433 executingHooks := []*release.Hook{} 434 for _, h := range hs { 435 for _, e := range h.Events { 436 if e == code { 437 executingHooks = append(executingHooks, h) 438 } 439 } 440 } 441 442 executingHooks = sortByHookWeight(executingHooks) 443 444 for _, h := range executingHooks { 445 if err := s.deleteHookByPolicy(h, hooks.BeforeHookCreation, name, namespace, hook, kubeCli); err != nil { 446 return err 447 } 448 449 b := bytes.NewBufferString(h.Manifest) 450 if err := kubeCli.Create(namespace, b, timeout, false); err != nil { 451 s.Log("warning: Release %s %s %s failed: %s", name, hook, h.Path, err) 452 return err 453 } 454 // No way to rewind a bytes.Buffer()? 455 b.Reset() 456 b.WriteString(h.Manifest) 457 458 // We can't watch CRDs, but need to wait until they reach the established state before continuing 459 if hook != hooks.CRDInstall { 460 if err := kubeCli.WatchUntilReady(namespace, b, timeout, false); err != nil { 461 s.Log("warning: Release %s %s %s could not complete: %s", name, hook, h.Path, err) 462 // If a hook is failed, checkout the annotation of the hook to determine whether the hook should be deleted 463 // under failed condition. If so, then clear the corresponding resource object in the hook 464 if err := s.deleteHookByPolicy(h, hooks.HookFailed, name, namespace, hook, kubeCli); err != nil { 465 return err 466 } 467 return err 468 } 469 } else { 470 if err := kubeCli.WaitUntilCRDEstablished(b, time.Duration(timeout)*time.Second); err != nil { 471 s.Log("warning: Release %s %s %s could not complete: %s", name, hook, h.Path, err) 472 return err 473 } 474 } 475 } 476 477 s.Log("hooks complete for %s %s", hook, name) 478 // If all hooks are succeeded, checkout the annotation of each hook to determine whether the hook should be deleted 479 // under succeeded condition. If so, then clear the corresponding resource object in each hook 480 for _, h := range executingHooks { 481 if err := s.deleteHookByPolicy(h, hooks.HookSucceeded, name, namespace, hook, kubeCli); err != nil { 482 return err 483 } 484 h.LastRun = timeconv.Now() 485 } 486 487 return nil 488 } 489 490 func validateManifest(c environment.KubeClient, ns string, manifest []byte) error { 491 r := bytes.NewReader(manifest) 492 return c.Validate(ns, r) 493 } 494 495 func validateReleaseName(releaseName string) error { 496 if releaseName == "" { 497 return errMissingRelease 498 } 499 500 if !ValidName.MatchString(releaseName) || (len(releaseName) > releaseNameMaxLen) { 501 return errInvalidName 502 } 503 504 return nil 505 } 506 507 func (s *ReleaseServer) deleteHookByPolicy(h *release.Hook, policy string, name, namespace, hook string, kubeCli environment.KubeClient) error { 508 b := bytes.NewBufferString(h.Manifest) 509 if hookHasDeletePolicy(h, policy) { 510 s.Log("deleting %s hook %s for release %s due to %q policy", hook, h.Name, name, policy) 511 waitForDelete := h.DeleteTimeout > 0 512 if errHookDelete := kubeCli.DeleteWithTimeout(namespace, b, h.DeleteTimeout, waitForDelete); errHookDelete != nil { 513 s.Log("warning: Release %s %s %S could not be deleted: %s", name, hook, h.Path, errHookDelete) 514 return errHookDelete 515 } 516 } 517 return nil 518 } 519 520 // hookHasDeletePolicy determines whether the defined hook deletion policy matches the hook deletion polices 521 // supported by helm. If so, mark the hook as one should be deleted. 522 func hookHasDeletePolicy(h *release.Hook, policy string) bool { 523 if dp, ok := deletePolices[policy]; ok { 524 for _, v := range h.DeletePolicies { 525 if dp == v { 526 return true 527 } 528 } 529 } 530 return false 531 }