github.com/Racer159/jackal@v0.32.7-0.20240401174413-0bd2339e4f2e/src/internal/packager/helm/repo.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 // SPDX-FileCopyrightText: 2021-Present The Jackal Authors 3 4 // Package helm contains operations for working with helm charts. 5 package helm 6 7 import ( 8 "fmt" 9 "os" 10 "path/filepath" 11 "strings" 12 13 "github.com/Racer159/jackal/src/config" 14 "github.com/Racer159/jackal/src/config/lang" 15 "github.com/Racer159/jackal/src/internal/packager/git" 16 "github.com/Racer159/jackal/src/pkg/message" 17 "github.com/Racer159/jackal/src/pkg/transform" 18 "github.com/Racer159/jackal/src/pkg/utils" 19 "github.com/Racer159/jackal/src/types" 20 "github.com/defenseunicorns/pkg/helpers" 21 "helm.sh/helm/v3/pkg/action" 22 "helm.sh/helm/v3/pkg/chart" 23 "helm.sh/helm/v3/pkg/cli" 24 "helm.sh/helm/v3/pkg/helmpath" 25 "helm.sh/helm/v3/pkg/registry" 26 "k8s.io/client-go/util/homedir" 27 28 "helm.sh/helm/v3/pkg/chart/loader" 29 "helm.sh/helm/v3/pkg/downloader" 30 "helm.sh/helm/v3/pkg/getter" 31 "helm.sh/helm/v3/pkg/repo" 32 ) 33 34 // PackageChart creates a chart archive from a path to a chart on the host os and builds chart dependencies 35 func (h *Helm) PackageChart(cosignKeyPath string) error { 36 if len(h.chart.URL) > 0 { 37 url, refPlain, err := transform.GitURLSplitRef(h.chart.URL) 38 // check if the chart is a git url with a ref (if an error is returned url will be empty) 39 isGitURL := strings.HasSuffix(url, ".git") 40 if err != nil { 41 message.Debugf("unable to parse the url, continuing with %s", h.chart.URL) 42 } 43 44 if isGitURL { 45 // if it is a git url append chart version as if its a tag 46 if refPlain == "" { 47 h.chart.URL = fmt.Sprintf("%s@%s", h.chart.URL, h.chart.Version) 48 } 49 50 err = h.PackageChartFromGit(cosignKeyPath) 51 if err != nil { 52 return fmt.Errorf("unable to pull the chart %q from git: %w", h.chart.Name, err) 53 } 54 } else { 55 err = h.DownloadPublishedChart(cosignKeyPath) 56 if err != nil { 57 return fmt.Errorf("unable to download the published chart %q: %w", h.chart.Name, err) 58 } 59 } 60 61 } else { 62 err := h.PackageChartFromLocalFiles(cosignKeyPath) 63 if err != nil { 64 return fmt.Errorf("unable to package the %q chart: %w", h.chart.Name, err) 65 } 66 } 67 return nil 68 } 69 70 // PackageChartFromLocalFiles creates a chart archive from a path to a chart on the host os. 71 func (h *Helm) PackageChartFromLocalFiles(cosignKeyPath string) error { 72 spinner := message.NewProgressSpinner("Processing helm chart %s:%s from %s", h.chart.Name, h.chart.Version, h.chart.LocalPath) 73 defer spinner.Stop() 74 75 // Load and validate the chart 76 cl, _, err := h.loadAndValidateChart(h.chart.LocalPath) 77 if err != nil { 78 return err 79 } 80 81 // Handle the chart directory or tarball 82 var saved string 83 temp := filepath.Join(h.chartPath, "temp") 84 if _, ok := cl.(loader.DirLoader); ok { 85 err = h.buildChartDependencies(spinner) 86 if err != nil { 87 return fmt.Errorf("unable to build dependencies for the chart: %w", err) 88 } 89 90 client := action.NewPackage() 91 92 client.Destination = temp 93 saved, err = client.Run(h.chart.LocalPath, nil) 94 } else { 95 saved = filepath.Join(temp, filepath.Base(h.chart.LocalPath)) 96 err = helpers.CreatePathAndCopy(h.chart.LocalPath, saved) 97 } 98 defer os.RemoveAll(temp) 99 100 if err != nil { 101 return fmt.Errorf("unable to save the archive and create the package %s: %w", saved, err) 102 } 103 104 // Finalize the chart 105 err = h.finalizeChartPackage(saved, cosignKeyPath) 106 if err != nil { 107 return err 108 } 109 110 spinner.Success() 111 112 return nil 113 } 114 115 // PackageChartFromGit is a special implementation of chart archiving that supports the https://p1.dso.mil/#/products/big-bang/ model. 116 func (h *Helm) PackageChartFromGit(cosignKeyPath string) error { 117 spinner := message.NewProgressSpinner("Processing helm chart %s", h.chart.Name) 118 defer spinner.Stop() 119 120 // Retrieve the repo containing the chart 121 gitPath, err := DownloadChartFromGitToTemp(h.chart.URL, spinner) 122 if err != nil { 123 return err 124 } 125 defer os.RemoveAll(gitPath) 126 127 // Set the directory for the chart and package it 128 h.chart.LocalPath = filepath.Join(gitPath, h.chart.GitPath) 129 return h.PackageChartFromLocalFiles(cosignKeyPath) 130 } 131 132 // DownloadPublishedChart loads a specific chart version from a remote repo. 133 func (h *Helm) DownloadPublishedChart(cosignKeyPath string) error { 134 spinner := message.NewProgressSpinner("Processing helm chart %s:%s from repo %s", h.chart.Name, h.chart.Version, h.chart.URL) 135 defer spinner.Stop() 136 137 // Set up the helm pull config 138 pull := action.NewPull() 139 pull.Settings = cli.New() 140 141 var ( 142 regClient *registry.Client 143 chartURL string 144 err error 145 ) 146 repoFile, err := repo.LoadFile(pull.Settings.RepositoryConfig) 147 148 // Not returning the error here since the repo file is only needed if we are pulling from a repo that requires authentication 149 if err != nil { 150 message.Debugf("Unable to load the repo file at %q: %s", pull.Settings.RepositoryConfig, err.Error()) 151 } 152 153 var username string 154 var password string 155 156 // Handle OCI registries 157 if registry.IsOCI(h.chart.URL) { 158 regClient, err = registry.NewClient(registry.ClientOptEnableCache(true)) 159 if err != nil { 160 spinner.Fatalf(err, "Unable to create a new registry client") 161 } 162 chartURL = h.chart.URL 163 // Explicitly set the pull version for OCI 164 pull.Version = h.chart.Version 165 } else { 166 chartName := h.chart.Name 167 if h.chart.RepoName != "" { 168 chartName = h.chart.RepoName 169 } 170 171 if repoFile != nil { 172 // TODO: @AustinAbro321 Currently this selects the last repo with the same url 173 // We should introduce a new field in jackal to allow users to specify the local repo they want 174 for _, repo := range repoFile.Repositories { 175 if repo.URL == h.chart.URL { 176 username = repo.Username 177 password = repo.Password 178 } 179 } 180 } 181 182 chartURL, err = repo.FindChartInAuthRepoURL(h.chart.URL, username, password, chartName, h.chart.Version, pull.CertFile, pull.KeyFile, pull.CaFile, getter.All(pull.Settings)) 183 if err != nil { 184 if strings.Contains(err.Error(), "not found") { 185 // Intentionally dogsled this error since this is just a nice to have helper 186 _ = h.listAvailableChartsAndVersions(pull) 187 } 188 return fmt.Errorf("unable to pull the helm chart: %w", err) 189 } 190 } 191 192 // Set up the chart chartDownloader 193 chartDownloader := downloader.ChartDownloader{ 194 Out: spinner, 195 RegistryClient: regClient, 196 // TODO: Further research this with regular/OCI charts 197 Verify: downloader.VerifyNever, 198 Getters: getter.All(pull.Settings), 199 Options: []getter.Option{ 200 getter.WithInsecureSkipVerifyTLS(config.CommonOptions.Insecure), 201 getter.WithBasicAuth(username, password), 202 }, 203 } 204 205 // Download the file into a temp directory since we don't control what name helm creates here 206 temp := filepath.Join(h.chartPath, "temp") 207 if err = helpers.CreateDirectory(temp, helpers.ReadWriteExecuteUser); err != nil { 208 return fmt.Errorf("unable to create helm chart temp directory: %w", err) 209 } 210 defer os.RemoveAll(temp) 211 212 saved, _, err := chartDownloader.DownloadTo(chartURL, pull.Version, temp) 213 if err != nil { 214 return fmt.Errorf("unable to download the helm chart: %w", err) 215 } 216 217 // Validate the chart 218 _, _, err = h.loadAndValidateChart(saved) 219 if err != nil { 220 return err 221 } 222 223 // Finalize the chart 224 err = h.finalizeChartPackage(saved, cosignKeyPath) 225 if err != nil { 226 return err 227 } 228 229 spinner.Success() 230 231 return nil 232 } 233 234 // DownloadChartFromGitToTemp downloads a chart from git into a temp directory 235 func DownloadChartFromGitToTemp(url string, spinner *message.Spinner) (string, error) { 236 // Create the Git configuration and download the repo 237 gitCfg := git.NewWithSpinner(types.GitServerInfo{}, spinner) 238 239 // Download the git repo to a temporary directory 240 err := gitCfg.DownloadRepoToTemp(url) 241 if err != nil { 242 return "", fmt.Errorf("unable to download the git repo %s: %w", url, err) 243 } 244 245 return gitCfg.GitPath, nil 246 } 247 248 func (h *Helm) finalizeChartPackage(saved, cosignKeyPath string) error { 249 // Ensure the name is consistent for deployments 250 destinationTarball := StandardName(h.chartPath, h.chart) + ".tgz" 251 err := os.Rename(saved, destinationTarball) 252 if err != nil { 253 return fmt.Errorf("unable to save the final chart tarball: %w", err) 254 } 255 256 err = h.packageValues(cosignKeyPath) 257 if err != nil { 258 return fmt.Errorf("unable to process the values for the package: %w", err) 259 } 260 return nil 261 } 262 263 func (h *Helm) packageValues(cosignKeyPath string) error { 264 for valuesIdx, path := range h.chart.ValuesFiles { 265 dst := StandardValuesName(h.valuesPath, h.chart, valuesIdx) 266 267 if helpers.IsURL(path) { 268 if err := utils.DownloadToFile(path, dst, cosignKeyPath); err != nil { 269 return fmt.Errorf(lang.ErrDownloading, path, err.Error()) 270 } 271 } else { 272 if err := helpers.CreatePathAndCopy(path, dst); err != nil { 273 return fmt.Errorf("unable to copy chart values file %s: %w", path, err) 274 } 275 } 276 } 277 278 return nil 279 } 280 281 // buildChartDependencies builds the helm chart dependencies 282 func (h *Helm) buildChartDependencies(spinner *message.Spinner) error { 283 // Download and build the specified dependencies 284 regClient, err := registry.NewClient(registry.ClientOptEnableCache(true)) 285 if err != nil { 286 spinner.Fatalf(err, "Unable to create a new registry client") 287 } 288 289 h.settings = cli.New() 290 defaultKeyring := filepath.Join(homedir.HomeDir(), ".gnupg", "pubring.gpg") 291 if v, ok := os.LookupEnv("GNUPGHOME"); ok { 292 defaultKeyring = filepath.Join(v, "pubring.gpg") 293 } 294 295 man := &downloader.Manager{ 296 Out: &message.DebugWriter{}, 297 ChartPath: h.chart.LocalPath, 298 Getters: getter.All(h.settings), 299 RegistryClient: regClient, 300 301 RepositoryConfig: h.settings.RepositoryConfig, 302 RepositoryCache: h.settings.RepositoryCache, 303 Debug: false, 304 Verify: downloader.VerifyIfPossible, 305 Keyring: defaultKeyring, 306 } 307 308 // Build the deps from the helm chart 309 err = man.Build() 310 if e, ok := err.(downloader.ErrRepoNotFound); ok { 311 // If we encounter a repo not found error point the user to `jackal tools helm repo add` 312 message.Warnf("%s. Please add the missing repo(s) via the following:", e.Error()) 313 for _, repository := range e.Repos { 314 message.JackalCommand(fmt.Sprintf("tools helm repo add <your-repo-name> %s", repository)) 315 } 316 } else if err != nil { 317 // Warn the user of any issues but don't fail - any actual issues will cause a fail during packaging (e.g. the charts we are building may exist already, we just can't get updates) 318 message.JackalCommand("tools helm dependency build --verify") 319 message.Warnf("Unable to perform a rebuild of Helm dependencies: %s", err.Error()) 320 } 321 322 return nil 323 } 324 325 func (h *Helm) loadAndValidateChart(location string) (loader.ChartLoader, *chart.Chart, error) { 326 // Validate the chart 327 cl, err := loader.Loader(location) 328 if err != nil { 329 return cl, nil, fmt.Errorf("unable to load the chart from %s: %w", location, err) 330 } 331 332 chart, err := cl.Load() 333 if err != nil { 334 return cl, chart, fmt.Errorf("validation failed for chart from %s: %w", location, err) 335 } 336 337 return cl, chart, nil 338 } 339 340 func (h *Helm) listAvailableChartsAndVersions(pull *action.Pull) error { 341 c := repo.Entry{ 342 URL: h.chart.URL, 343 CertFile: pull.CertFile, 344 KeyFile: pull.KeyFile, 345 CAFile: pull.CaFile, 346 Name: h.chart.Name, 347 } 348 349 r, err := repo.NewChartRepository(&c, getter.All(pull.Settings)) 350 if err != nil { 351 return err 352 } 353 idx, err := r.DownloadIndexFile() 354 if err != nil { 355 return fmt.Errorf("looks like %q is not a valid chart repository or cannot be reached: %w", h.chart.URL, err) 356 } 357 defer func() { 358 os.RemoveAll(filepath.Join(r.CachePath, helmpath.CacheChartsFile(r.Config.Name))) 359 os.RemoveAll(filepath.Join(r.CachePath, helmpath.CacheIndexFile(r.Config.Name))) 360 }() 361 362 // Read the index file for the repository to get chart information and return chart URL 363 repoIndex, err := repo.LoadIndexFile(idx) 364 if err != nil { 365 return err 366 } 367 368 chartData := [][]string{} 369 for name, entries := range repoIndex.Entries { 370 versions := "" 371 for idx, entry := range entries { 372 separator := "" 373 if idx < len(entries)-1 { 374 separator = ", " 375 } 376 versions += entry.Version + separator 377 } 378 379 versions = message.Truncate(versions, 75, false) 380 chartData = append(chartData, []string{name, versions}) 381 } 382 383 message.Notef("Available charts and versions from %q:", h.chart.URL) 384 385 // Print out the table for the user 386 header := []string{"Chart", "Versions"} 387 message.Table(header, chartData) 388 return nil 389 }