github.com/rakanixu/helm@v2.8.2+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 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 "k8s.io/client-go/discovery" 30 "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset" 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 internalclientset.Interface 86 Log func(string, ...interface{}) 87 } 88 89 // NewReleaseServer creates a new release server. 90 func NewReleaseServer(env *environment.Environment, clientset internalclientset.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 return nil 140 } 141 142 // If req.Values is empty, but current.Config is not, copy current into the 143 // request. 144 if (req.Values == nil || req.Values.Raw == "" || req.Values.Raw == "{}\n") && 145 current.Config != nil && 146 current.Config.Raw != "" && 147 current.Config.Raw != "{}\n" { 148 s.Log("copying values from %s (v%d) to new release.", current.Name, current.Version) 149 req.Values = current.Config 150 } 151 return nil 152 } 153 154 func (s *ReleaseServer) uniqName(start string, reuse bool) (string, error) { 155 156 // If a name is supplied, we check to see if that name is taken. If not, it 157 // is granted. If reuse is true and a deleted release with that name exists, 158 // we re-grant it. Otherwise, an error is returned. 159 if start != "" { 160 161 if len(start) > releaseNameMaxLen { 162 return "", fmt.Errorf("release name %q exceeds max length of %d", start, releaseNameMaxLen) 163 } 164 165 h, err := s.env.Releases.History(start) 166 if err != nil || len(h) < 1 { 167 return start, nil 168 } 169 relutil.Reverse(h, relutil.SortByRevision) 170 rel := h[0] 171 172 if st := rel.Info.Status.Code; reuse && (st == release.Status_DELETED || st == release.Status_FAILED) { 173 // Allowe re-use of names if the previous release is marked deleted. 174 s.Log("name %s exists but is not in use, reusing name", start) 175 return start, nil 176 } else if reuse { 177 return "", errors.New("cannot re-use a name that is still in use") 178 } 179 180 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) 181 } 182 183 maxTries := 5 184 for i := 0; i < maxTries; i++ { 185 namer := moniker.New() 186 name := namer.NameSep("-") 187 if len(name) > releaseNameMaxLen { 188 name = name[:releaseNameMaxLen] 189 } 190 if _, err := s.env.Releases.Get(name, 1); strings.Contains(err.Error(), "not found") { 191 return name, nil 192 } 193 s.Log("info: generated name %s is taken. Searching again.", name) 194 } 195 s.Log("warning: No available release names found after %d tries", maxTries) 196 return "ERROR", errors.New("no available release name found") 197 } 198 199 func (s *ReleaseServer) engine(ch *chart.Chart) environment.Engine { 200 renderer := s.env.EngineYard.Default() 201 if ch.Metadata.Engine != "" { 202 if r, ok := s.env.EngineYard.Get(ch.Metadata.Engine); ok { 203 renderer = r 204 } else { 205 s.Log("warning: %s requested non-existent template engine %s", ch.Metadata.Name, ch.Metadata.Engine) 206 } 207 } 208 return renderer 209 } 210 211 // capabilities builds a Capabilities from discovery information. 212 func capabilities(disc discovery.DiscoveryInterface) (*chartutil.Capabilities, error) { 213 sv, err := disc.ServerVersion() 214 if err != nil { 215 return nil, err 216 } 217 vs, err := GetVersionSet(disc) 218 if err != nil { 219 return nil, fmt.Errorf("Could not get apiVersions from Kubernetes: %s", err) 220 } 221 return &chartutil.Capabilities{ 222 APIVersions: vs, 223 KubeVersion: sv, 224 TillerVersion: version.GetVersionProto(), 225 }, nil 226 } 227 228 // GetVersionSet retrieves a set of available k8s API versions 229 func GetVersionSet(client discovery.ServerGroupsInterface) (chartutil.VersionSet, error) { 230 groups, err := client.ServerGroups() 231 if err != nil { 232 return chartutil.DefaultVersionSet, err 233 } 234 235 // FIXME: The Kubernetes test fixture for cli appears to always return nil 236 // for calls to Discovery().ServerGroups(). So in this case, we return 237 // the default API list. This is also a safe value to return in any other 238 // odd-ball case. 239 if groups.Size() == 0 { 240 return chartutil.DefaultVersionSet, nil 241 } 242 243 versions := metav1.ExtractGroupVersions(groups) 244 return chartutil.NewVersionSet(versions...), nil 245 } 246 247 func (s *ReleaseServer) renderResources(ch *chart.Chart, values chartutil.Values, vs chartutil.VersionSet) ([]*release.Hook, *bytes.Buffer, string, error) { 248 // Guard to make sure Tiller is at the right version to handle this chart. 249 sver := version.GetVersion() 250 if ch.Metadata.TillerVersion != "" && 251 !version.IsCompatibleRange(ch.Metadata.TillerVersion, sver) { 252 return nil, nil, "", fmt.Errorf("Chart incompatible with Tiller %s", sver) 253 } 254 255 if ch.Metadata.KubeVersion != "" { 256 cap, _ := values["Capabilities"].(*chartutil.Capabilities) 257 gitVersion := cap.KubeVersion.String() 258 k8sVersion := strings.Split(gitVersion, "+")[0] 259 if !version.IsCompatibleRange(ch.Metadata.KubeVersion, k8sVersion) { 260 return nil, nil, "", fmt.Errorf("Chart requires kubernetesVersion: %s which is incompatible with Kubernetes %s", ch.Metadata.KubeVersion, k8sVersion) 261 } 262 } 263 264 s.Log("rendering %s chart using values", ch.GetMetadata().Name) 265 renderer := s.engine(ch) 266 files, err := renderer.Render(ch, values) 267 if err != nil { 268 return nil, nil, "", err 269 } 270 271 // NOTES.txt gets rendered like all the other files, but because it's not a hook nor a resource, 272 // pull it out of here into a separate file so that we can actually use the output of the rendered 273 // text file. We have to spin through this map because the file contains path information, so we 274 // look for terminating NOTES.txt. We also remove it from the files so that we don't have to skip 275 // it in the sortHooks. 276 notes := "" 277 for k, v := range files { 278 if strings.HasSuffix(k, notesFileSuffix) { 279 // Only apply the notes if it belongs to the parent chart 280 // Note: Do not use filePath.Join since it creates a path with \ which is not expected 281 if k == path.Join(ch.Metadata.Name, "templates", notesFileSuffix) { 282 notes = v 283 } 284 delete(files, k) 285 } 286 } 287 288 // Sort hooks, manifests, and partials. Only hooks and manifests are returned, 289 // as partials are not used after renderer.Render. Empty manifests are also 290 // removed here. 291 hooks, manifests, err := sortManifests(files, vs, InstallOrder) 292 if err != nil { 293 // By catching parse errors here, we can prevent bogus releases from going 294 // to Kubernetes. 295 // 296 // We return the files as a big blob of data to help the user debug parser 297 // errors. 298 b := bytes.NewBuffer(nil) 299 for name, content := range files { 300 if len(strings.TrimSpace(content)) == 0 { 301 continue 302 } 303 b.WriteString("\n---\n# Source: " + name + "\n") 304 b.WriteString(content) 305 } 306 return nil, b, "", err 307 } 308 309 // Aggregate all valid manifests into one big doc. 310 b := bytes.NewBuffer(nil) 311 for _, m := range manifests { 312 b.WriteString("\n---\n# Source: " + m.Name + "\n") 313 b.WriteString(m.Content) 314 } 315 316 return hooks, b, notes, nil 317 } 318 319 // recordRelease with an update operation in case reuse has been set. 320 func (s *ReleaseServer) recordRelease(r *release.Release, reuse bool) { 321 if reuse { 322 if err := s.env.Releases.Update(r); err != nil { 323 s.Log("warning: Failed to update release %s: %s", r.Name, err) 324 } 325 } else if err := s.env.Releases.Create(r); err != nil { 326 s.Log("warning: Failed to record release %s: %s", r.Name, err) 327 } 328 } 329 330 func (s *ReleaseServer) execHook(hs []*release.Hook, name, namespace, hook string, timeout int64) error { 331 kubeCli := s.env.KubeClient 332 code, ok := events[hook] 333 if !ok { 334 return fmt.Errorf("unknown hook %s", hook) 335 } 336 337 s.Log("executing %d %s hooks for %s", len(hs), hook, name) 338 executingHooks := []*release.Hook{} 339 for _, h := range hs { 340 for _, e := range h.Events { 341 if e == code { 342 executingHooks = append(executingHooks, h) 343 } 344 } 345 } 346 347 executingHooks = sortByHookWeight(executingHooks) 348 349 for _, h := range executingHooks { 350 351 b := bytes.NewBufferString(h.Manifest) 352 if err := kubeCli.Create(namespace, b, timeout, false); err != nil { 353 s.Log("warning: Release %s %s %s failed: %s", name, hook, h.Path, err) 354 return err 355 } 356 // No way to rewind a bytes.Buffer()? 357 b.Reset() 358 b.WriteString(h.Manifest) 359 if err := kubeCli.WatchUntilReady(namespace, b, timeout, false); err != nil { 360 s.Log("warning: Release %s %s %s could not complete: %s", name, hook, h.Path, err) 361 // If a hook is failed, checkout the annotation of the hook to determine whether the hook should be deleted 362 // under failed condition. If so, then clear the corresponding resource object in the hook 363 if hookShouldBeDeleted(h, hooks.HookFailed) { 364 b.Reset() 365 b.WriteString(h.Manifest) 366 s.Log("deleting %s hook %s for release %s due to %q policy", hook, h.Name, name, hooks.HookFailed) 367 if errHookDelete := kubeCli.Delete(namespace, b); errHookDelete != nil { 368 s.Log("warning: Release %s %s %S could not be deleted: %s", name, hook, h.Path, errHookDelete) 369 return errHookDelete 370 } 371 } 372 return err 373 } 374 } 375 376 s.Log("hooks complete for %s %s", hook, name) 377 // If all hooks are succeeded, checkout the annotation of each hook to determine whether the hook should be deleted 378 // under succeeded condition. If so, then clear the corresponding resource object in each hook 379 for _, h := range executingHooks { 380 b := bytes.NewBufferString(h.Manifest) 381 if hookShouldBeDeleted(h, hooks.HookSucceeded) { 382 s.Log("deleting %s hook %s for release %s due to %q policy", hook, h.Name, name, hooks.HookSucceeded) 383 if errHookDelete := kubeCli.Delete(namespace, b); errHookDelete != nil { 384 s.Log("warning: Release %s %s %S could not be deleted: %s", name, hook, h.Path, errHookDelete) 385 return errHookDelete 386 } 387 } 388 h.LastRun = timeconv.Now() 389 } 390 391 return nil 392 } 393 394 func validateManifest(c environment.KubeClient, ns string, manifest []byte) error { 395 r := bytes.NewReader(manifest) 396 _, err := c.BuildUnstructured(ns, r) 397 return err 398 } 399 400 func validateReleaseName(releaseName string) error { 401 if releaseName == "" { 402 return errMissingRelease 403 } 404 405 if !ValidName.MatchString(releaseName) || (len(releaseName) > releaseNameMaxLen) { 406 return errInvalidName 407 } 408 409 return nil 410 } 411 412 // hookShouldBeDeleted determines whether the defined hook deletion policy matches the hook deletion polices 413 // supported by helm. If so, mark the hook as one should be deleted. 414 func hookShouldBeDeleted(hook *release.Hook, policy string) bool { 415 if dp, ok := deletePolices[policy]; ok { 416 for _, v := range hook.DeletePolicies { 417 if dp == v { 418 return true 419 } 420 } 421 } 422 return false 423 }