@ -1,3 +1,583 @@
/ *
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"
"context"
"errors"
"fmt"
"io"
"io/fs"
"log/slog"
"net/url"
"os"
"path/filepath"
"strings"
"sync"
"sync/atomic"
"text/template"
"time"
"github.com/Masterminds/sprig/v3"
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/cli-runtime/pkg/resource"
"sigs.k8s.io/yaml"
ci "helm.sh/helm/v4/pkg/chart"
"helm.sh/helm/v4/pkg/chart/common"
"helm.sh/helm/v4/pkg/chart/common/util"
chart "helm.sh/helm/v4/pkg/chart/v2"
chartutil "helm.sh/helm/v4/pkg/chart/v2/util"
"helm.sh/helm/v4/pkg/cli"
"helm.sh/helm/v4/pkg/downloader"
"helm.sh/helm/v4/pkg/getter"
"helm.sh/helm/v4/pkg/kube"
kubefake "helm.sh/helm/v4/pkg/kube/fake"
"helm.sh/helm/v4/pkg/postrenderer"
"helm.sh/helm/v4/pkg/registry"
ri "helm.sh/helm/v4/pkg/release"
rcommon "helm.sh/helm/v4/pkg/release/common"
release "helm.sh/helm/v4/pkg/release/v1"
releaseutil "helm.sh/helm/v4/pkg/release/v1/util"
"helm.sh/helm/v4/pkg/repo/v1"
"helm.sh/helm/v4/pkg/storage"
"helm.sh/helm/v4/pkg/storage/driver"
)
// notesFileSuffix that we want to treat specially. 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"
const defaultDirectoryPermission = 0 o755
// Install performs an installation operation.
type Install struct {
cfg * Configuration
ChartPathOptions
// ForceReplace will, if set to `true`, ignore certain warnings and perform the install anyway.
//
// This should be used with caution.
ForceReplace bool
// ForceConflicts causes server-side apply to force conflicts ("Overwrite value, become sole manager")
// see: https://kubernetes.io/docs/reference/using-api/server-side-apply/#conflicts
ForceConflicts bool
// ServerSideApply when true (default) will enable changes to be applied via Kubernetes server-side apply
// see: https://kubernetes.io/docs/reference/using-api/server-side-apply/
ServerSideApply bool
CreateNamespace bool
// DryRunStrategy can be set to prepare, but not execute the operation and whether or not to interact with the remote cluster
DryRunStrategy DryRunStrategy
// HideSecret can be set to true when DryRun is enabled in order to hide
// Kubernetes Secrets in the output. It cannot be used outside of DryRun.
HideSecret bool
DisableHooks bool
Replace bool
WaitStrategy kube . WaitStrategy
WaitOptions [ ] kube . WaitOption
WaitForJobs bool
Devel bool
DependencyUpdate bool
Timeout time . Duration
Namespace string
ReleaseName string
GenerateName bool
NameTemplate string
Description string
OutputDir string
// RollbackOnFailure enables rolling back (uninstalling) the release on failure if set
RollbackOnFailure bool
SkipCRDs bool
SubNotes bool
HideNotes bool
SkipSchemaValidation bool
DisableOpenAPIValidation bool
IncludeCRDs bool
Labels map [ string ] string
// KubeVersion allows specifying a custom kubernetes version to use and
// APIVersions allows a manual set of supported API Versions to be passed
// (for things like templating).
KubeVersion * common . KubeVersion
APIVersions common . VersionSet
// Used by helm template to render charts with .Release.IsUpgrade. Ignored if Dry-Run is false
IsUpgrade bool
// Enable DNS lookups when rendering templates
EnableDNS bool
// Used by helm template to add the release as part of OutputDir path
// OutputDir/<ReleaseName>
UseReleaseName bool
// TakeOwnership will ignore the check for helm annotations and take ownership of the resources.
TakeOwnership bool
PostRenderer postrenderer . PostRenderer
// PostRenderStrategy controls how hooks and regular templates are passed
// to the configured post-renderer. See PostRenderStrategy for the
// available modes. Defaults to PostRenderStrategyCombined.
PostRenderStrategy PostRenderStrategy
// Lock to control raceconditions when the process receives a SIGTERM
Lock sync . Mutex
goroutineCount atomic . Int32
}
// ChartPathOptions captures common options used for controlling chart paths
type ChartPathOptions struct {
CaFile string // --ca-file
CertFile string // --cert-file
KeyFile string // --key-file
InsecureSkipTLSVerify bool // --insecure-skip-verify
PlainHTTP bool // --plain-http
Keyring string // --keyring
Password string // --password
PassCredentialsAll bool // --pass-credentials
RepoURL string // --repo
Username string // --username
Verify bool // --verify
Version string // --version
// registryClient provides a registry client but is not added with
// options from a flag
registryClient * registry . Client
}
// NewInstall creates a new Install object with the given configuration.
func NewInstall ( cfg * Configuration ) * Install {
in := & Install {
cfg : cfg ,
ServerSideApply : true , // Must always match the CLI default.
DryRunStrategy : DryRunNone ,
PostRenderStrategy : PostRenderStrategyCombined ,
}
in . registryClient = cfg . RegistryClient
return in
}
// SetRegistryClient sets the registry client for the install action
func ( i * Install ) SetRegistryClient ( registryClient * registry . Client ) {
i . registryClient = registryClient
}
// GetRegistryClient get the registry client.
func ( i * Install ) GetRegistryClient ( ) * registry . Client {
return i . registryClient
}
func ( i * Install ) installCRDs ( crds [ ] chart . CRD ) error {
// We do these one file at a time in the order they were read.
totalItems := [ ] * resource . Info { }
for _ , obj := range crds {
if obj . File == nil {
return fmt . Errorf ( "failed to install CRD %s: file is empty" , obj . Name )
}
if obj . File . Data == nil {
return fmt . Errorf ( "failed to install CRD %s: file data is empty" , obj . Name )
}
// Read in the resources
res , err := i . cfg . KubeClient . Build ( bytes . NewBuffer ( obj . File . Data ) , false )
if err != nil {
return fmt . Errorf ( "failed to install CRD %s: %w" , obj . Name , err )
}
if len ( res ) == 0 {
return fmt . Errorf ( "failed to install CRD %s: resources are empty" , obj . Name )
}
// Send them to Kube
if _ , err := i . cfg . KubeClient . Create (
res ,
kube . ClientCreateOptionServerSideApply ( i . ServerSideApply , i . ForceConflicts ) ) ; err != nil {
// If the error is CRD already exists, continue.
if apierrors . IsAlreadyExists ( err ) {
crdName := obj . Name
i . cfg . Logger ( ) . Debug ( "CRD is already present. Skipping" , "crd" , crdName )
continue
}
return fmt . Errorf ( "failed to install CRD %s: %w" , obj . Name , err )
}
totalItems = append ( totalItems , res ... )
}
if len ( totalItems ) > 0 {
var waiter kube . Waiter
var err error
if c , supportsOptions := i . cfg . KubeClient . ( kube . InterfaceWaitOptions ) ; supportsOptions {
waiter , err = c . GetWaiterWithOptions ( i . WaitStrategy , i . WaitOptions ... )
} else {
waiter , err = i . cfg . KubeClient . GetWaiter ( i . WaitStrategy )
}
if err != nil {
return fmt . Errorf ( "unable to get waiter: %w" , err )
}
// Give time for the CRD to be recognized.
if err := waiter . Wait ( totalItems , 60 * time . Second ) ; err != nil {
return err
}
// If we have already gathered the capabilities, we need to invalidate
// the cache so that the new CRDs are recognized. This should only be
// the case when an action configuration is reused for multiple actions,
// as otherwise it is later loaded by ourselves when getCapabilities
// is called later on in the installation process.
if i . cfg . RESTClientGetter != nil {
if i . cfg . Capabilities != nil {
discoveryClient , err := i . cfg . RESTClientGetter . ToDiscoveryClient ( )
if err != nil {
return err
}
if discoveryClient != nil {
i . cfg . Logger ( ) . Debug ( "clearing discovery cache" )
discoveryClient . Invalidate ( )
_ , _ = discoveryClient . ServerGroups ( )
}
}
// Invalidate the REST mapper, since it will not have the new CRDs
// present.
restMapper , err := i . cfg . RESTClientGetter . ToRESTMapper ( )
if err != nil {
return err
}
if resettable , ok := restMapper . ( meta . ResettableRESTMapper ) ; ok {
i . cfg . Logger ( ) . Debug ( "clearing REST mapper cache" )
resettable . Reset ( )
}
}
}
return nil
}
// Run executes the installation
//
// If DryRun is set to true, this will prepare the release, but not install it
func ( i * Install ) Run ( chrt ci . Charter , vals map [ string ] any ) ( ri . Releaser , error ) {
ctx := context . Background ( )
return i . RunWithContext ( ctx , chrt , vals )
}
// RunWithContext executes the installation with Context
//
// When the task is cancelled through ctx, the function returns and the install
// proceeds in the background.
func ( i * Install ) RunWithContext ( ctx context . Context , ch ci . Charter , vals map [ string ] any ) ( ri . Releaser , error ) {
var chrt * chart . Chart
switch c := ch . ( type ) {
case * chart . Chart :
chrt = c
case chart . Chart :
chrt = & c
default :
return nil , errors . New ( "invalid chart apiVersion" )
}
if interactWithServer ( i . DryRunStrategy ) {
if err := i . cfg . KubeClient . IsReachable ( ) ; err != nil {
i . cfg . Logger ( ) . Error ( fmt . Sprintf ( "cluster reachability check failed: %v" , err ) )
return nil , fmt . Errorf ( "cluster reachability check failed: %w" , err )
}
}
// HideSecret must be used with dry run. Otherwise, return an error.
if ! isDryRun ( i . DryRunStrategy ) && i . HideSecret {
i . cfg . Logger ( ) . Error ( "hiding Kubernetes secrets requires a dry-run mode" )
return nil , errors . New ( "hiding Kubernetes secrets requires a dry-run mode" )
}
if err := i . availableName ( ) ; err != nil {
i . cfg . Logger ( ) . Error ( "release name check failed" , slog . Any ( "error" , err ) )
return nil , fmt . Errorf ( "release name check failed: %w" , err )
}
if err := chartutil . ProcessDependencies ( chrt , vals ) ; err != nil {
i . cfg . Logger ( ) . Error ( "chart dependencies processing failed" , slog . Any ( "error" , err ) )
return nil , fmt . Errorf ( "chart dependencies processing failed: %w" , err )
}
// Pre-install anything in the crd/ directory. We do this before Helm
// contacts the upstream server and builds the capabilities object.
if crds := chrt . CRDObjects ( ) ; interactWithServer ( i . DryRunStrategy ) && ! i . SkipCRDs && len ( crds ) > 0 {
// On dry run, bail here
if isDryRun ( i . DryRunStrategy ) {
i . cfg . Logger ( ) . Warn ( "This chart or one of its subcharts contains CRDs. Rendering may fail or contain inaccuracies." )
} else if err := i . installCRDs ( crds ) ; err != nil {
return nil , err
}
}
if ! interactWithServer ( i . DryRunStrategy ) {
// Add mock objects in here so it doesn't use Kube API server
// NOTE(bacongobbler): used for `helm template`
i . cfg . Capabilities = common . DefaultCapabilities . Copy ( )
if i . KubeVersion != nil {
i . cfg . Capabilities . KubeVersion = * i . KubeVersion
}
i . cfg . Capabilities . APIVersions = append ( i . cfg . Capabilities . APIVersions , i . APIVersions ... )
i . cfg . KubeClient = & kubefake . PrintingKubeClient { Out : io . Discard }
mem := driver . NewMemory ( )
mem . SetNamespace ( i . Namespace )
i . cfg . Releases = storage . Init ( mem )
} else if interactWithServer ( i . DryRunStrategy ) && len ( i . APIVersions ) > 0 {
i . cfg . Logger ( ) . Debug ( "API Version list given outside of client only mode, this list will be ignored" )
}
// Make sure if RollbackOnFailure is set, that wait is set as well. This makes it so
// the user doesn't have to specify both
if i . WaitStrategy == kube . HookOnlyStrategy && i . RollbackOnFailure {
i . WaitStrategy = kube . StatusWatcherStrategy
}
caps , err := i . cfg . getCapabilities ( )
if err != nil {
return nil , err
}
// special case for helm template --is-upgrade
isUpgrade := i . IsUpgrade && isDryRun ( i . DryRunStrategy )
options := common . ReleaseOptions {
Name : i . ReleaseName ,
Namespace : i . Namespace ,
Revision : 1 ,
IsInstall : ! isUpgrade ,
IsUpgrade : isUpgrade ,
}
valuesToRender , err := util . ToRenderValuesWithSchemaValidation ( chrt , vals , options , caps , i . SkipSchemaValidation )
if err != nil {
return nil , err
}
if driver . ContainsSystemLabels ( i . Labels ) {
return nil , fmt . Errorf ( "user supplied labels contains system reserved label name. System labels: %+v" , driver . GetSystemLabels ( ) )
}
rel := i . createRelease ( chrt , vals , i . Labels )
var manifestDoc * bytes . Buffer
rel . Hooks , manifestDoc , rel . Info . Notes , err = i . cfg . renderResources ( ctx , chrt , valuesToRender , i . ReleaseName , i . OutputDir , i . SubNotes , i . UseReleaseName , i . IncludeCRDs , i . PostRenderer , interactWithServer ( i . DryRunStrategy ) , i . EnableDNS , i . HideSecret , i . PostRenderStrategy )
// Even for errors, attach this if available
if manifestDoc != nil {
rel . Manifest = manifestDoc . String ( )
}
// Check error from render
if err != nil {
rel . SetStatus ( rcommon . StatusFailed , "failed to render resource: " + err . Error ( ) )
// Return a release with partial data so that the client can show debugging information.
return rel , err
}
// Mark this release as in-progress
rel . SetStatus ( rcommon . StatusPendingInstall , "Initial install underway" )
var toBeAdopted kube . ResourceList
resources , err := i . cfg . KubeClient . Build ( bytes . NewBufferString ( rel . Manifest ) , ! i . DisableOpenAPIValidation )
if err != nil {
return nil , fmt . Errorf ( "unable to build kubernetes objects from release manifest: %w" , err )
}
// It is safe to use "forceOwnership" here because these are resources currently rendered by the chart.
err = resources . Visit ( setMetadataVisitor ( rel . Name , rel . Namespace , true ) )
if err != nil {
return nil , err
}
// Install requires an extra validation step of checking that resources
// don't already exist before we actually create resources. If we continue
// forward and create the release object with resources that already exist,
// we'll end up in a state where we will delete those resources upon
// deleting the release because the manifest will be pointing at that
// resource
if interactWithServer ( i . DryRunStrategy ) && ! isUpgrade && len ( resources ) > 0 {
if i . TakeOwnership {
toBeAdopted , err = requireAdoption ( resources )
} else {
toBeAdopted , err = existingResourceConflict ( resources , rel . Name , rel . Namespace )
}
if err != nil {
return nil , fmt . Errorf ( "unable to continue with install: %w" , err )
}
}
// Bail out here if it is a dry run
if isDryRun ( i . DryRunStrategy ) {
rel . Info . Description = "Dry run complete"
return rel , nil
}
if i . CreateNamespace {
ns := & v1 . Namespace {
TypeMeta : metav1 . TypeMeta {
APIVersion : "v1" ,
Kind : "Namespace" ,
} ,
ObjectMeta : metav1 . ObjectMeta {
Name : i . Namespace ,
Labels : map [ string ] string {
"name" : i . Namespace ,
} ,
} ,
}
buf , err := yaml . Marshal ( ns )
if err != nil {
return nil , err
}
resourceList , err := i . cfg . KubeClient . Build ( bytes . NewBuffer ( buf ) , true )
if err != nil {
return nil , err
}
if _ , err := i . cfg . KubeClient . Create (
resourceList ,
kube . ClientCreateOptionServerSideApply ( i . ServerSideApply , false ) ) ; err != nil && ! apierrors . IsAlreadyExists ( err ) {
return nil , err
}
}
// If Replace is true, we need to supersede the last release.
if i . Replace {
if err := i . replaceRelease ( rel ) ; err != nil {
return nil , err
}
}
// Store the release in history before continuing. We always know that this is a create operation
if err := i . cfg . Releases . Create ( rel ) ; err != nil {
// We could try to recover gracefully here, but since nothing has been installed
// yet, this is probably safer than trying to continue when we know storage is
// not working.
return rel , err
}
rel , err = i . performInstallCtx ( ctx , rel , toBeAdopted , resources )
if err != nil {
rel , err = i . failRelease ( rel , err )
}
return rel , err
}
func ( i * Install ) performInstallCtx ( ctx context . Context , rel * release . Release , toBeAdopted , resources kube . ResourceList ) ( * release . Release , error ) {
type Msg struct {
r * release . Release
e error
}
resultChan := make ( chan Msg , 1 )
go func ( ) {
i . goroutineCount . Add ( 1 )
rel , err := i . performInstall ( rel , toBeAdopted , resources )
resultChan <- Msg { rel , err }
i . goroutineCount . Add ( - 1 )
} ( )
select {
case <- ctx . Done ( ) :
err := ctx . Err ( )
return rel , err
case msg := <- resultChan :
return msg . r , msg . e
}
}
// getGoroutineCount return the number of running routines
func ( i * Install ) getGoroutineCount ( ) int32 {
return i . goroutineCount . Load ( )
}
func ( i * Install ) performInstall ( rel * release . Release , toBeAdopted , resources kube . ResourceList ) ( * release . Release , error ) {
var err error
// pre-install hooks
if ! i . DisableHooks {
if err := i . cfg . execHook ( rel , release . HookPreInstall , i . WaitStrategy , i . WaitOptions , i . Timeout , i . ServerSideApply ) ; err != nil {
return rel , fmt . Errorf ( "failed pre-install: %w" , err )
}
}
// At this point, we can do the install. Note that before we were detecting whether to
// do an update, but it's not clear whether we WANT to do an update if the reuse is set
// to true, since that is basically an upgrade operation.
if len ( toBeAdopted ) == 0 && len ( resources ) > 0 {
_ , err = i . cfg . KubeClient . Create (
resources ,
kube . ClientCreateOptionServerSideApply ( i . ServerSideApply , false ) )
} else if len ( resources ) > 0 {
updateThreeWayMergeForUnstructured := i . TakeOwnership && ! i . ServerSideApply // Use three-way merge when taking ownership (and not using server-side apply)
_ , err = i . cfg . KubeClient . Update (
toBeAdopted ,
resources ,
kube . ClientUpdateOptionForceReplace ( i . ForceReplace ) ,
kube . ClientUpdateOptionServerSideApply ( i . ServerSideApply , i . ForceConflicts ) ,
kube . ClientUpdateOptionThreeWayMergeForUnstructured ( updateThreeWayMergeForUnstructured ) ,
kube . ClientUpdateOptionUpgradeClientSideFieldManager ( true ) )
}
if err != nil {
return rel , err
}
var waiter kube . Waiter
if c , supportsOptions := i . cfg . KubeClient . ( kube . InterfaceWaitOptions ) ; supportsOptions {
waiter , err = c . GetWaiterWithOptions ( i . WaitStrategy , i . WaitOptions ... )
} else {
waiter , err = i . cfg . KubeClient . GetWaiter ( i . WaitStrategy )
}
if err != nil {
return rel , fmt . Errorf ( "failed to get waiter: %w" , err )
}
if i . WaitForJobs {
err = waiter . WaitWithJobs ( resources , i . Timeout )
} else {
err = waiter . Wait ( resources , i . Timeout )
}
if err != nil {
return rel , err
}
if ! i . DisableHooks {
if err := i . cfg . execHook ( rel , release . HookPostInstall , i . WaitStrategy , i . WaitOptions , i . Timeout , i . ServerSideApply ) ; err != nil {
return rel , fmt . Errorf ( "failed post-install: %w" , err )
}
}
if i . Description != "" {
rel . SetStatus ( rcommon . StatusDeployed , i . Description )
} else {
rel . SetStatus ( rcommon . StatusDeployed , "Install complete" )
}
// 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 := i . recordRelease ( rel ) ; err != nil {
i . cfg . Logger ( ) . Error ( "failed to record the release" , slog . Any ( "error" , err ) )
}
return rel , nil
}
func ( i * Install ) failRelease ( rel * release . Release , err error ) ( * release . Release , error ) {
rel . SetStatus ( rcommon . StatusFailed , fmt . Sprintf ( "Release %q failed: %s" , i . ReleaseName , err . Error ( ) ) )
i . recordRelease ( rel ) // Ignore the error, since we have another error to deal with.
@ -15,4 +595,398 @@ func (i *Install) failRelease(rel *release.Release, err error) (*release.Release
return rel , fmt . Errorf ( "release %s failed, and has been uninstalled due to rollback-on-failure being set: %w" , i . ReleaseName , err )
}
return rel , err
}
}
// availableName tests whether a name is available
//
// Roughly, this will return an error if name is
//
// - empty
// - too long
// - already in use, and not deleted
// - used by a deleted release, and i.Replace is false
func ( i * Install ) availableName ( ) error {
start := i . ReleaseName
if err := chartutil . ValidateReleaseName ( start ) ; err != nil {
return fmt . Errorf ( "release name %q: %w" , start , err )
}
// On dry run, bail here
if isDryRun ( i . DryRunStrategy ) {
return nil
}
h , err := i . cfg . Releases . History ( start )
if err != nil || len ( h ) < 1 {
return nil
}
hl , err := releaseListToV1List ( h )
if err != nil {
return err
}
releaseutil . Reverse ( hl , releaseutil . SortByRevision )
rel := hl [ 0 ]
if st := rel . Info . Status ; i . Replace && ( st == rcommon . StatusUninstalled || st == rcommon . StatusFailed ) {
return nil
}
return errors . New ( "cannot reuse a name that is still in use" )
}
func releaseListToV1List ( ls [ ] ri . Releaser ) ( [ ] * release . Release , error ) {
rls := make ( [ ] * release . Release , 0 , len ( ls ) )
for _ , val := range ls {
rel , err := releaserToV1Release ( val )
if err != nil {
return nil , err
}
rls = append ( rls , rel )
}
return rls , nil
}
func releaseV1ListToReleaserList ( ls [ ] * release . Release ) ( [ ] ri . Releaser , error ) {
rls := make ( [ ] ri . Releaser , 0 , len ( ls ) )
for _ , val := range ls {
rls = append ( rls , val )
}
return rls , nil
}
// createRelease creates a new release object
func ( i * Install ) createRelease ( chrt * chart . Chart , rawVals map [ string ] any , labels map [ string ] string ) * release . Release {
ts := i . cfg . Now ( )
r := & release . Release {
Name : i . ReleaseName ,
Namespace : i . Namespace ,
Chart : chrt ,
Config : rawVals ,
Info : & release . Info {
FirstDeployed : ts ,
LastDeployed : ts ,
Status : rcommon . StatusUnknown ,
} ,
Version : 1 ,
Labels : labels ,
ApplyMethod : string ( determineReleaseSSApplyMethod ( i . ServerSideApply ) ) ,
}
return r
}
// recordRelease with an update operation in case reuse has been set.
func ( i * Install ) recordRelease ( r * release . Release ) error {
// This is a legacy function which has been reduced to a oneliner. Could probably
// refactor it out.
return i . cfg . Releases . Update ( r )
}
// replaceRelease replaces an older release with this one
//
// This allows us to reuse names by superseding an existing release with a new one
func ( i * Install ) replaceRelease ( rel * release . Release ) error {
hist , err := i . cfg . Releases . History ( rel . Name )
if err != nil || len ( hist ) == 0 {
// No releases exist for this name, so we can return early
return nil
}
hl , err := releaseListToV1List ( hist )
if err != nil {
return err
}
releaseutil . Reverse ( hl , releaseutil . SortByRevision )
last := hl [ 0 ]
// Update version to the next available
rel . Version = last . Version + 1
// Do not change the status of a failed release.
if last . Info . Status == rcommon . StatusFailed {
return nil
}
// For any other status, mark it as superseded and store the old record
last . SetStatus ( rcommon . StatusSuperseded , "superseded by new release" )
return i . recordRelease ( last )
}
// write the <data> to <output-dir>/<name>. <appendData> controls if the file is created or content will be appended
func writeToFile ( outputDir , name , data string , appendData bool ) error {
outfileName := outputDir + string ( filepath . Separator ) + name
err := ensureDirectoryForFile ( outfileName )
if err != nil {
return err
}
f , err := createOrOpenFile ( outfileName , appendData )
if err != nil {
return err
}
defer f . Close ( )
_ , err = fmt . Fprintf ( f , "---\n# Source: %s\n%s\n" , name , data )
if err != nil {
return err
}
fmt . Printf ( "wrote %s\n" , outfileName )
return nil
}
func createOrOpenFile ( filename string , appendData bool ) ( * os . File , error ) {
if appendData {
return os . OpenFile ( filename , os . O_APPEND | os . O_WRONLY , 0 o600 )
}
return os . Create ( filename )
}
// check if the directory exists to create file. creates if doesn't exist
func ensureDirectoryForFile ( file string ) error {
baseDir := filepath . Dir ( file )
_ , err := os . Stat ( baseDir )
if err != nil && ! errors . Is ( err , fs . ErrNotExist ) {
return err
}
return os . MkdirAll ( baseDir , defaultDirectoryPermission )
}
// NameAndChart returns the name and chart that should be used.
//
// This will read the flags and handle name generation if necessary.
func ( i * Install ) NameAndChart ( args [ ] string ) ( string , string , error ) {
flagsNotSet := func ( ) error {
if i . GenerateName {
return errors . New ( "cannot set --generate-name and also specify a name" )
}
if i . NameTemplate != "" {
return errors . New ( "cannot set --name-template and also specify a name" )
}
return nil
}
if len ( args ) > 2 {
return args [ 0 ] , args [ 1 ] , fmt . Errorf ( "expected at most two arguments, unexpected arguments: %v" , strings . Join ( args [ 2 : ] , ", " ) )
}
if len ( args ) == 2 {
return args [ 0 ] , args [ 1 ] , flagsNotSet ( )
}
if i . NameTemplate != "" {
name , err := TemplateName ( i . NameTemplate )
return name , args [ 0 ] , err
}
if i . ReleaseName != "" {
return i . ReleaseName , args [ 0 ] , nil
}
if ! i . GenerateName {
return "" , args [ 0 ] , errors . New ( "must either provide a name or specify --generate-name" )
}
base := filepath . Base ( args [ 0 ] )
if base == "." || base == "" {
base = "chart"
}
// if present, strip out the file extension from the name
if idx := strings . Index ( base , "." ) ; idx != - 1 {
base = base [ 0 : idx ]
}
return fmt . Sprintf ( "%s-%d" , base , time . Now ( ) . Unix ( ) ) , args [ 0 ] , nil
}
// TemplateName renders a name template, returning the name or an error.
func TemplateName ( nameTemplate string ) ( string , error ) {
if nameTemplate == "" {
return "" , nil
}
t , err := template . New ( "name-template" ) . Funcs ( sprig . TxtFuncMap ( ) ) . Parse ( nameTemplate )
if err != nil {
return "" , err
}
var b bytes . Buffer
if err := t . Execute ( & b , nil ) ; err != nil {
return "" , err
}
return b . String ( ) , nil
}
// CheckDependencies checks the dependencies for a chart.
func CheckDependencies ( ch ci . Charter , reqs [ ] ci . Dependency ) error {
ac , err := ci . NewAccessor ( ch )
if err != nil {
return err
}
var missing [ ] string
OUTER :
for _ , r := range reqs {
rac , err := ci . NewDependencyAccessor ( r )
if err != nil {
return err
}
for _ , d := range ac . Dependencies ( ) {
dac , err := ci . NewAccessor ( d )
if err != nil {
return err
}
if dac . Name ( ) == rac . Name ( ) {
continue OUTER
}
}
missing = append ( missing , rac . Name ( ) )
}
if len ( missing ) > 0 {
return fmt . Errorf ( "found in Chart.yaml, but missing in charts/ directory: %s" , strings . Join ( missing , ", " ) )
}
return nil
}
func portOrDefault ( u * url . URL ) string {
if p := u . Port ( ) ; p != "" {
return p
}
switch u . Scheme {
case "http" :
return "80"
case "https" :
return "443"
default :
return ""
}
}
func urlEqual ( u1 , u2 * url . URL ) bool {
return u1 . Scheme == u2 . Scheme && u1 . Hostname ( ) == u2 . Hostname ( ) && portOrDefault ( u1 ) == portOrDefault ( u2 )
}
// LocateChart looks for a chart directory in known places, and returns either the full path or an error.
//
// This does not ensure that the chart is well-formed; only that the requested filename exists.
//
// Order of resolution:
// - relative to current working directory when --repo flag is not presented
// - if path is absolute or begins with '.', error out here
// - URL
//
// If 'verify' was set on ChartPathOptions, this will attempt to also verify the chart.
func ( c * ChartPathOptions ) LocateChart ( name string , settings * cli . EnvSettings ) ( string , error ) {
if registry . IsOCI ( name ) && c . registryClient == nil {
return "" , fmt . Errorf ( "unable to lookup chart %q, missing registry client" , name )
}
name = strings . TrimSpace ( name )
version := strings . TrimSpace ( c . Version )
if c . RepoURL == "" {
if _ , err := os . Stat ( name ) ; err == nil {
abs , err := filepath . Abs ( name )
if err != nil {
return abs , err
}
if c . Verify {
if _ , err := downloader . VerifyChart ( abs , abs + ".prov" , c . Keyring ) ; err != nil {
return "" , err
}
}
return abs , nil
}
if filepath . IsAbs ( name ) || strings . HasPrefix ( name , "." ) {
return name , fmt . Errorf ( "path %q not found" , name )
}
}
dl := downloader . ChartDownloader {
Out : os . Stdout ,
Keyring : c . Keyring ,
Getters : getter . All ( settings ) ,
Options : [ ] getter . Option {
getter . WithPassCredentialsAll ( c . PassCredentialsAll ) ,
getter . WithTLSClientConfig ( c . CertFile , c . KeyFile , c . CaFile ) ,
getter . WithInsecureSkipVerifyTLS ( c . InsecureSkipTLSVerify ) ,
getter . WithPlainHTTP ( c . PlainHTTP ) ,
getter . WithBasicAuth ( c . Username , c . Password ) ,
} ,
RepositoryConfig : settings . RepositoryConfig ,
RepositoryCache : settings . RepositoryCache ,
ContentCache : settings . ContentCache ,
RegistryClient : c . registryClient ,
}
if registry . IsOCI ( name ) {
dl . Options = append ( dl . Options , getter . WithRegistryClient ( c . registryClient ) )
}
if c . Verify {
dl . Verify = downloader . VerifyAlways
}
if c . RepoURL != "" {
chartURL , err := repo . FindChartInRepoURL (
c . RepoURL ,
name ,
getter . All ( settings ) ,
repo . WithChartVersion ( version ) ,
repo . WithClientTLS ( c . CertFile , c . KeyFile , c . CaFile ) ,
repo . WithUsernamePassword ( c . Username , c . Password ) ,
repo . WithInsecureSkipTLSVerify ( c . InsecureSkipTLSVerify ) ,
repo . WithPassCredentialsAll ( c . PassCredentialsAll ) ,
)
if err != nil {
return "" , err
}
name = chartURL
// Only pass the user/pass on when the user has said to or when the
// location of the chart repo and the chart are the same domain.
u1 , err := url . Parse ( c . RepoURL )
if err != nil {
return "" , err
}
u2 , err := url . Parse ( chartURL )
if err != nil {
return "" , err
}
// Host on URL (returned from url.Parse) contains the port if present.
// This check ensures credentials are not passed between different
// services on different ports.
if c . PassCredentialsAll || urlEqual ( u1 , u2 ) {
dl . Options = append ( dl . Options , getter . WithBasicAuth ( c . Username , c . Password ) )
} else {
dl . Options = append ( dl . Options , getter . WithBasicAuth ( "" , "" ) )
}
} else {
dl . Options = append ( dl . Options , getter . WithBasicAuth ( c . Username , c . Password ) )
}
if err := os . MkdirAll ( settings . RepositoryCache , 0 o755 ) ; err != nil {
return "" , err
}
filename , _ , err := dl . DownloadToCache ( name , version )
if err != nil {
return "" , err
}
lname , err := filepath . Abs ( filename )
if err != nil {
return filename , err
}
return lname , nil
}