/ *
Copyright The Helm Authors .
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 action
import (
"bytes"
"path"
"regexp"
"sort"
"strings"
"time"
"github.com/pkg/errors"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/client-go/discovery"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"helm.sh/helm/pkg/chartutil"
"helm.sh/helm/pkg/hooks"
"helm.sh/helm/pkg/kube"
"helm.sh/helm/pkg/registry"
"helm.sh/helm/pkg/release"
"helm.sh/helm/pkg/storage"
)
// Timestamper is a function capable of producing a timestamp.Timestamper.
//
// By default, this is a time.Time function. This can be overridden for testing,
// though, so that timestamps are predictable.
var Timestamper = time . Now
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" )
// errInvalidRevision indicates that an invalid release revision number was provided.
errInvalidRevision = errors . New ( "invalid release revision" )
// errInvalidName indicates that an invalid release name was provided
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" )
)
// ValidName is a regular expression for names.
//
// According to the Kubernetes help text, the regular expression it uses is:
//
// (([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])?
//
// We modified that. First, we added start and end delimiters. Second, we changed
// the final ? to + to require that the pattern match at least once. This modification
// prevents an empty string from matching.
var ValidName = regexp . MustCompile ( "^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])+$" )
// Configuration injects the dependencies that all actions share.
type Configuration struct {
// RESTClientGetter is an interface that loads Kuberbetes clients.
RESTClientGetter RESTClientGetter
// Releases stores records of releases.
Releases * storage . Storage
// KubeClient is a Kubernetes API client.
KubeClient kube . Interface
// RegistryClient is a client for working with registries
RegistryClient * registry . Client
// Capabilities describes the capabilities of the Kubernetes cluster.
Capabilities * chartutil . Capabilities
Log func ( string , ... interface { } )
}
// capabilities builds a Capabilities from discovery information.
func ( c * Configuration ) getCapabilities ( ) ( * chartutil . Capabilities , error ) {
if c . Capabilities != nil {
return c . Capabilities , nil
}
dc , err := c . RESTClientGetter . ToDiscoveryClient ( )
if err != nil {
return nil , errors . Wrap ( err , "could not get Kubernetes discovery client" )
}
kubeVersion , err := dc . ServerVersion ( )
if err != nil {
return nil , errors . Wrap ( err , "could not get server version from Kubernetes" )
}
apiVersions , err := GetVersionSet ( dc )
if err != nil {
return nil , errors . Wrap ( err , "could not get apiVersions from Kubernetes" )
}
c . Capabilities = & chartutil . Capabilities {
APIVersions : apiVersions ,
KubeVersion : chartutil . KubeVersion {
Version : kubeVersion . GitVersion ,
Major : kubeVersion . Major ,
Minor : kubeVersion . Minor ,
} ,
}
return c . Capabilities , nil
}
func ( c * Configuration ) KubernetesClientSet ( ) ( kubernetes . Interface , error ) {
conf , err := c . RESTClientGetter . ToRESTConfig ( )
if err != nil {
return nil , errors . Wrap ( err , "unable to generate config for kubernetes client" )
}
return kubernetes . NewForConfig ( conf )
}
// Now generates a timestamp
//
// If the configuration has a Timestamper on it, that will be used.
// Otherwise, this will use time.Now().
func ( c * Configuration ) Now ( ) time . Time {
return Timestamper ( )
}
func ( c * Configuration ) releaseContent ( name string , version int ) ( * release . Release , error ) {
if err := validateReleaseName ( name ) ; err != nil {
return nil , errors . Errorf ( "releaseContent: Release name is invalid: %s" , name )
}
if version <= 0 {
return c . Releases . Last ( name )
}
return c . Releases . Get ( name , version )
}
// GetVersionSet retrieves a set of available k8s API versions
func GetVersionSet ( client discovery . ServerResourcesInterface ) ( chartutil . VersionSet , error ) {
groups , resources , err := client . ServerGroupsAndResources ( )
if err != nil {
return chartutil . DefaultVersionSet , err
}
// FIXME: The Kubernetes test fixture for cli appears to always return nil
// for calls to Discovery().ServerGroupsAndResources(). 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 len ( groups ) == 0 && len ( resources ) == 0 {
return chartutil . DefaultVersionSet , nil
}
versionMap := make ( map [ string ] interface { } )
versions := [ ] string { }
// Extract the groups
for _ , g := range groups {
for _ , gv := range g . Versions {
versionMap [ gv . GroupVersion ] = struct { } { }
}
}
// Extract the resources
var id string
var ok bool
for _ , r := range resources {
for _ , rl := range r . APIResources {
// A Kind at a GroupVersion can show up more than once. We only want
// it displayed once in the final output.
id = path . Join ( r . GroupVersion , rl . Kind )
if _ , ok = versionMap [ id ] ; ! ok {
versionMap [ id ] = struct { } { }
}
}
}
// Convert to a form that NewVersionSet can use
for k := range versionMap {
versions = append ( versions , k )
}
return chartutil . VersionSet ( versions ) , nil
}
// recordRelease with an update operation in case reuse has been set.
func ( c * Configuration ) recordRelease ( r * release . Release ) {
if err := c . Releases . Update ( r ) ; err != nil {
c . Log ( "warning: Failed to update release %s: %s" , r . Name , err )
}
}
type RESTClientGetter interface {
ToRESTConfig ( ) ( * rest . Config , error )
ToDiscoveryClient ( ) ( discovery . CachedDiscoveryInterface , error )
ToRESTMapper ( ) ( meta . RESTMapper , error )
}
// execHooks is a method for exec-ing all hooks of the given type. This is to
// avoid duplicate code in various actions
func execHooks ( client kube . Interface , hs [ ] * release . Hook , hook string , timeout time . Duration ) error {
executingHooks := [ ] * release . Hook { }
for _ , h := range hs {
for _ , e := range h . Events {
if string ( e ) == hook {
executingHooks = append ( executingHooks , h )
}
}
}
sort . Sort ( hookByWeight ( executingHooks ) )
for _ , h := range executingHooks {
if err := deleteHookByPolicy ( client , h , hooks . BeforeHookCreation ) ; err != nil {
return err
}
resources , err := client . Build ( bytes . NewBufferString ( h . Manifest ) )
if err != nil {
return errors . Wrapf ( err , "unable to build kubernetes object for %s hook %s" , hook , h . Path )
}
if _ , err := client . Create ( resources ) ; err != nil {
return errors . Wrapf ( err , "warning: Hook %s %s failed" , hook , h . Path )
}
if err := client . WatchUntilReady ( resources , timeout ) ; err != nil {
// If a hook is failed, checkout the annotation of the hook to determine whether the hook should be deleted
// under failed condition. If so, then clear the corresponding resource object in the hook
if err := deleteHookByPolicy ( client , h , hooks . HookFailed ) ; err != nil {
return err
}
return err
}
}
// If all hooks are succeeded, checkout the annotation of each hook to determine whether the hook should be deleted
// under succeeded condition. If so, then clear the corresponding resource object in each hook
for _ , h := range executingHooks {
if err := deleteHookByPolicy ( client , h , hooks . HookSucceeded ) ; err != nil {
return err
}
h . LastRun = time . Now ( )
}
return nil
}
// deleteHookByPolicy deletes a hook if the hook policy instructs it to
func deleteHookByPolicy ( client kube . Interface , h * release . Hook , policy string ) error {
if hookHasDeletePolicy ( h , policy ) {
resources , err := client . Build ( bytes . NewBufferString ( h . Manifest ) )
if err != nil {
return errors . Wrapf ( err , "unable to build kubernetes object for deleting hook %s" , h . Path )
}
_ , errs := client . Delete ( resources )
return errors . New ( joinErrors ( errs ) )
}
return nil
}
func joinErrors ( errs [ ] error ) string {
es := make ( [ ] string , 0 , len ( errs ) )
for _ , e := range errs {
es = append ( es , e . Error ( ) )
}
return strings . Join ( es , "; " )
}