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