diff --git a/docs/how_to/helm_repos/README.md b/docs/how_to/helm_repos/README.md index 5f7eeeb8..134fda46 100644 --- a/docs/how_to/helm_repos/README.md +++ b/docs/how_to/helm_repos/README.md @@ -9,3 +9,4 @@ Following list contains guides on how to define helm repositories - [Using private repos with basic auth](basic_auth.md) - [Using pre-configured repos](pre_configured.md) - [Using local charts](local.md) +- [Using OCI registries](oci.md) diff --git a/docs/how_to/helm_repos/oci.md b/docs/how_to/helm_repos/oci.md new file mode 100644 index 00000000..2ebdf9ef --- /dev/null +++ b/docs/how_to/helm_repos/oci.md @@ -0,0 +1,34 @@ +--- +version: v3.7.0 +--- + +# Using OCI registries for helm charts + +Helmsman allows you to use charts stored in OCI registries. + +You need to export the following env variables: + +- `HELM_EXPERIMENTAL_OCI=1` + +if the registry requires authentication, you must login before running Helmsman + +```sh +helm registry login -u myuser my-registry.local +``` + +```toml +[apps] + [apps.my-app] + chart = "oci://my-registry.local/my-chart" + version = "1.0.0" +``` + +```yaml +#... +apps: + my-app: + chart: oci://my-registry.local/my-chart + version: 1.0.0 +``` + +For more information, read the [helm registries documentation](https://helm.sh/docs/topics/registries/). diff --git a/internal/app/helm_helpers.go b/internal/app/helm_helpers.go index d32cc526..59c6de50 100644 --- a/internal/app/helm_helpers.go +++ b/internal/app/helm_helpers.go @@ -110,6 +110,19 @@ func updateChartDep(chartPath string) error { return nil } +// helmExportChart pulls chart and exports it to the specified destination +func helmExportChart(chart, dest string) error { + cmd := helmCmd([]string{"chart", "pull", chart}, "Pulling chart [ "+chart+" ] to local registry cache") + if _, err := cmd.Exec(); err != nil { + return err + } + cmd = helmCmd([]string{"chart", "export", chart, "-d", dest}, "Exporting chart [ "+chart+" ] to "+dest) + if _, err := cmd.Exec(); err != nil { + return err + } + return nil +} + // addHelmRepos adds repositories to Helm if they don't exist already. // Helm does not mind if a repo with the same name exists. It treats it as an update. func addHelmRepos(repos map[string]string) error { diff --git a/internal/app/state_files.go b/internal/app/state_files.go index 29c4d02e..e9059ba6 100644 --- a/internal/app/state_files.go +++ b/internal/app/state_files.go @@ -153,8 +153,11 @@ func (s *state) expand(relativeToFile string) { repoName := strings.Split(r.Chart, "/")[0] _, isRepo := s.HelmRepos[repoName] isRepo = isRepo || stringInSlice(repoName, s.PreconfiguredHelmRepos) - // if there is no repo for the chart, we assume it's intended to be a local path + // if there is no repo for the chart, we assume it's intended to be a local path or url if !isRepo { + if strings.HasPrefix(r.Chart, "oci://") && !strings.HasSuffix(r.Chart, r.Version) { + r.Chart = fmt.Sprintf("%s:%s", r.Chart, r.Version) + } r.Chart, _ = resolveOnePath(r.Chart, dir, downloadDest) } } diff --git a/internal/app/utils.go b/internal/app/utils.go index 08950cd5..fefb8c3a 100644 --- a/internal/app/utils.go +++ b/internal/app/utils.go @@ -88,12 +88,12 @@ func stringInSlice(a string, list []string) bool { // and downloads/fetches the file locally into helmsman temp directory and returns // its absolute path func resolveOnePath(file string, dir string, downloadDest string) (string, error) { - if destFile, err := ioutil.TempFile(downloadDest, fmt.Sprintf("*%s", path.Base(file))); err != nil { + destFile, err := ioutil.TempFile(downloadDest, fmt.Sprintf("*%s", path.Base(file))) + if err != nil { return "", err - } else { - _ = destFile.Close() - return filepath.Abs(downloadFile(file, dir, destFile.Name())) } + destFile.Close() + return filepath.Abs(downloadFile(file, dir, destFile.Name())) } // createTempDir creates a temp directory in a specific location with a pattern @@ -208,6 +208,11 @@ func downloadFile(file string, dir string, outfile string) string { } switch u.Scheme { + case "oci": + dest := filepath.Dir(outfile) + fileName := strings.Split(filepath.Base(file), ":")[0] + helmExportChart(strings.ReplaceAll(file, "oci://", ""), dest) + return filepath.Join(dest, fileName) case "https", "http": if err := downloadFileFromURL(file, outfile); err != nil { log.Fatal(err.Error()) @@ -446,18 +451,27 @@ func isLocalChart(chart string) bool { // isValidCert checks if a certificate/key path/URI is valid func isValidCert(value string) bool { - if _, err := os.Stat(value); err != nil { - _, err1 := url.ParseRequestURI(value) - if err1 != nil || (!strings.HasPrefix(value, "s3://") && !strings.HasPrefix(value, "gs://") && !strings.HasPrefix(value, "az://")) { + if _, err := os.Stat(value); err == nil { + return true + } + u, err := url.ParseRequestURI(value) + if err != nil { + return false + } + switch u.Scheme { + case "http", "https", "s3", "gs", "az": + if !isOfType(u.Path, []string{".cert", ".key", ".pem", ".crt"}) { return false } + return true + default: + return false } - return true } // isValidFile checks if the file exists in the given path or accessible via http and is of allowed file extension (e.g. yaml, json ...) func isValidFile(filePath string, allowedFileTypes []string) error { - if strings.HasPrefix(filePath, "http") { + if strings.HasPrefix(filePath, "http") || strings.HasPrefix(filePath, "s3://") || strings.HasPrefix(filePath, "gs://") || strings.HasPrefix(filePath, "az://") { if _, err := url.ParseRequestURI(filePath); err != nil { return fmt.Errorf("%s must be valid URL path to a raw file", filePath) } @@ -475,11 +489,11 @@ func checkVersion(version, constraint string) bool { return false } - jsonConstraint, err := semver.NewConstraint(constraint) + c, err := semver.NewConstraint(constraint) if err != nil { return false } - return jsonConstraint.Check(v) + return c.Check(v) } // notify MSTeams sends a JSON formatted message to MSTeams channel over a webhook url