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