@ -249,25 +249,24 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error {
destPath := filepath . Join ( m . ChartPath , "charts" )
destPath := filepath . Join ( m . ChartPath , "charts" )
tmpPath := filepath . Join ( m . ChartPath , "tmpcharts" )
tmpPath := filepath . Join ( m . ChartPath , "tmpcharts" )
// Create 'charts' directory if it doesn't already exist.
// Check if 'charts' directory is not actally a directory. If it does not exist, create it.
if fi , err := os . Stat ( destPath ) ; err != nil {
if fi , err := os . Stat ( destPath ) ; err == nil {
if err := os . MkdirAll ( destPath , 0755 ) ; err != nil {
if ! fi . IsDir ( ) {
return err
}
} else if ! fi . IsDir ( ) {
return errors . Errorf ( "%q is not a directory" , destPath )
return errors . Errorf ( "%q is not a directory" , destPath )
}
}
} else if os . IsNotExist ( err ) {
if err := os . RemoveAll( tmpPath ) ; err != nil {
if err := os . MkdirAll( destPath , 0755 ) ; err != nil {
return err ors. Wrapf ( err , "failed to remove %v" , tmpPath )
return err
}
}
if err := fs . RenameWithFallback ( destPath , tmpPath ) ; err != nil {
} else {
return errors. Wrap ( err , "unable to move current charts to tmp dir" )
return fmt. Errorf ( "unable to retrieve file info for '%s': %v" , destPath , err )
}
}
if err := os . MkdirAll ( destPath , 0755 ) ; err != nil {
// Prepare tmpPath
if err := os . MkdirAll ( tmpPath , 0755 ) ; err != nil {
return err
return err
}
}
defer os . RemoveAll ( tmpPath )
fmt . Fprintf ( m . Out , "Saving %d charts\n" , len ( deps ) )
fmt . Fprintf ( m . Out , "Saving %d charts\n" , len ( deps ) )
var saveError error
var saveError error
@ -276,10 +275,11 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error {
// No repository means the chart is in charts directory
// No repository means the chart is in charts directory
if dep . Repository == "" {
if dep . Repository == "" {
fmt . Fprintf ( m . Out , "Dependency %s did not declare a repository. Assuming it exists in the charts directory\n" , dep . Name )
fmt . Fprintf ( m . Out , "Dependency %s did not declare a repository. Assuming it exists in the charts directory\n" , dep . Name )
chartPath := filepath . Join ( tmpPath , dep . Name )
// NOTE: we are only validating the local dependency conforms to the constraints. No copying to tmpPath is necessary.
chartPath := filepath . Join ( destPath , dep . Name )
ch , err := loader . LoadDir ( chartPath )
ch , err := loader . LoadDir ( chartPath )
if err != nil {
if err != nil {
return fmt . Errorf ( "unable to load chart : %v", err )
return fmt . Errorf ( "unable to load chart '%s' : %v", chartPath , err )
}
}
constraint , err := semver . NewConstraint ( dep . Version )
constraint , err := semver . NewConstraint ( dep . Version )
@ -342,7 +342,7 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error {
}
}
version := ""
version := ""
if registry. IsOCI ( churl ) {
if strings. HasPrefix ( churl , "oci://" ) {
if ! resolver . FeatureGateOCI . IsEnabled ( ) {
if ! resolver . FeatureGateOCI . IsEnabled ( ) {
return errors . Wrapf ( resolver . FeatureGateOCI . Error ( ) ,
return errors . Wrapf ( resolver . FeatureGateOCI . Error ( ) ,
"the repository %s is an OCI registry" , churl )
"the repository %s is an OCI registry" , churl )
@ -357,8 +357,7 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error {
getter . WithTagName ( version ) )
getter . WithTagName ( version ) )
}
}
_ , _ , err = dl . DownloadTo ( churl , version , destPath )
if _ , _ , err = dl . DownloadTo ( churl , version , tmpPath ) ; err != nil {
if err != nil {
saveError = errors . Wrapf ( err , "could not download %s" , churl )
saveError = errors . Wrapf ( err , "could not download %s" , churl )
break
break
}
}
@ -366,43 +365,21 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error {
churls [ churl ] = struct { } { }
churls [ churl ] = struct { } { }
}
}
// TODO: this should probably be refactored to be a []error, so we can capture and provide more information rather than "last error wins".
if saveError == nil {
if saveError == nil {
fmt . Fprintln ( m . Out , "Deleting outdated charts" )
// now we can move all downloaded charts to destPath and delete outdated dependencies
for _ , dep := range deps {
if err := m . safeMoveDeps ( deps , tmpPath , destPath ) ; err != nil {
// Chart from local charts directory stays in place
if dep . Repository != "" {
if err := m . safeDeleteDep ( dep . Name , tmpPath ) ; err != nil {
return err
}
}
}
if err := move ( tmpPath , destPath ) ; err != nil {
return err
return err
}
}
if err := os . RemoveAll ( tmpPath ) ; err != nil {
return errors . Wrapf ( err , "failed to remove %v" , tmpPath )
}
} else {
} else {
fmt . Fprintln ( m . Out , "Save error occurred: " , saveError )
fmt . Fprintln ( m . Out , "Save error occurred: " , saveError )
fmt . Fprintln ( m . Out , "Deleting newly downloaded charts, restoring pre-update state" )
for _ , dep := range deps {
if err := m . safeDeleteDep ( dep . Name , destPath ) ; err != nil {
return err
}
}
if err := os . RemoveAll ( destPath ) ; err != nil {
return errors . Wrapf ( err , "failed to remove %v" , destPath )
}
if err := fs . RenameWithFallback ( tmpPath , destPath ) ; err != nil {
return errors . Wrap ( err , "unable to move current charts to tmp dir" )
}
return saveError
return saveError
}
}
return nil
return nil
}
}
func parseOCIRef ( chartRef string ) ( string , string , error ) {
func parseOCIRef ( chartRef string ) ( string , string , error ) {
refTagRegexp := regexp . MustCompile ( fmt . Sprintf ( ` ^(%s ://[^:]+(:[0-9]{ 1,5})?[^:]+):(.*)$ ` , registry . OCIScheme ) )
refTagRegexp := regexp . MustCompile ( ` ^(oci://[^:]+(:[0-9] { 1,5})?[^:]+):(.*)$ ` )
caps := refTagRegexp . FindStringSubmatch ( chartRef )
caps := refTagRegexp . FindStringSubmatch ( chartRef )
if len ( caps ) != 4 {
if len ( caps ) != 4 {
return "" , "" , errors . Errorf ( "improperly formatted oci chart reference: %s" , chartRef )
return "" , "" , errors . Errorf ( "improperly formatted oci chart reference: %s" , chartRef )
@ -413,29 +390,66 @@ func parseOCIRef(chartRef string) (string, string, error) {
return chartRef , tag , nil
return chartRef , tag , nil
}
}
// safe DeleteDep deletes any versions of the given dependency in the given directory .
// safe MoveDep moves all dependencies in the source and moves them into dest .
//
//
// It does this by first matching the file name to an expected pattern, then loading
// It does this by first matching the file name to an expected pattern, then loading
// the file to verify that it is a chart with the same name as the given name .
// the file to verify that it is a chart .
//
//
// Because it requires tar file introspection, it is more intensive than a basic delete.
// Any charts in dest that do not exist in source are removed (barring local dependencies)
//
// Because it requires tar file introspection, it is more intensive than a basic move.
//
//
// This will only return errors that should stop processing entirely. Other errors
// This will only return errors that should stop processing entirely. Other errors
// will emit log messages or be ignored.
// will emit log messages or be ignored.
func ( m * Manager ) safeDeleteDep ( name , dir string ) error {
func ( m * Manager ) safeMoveDeps ( deps [ ] * chart . Dependency , source , dest string ) error {
files , err := filepath . Glob ( filepath . Join ( dir , name + "-*.tgz" ) )
existsInSourceDirectory := map [ string ] bool { }
isLocalDependency := map [ string ] bool { }
sourceFiles , err := ioutil . ReadDir ( source )
if err != nil {
if err != nil {
// Only for ErrBadPattern
return err
return err
}
}
for _ , fname := range files {
// attempt to read destFiles; fail fast if we can't
destFiles , err := ioutil . ReadDir ( dest )
if err != nil {
return err
}
for _ , dep := range deps {
if dep . Repository == "" {
isLocalDependency [ dep . Name ] = true
}
}
for _ , file := range sourceFiles {
if file . IsDir ( ) {
continue
}
filename := file . Name ( )
sourcefile := filepath . Join ( source , filename )
destfile := filepath . Join ( dest , filename )
existsInSourceDirectory [ filename ] = true
if _ , err := loader . LoadFile ( sourcefile ) ; err != nil {
fmt . Fprintf ( m . Out , "Could not verify %s for moving: %s (Skipping)" , sourcefile , err )
continue
}
// NOTE: no need to delete the dest; os.Rename replaces it.
if err := fs . RenameWithFallback ( sourcefile , destfile ) ; err != nil {
fmt . Fprintf ( m . Out , "Unable to move %s to charts dir %s (Skipping)" , sourcefile , err )
continue
}
}
fmt . Fprintln ( m . Out , "Deleting outdated charts" )
// find all files that exist in dest that do not exist in source; delete them (outdated dependendencies)
for _ , file := range destFiles {
if ! file . IsDir ( ) && ! existsInSourceDirectory [ file . Name ( ) ] {
fname := filepath . Join ( dest , file . Name ( ) )
ch , err := loader . LoadFile ( fname )
ch , err := loader . LoadFile ( fname )
if err != nil {
if err != nil {
fmt . Fprintf ( m . Out , "Could not verify %s for deletion: %s (Skipping)" , fname , err )
fmt . Fprintf ( m . Out , "Could not verify %s for deletion: %s (Skipping)" , fname , err )
continue
}
}
if ch . Name ( ) != name {
// local dependency - skip
// This is not the file you are looking for.
if isLocalDependency [ ch . Name ( ) ] {
continue
continue
}
}
if err := os . Remove ( fname ) ; err != nil {
if err := os . Remove ( fname ) ; err != nil {
@ -443,6 +457,8 @@ func (m *Manager) safeDeleteDep(name, dir string) error {
continue
continue
}
}
}
}
}
return nil
return nil
}
}
@ -460,7 +476,7 @@ func (m *Manager) hasAllRepos(deps []*chart.Dependency) error {
Loop :
Loop :
for _ , dd := range deps {
for _ , dd := range deps {
// If repo is from local path or OCI, continue
// If repo is from local path or OCI, continue
if strings . HasPrefix ( dd . Repository , "file://" ) || registry. IsOCI ( dd . Repository ) {
if strings . HasPrefix ( dd . Repository , "file://" ) || strings. HasPrefix ( dd . Repository , "oci://" ) {
continue
continue
}
}
@ -497,11 +513,6 @@ func (m *Manager) ensureMissingRepos(repoNames map[string]string, deps []*chart.
continue
continue
}
}
// OCI-based dependencies do not have a local cache, so skip them
if registry . IsOCI ( dd . Repository ) {
continue
}
// When the repoName for a dependency is known we can skip ensuring
// When the repoName for a dependency is known we can skip ensuring
if _ , ok := repoNames [ dd . Name ] ; ok {
if _ , ok := repoNames [ dd . Name ] ; ok {
continue
continue
@ -567,7 +578,7 @@ func (m *Manager) resolveRepoNames(deps []*chart.Dependency) (map[string]string,
for _ , dd := range deps {
for _ , dd := range deps {
// Don't map the repository, we don't need to download chart from charts directory
// Don't map the repository, we don't need to download chart from charts directory
// When OCI is used there is no Helm repository
// When OCI is used there is no Helm repository
if dd . Repository == "" || registry. IsOCI ( dd . Repository ) {
if dd . Repository == "" || strings. HasPrefix ( dd . Repository , "oci://" ) {
continue
continue
}
}
// if dep chart is from local path, verify the path is valid
// if dep chart is from local path, verify the path is valid
@ -583,7 +594,7 @@ func (m *Manager) resolveRepoNames(deps []*chart.Dependency) (map[string]string,
continue
continue
}
}
if registry. IsOCI ( dd . Repository ) {
if strings. HasPrefix ( dd . Repository , "oci://" ) {
reposMap [ dd . Name ] = dd . Repository
reposMap [ dd . Name ] = dd . Repository
continue
continue
}
}
@ -697,7 +708,7 @@ func (m *Manager) parallelRepoUpdate(repos []*repo.Entry) error {
//
//
// If it finds a URL that is "relative", it will prepend the repoURL.
// If it finds a URL that is "relative", it will prepend the repoURL.
func ( m * Manager ) findChartURL ( name , version , repoURL string , repos map [ string ] * repo . ChartRepository ) ( url , username , password string , insecureskiptlsverify , passcredentialsall bool , caFile , certFile , keyFile string , err error ) {
func ( m * Manager ) findChartURL ( name , version , repoURL string , repos map [ string ] * repo . ChartRepository ) ( url , username , password string , insecureskiptlsverify , passcredentialsall bool , caFile , certFile , keyFile string , err error ) {
if registry. IsOCI ( repoURL ) {
if strings. HasPrefix ( repoURL , "oci://" ) {
return fmt . Sprintf ( "%s/%s:%s" , repoURL , name , version ) , "" , "" , false , false , "" , "" , "" , nil
return fmt . Sprintf ( "%s/%s:%s" , repoURL , name , version ) , "" , "" , false , false , "" , "" , "" , nil
}
}
@ -876,20 +887,6 @@ func tarFromLocalDir(chartpath, name, repo, version string) (string, error) {
return "" , errors . Errorf ( "can't get a valid version for dependency %s" , name )
return "" , errors . Errorf ( "can't get a valid version for dependency %s" , name )
}
}
// move files from tmppath to destpath
func move ( tmpPath , destPath string ) error {
files , _ := os . ReadDir ( tmpPath )
for _ , file := range files {
filename := file . Name ( )
tmpfile := filepath . Join ( tmpPath , filename )
destfile := filepath . Join ( destPath , filename )
if err := fs . RenameWithFallback ( tmpfile , destfile ) ; err != nil {
return errors . Wrap ( err , "unable to move local charts to charts dir" )
}
}
return nil
}
// The prefix to use for cache keys created by the manager for repo names
// The prefix to use for cache keys created by the manager for repo names
const managerKeyPrefix = "helm-manager-"
const managerKeyPrefix = "helm-manager-"