func (s *releaseServer) UninstallRelease(c ctx.Context, req *services.UninstallReleaseRequest) (*services.UninstallReleaseResponse, error) { if req.Name == "" { log.Printf("uninstall: Release not found: %s", req.Name) return nil, errMissingRelease } rel, err := s.env.Releases.Read(req.Name) if err != nil { log.Printf("uninstall: Release not loaded: %s", req.Name) return nil, err } log.Printf("uninstall: Deleting %s", req.Name) rel.Info.Status.Code = release.Status_DELETED rel.Info.Deleted = timeconv.Now() b := bytes.NewBuffer([]byte(rel.Manifest)) if err := s.env.KubeClient.Delete(s.env.Namespace, b); err != nil { log.Printf("uninstall: Failed deletion of %q: %s", req.Name, err) return nil, err } if err := s.env.Releases.Update(rel); err != nil { log.Printf("uninstall: Failed to store updated release: %s", err) } res := services.UninstallReleaseResponse{Release: rel} return &res, nil }
func TestUninstallRelease(t *testing.T) { c := context.Background() rs := rsFixture() rs.env.Releases.Create(&release.Release{ Name: "angry-panda", Info: &release.Info{ FirstDeployed: timeconv.Now(), Status: &release.Status{ Code: release.Status_DEPLOYED, }, }, }) req := &services.UninstallReleaseRequest{ Name: "angry-panda", } res, err := rs.UninstallRelease(c, req) if err != nil { t.Errorf("Failed uninstall: %s", err) } if res.Release.Name != "angry-panda" { t.Errorf("Expected angry-panda, got %q", res.Release.Name) } if res.Release.Info.Status.Code != release.Status_DELETED { t.Errorf("Expected status code to be DELETED, got %d", res.Release.Info.Status.Code) } if res.Release.Info.Deleted.Seconds <= 0 { t.Errorf("Expected valid UNIX date, got %d", res.Release.Info.Deleted.Seconds) } }
// prepareUpdate builds an updated release for an update operation. func (s *releaseServer) prepareUpdate(req *services.UpdateReleaseRequest) (*release.Release, *release.Release, error) { if req.Name == "" { return nil, nil, errMissingRelease } if req.Chart == nil { return nil, nil, errMissingChart } // finds the non-deleted release with the given name currentRelease, err := s.env.Releases.Deployed(req.Name) if err != nil { return nil, nil, err } // If new values were not supplied in the upgrade, re-use the existing values. s.reuseValues(req, currentRelease) ts := timeconv.Now() options := chartutil.ReleaseOptions{ Name: req.Name, Time: ts, Namespace: currentRelease.Namespace, } valuesToRender, err := chartutil.ToRenderValues(req.Chart, req.Values, options) if err != nil { return nil, nil, err } hooks, manifestDoc, notesTxt, err := s.renderResources(req.Chart, valuesToRender) if err != nil { return nil, nil, err } // Store an updated release. updatedRelease := &release.Release{ Name: req.Name, Namespace: currentRelease.Namespace, Chart: req.Chart, Config: req.Values, Info: &release.Info{ FirstDeployed: currentRelease.Info.FirstDeployed, LastDeployed: ts, Status: &release.Status{Code: release.Status_UNKNOWN}, }, Version: currentRelease.Version + 1, Manifest: manifestDoc.String(), Hooks: hooks, } if len(notesTxt) > 0 { updatedRelease.Info.Status.Notes = notesTxt } return currentRelease, updatedRelease, nil }
// prepareRollback finds the previous release and prepares a new release object with // the previous release's configuration func (s *releaseServer) prepareRollback(req *services.RollbackReleaseRequest) (*release.Release, *release.Release, error) { switch { case req.Name == "": return nil, nil, errMissingRelease case req.Version < 0: return nil, nil, errInvalidRevision } // finds the non-deleted release with the given name h, err := s.env.Releases.History(req.Name) if err != nil { return nil, nil, err } if len(h) <= 1 { return nil, nil, errors.New("no revision to rollback") } relutil.SortByRevision(h) crls := h[len(h)-1] rbv := req.Version if req.Version == 0 { rbv = crls.Version - 1 } log.Printf("rolling back %s (current: v%d, target: v%d)", req.Name, crls.Version, rbv) prls, err := s.env.Releases.Get(req.Name, rbv) if err != nil { return nil, nil, err } // Store a new release object with previous release's configuration // Store a new release object with previous release's configuration target := &release.Release{ Name: req.Name, Namespace: crls.Namespace, Chart: prls.Chart, Config: prls.Config, Info: &release.Info{ FirstDeployed: crls.Info.FirstDeployed, LastDeployed: timeconv.Now(), Status: &release.Status{ Code: release.Status_UNKNOWN, Notes: prls.Info.Status.Notes, }, }, Version: crls.Version + 1, Manifest: prls.Manifest, Hooks: prls.Hooks, } return crls, target, nil }
// prepareRollback finds the previous release and prepares a new release object with // the previous release's configuration func (s *ReleaseServer) prepareRollback(req *services.RollbackReleaseRequest) (*release.Release, *release.Release, error) { switch { case !ValidName.MatchString(req.Name): return nil, nil, errMissingRelease case req.Version < 0: return nil, nil, errInvalidRevision } crls, err := s.env.Releases.Last(req.Name) if err != nil { return nil, nil, err } rbv := req.Version if req.Version == 0 { rbv = crls.Version - 1 } log.Printf("rolling back %s (current: v%d, target: v%d)", req.Name, crls.Version, rbv) prls, err := s.env.Releases.Get(req.Name, rbv) if err != nil { return nil, nil, err } // Store a new release object with previous release's configuration target := &release.Release{ Name: req.Name, Namespace: crls.Namespace, Chart: prls.Chart, Config: prls.Config, Info: &release.Info{ FirstDeployed: crls.Info.FirstDeployed, LastDeployed: timeconv.Now(), Status: &release.Status{ Code: release.Status_UNKNOWN, Notes: prls.Info.Status.Notes, }, }, Version: crls.Version + 1, Manifest: prls.Manifest, Hooks: prls.Hooks, } return crls, target, nil }
// prepareRelease builds a release for an install operation. func (s *releaseServer) prepareRelease(req *services.InstallReleaseRequest) (*release.Release, error) { if req.Chart == nil { return nil, errMissingChart } name, err := s.uniqName(req.Name, req.ReuseName) if err != nil { return nil, err } ts := timeconv.Now() options := chartutil.ReleaseOptions{Name: name, Time: ts, Namespace: req.Namespace} valuesToRender, err := chartutil.ToRenderValues(req.Chart, req.Values, options) if err != nil { return nil, err } hooks, manifestDoc, notesTxt, err := s.renderResources(req.Chart, valuesToRender) if err != nil { return nil, err } // Store a release. rel := &release.Release{ Name: name, Namespace: req.Namespace, Chart: req.Chart, Config: req.Values, Info: &release.Info{ FirstDeployed: ts, LastDeployed: ts, Status: &release.Status{Code: release.Status_UNKNOWN}, }, Manifest: manifestDoc.String(), Hooks: hooks, Version: 1, } if len(notesTxt) > 0 { rel.Info.Status.Notes = notesTxt } return rel, nil }
func (s *releaseServer) execHook(hs []*release.Hook, name, namespace, hook string) error { kubeCli := s.env.KubeClient code, ok := events[hook] if !ok { return fmt.Errorf("unknown hook %q", hook) } log.Printf("Executing %s hooks for %s", hook, name) for _, h := range hs { found := false for _, e := range h.Events { if e == code { found = true } } // If this doesn't implement the hook, skip it. if !found { continue } b := bytes.NewBufferString(h.Manifest) if err := kubeCli.Create(namespace, b); err != nil { log.Printf("warning: Release %q pre-install %s failed: %s", name, h.Path, err) return err } // No way to rewind a bytes.Buffer()? b.Reset() b.WriteString(h.Manifest) if err := kubeCli.WatchUntilReady(namespace, b); err != nil { log.Printf("warning: Release %q pre-install %s could not complete: %s", name, h.Path, err) return err } h.LastRun = timeconv.Now() } log.Printf("Hooks complete for %s %s", hook, name) return nil }
func (s *releaseServer) UninstallRelease(c ctx.Context, req *services.UninstallReleaseRequest) (*services.UninstallReleaseResponse, error) { if !checkClientVersion(c) { return nil, errIncompatibleVersion } if req.Name == "" { log.Printf("uninstall: Release not found: %s", req.Name) return nil, errMissingRelease } rels, err := s.env.Releases.History(req.Name) if err != nil { log.Printf("uninstall: Release not loaded: %s", req.Name) return nil, err } if len(rels) < 1 { return nil, errMissingRelease } relutil.SortByRevision(rels) rel := rels[len(rels)-1] // TODO: Are there any cases where we want to force a delete even if it's // already marked deleted? if rel.Info.Status.Code == release.Status_DELETED { if req.Purge { if err := s.purgeReleases(rels...); err != nil { log.Printf("uninstall: Failed to purge the release: %s", err) return nil, err } return &services.UninstallReleaseResponse{Release: rel}, nil } return nil, fmt.Errorf("the release named %q is already deleted", req.Name) } log.Printf("uninstall: Deleting %s", req.Name) rel.Info.Status.Code = release.Status_DELETED rel.Info.Deleted = timeconv.Now() res := &services.UninstallReleaseResponse{Release: rel} if !req.DisableHooks { if err := s.execHook(rel.Hooks, rel.Name, rel.Namespace, preDelete); err != nil { return res, err } } vs, err := s.getVersionSet() if err != nil { return nil, fmt.Errorf("Could not get apiVersions from Kubernetes: %s", err) } manifests := splitManifests(rel.Manifest) _, files, err := sortManifests(manifests, vs, UninstallOrder) if err != nil { // We could instead just delete everything in no particular order. return nil, err } // Collect the errors, and return them later. es := []string{} for _, file := range files { b := bytes.NewBufferString(file.content) if err := s.env.KubeClient.Delete(rel.Namespace, b); err != nil { log.Printf("uninstall: Failed deletion of %q: %s", req.Name, err) es = append(es, err.Error()) } } if !req.DisableHooks { if err := s.execHook(rel.Hooks, rel.Name, rel.Namespace, postDelete); err != nil { es = append(es, err.Error()) } } if !req.Purge { if err := s.env.Releases.Update(rel); err != nil { log.Printf("uninstall: Failed to store updated release: %s", err) } } else { if err := s.purgeReleases(rels...); err != nil { log.Printf("uninstall: Failed to purge the release: %s", err) } } var errs error if len(es) > 0 { errs = fmt.Errorf("deletion error count %d: %s", len(es), strings.Join(es, "; ")) } return res, errs }
func TestToRenderValues(t *testing.T) { chartValues := ` name: al Rashid where: city: Basrah title: caliph ` overideValues := ` name: Haroun where: city: Baghdad date: 809 CE ` c := &chart.Chart{ Metadata: &chart.Metadata{Name: "test"}, Templates: []*chart.Template{}, Values: &chart.Config{Raw: chartValues}, Dependencies: []*chart.Chart{ { Metadata: &chart.Metadata{Name: "where"}, Values: &chart.Config{Raw: ""}, }, }, Files: []*any.Any{ {TypeUrl: "scheherazade/shahryar.txt", Value: []byte("1,001 Nights")}, }, } v := &chart.Config{Raw: overideValues} o := ReleaseOptions{ Name: "Seven Voyages", Time: timeconv.Now(), Namespace: "al Basrah", } res, err := ToRenderValues(c, v, o) if err != nil { t.Fatal(err) } // Ensure that the top-level values are all set. if name := res["Chart"].(*chart.Metadata).Name; name != "test" { t.Errorf("Expected chart name 'test', got %q", name) } if name := res["Release"].(map[string]interface{})["Name"]; fmt.Sprint(name) != "Seven Voyages" { t.Errorf("Expected release name 'Seven Voyages', got %q", name) } if data := res["Files"].(Files)["scheherazade/shahryar.txt"]; string(data) != "1,001 Nights" { t.Errorf("Expected file '1,001 Nights', got %q", string(data)) } var vals Values vals = res["Values"].(Values) if vals["name"] != "Haroun" { t.Errorf("Expected 'Haroun', got %q (%v)", vals["name"], vals) } where := vals["where"].(map[string]interface{}) expects := map[string]string{ "city": "Baghdad", "date": "809 CE", "title": "caliph", } for field, expect := range expects { if got := where[field]; got != expect { t.Errorf("Expected %q, got %q (%v)", expect, got, where) } } }
// Templates lints the templates in the Linter. func Templates(linter *support.Linter) { templatesPath := filepath.Join(linter.ChartDir, "templates") templatesDirExist := linter.RunLinterRule(support.WarningSev, validateTemplatesDir(templatesPath)) // Templates directory is optional for now if !templatesDirExist { return } // Load chart and parse templates, based on tiller/release_server chart, err := chartutil.Load(linter.ChartDir) chartLoaded := linter.RunLinterRule(support.ErrorSev, validateNoError(err)) if !chartLoaded { return } options := chartutil.ReleaseOptions{Name: "testRelease", Time: timeconv.Now(), Namespace: "testNamespace"} valuesToRender, err := chartutil.ToRenderValues(chart, chart.Values, options) if err != nil { // FIXME: This seems to generate a duplicate, but I can't find where the first // error is coming from. //linter.RunLinterRule(support.ErrorSev, err) return } renderedContentMap, err := engine.New().Render(chart, valuesToRender) renderOk := linter.RunLinterRule(support.ErrorSev, validateNoError(err)) if !renderOk { return } /* Iterate over all the templates to check: - It is a .yaml file - All the values in the template file is defined - {{}} include | quote - Generated content is a valid Yaml file - Metadata.Namespace is not set */ for _, template := range chart.Templates { fileName, preExecutedTemplate := template.Name, template.Data linter.RunLinterRule(support.ErrorSev, validateAllowedExtension(fileName)) // We only apply the following lint rules to yaml files if filepath.Ext(fileName) != ".yaml" { continue } // Check that all the templates have a matching value linter.RunLinterRule(support.WarningSev, validateNonMissingValues(fileName, templatesPath, valuesToRender, preExecutedTemplate)) linter.RunLinterRule(support.WarningSev, validateQuotes(fileName, string(preExecutedTemplate))) renderedContent := renderedContentMap[fileName] var yamlStruct K8sYamlStruct // Even though K8sYamlStruct only defines Metadata namespace, an error in any other // key will be raised as well err := yaml.Unmarshal([]byte(renderedContent), &yamlStruct) validYaml := linter.RunLinterRule(support.ErrorSev, validateYamlContent(fileName, err)) if !validYaml { continue } linter.RunLinterRule(support.ErrorSev, validateNoNamespace(fileName, yamlStruct)) } }
func (s *releaseServer) UninstallRelease(c ctx.Context, req *services.UninstallReleaseRequest) (*services.UninstallReleaseResponse, error) { if !checkClientVersion(c) { return nil, errIncompatibleVersion } if req.Name == "" { log.Printf("uninstall: Release not found: %s", req.Name) return nil, errMissingRelease } rel, err := s.env.Releases.Deployed(req.Name) if err != nil { log.Printf("uninstall: Release not loaded: %s", req.Name) return nil, err } // TODO: Are there any cases where we want to force a delete even if it's // already marked deleted? if rel.Info.Status.Code == release.Status_DELETED { if req.Purge { if _, err := s.env.Releases.Delete(rel.Name, rel.Version); err != nil { log.Printf("uninstall: Failed to purge the release: %s", err) return nil, err } return &services.UninstallReleaseResponse{Release: rel}, nil } return nil, fmt.Errorf("the release named %q is already deleted", req.Name) } log.Printf("uninstall: Deleting %s", req.Name) rel.Info.Status.Code = release.Status_DELETED rel.Info.Deleted = timeconv.Now() res := &services.UninstallReleaseResponse{Release: rel} if !req.DisableHooks { if err := s.execHook(rel.Hooks, rel.Name, rel.Namespace, preDelete); err != nil { return res, err } } vs, err := s.getVersionSet() if err != nil { return nil, fmt.Errorf("Could not get apiVersions from Kubernetes: %s", err) } manifests := splitManifests(rel.Manifest) _, files, err := sortManifests(manifests, vs, UninstallOrder) if err != nil { // We could instead just delete everything in no particular order. return nil, err } // Note: We could re-join these into one file and delete just that one. Or // we could collect errors (instead of bailing on the first error) and try // to delete as much as possible instead of failing at the first error. for _, file := range files { b := bytes.NewBufferString(file.content) if err := s.env.KubeClient.Delete(rel.Namespace, b); err != nil { log.Printf("uninstall: Failed deletion of %q: %s", req.Name, err) return nil, err } } if !req.DisableHooks { if err := s.execHook(rel.Hooks, rel.Name, rel.Namespace, postDelete); err != nil { return res, err } } if !req.Purge { if err := s.env.Releases.Update(rel); err != nil { log.Printf("uninstall: Failed to store updated release: %s", err) } } else { if _, err := s.env.Releases.Delete(rel.Name, rel.Version); err != nil { log.Printf("uninstall: Failed to purge the release: %s", err) } } return res, nil }
// UninstallRelease deletes all of the resources associated with this release, and marks the release DELETED. func (s *ReleaseServer) UninstallRelease(c ctx.Context, req *services.UninstallReleaseRequest) (*services.UninstallReleaseResponse, error) { if !checkClientVersion(c) { return nil, errIncompatibleVersion } if !ValidName.MatchString(req.Name) { log.Printf("uninstall: Release not found: %s", req.Name) return nil, errMissingRelease } rels, err := s.env.Releases.History(req.Name) if err != nil { log.Printf("uninstall: Release not loaded: %s", req.Name) return nil, err } if len(rels) < 1 { return nil, errMissingRelease } relutil.SortByRevision(rels) rel := rels[len(rels)-1] // TODO: Are there any cases where we want to force a delete even if it's // already marked deleted? if rel.Info.Status.Code == release.Status_DELETED { if req.Purge { if err := s.purgeReleases(rels...); err != nil { log.Printf("uninstall: Failed to purge the release: %s", err) return nil, err } return &services.UninstallReleaseResponse{Release: rel}, nil } return nil, fmt.Errorf("the release named %q is already deleted", req.Name) } log.Printf("uninstall: Deleting %s", req.Name) rel.Info.Status.Code = release.Status_DELETING rel.Info.Deleted = timeconv.Now() res := &services.UninstallReleaseResponse{Release: rel} if !req.DisableHooks { if err := s.execHook(rel.Hooks, rel.Name, rel.Namespace, preDelete); err != nil { return res, err } } vs, err := getVersionSet(s.clientset.Discovery()) if err != nil { return nil, fmt.Errorf("Could not get apiVersions from Kubernetes: %s", err) } // From here on out, the release is currently considered to be in Status_DELETING // state. if err := s.env.Releases.Update(rel); err != nil { log.Printf("uninstall: Failed to store updated release: %s", err) } manifests := splitManifests(rel.Manifest) _, files, err := sortManifests(manifests, vs, UninstallOrder) if err != nil { // We could instead just delete everything in no particular order. // FIXME: One way to delete at this point would be to try a label-based // deletion. The problem with this is that we could get a false positive // and delete something that was not legitimately part of this release. return nil, fmt.Errorf("corrupted release record. You must manually delete the resources: %s", err) } filesToKeep, filesToDelete := filterManifestsToKeep(files) if len(filesToKeep) > 0 { res.Info = summarizeKeptManifests(filesToKeep) } // Collect the errors, and return them later. es := []string{} for _, file := range filesToDelete { b := bytes.NewBufferString(file.content) if err := s.env.KubeClient.Delete(rel.Namespace, b); err != nil { log.Printf("uninstall: Failed deletion of %q: %s", req.Name, err) if err == kube.ErrNoObjectsVisited { // Rewrite the message from "no objects visited" err = errors.New("object not found, skipping delete") } es = append(es, err.Error()) } } if !req.DisableHooks { if err := s.execHook(rel.Hooks, rel.Name, rel.Namespace, postDelete); err != nil { es = append(es, err.Error()) } } if req.Purge { if err := s.purgeReleases(rels...); err != nil { log.Printf("uninstall: Failed to purge the release: %s", err) } } rel.Info.Status.Code = release.Status_DELETED if err := s.env.Releases.Update(rel); err != nil { log.Printf("uninstall: Failed to store updated release: %s", err) } var errs error if len(es) > 0 { errs = fmt.Errorf("deletion completed with %d error(s): %s", len(es), strings.Join(es, "; ")) } return res, errs }
// prepareRelease builds a release for an install operation. func (s *ReleaseServer) prepareRelease(req *services.InstallReleaseRequest) (*release.Release, error) { if req.Chart == nil { return nil, errMissingChart } name, err := s.uniqName(req.Name, req.ReuseName) if err != nil { return nil, err } revision := 1 ts := timeconv.Now() options := chartutil.ReleaseOptions{ Name: name, Time: ts, Namespace: req.Namespace, Revision: revision, IsInstall: true, } valuesToRender, err := chartutil.ToRenderValues(req.Chart, req.Values, options) if err != nil { return nil, err } hooks, manifestDoc, notesTxt, err := s.renderResources(req.Chart, valuesToRender) if err != nil { // Return a release with partial data so that client can show debugging // information. rel := &release.Release{ Name: name, Namespace: req.Namespace, Chart: req.Chart, Config: req.Values, Info: &release.Info{ FirstDeployed: ts, LastDeployed: ts, Status: &release.Status{Code: release.Status_UNKNOWN}, }, Version: 0, } if manifestDoc != nil { rel.Manifest = manifestDoc.String() } return rel, err } // Store a release. rel := &release.Release{ Name: name, Namespace: req.Namespace, Chart: req.Chart, Config: req.Values, Info: &release.Info{ FirstDeployed: ts, LastDeployed: ts, Status: &release.Status{Code: release.Status_UNKNOWN}, }, Manifest: manifestDoc.String(), Hooks: hooks, Version: int32(revision), } if len(notesTxt) > 0 { rel.Info.Status.Notes = notesTxt } return rel, nil }
func (s *releaseServer) InstallRelease(c ctx.Context, req *services.InstallReleaseRequest) (*services.InstallReleaseResponse, error) { if req.Chart == nil { return nil, errMissingChart } name, err := s.uniqName(req.Name) if err != nil { return nil, err } ts := timeconv.Now() options := chartutil.ReleaseOptions{Name: name, Time: ts, Namespace: s.env.Namespace} valuesToRender, err := chartutil.ToRenderValues(req.Chart, req.Values, options) if err != nil { return nil, err } renderer := s.engine(req.Chart) files, err := renderer.Render(req.Chart, valuesToRender) if err != nil { return nil, err } b := bytes.NewBuffer(nil) for name, file := range files { // Ignore templates that starts with underscore to handle them as partials if strings.HasPrefix(path.Base(name), "_") { continue } // Ignore empty documents because the Kubernetes library can't handle // them. if len(file) > 0 { b.WriteString("\n---\n# Source: " + name + "\n") b.WriteString(file) } } // Store a release. r := &release.Release{ Name: name, Chart: req.Chart, Config: req.Values, Info: &release.Info{ FirstDeployed: ts, LastDeployed: ts, Status: &release.Status{Code: release.Status_UNKNOWN}, }, Manifest: b.String(), } res := &services.InstallReleaseResponse{Release: r} if req.DryRun { log.Printf("Dry run for %s", name) return res, nil } if err := s.env.KubeClient.Create(s.env.Namespace, b); err != nil { r.Info.Status.Code = release.Status_FAILED log.Printf("warning: Release %q failed: %s", name, err) if err := s.env.Releases.Create(r); err != nil { log.Printf("warning: Failed to record release %q: %s", name, err) } return res, fmt.Errorf("release %s failed: %s", name, err) } // This is a tricky case. The release has been created, but the result // cannot be recorded. The truest thing to tell the user is that the // release was created. However, the user will not be able to do anything // further with this release. // // One possible strategy would be to do a timed retry to see if we can get // this stored in the future. if err := s.env.Releases.Create(r); err != nil { log.Printf("warning: Failed to record release %q: %s", name, err) return res, nil } r.Info.Status.Code = release.Status_DEPLOYED return res, nil }