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