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