github.com/helmwave/helmwave@v0.36.4-0.20240509190856-b35563eba4c6/pkg/release/chart.go (about) 1 package release 2 3 import ( 4 "crypto/sha256" 5 "encoding/hex" 6 "errors" 7 "fmt" 8 "io" 9 "os" 10 "path" 11 "path/filepath" 12 "slices" 13 "strings" 14 15 "github.com/helmwave/helmwave/pkg/helper" 16 log "github.com/sirupsen/logrus" 17 "gopkg.in/yaml.v3" 18 "helm.sh/helm/v3/pkg/action" 19 "helm.sh/helm/v3/pkg/chart" 20 "helm.sh/helm/v3/pkg/chart/loader" 21 "helm.sh/helm/v3/pkg/downloader" 22 "helm.sh/helm/v3/pkg/getter" 23 "helm.sh/helm/v3/pkg/helmpath" 24 "helm.sh/helm/v3/pkg/registry" 25 "helm.sh/helm/v3/pkg/repo" 26 ) 27 28 // Chart is a structure for chart download options. 29 // 30 //nolint:lll 31 type Chart struct { 32 Name string `yaml:"name" json:"name" jsonschema:"required,description=Name of the chart,example=bitnami/nginx,example=oci://ghcr.io/helmwave/unit-test-oci"` 33 CaFile string `yaml:"ca_file" json:"ca_file" jsonschema:"description=Verify certificates of HTTPS-enabled servers using this CA bundle"` 34 CertFile string `yaml:"cert_file" json:"cert_file" jsonschema:"description=Identify HTTPS client using this SSL certificate file"` 35 KeyFile string `yaml:"key_file" json:"key_file" jsonschema:"description=Identify HTTPS client using this SSL key file"` 36 Keyring string `yaml:"keyring" json:"keyring" jsonschema:"description=Location of public keys used for verification"` 37 RepoURL string `yaml:"repo_url" json:"repo_url" jsonschema:"description=Chart repository url"` 38 Username string `yaml:"username" json:"username" jsonschema:"description=Chart repository username"` 39 Password string `yaml:"password" json:"password" jsonschema:"description=Chart repository password"` 40 Version string `yaml:"version" json:"version" jsonschema:"description=Chart version"` 41 InsecureSkipTLSverify bool `yaml:"insecure" json:"insecure" jsonschema:"description=Connect to server with an insecure way by skipping certificate verification"` 42 Verify bool `yaml:"verify" json:"verify" jsonschema:"description=Verify the provenance of the chart before using it"` 43 PassCredentialsAll bool `yaml:"pass_credentials" json:"pass_credentials" jsonschema:"description=Pass credentials to all domains"` 44 PlainHTTP bool `yaml:"plain_http" json:"plain_http" jsonschema:"description=Connect to server with plain http and not https,default=false"` 45 SkipDependencyUpdate bool `yaml:"skip_dependency_update" json:"skip_dependency_update" jsonschema:"description=Skip updating and downloading dependencies,default=false"` 46 SkipRefresh bool `yaml:"skip_refresh,omitempty" json:"skip_refresh,omitempty" jsonschema:"description=Skip refreshing repositories,default=false"` 47 } 48 49 // CopyOptions is a helper for copy options from Chart to ChartPathOptions. 50 func (c *Chart) CopyOptions(cpo *action.ChartPathOptions) { 51 // I hate private field without normal New(...Options) 52 cpo.CaFile = c.CaFile 53 cpo.CertFile = c.CertFile 54 cpo.KeyFile = c.KeyFile 55 cpo.InsecureSkipTLSverify = c.InsecureSkipTLSverify 56 cpo.PlainHTTP = c.PlainHTTP 57 cpo.Keyring = c.Keyring 58 cpo.Password = c.Password 59 cpo.PassCredentialsAll = c.PassCredentialsAll 60 cpo.RepoURL = c.RepoURL 61 cpo.Username = c.Username 62 cpo.Verify = c.Verify 63 cpo.Version = c.Version 64 } 65 66 // UnmarshalYAML flexible config. 67 func (c *Chart) UnmarshalYAML(node *yaml.Node) error { 68 type raw Chart 69 var err error 70 71 switch node.Kind { 72 case yaml.ScalarNode, yaml.AliasNode: 73 err = node.Decode(&(c.Name)) 74 case yaml.MappingNode: 75 err = node.Decode((*raw)(c)) 76 default: 77 err = ErrUnknownFormat 78 } 79 80 if err != nil { 81 return fmt.Errorf("failed to decode chart %q from YAML at %d line: %w", node.Value, node.Line, err) 82 } 83 84 return nil 85 } 86 87 func (c *Chart) IsRemote() bool { 88 return !helper.IsExists(filepath.Clean(c.Name)) 89 } 90 91 func (rel *config) LocateChartWithCache() (string, error) { 92 if !rel.Chart().IsRemote() { 93 return rel.Chart().Name, nil 94 } 95 96 ch, err := rel.findChartInHelmCache() 97 if err == nil { 98 rel.Logger().WithField("path", ch).Info("❎ found chart in helm cache, using it") 99 100 return ch, nil 101 } 102 103 rel.Logger().WithError(err).Debug("haven't found chart in helm cache, need to download it") 104 105 // nice action bro 106 client := rel.newInstall() 107 108 ch, err = client.ChartPathOptions.LocateChart(rel.Chart().Name, rel.Helm()) 109 if err != nil { 110 return "", fmt.Errorf("failed to locate chart %s: %w", rel.Chart().Name, err) 111 } 112 113 return ch, nil 114 } 115 116 func (rel *config) getDownloader() downloader.ChartDownloader { 117 settings := rel.Helm() 118 client := rel.newInstall() 119 120 return downloader.ChartDownloader{ 121 Getters: getter.All(settings), 122 Options: []getter.Option{ 123 getter.WithPassCredentialsAll(client.ChartPathOptions.PassCredentialsAll), 124 getter.WithTLSClientConfig( 125 client.ChartPathOptions.CertFile, 126 client.ChartPathOptions.KeyFile, 127 client.ChartPathOptions.CaFile, 128 ), 129 getter.WithInsecureSkipVerifyTLS(client.ChartPathOptions.InsecureSkipTLSverify), 130 }, 131 RepositoryConfig: settings.RepositoryConfig, 132 RepositoryCache: settings.RepositoryCache, 133 RegistryClient: client.GetRegistryClient(), 134 } 135 } 136 137 // Helm doesn't use its own charts cache, it only stores charts there. So we copypaste some code from 138 // *downloader.ChartDownloader to find already downloaded charts in our cache. 139 // We also check chart file digest in case of any collision. 140 func (rel *config) findChartInHelmCache() (string, error) { 141 settings := rel.Helm() 142 143 dl := rel.getDownloader() 144 145 u, err := dl.ResolveChartVersion(rel.Chart().Name, rel.Chart().Version) 146 if err != nil { 147 return "", NewChartCacheError(err) 148 } 149 150 name := filepath.Base(u.Path) 151 if u.Scheme == registry.OCIScheme { 152 idx := strings.LastIndexByte(name, ':') 153 name = fmt.Sprintf("%s-%s.tgz", name[:idx], name[idx+1:]) 154 155 rel.Logger().Debug("digest validation is not supported for OCI charts, skipping it") 156 157 chartFile := filepath.Join(settings.RepositoryCache, name) 158 159 _, err := os.Stat(chartFile) 160 if err != nil { 161 return "", NewChartCacheError(err) 162 } 163 164 return chartFile, nil 165 } 166 167 chartFile := filepath.Join(settings.RepositoryCache, name) 168 169 ch, err := rel.getChartRepoEntryFromIndex(u.String(), settings.RepositoryCache) 170 if err != nil { 171 return "", NewChartCacheError(err) 172 } 173 174 digest := ch.Digest 175 hasher := sha256.New() 176 177 f, err := os.Open(chartFile) 178 if err != nil { 179 return "", NewChartCacheError(err) 180 } 181 defer func() { 182 _ = f.Close() 183 }() 184 185 _, err = io.Copy(hasher, f) 186 if err != nil { 187 return "", NewChartCacheError(err) 188 } 189 190 hashSum := hex.EncodeToString(hasher.Sum(nil)) 191 192 if hashSum != digest { 193 return "", NewChartCacheError(ErrDigestNotMatch) 194 } 195 196 return chartFile, nil 197 } 198 199 func (rel *config) getChartRepoEntryFromIndex(u, repositoryCache string) (*repo.ChartVersion, error) { 200 repoName := strings.SplitN(rel.Chart().Name, "/", 2)[0] 201 idxFile := filepath.Join(repositoryCache, helmpath.CacheIndexFile(repoName)) 202 i, err := repo.LoadIndexFile(idxFile) 203 if err != nil { 204 return nil, fmt.Errorf("no cached repo found: %w", err) 205 } 206 207 for _, entry := range i.Entries { 208 for _, ver := range entry { 209 if slices.Contains(ver.URLs, u) { 210 return ver, nil 211 } 212 } 213 } 214 215 return nil, errors.New("repo not found") 216 } 217 218 func (rel *config) GetChart() (*chart.Chart, error) { 219 ch, err := rel.LocateChartWithCache() 220 if err != nil { 221 return nil, err 222 } 223 224 c, err := loader.Load(ch) 225 if err != nil { 226 return nil, fmt.Errorf("failed to load chart %s: %w", rel.Chart().Name, err) 227 } 228 229 if err := rel.chartCheck(c); err != nil { 230 return nil, err 231 } 232 233 return c, nil 234 } 235 236 func (rel *config) chartCheck(ch *chart.Chart) error { 237 if req := ch.Metadata.Dependencies; req != nil { 238 if err := action.CheckDependencies(ch, req); err != nil { 239 return fmt.Errorf("failed to check chart %s dependencies: %w", ch.Name(), err) 240 } 241 } 242 243 if !(ch.Metadata.Type == "" || ch.Metadata.Type == "application") { 244 rel.Logger().Warnf("%s charts are not installable", ch.Metadata.Type) 245 } 246 247 if ch.Metadata.Deprecated { 248 rel.Logger().Warnf("⚠️ Chart %s is deprecated. Please update your chart.", ch.Name()) 249 } 250 251 return nil 252 } 253 254 func (rel *config) ChartDepsUpd() error { 255 if rel.Chart().IsRemote() { 256 rel.Logger().Info("❎ skipping updating dependencies for remote chart") 257 258 return nil 259 } 260 261 if rel.Chart().SkipDependencyUpdate { 262 rel.Logger().Info("❎ forced skipping updating dependencies for local chart") 263 264 return nil 265 } 266 267 settings := rel.Helm() 268 269 client := action.NewDependency() 270 man := &downloader.Manager{ 271 Out: log.StandardLogger().Writer(), 272 ChartPath: filepath.Clean(rel.Chart().Name), 273 Keyring: client.Keyring, 274 RegistryClient: helper.HelmRegistryClient, 275 SkipUpdate: rel.Chart().SkipRefresh, 276 Getters: getter.All(settings), 277 RepositoryConfig: settings.RepositoryConfig, 278 RepositoryCache: settings.RepositoryCache, 279 Debug: settings.Debug, 280 } 281 if client.Verify { 282 man.Verify = downloader.VerifyAlways 283 } 284 285 if err := man.Update(); err != nil { 286 return fmt.Errorf("failed to update %s chart dependencies: %w", rel.Chart().Name, err) 287 } 288 289 return nil 290 } 291 292 func (rel *config) DownloadChart(tmpDir string) error { 293 if !rel.Chart().IsRemote() { 294 rel.Logger().Info("❎ chart is local, skipping exporting") 295 296 return nil 297 } 298 299 destDir := path.Join(tmpDir, "charts", rel.Uniq().String()) 300 if err := os.MkdirAll(destDir, 0o750); err != nil { 301 return fmt.Errorf("failed to create temporary directory for chart: %w", err) 302 } 303 304 ch, err := rel.LocateChartWithCache() 305 if err != nil { 306 return err 307 } 308 309 return helper.CopyFile(ch, destDir) 310 } 311 312 func (rel *config) SetChartName(name string) { 313 rel.lock.Lock() 314 rel.ChartF.Name = name 315 rel.lock.Unlock() 316 }