mirror of https://github.com/helm/helm
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
757 lines
21 KiB
757 lines
21 KiB
/*
|
|
Copyright 2016 The Kubernetes Authors All rights reserved.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
|
|
"google.golang.org/grpc/metadata"
|
|
|
|
"github.com/ghodss/yaml"
|
|
"github.com/technosophos/moniker"
|
|
ctx "golang.org/x/net/context"
|
|
|
|
"k8s.io/helm/cmd/tiller/environment"
|
|
"k8s.io/helm/pkg/chartutil"
|
|
"k8s.io/helm/pkg/proto/hapi/chart"
|
|
"k8s.io/helm/pkg/proto/hapi/release"
|
|
"k8s.io/helm/pkg/proto/hapi/services"
|
|
"k8s.io/helm/pkg/storage/driver"
|
|
"k8s.io/helm/pkg/timeconv"
|
|
"k8s.io/helm/pkg/version"
|
|
"k8s.io/kubernetes/pkg/api/unversioned"
|
|
)
|
|
|
|
var srv *releaseServer
|
|
|
|
// releaseNameMaxLen is the maximum length of a release name.
|
|
//
|
|
// This is designed to accommodate the usage of release name in the 'name:'
|
|
// field of Kubernetes resources. Many of those fields are limited to 24
|
|
// characters in length. See https://github.com/kubernetes/helm/issues/1071
|
|
const releaseNameMaxLen = 14
|
|
|
|
// NOTESFILE_SUFFIX that we want to treat special. It goes through the templating engine
|
|
// but it's not a yaml file (resource) hence can't have hooks, etc. And the user actually
|
|
// wants to see this file after rendering in the status command. However, it must be a suffix
|
|
// since there can be filepath in front of it.
|
|
const notesFileSuffix = "NOTES.txt"
|
|
|
|
func init() {
|
|
srv = &releaseServer{
|
|
env: env,
|
|
}
|
|
services.RegisterReleaseServiceServer(rootServer, srv)
|
|
}
|
|
|
|
var (
|
|
// errMissingChart indicates that a chart was not provided.
|
|
errMissingChart = errors.New("no chart provided")
|
|
// errMissingRelease indicates that a release (name) was not provided.
|
|
errMissingRelease = errors.New("no release provided")
|
|
// errIncompatibleVersion indicates incompatible client/server versions.
|
|
errIncompatibleVersion = errors.New("client version is incompatible")
|
|
)
|
|
|
|
// ListDefaultLimit is the default limit for number of items returned in a list.
|
|
var ListDefaultLimit int64 = 512
|
|
|
|
type releaseServer struct {
|
|
env *environment.Environment
|
|
}
|
|
|
|
func getVersion(c ctx.Context) string {
|
|
if md, ok := metadata.FromContext(c); ok {
|
|
if v, ok := md["x-helm-api-client"]; ok {
|
|
return v[0]
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (s *releaseServer) ListReleases(req *services.ListReleasesRequest, stream services.ReleaseService_ListReleasesServer) error {
|
|
if !checkClientVersion(stream.Context()) {
|
|
return errIncompatibleVersion
|
|
}
|
|
|
|
if len(req.StatusCodes) == 0 {
|
|
req.StatusCodes = []release.Status_Code{release.Status_DEPLOYED}
|
|
}
|
|
|
|
//rels, err := s.env.Releases.ListDeployed()
|
|
rels, err := s.env.Releases.ListFilterAll(func(r *release.Release) bool {
|
|
for _, sc := range req.StatusCodes {
|
|
if sc == r.Info.Status.Code {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(req.Filter) != 0 {
|
|
rels, err = filterReleases(req.Filter, rels)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
total := int64(len(rels))
|
|
|
|
switch req.SortBy {
|
|
case services.ListSort_NAME:
|
|
sort.Sort(byName(rels))
|
|
case services.ListSort_LAST_RELEASED:
|
|
sort.Sort(byDate(rels))
|
|
}
|
|
|
|
if req.SortOrder == services.ListSort_DESC {
|
|
ll := len(rels)
|
|
rr := make([]*release.Release, ll)
|
|
for i, item := range rels {
|
|
rr[ll-i-1] = item
|
|
}
|
|
rels = rr
|
|
}
|
|
|
|
l := int64(len(rels))
|
|
if req.Offset != "" {
|
|
|
|
i := -1
|
|
for ii, cur := range rels {
|
|
if cur.Name == req.Offset {
|
|
i = ii
|
|
}
|
|
}
|
|
if i == -1 {
|
|
return fmt.Errorf("offset %q not found", req.Offset)
|
|
}
|
|
|
|
if len(rels) < i {
|
|
return fmt.Errorf("no items after %q", req.Offset)
|
|
}
|
|
|
|
rels = rels[i:]
|
|
l = int64(len(rels))
|
|
}
|
|
|
|
if req.Limit == 0 {
|
|
req.Limit = ListDefaultLimit
|
|
}
|
|
|
|
next := ""
|
|
if l > req.Limit {
|
|
next = rels[req.Limit].Name
|
|
rels = rels[0:req.Limit]
|
|
l = int64(len(rels))
|
|
}
|
|
|
|
res := &services.ListReleasesResponse{
|
|
Next: next,
|
|
Count: l,
|
|
Total: total,
|
|
Releases: rels,
|
|
}
|
|
stream.Send(res)
|
|
return nil
|
|
}
|
|
|
|
func filterReleases(filter string, rels []*release.Release) ([]*release.Release, error) {
|
|
preg, err := regexp.Compile(filter)
|
|
if err != nil {
|
|
return rels, err
|
|
}
|
|
matches := []*release.Release{}
|
|
for _, r := range rels {
|
|
if preg.MatchString(r.Name) {
|
|
matches = append(matches, r)
|
|
}
|
|
}
|
|
return matches, nil
|
|
}
|
|
|
|
func (s *releaseServer) GetVersion(c ctx.Context, req *services.GetVersionRequest) (*services.GetVersionResponse, error) {
|
|
v := version.GetVersionProto()
|
|
return &services.GetVersionResponse{Version: v}, nil
|
|
}
|
|
|
|
func checkClientVersion(c ctx.Context) bool {
|
|
v := getVersion(c)
|
|
return version.IsCompatible(v, version.Version)
|
|
}
|
|
|
|
func (s *releaseServer) GetReleaseStatus(c ctx.Context, req *services.GetReleaseStatusRequest) (*services.GetReleaseStatusResponse, error) {
|
|
if !checkClientVersion(c) {
|
|
return nil, errIncompatibleVersion
|
|
}
|
|
|
|
if req.Name == "" {
|
|
return nil, errMissingRelease
|
|
}
|
|
|
|
var rel *release.Release
|
|
var err error
|
|
|
|
if req.Version <= 0 {
|
|
if rel, err = s.env.Releases.Deployed(req.Name); err != nil {
|
|
return nil, fmt.Errorf("getting deployed release '%s': %s", req.Name, err)
|
|
}
|
|
} else {
|
|
if rel, err = s.env.Releases.Get(req.Name, req.Version); err != nil {
|
|
return nil, fmt.Errorf("getting release '%s' (v%d): %s", req.Name, req.Version, err)
|
|
}
|
|
}
|
|
|
|
if rel.Info == nil {
|
|
return nil, errors.New("release info is missing")
|
|
}
|
|
if rel.Chart == nil {
|
|
return nil, errors.New("release chart is missing")
|
|
}
|
|
|
|
sc := rel.Info.Status.Code
|
|
statusResp := &services.GetReleaseStatusResponse{Info: rel.Info, Namespace: rel.Namespace}
|
|
|
|
// Ok, we got the status of the release as we had jotted down, now we need to match the
|
|
// manifest we stashed away with reality from the cluster.
|
|
kubeCli := s.env.KubeClient
|
|
resp, err := kubeCli.Get(rel.Namespace, bytes.NewBufferString(rel.Manifest))
|
|
if sc == release.Status_DELETED || sc == release.Status_FAILED {
|
|
// Skip errors if this is already deleted or failed.
|
|
return statusResp, nil
|
|
} else if err != nil {
|
|
log.Printf("warning: Get for %s failed: %v", rel.Name, err)
|
|
return nil, err
|
|
}
|
|
rel.Info.Status.Resources = resp
|
|
return statusResp, nil
|
|
}
|
|
|
|
func (s *releaseServer) GetReleaseContent(c ctx.Context, req *services.GetReleaseContentRequest) (*services.GetReleaseContentResponse, error) {
|
|
if !checkClientVersion(c) {
|
|
return nil, errIncompatibleVersion
|
|
}
|
|
|
|
if req.Name == "" {
|
|
return nil, errMissingRelease
|
|
}
|
|
if req.Version <= 0 {
|
|
rel, err := s.env.Releases.Deployed(req.Name)
|
|
return &services.GetReleaseContentResponse{Release: rel}, err
|
|
}
|
|
|
|
rel, err := s.env.Releases.Get(req.Name, req.Version)
|
|
return &services.GetReleaseContentResponse{Release: rel}, err
|
|
}
|
|
|
|
func (s *releaseServer) UpdateRelease(c ctx.Context, req *services.UpdateReleaseRequest) (*services.UpdateReleaseResponse, error) {
|
|
if !checkClientVersion(c) {
|
|
return nil, errIncompatibleVersion
|
|
}
|
|
|
|
currentRelease, updatedRelease, err := s.prepareUpdate(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
res, err := s.performUpdate(currentRelease, updatedRelease, req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := s.env.Releases.Create(updatedRelease); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
func (s *releaseServer) performUpdate(originalRelease, updatedRelease *release.Release, req *services.UpdateReleaseRequest) (*services.UpdateReleaseResponse, error) {
|
|
res := &services.UpdateReleaseResponse{Release: updatedRelease}
|
|
|
|
if req.DryRun {
|
|
log.Printf("Dry run for %s", updatedRelease.Name)
|
|
return res, nil
|
|
}
|
|
|
|
// pre-ugrade hooks
|
|
if !req.DisableHooks {
|
|
if err := s.execHook(updatedRelease.Hooks, updatedRelease.Name, updatedRelease.Namespace, preUpgrade); err != nil {
|
|
return res, err
|
|
}
|
|
}
|
|
|
|
kubeCli := s.env.KubeClient
|
|
original := bytes.NewBufferString(originalRelease.Manifest)
|
|
modified := bytes.NewBufferString(updatedRelease.Manifest)
|
|
if err := kubeCli.Update(updatedRelease.Namespace, original, modified); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// post-upgrade hooks
|
|
if !req.DisableHooks {
|
|
if err := s.execHook(updatedRelease.Hooks, updatedRelease.Name, updatedRelease.Namespace, postUpgrade); err != nil {
|
|
return res, err
|
|
}
|
|
}
|
|
|
|
originalRelease.Info.Status.Code = release.Status_SUPERSEDED
|
|
if err := s.env.Releases.Update(originalRelease); err != nil {
|
|
return nil, fmt.Errorf("Update of %s failed: %s", originalRelease.Name, err)
|
|
}
|
|
|
|
updatedRelease.Info.Status.Code = release.Status_DEPLOYED
|
|
|
|
return res, nil
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func (s *releaseServer) uniqName(start string, reuse bool) (string, error) {
|
|
|
|
// If a name is supplied, we check to see if that name is taken. If not, it
|
|
// is granted. If reuse is true and a deleted release with that name exists,
|
|
// we re-grant it. Otherwise, an error is returned.
|
|
if start != "" {
|
|
|
|
if len(start) > releaseNameMaxLen {
|
|
return "", fmt.Errorf("release name %q exceeds max length of %d", start, releaseNameMaxLen)
|
|
}
|
|
|
|
if rel, err := s.env.Releases.Get(start, 1); err == driver.ErrReleaseNotFound {
|
|
return start, nil
|
|
} else if st := rel.Info.Status.Code; reuse && (st == release.Status_DELETED || st == release.Status_FAILED) {
|
|
// Allowe re-use of names if the previous release is marked deleted.
|
|
log.Printf("reusing name %q", start)
|
|
return start, nil
|
|
} else if reuse {
|
|
return "", errors.New("cannot re-use a name that is still in use")
|
|
}
|
|
|
|
return "", fmt.Errorf("a release named %q already exists", start)
|
|
}
|
|
|
|
maxTries := 5
|
|
for i := 0; i < maxTries; i++ {
|
|
namer := moniker.New()
|
|
name := namer.NameSep("-")
|
|
if len(name) > releaseNameMaxLen {
|
|
name = name[:releaseNameMaxLen]
|
|
}
|
|
if _, err := s.env.Releases.Get(name, 1); err == driver.ErrReleaseNotFound {
|
|
return name, nil
|
|
}
|
|
log.Printf("info: Name %q is taken. Searching again.", name)
|
|
}
|
|
log.Printf("warning: No available release names found after %d tries", maxTries)
|
|
return "ERROR", errors.New("no available release name found")
|
|
}
|
|
|
|
func (s *releaseServer) engine(ch *chart.Chart) environment.Engine {
|
|
renderer := s.env.EngineYard.Default()
|
|
if ch.Metadata.Engine != "" {
|
|
if r, ok := s.env.EngineYard.Get(ch.Metadata.Engine); ok {
|
|
renderer = r
|
|
} else {
|
|
log.Printf("warning: %s requested non-existent template engine %s", ch.Metadata.Name, ch.Metadata.Engine)
|
|
}
|
|
}
|
|
return renderer
|
|
}
|
|
|
|
func (s *releaseServer) InstallRelease(c ctx.Context, req *services.InstallReleaseRequest) (*services.InstallReleaseResponse, error) {
|
|
if !checkClientVersion(c) {
|
|
return nil, errIncompatibleVersion
|
|
}
|
|
|
|
rel, err := s.prepareRelease(req)
|
|
if err != nil {
|
|
log.Printf("Failed install prepare step: %s", err)
|
|
return nil, err
|
|
}
|
|
|
|
res, err := s.performRelease(rel, req)
|
|
if err != nil {
|
|
log.Printf("Failed install perform step: %s", err)
|
|
}
|
|
return res, err
|
|
}
|
|
|
|
// 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) getVersionSet() (versionSet, error) {
|
|
defVersions := newVersionSet("v1")
|
|
cli, err := s.env.KubeClient.APIClient()
|
|
if err != nil {
|
|
log.Printf("API Client for Kubernetes is missing: %s.", err)
|
|
return defVersions, err
|
|
}
|
|
|
|
groups, err := cli.Discovery().ServerGroups()
|
|
if err != nil {
|
|
return defVersions, err
|
|
}
|
|
|
|
// FIXME: The Kubernetes test fixture for cli appears to always return nil
|
|
// for calls to Discovery().ServerGroups(). So in this case, we return
|
|
// the default API list. This is also a safe value to return in any other
|
|
// odd-ball case.
|
|
if groups == nil {
|
|
return defVersions, nil
|
|
}
|
|
|
|
versions := unversioned.ExtractGroupVersions(groups)
|
|
return newVersionSet(versions...), nil
|
|
}
|
|
|
|
func (s *releaseServer) renderResources(ch *chart.Chart, values chartutil.Values) ([]*release.Hook, *bytes.Buffer, string, error) {
|
|
renderer := s.engine(ch)
|
|
files, err := renderer.Render(ch, values)
|
|
if err != nil {
|
|
return nil, nil, "", err
|
|
}
|
|
|
|
// NOTES.txt gets rendered like all the other files, but because it's not a hook nor a resource,
|
|
// pull it out of here into a separate file so that we can actually use the output of the rendered
|
|
// text file. We have to spin through this map because the file contains path information, so we
|
|
// look for terminating NOTES.txt. We also remove it from the files so that we don't have to skip
|
|
// it in the sortHooks.
|
|
notes := ""
|
|
for k, v := range files {
|
|
if strings.HasSuffix(k, notesFileSuffix) {
|
|
notes = v
|
|
delete(files, k)
|
|
}
|
|
}
|
|
|
|
// Sort hooks, manifests, and partials. Only hooks and manifests are returned,
|
|
// as partials are not used after renderer.Render. Empty manifests are also
|
|
// removed here.
|
|
vs, err := s.getVersionSet()
|
|
if err != nil {
|
|
return nil, nil, "", fmt.Errorf("Could not get apiVersions from Kubernetes: %s", err)
|
|
}
|
|
hooks, manifests, err := sortManifests(files, vs)
|
|
if err != nil {
|
|
// By catching parse errors here, we can prevent bogus releases from going
|
|
// to Kubernetes.
|
|
return nil, nil, "", err
|
|
}
|
|
|
|
// Aggregate all valid manifests into one big doc.
|
|
b := bytes.NewBuffer(nil)
|
|
for name, file := range manifests {
|
|
b.WriteString("\n---\n# Source: " + name + "\n")
|
|
b.WriteString(file)
|
|
}
|
|
|
|
return hooks, b, notes, nil
|
|
}
|
|
|
|
// validateYAML checks to see if YAML is well-formed.
|
|
func validateYAML(data string) error {
|
|
b := map[string]interface{}{}
|
|
return yaml.Unmarshal([]byte(data), b)
|
|
}
|
|
|
|
func (s *releaseServer) recordRelease(r *release.Release, reuse bool) {
|
|
if reuse {
|
|
if err := s.env.Releases.Update(r); err != nil {
|
|
log.Printf("warning: Failed to update release %q: %s", r.Name, err)
|
|
}
|
|
} else if err := s.env.Releases.Create(r); err != nil {
|
|
log.Printf("warning: Failed to record release %q: %s", r.Name, err)
|
|
}
|
|
}
|
|
|
|
// performRelease runs a release.
|
|
func (s *releaseServer) performRelease(r *release.Release, req *services.InstallReleaseRequest) (*services.InstallReleaseResponse, error) {
|
|
res := &services.InstallReleaseResponse{Release: r}
|
|
|
|
if req.DryRun {
|
|
log.Printf("Dry run for %s", r.Name)
|
|
return res, nil
|
|
}
|
|
|
|
// pre-install hooks
|
|
if !req.DisableHooks {
|
|
if err := s.execHook(r.Hooks, r.Name, r.Namespace, preInstall); err != nil {
|
|
return res, err
|
|
}
|
|
}
|
|
|
|
// regular manifests
|
|
kubeCli := s.env.KubeClient
|
|
b := bytes.NewBufferString(r.Manifest)
|
|
if err := kubeCli.Create(r.Namespace, b); err != nil {
|
|
log.Printf("warning: Release %q failed: %s", r.Name, err)
|
|
r.Info.Status.Code = release.Status_FAILED
|
|
s.recordRelease(r, req.ReuseName)
|
|
return res, fmt.Errorf("release %s failed: %s", r.Name, err)
|
|
}
|
|
|
|
// post-install hooks
|
|
if !req.DisableHooks {
|
|
if err := s.execHook(r.Hooks, r.Name, r.Namespace, postInstall); err != nil {
|
|
log.Printf("warning: Release %q failed post-install: %s", r.Name, err)
|
|
r.Info.Status.Code = release.Status_FAILED
|
|
s.recordRelease(r, req.ReuseName)
|
|
return res, 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.
|
|
r.Info.Status.Code = release.Status_DEPLOYED
|
|
s.recordRelease(r, req.ReuseName)
|
|
return res, 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
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
b := bytes.NewBuffer([]byte(rel.Manifest))
|
|
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
|
|
}
|
|
|
|
// byName implements the sort.Interface for []*release.Release.
|
|
type byName []*release.Release
|
|
|
|
func (r byName) Len() int {
|
|
return len(r)
|
|
}
|
|
func (r byName) Swap(p, q int) {
|
|
r[p], r[q] = r[q], r[p]
|
|
}
|
|
func (r byName) Less(i, j int) bool {
|
|
return r[i].Name < r[j].Name
|
|
}
|
|
|
|
type byDate []*release.Release
|
|
|
|
func (r byDate) Len() int { return len(r) }
|
|
func (r byDate) Swap(p, q int) {
|
|
r[p], r[q] = r[q], r[p]
|
|
}
|
|
func (r byDate) Less(p, q int) bool {
|
|
return r[p].Info.LastDeployed.Seconds < r[q].Info.LastDeployed.Seconds
|
|
}
|