/ *
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"
"github.com/Masterminds/semver"
"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"
)
var srv * releaseServer
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" )
)
// ListDefaultLimit is the default limit for number of items returned in a list.
var ListDefaultLimit int64 = 512
type releaseServer struct {
env * environment . Environment
}
func ( s * releaseServer ) ListReleases ( req * services . ListReleasesRequest , stream services . ReleaseService_ListReleasesServer ) error {
rels , err := s . env . Releases . ListReleases ( )
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 ) GetReleaseStatus ( c ctx . Context , req * services . GetReleaseStatusRequest ) ( * services . GetReleaseStatusResponse , error ) {
if req . Name == "" {
return nil , errMissingRelease
}
rel , err := s . env . Releases . Get ( req . Name )
if err != nil {
return nil , err
}
if rel . Info == nil {
return nil , errors . New ( "release info is missing" )
}
return & services . GetReleaseStatusResponse { Info : rel . Info } , nil
}
func ( s * releaseServer ) GetReleaseContent ( c ctx . Context , req * services . GetReleaseContentRequest ) ( * services . GetReleaseContentResponse , error ) {
if req . Name == "" {
return nil , errMissingRelease
}
rel , err := s . env . Releases . Get ( req . Name )
return & services . GetReleaseContentResponse { Release : rel } , err
}
func ( s * releaseServer ) UpdateRelease ( c ctx . Context , req * services . UpdateReleaseRequest ) ( * services . UpdateReleaseResponse , error ) {
rel , err := s . prepareUpdate ( req )
if err != nil {
return nil , err
}
// TODO: perform update
return & services . UpdateReleaseResponse { Release : rel } , nil
}
// prepareUpdate builds a release for an update operation.
func ( s * releaseServer ) prepareUpdate ( req * services . UpdateReleaseRequest ) ( * release . Release , error ) {
if req . Name == "" {
return nil , errMissingRelease
}
if req . Chart == nil {
return nil , errMissingChart
}
// finds the non-deleted release with the given name
rel , err := s . env . Releases . Get ( req . Name )
if err != nil {
return nil , err
}
//validate chart name is same as previous release
givenChart := req . Chart . Metadata . Name
releasedChart := rel . Chart . Metadata . Name
if givenChart != releasedChart {
return nil , fmt . Errorf ( "Given chart, %s, does not match chart originally released, %s" , givenChart , releasedChart )
}
// validate new chart version is higher than old
givenChartVersion := req . Chart . Metadata . Version
releasedChartVersion := rel . Chart . Metadata . Version
c , err := semver . NewConstraint ( "> " + releasedChartVersion )
if err != nil {
return nil , err
}
v , err := semver . NewVersion ( givenChartVersion )
if err != nil {
return nil , err
}
if a := c . Check ( v ) ; ! a {
return nil , fmt . Errorf ( "Given chart (%s-%v) must be a higher version than released chart (%s-%v)" , givenChart , givenChartVersion , releasedChart , releasedChartVersion )
}
// Store an updated release.
updatedRelease := & release . Release {
Name : req . Name ,
Chart : req . Chart ,
Config : req . Values ,
Version : rel . Version + 1 ,
}
return 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 rel , err := s . env . Releases . Get ( start ) ; err == driver . ErrReleaseNotFound {
return start , nil
} else if reuse && rel . Info . Status . Code == release . Status_DELETED {
// Allowe re-use of names if the previous release is marked deleted.
log . Printf ( "reusing name %q" , start )
return start , nil
}
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 _ , err := s . env . Releases . Get ( name ) ; 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 ) {
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
}
renderer := s . engine ( req . Chart )
files , err := renderer . Render ( req . Chart , valuesToRender )
if err != nil {
return nil , err
}
// 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.
hooks , manifests , err := sortHooks ( files )
if err != nil {
// By catching parse errors here, we can prevent bogus releases from going
// to Kubernetes.
return 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 )
}
// 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 : b . String ( ) ,
Hooks : hooks ,
Version : 1 ,
}
return rel , 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 )
}
// 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 {
r . Info . Status . Code = release . Status_FAILED
log . Printf ( "warning: Release %q failed: %s" , r . Name , err )
if err := s . env . Releases . Create ( r ) ; err != nil {
log . Printf ( "warning: Failed to record release %q: %s" , r . Name , err )
}
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 {
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
if err := s . env . Releases . Create ( r ) ; err != nil {
log . Printf ( "warning: Failed to record release %q: %s" , r . Name , err )
}
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 ( "wrning: 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 req . Name == "" {
log . Printf ( "uninstall: Release not found: %s" , req . Name )
return nil , errMissingRelease
}
rel , err := s . env . Releases . Get ( 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 {
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 err := s . env . Releases . Update ( rel ) ; err != nil {
log . Printf ( "uninstall: Failed to store updated 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
}