diff --git a/helm/dagger.json b/helm/dagger.json index e9f91a4..733c4b0 100644 --- a/helm/dagger.json +++ b/helm/dagger.json @@ -1,6 +1,7 @@ { "name": "helm", - "engineVersion": "v0.14.0", - "sdk": "go", - "source": "." + "engineVersion": "v0.18.5", + "sdk": { + "source": "go" + } } diff --git a/helm/main.go b/helm/main.go index 62bac72..41b6b79 100644 --- a/helm/main.go +++ b/helm/main.go @@ -12,18 +12,30 @@ import ( "fmt" "os" "strings" + "hash/crc32" + "bufio" + "time" ) const HELM_IMAGE string = "quay.io/puzzle/dagger-module-helm:latest" -type Helm struct{} +var InvalidatedCache dagger.WithContainerFunc = func(c *dagger.Container) *dagger.Container { + return c.WithEnvVariable("CACHEBUSTER", time.Now().String()) +} + +type Helm struct{ + Directory *dagger.Directory +} type PushOpts struct { - Registry string `yaml:"registry"` - Repository string `yaml:"repository"` - Oci bool `yaml:"oci"` - Username string `yaml:"username"` - Password *dagger.Secret + Registry string `yaml:"registry"` + Repository string `yaml:"repository"` + Oci bool `yaml:"oci"` + NonOCISubpath string `yaml:"nonOciSubpath"` + Username string `yaml:"username"` + Password *dagger.Secret + Version string `yaml:"version"` + AppVersion string `yaml:"appVersion"` } func (p PushOpts) getProtocol() string { @@ -39,9 +51,35 @@ func (p PushOpts) getRepoFqdn() string { } func (p PushOpts) getChartFqdn(name string) string { + if !p.Oci && p.NonOCISubpath != "" { + return fmt.Sprintf("%s/%s/%s", p.getRepoFqdn(), p.NonOCISubpath, name) + } return fmt.Sprintf("%s/%s", p.getRepoFqdn(), name) } +func (p PushOpts) getHelmPkgCmd() []string { + helmPkgCmd := []string{"helm", "package", "."} + if p.Version != "" { + helmPkgCmd = append(helmPkgCmd, "--version", p.Version) + } + if p.AppVersion != "" { + helmPkgCmd = append(helmPkgCmd, "--app-version", p.AppVersion) + } + return helmPkgCmd +} + +// Get and display the name of the Helm Chart located inside the given directory. +// +// Example usage: dagger call name --directory ./helm/examples/testdata/mychart/ +func (h *Helm) Name( + // method call context + ctx context.Context, + // directory that contains the Helm Chart + directory *dagger.Directory, +) (string, error) { + return h.queryChartWithYq(ctx, directory, ".name") +} + // Get and display the version of the Helm Chart located inside the given directory. // // Example usage: dagger call version --directory ./helm/examples/testdata/mychart/ @@ -51,18 +89,29 @@ func (h *Helm) Version( // directory that contains the Helm Chart directory *dagger.Directory, ) (string, error) { - c := h.createContainer(directory) - version, err := c.WithExec([]string{"sh", "-c", "helm show chart . | yq eval '.version' -"}).Stdout(ctx) - if err != nil { - return "", err - } + return h.queryChartWithYq(ctx, directory, ".version") +} - return strings.TrimSpace(version), nil +// Get and display the appVersion of the Helm Chart located inside the given directory. +// +// Example usage: dagger call app-version --directory ./helm/examples/testdata/mychart/ +func (h *Helm) AppVersion( + // method call context + ctx context.Context, + // directory that contains the Helm Chart + directory *dagger.Directory, +) (string, error) { + return h.queryChartWithYq(ctx, directory, ".appVersion") } // Packages and pushes a Helm chart to a specified OCI-compatible (by default) registry with authentication. // -// Returns true if the chart was successfully pushed, or false if the chart already exists, with error handling for push failures. +// Returns true if the chart was successfully pushed, or false if the chart already exists, with error handling for +// push failures. +// +// If the chart you want to push has dependencies, then we will assume you will be using the same set of credentials +// given with `--username` and `--password` for each and every dependency in your parent chart and we will automatically +// log into the registry for each dependency if the registry differs from the `--registry` argument. // // Example usage: // @@ -75,16 +124,26 @@ func (h *Helm) Version( // // Example usage for pushing to a legacy (non-OCI) Helm repository assuming the repo name is 'helm'. If your target URL // requires a vendor-specific path prefix (for example, JFrog Artifactory usually requires 'artifactory' before the repo -// name) then add it before the repository name. If you want to put the chart in a subpath in -// the repository, then append that to the end of the repository name. +// name) then add it before the repository name. If you want to push the chart into a subpath in the repository, then use +// non-oci-repo-subpath. If the non-oci-repo-subpath option is used, specifying use-non-oci-helm-repo as true is optional. +// +// Example usage without a subpath and with a subpath of 'optional/subpath/in/repository': // // dagger call package-push \ // --registry registry.puzzle.ch \ -// --repository vendor-specific-prefix/helm/optional/subpath/in/repository \ +// --repository vendor-specific-prefix/helm \ // --username $REGISTRY_HELM_USER \ // --password env:REGISTRY_HELM_PASSWORD \ // --directory ./examples/testdata/mychart/ \ // --use-non-oci-helm-repo=true +// +// dagger call package-push \ +// --registry registry.puzzle.ch \ +// --repository vendor-specific-prefix/helm \ +// --username $REGISTRY_HELM_USER \ +// --password env:REGISTRY_HELM_PASSWORD \ +// --directory ./examples/testdata/mychart/ \ +// --non-oci-repo-subpath optional/subpath/in/repository func (h *Helm) PackagePush( // method call context ctx context.Context, @@ -101,37 +160,72 @@ func (h *Helm) PackagePush( // use a non-OCI (legacy) Helm repository // +optional // +default=false - useNonOciHelmRepo bool, // Dev note: We are forced to use default=false due to https://github.com/dagger/dagger/issues/8810 + useNonOciHelmRepo bool, // Dev note: We must use default=false due to https://github.com/dagger/dagger/issues/8810 + // when using a non-OCI Helm repository, optionally specify a subpath location in the repository + // +optional + // +default="" + nonOciRepoSubpath string, + // set chart version when packaging + // +optional + // default="" + setVersionTo string, + // set chart appVersion when packaging + // +optional + // default="" + setAppVersionTo string, ) (bool, error) { + if nonOciRepoSubpath != "" { + useNonOciHelmRepo = true + if nonOciRepoSubpath[0] == '/' { + nonOciRepoSubpath = nonOciRepoSubpath[1:] + } + } opts := PushOpts{ - Registry: registry, - Repository: repository, - Oci: !useNonOciHelmRepo, - Username: username, - Password: password, + Registry: registry, + Repository: repository, + Oci: !useNonOciHelmRepo, + NonOCISubpath: nonOciRepoSubpath, + Username: username, + Password: password, + Version: setVersionTo, + AppVersion: setAppVersionTo, } fmt.Fprintf(os.Stdout, "☸️ Helm package and Push") - c := dag.Container(). - From("registry.puzzle.ch/cicd/alpine-base:latest"). - WithDirectory("/helm", directory). - WithWorkdir("/helm") - version, err := c.WithExec([]string{"sh", "-c", "helm show chart . | yq eval '.version' -"}).Stdout(ctx) + c := h.createContainer(directory) + //c := dag.Container(). + // From("registry.puzzle.ch/cicd/alpine-base:latest"). + // WithDirectory("/helm", directory). + // WithWorkdir("/helm") + + name, err := h.Name(ctx, directory) if err != nil { return false, err } - version = strings.TrimSpace(version) + version := opts.Version + if version == "" { + version, err = h.Version(ctx, directory) + if err != nil { + return false, err + } + } + + pkgFile := fmt.Sprintf("%s-%s.tgz", name, version) + + if !opts.Oci { + c, err = h.registryLogin(ctx, opts.getRepoFqdn(), opts.Username, opts.Password, opts.Oci, c) + } else { + c, err = h.registryLogin(ctx, opts.Registry, opts.Username, opts.Password, opts.Oci, c) + } - name, err := c.WithExec([]string{"sh", "-c", "helm show chart . | yq eval '.name' -"}).Stdout(ctx) if err != nil { return false, err } - name = strings.TrimSpace(name) - pkgFile := fmt.Sprintf("%s-%s.tgz", name, version) + c = h.debugEnv(c, ctx, "in packagepush after registryLogin") - chartExists, err := h.doesChartExistOnRepo(ctx, c, &opts, name, version) + c, chartExists, err := h.doesChartExistOnRepo(ctx, c, &opts, name, version) if err != nil { return false, err } @@ -140,38 +234,63 @@ func (h *Helm) PackagePush( return false, nil } - c, err = c.WithExec([]string{"helm", "dependency", "update", "."}). - WithExec([]string{"helm", "package", "."}). - WithExec([]string{"sh", "-c", "ls"}). - Sync(ctx) + c = h.debugEnv(c, ctx, "in packagepush after doesChartExistOnRepo") + missingDependencies, err := h.hasMissingDependencies(ctx, c) if err != nil { return false, err } + if missingDependencies { + c, err = h.setupContainerForDependentCharts(ctx, username, password, useNonOciHelmRepo, c, opts.getRepoFqdn()) + if err != nil { + return false, err + } - c = c. - WithEnvVariable("REGISTRY_USERNAME", opts.Username). - WithSecretVariable("REGISTRY_PASSWORD", opts.Password) + c, err = c. + With(InvalidatedCache). + With(h.reuseDirectoryChanges). + WithExec([]string{"helm", "dependency", "update", "."}). + With(h.retainDirectoryChanges). + Sync(ctx) + + if err != nil { + return false, err + } + } + + c, err = c. + With(InvalidatedCache). + With(h.reuseDirectoryChanges). + WithExec(opts.getHelmPkgCmd()). + With(h.retainDirectoryChanges). + Sync(ctx) + + if err != nil { + return false, err + } if useNonOciHelmRepo { curlCmd := []string{ - `curl --variable %REGISTRY_USERNAME`, - `--variable %REGISTRY_PASSWORD`, - `--expand-user "{{REGISTRY_USERNAME}}:{{REGISTRY_PASSWORD}}"`, + `curl`, + `-u "${REGISTRY_USERNAME}:${REGISTRY_PASSWORD}"`, `-T`, pkgFile, opts.getRepoFqdn() + "/", } c, err = c. + WithEnvVariable("REGISTRY_USERNAME", opts.Username). + WithSecretVariable("REGISTRY_PASSWORD", opts.Password). + With(InvalidatedCache). + With(h.reuseDirectoryChanges). WithExec([]string{"sh", "-c", strings.Join(curlCmd, " ")}). + WithoutSecretVariable("REGISTRY_PASSWORD"). Sync(ctx) } else { c, err = c. - WithEnvVariable("REGISTRY_URL", opts.Registry). - WithExec([]string{"sh", "-c", `echo ${REGISTRY_PASSWORD} | helm registry login ${REGISTRY_URL} --username ${REGISTRY_USERNAME} --password-stdin`}). + With(InvalidatedCache). + With(h.reuseDirectoryChanges). WithExec([]string{"helm", "push", pkgFile, opts.getRepoFqdn()}). - WithoutSecretVariable("REGISTRY_PASSWORD"). Sync(ctx) } @@ -185,7 +304,8 @@ func (h *Helm) PackagePush( // Run Helm unittests with the given directory and files. // // Provide the helm chart directory with pointing to it with the `--directory` flag. -// Add the directory location with `"."` as `--args` parameter to tell helm unittest where to find the helm chart with the tests. +// Add the directory location with `"."` as `--args` parameter to tell helm unittest where to find the helm chart with +// the tests. // // Example usage: dagger call test --directory ./helm/examples/testdata/mychart/ --args "." func (h *Helm) Test( @@ -197,7 +317,8 @@ func (h *Helm) Test( args []string, ) (string, error) { c := h.createContainer(directory) - out, err := c.WithExec([]string{"sh", "-c", fmt.Sprintf("%s %s", "helm-unittest", strings.Join(args, " "))}).Stdout(ctx) + out, err := c.WithExec([]string{"sh", "-c", + fmt.Sprintf("%s %s", "helm-unittest", strings.Join(args, " "))}).Stdout(ctx) if err != nil { return "", err } @@ -208,9 +329,18 @@ func (h *Helm) Test( // Run Helm lint with the given directory. // // Provide the helm chart directory with pointing to it with the `--directory` flag. -// Use `--args` parameter to pass alternative chart locations or additional options to Helm lint - see https://helm.sh/docs/helm/helm_lint/#options +// Use `--args` parameter to pass alternative chart locations or additional options to Helm lint - see +// https://helm.sh/docs/helm/helm_lint/#options +// +// If you need to be able to pull dependent charts but you need to supply credentials for them, then +// you can optionally supply the `--username` and `--password` parameters. In this case, this function +// will assume you will be using the same set of credentials for each and every dependency in your parent +// chart and will automatically log into the registry for each dependency. If you are using non-OCI Helm repositories, +// you can also specify the `--use-non-oci-helm-repo` parameter to use the legacy Helm repository format. // -// Example usage: dagger call lint --directory ./helm/examples/testdata/mychart/ --args "--quiet" +// Example usage without supplying credentials: +// +// dagger call lint --directory ./helm/examples/testdata/mychart/ --args "--quiet" func (h *Helm) Lint( // method call context ctx context.Context, @@ -219,16 +349,44 @@ func (h *Helm) Lint( // Helm lint arguments // +optional args []string, + // supplemental credentials for dependent charts - username + // +optional + username string, + // supplemental credentials for dependent charts - password + // +optional + password *dagger.Secret, + // use a non-OCI (legacy) Helm repository when pulling dependent charts + // +optional + // +default=false + useNonOciHelmRepo bool, // Dev note: We are forced to use default=false due to https://github.com/dagger/dagger/issues/8810 ) (string, error) { var c *dagger.Container + var err error - if h.hasMissingDependencies(ctx, directory) { - c = h.createContainer(directory).WithMountedDirectory("./charts", h.dependencyUpdate(directory)) - } else { - c = h.createContainer(directory) + c = h.createContainer(directory) + if username != "" { // setup credentials for dependent charts + c, err = h.setupContainerForDependentCharts(ctx, username, password, useNonOciHelmRepo, c, "") + if err != nil { + return "", err + } + } + + c = h.debugEnv(c, ctx, "before hasMissingDependencies") + + missingDependencies, err := h.hasMissingDependencies(ctx, c) + if err != nil { + return "", err + } + + if missingDependencies { + c, err = c.With(InvalidatedCache).WithExec([]string{"helm", "dependency", "update", "."}).Sync(ctx) + + if err != nil { + return "", err + } } - out, err := c.WithExec([]string{"sh", "-c", fmt.Sprintf("%s %s", "helm lint", strings.Join(args, " "))}).Stdout(ctx) + out, err := c.With(InvalidatedCache).WithExec([]string{"sh", "-c", fmt.Sprintf("%s %s", "helm lint", strings.Join(args, " "))}).Stdout(ctx) if err != nil { return "", err } @@ -236,50 +394,147 @@ func (h *Helm) Lint( return out, nil } +func (h *Helm) registryLogin( + ctx context.Context, + registry string, + username string, + password *dagger.Secret, + useOciHelmRepo bool, + c *dagger.Container, +) (*dagger.Container, error) { + c = c. + WithEnvVariable("REGISTRY_USERNAME", username). + WithEnvVariable("REGISTRY_URL", registry). + WithSecretVariable("REGISTRY_PASSWORD", password) + + var cmd []string + if !useOciHelmRepo { + // The hash of registry is used as a repository name when setting up non-OCI. + // The repo name can be anything since we only login to resolve dependencies. + hash := crc32.ChecksumIEEE([]byte(registry)) + repoHashKey := fmt.Sprintf("%x", hash) + c = c.WithEnvVariable("REPO_NAME", repoHashKey) + cmd = []string{ + "sh", "-c", + `(echo ${REGISTRY_PASSWORD} | helm repo add ${REPO_NAME} ${REGISTRY_URL} --username ${REGISTRY_USERNAME} --password-stdin --pass-credentials) ; helm repo update ; helm repo list ; cat Chart.yaml ; cat /helm/.config/helm/repositories.yaml`, + } + } else { + cmd = []string{ + "sh", "-c", + `echo ${REGISTRY_PASSWORD} | helm registry login ${REGISTRY_URL} --username ${REGISTRY_USERNAME} --password-stdin`, + } + } + + c = h.debugEnv(c, ctx, "in registryLogin before repo add") + + c, err := c. + With(InvalidatedCache). + WithExec(cmd). + With(h.retainDirectoryChanges). + Sync(ctx) + + + c = h.debugEnv(c, ctx, "in registryLogin after repo add") + + return c, err +} + +func (h *Helm) debugEnv( + c *dagger.Container, + ctx context.Context, + label string, +) *dagger.Container { + digest_str, _ := h.Directory.Digest(ctx) + fmt.Fprintf(os.Stdout, "DEBUG(%s):\nDirectory digest: %s\n", label, digest_str) + return c +} + +func (h *Helm) setupContainerForDependentCharts( + ctx context.Context, + username string, + password *dagger.Secret, + useNonOciHelmRepo bool, + c *dagger.Container, + alreadyLoggedInto string, +) (*dagger.Container, error) { + c = c. + WithEnvVariable("REGISTRY_USERNAME", username). + WithSecretVariable("REGISTRY_PASSWORD", password) + + c, err := c.WithExec([]string{"sh", "-c", `helm show chart . | yq '[.dependencies[].repository] | unique | sort | .[]'`}).Sync(ctx) + if err != nil { + return c, err + } + + valuesString, err := c.Stdout(ctx) + if err != nil { + return c, err + } + + repoURLs := h.getRepoURLs(valuesString) + for _, repoURL := range repoURLs { + if repositoriesAreNotEquivalent(alreadyLoggedInto, repoURL) { // avoid logging into the same repo twice + c, err = h.registryLogin(ctx, repoURL, username, password, !useNonOciHelmRepo, c) + if err != nil { + return c, err + } + c = h.debugEnv(c, ctx, "in setupContainerForDependentCharts after registryLogin") + } + } + + return c, nil +} + +func repositoriesAreNotEquivalent( + repositoryOne string, + repositoryTwo string, +) bool { + if repositoryOne == "" { + return true + } + normalize := func(input string) string { + result := strings.TrimSpace(input) + result = result[:strings.Index(result, "://")] + result = strings.ToLower(result) + return result + } + return !strings.Contains(normalize(repositoryOne), normalize(repositoryTwo)) +} + + + func (h *Helm) doesChartExistOnRepo( ctx context.Context, c *dagger.Container, opts *PushOpts, name string, version string, -) (bool, error) { +) (*dagger.Container, bool, error) { if opts.Oci { - c, err := c. - WithEnvVariable("REGISTRY_URL", opts.Registry). - WithEnvVariable("REGISTRY_USERNAME", opts.Username). - WithSecretVariable("REGISTRY_PASSWORD", opts.Password). - WithExec([]string{"sh", "-c", `echo ${REGISTRY_PASSWORD} | helm registry login ${REGISTRY_URL} --username ${REGISTRY_USERNAME} --password-stdin`}). - WithoutSecretVariable("REGISTRY_PASSWORD"). - Sync(ctx) - if err != nil { - return false, err - } - - //TODO: Refactor with return - c, err = c.WithExec([]string{"sh", "-c", fmt.Sprintf("helm show chart %s --version %s; echo -n $? > /ec", opts.getChartFqdn(name), version)}).Sync(ctx) + // We assume we are already logged into an OCI registry + c, err := c.WithExec([]string{"sh", "-c", fmt.Sprintf("helm show chart %s --version %s; echo -n $? > /ec", opts.getChartFqdn(name), version)}).Sync(ctx) if err != nil { - return false, err + return c, false, err } exc, err := c.File("/ec").Contents(ctx) if err != nil { - return false, err + return c, false, err } if exc == "0" { //Chart exists - return true, nil + return c, true, nil } - return false, nil + return c, false, nil } // else non-OCI pkgFile := fmt.Sprintf("%s-%s.tgz", name, version) // Do a GET of the chart but with response headers only so we do not download the chart curlCmd := []string{ - `curl --variable %REGISTRY_USERNAME`, - `--variable %REGISTRY_PASSWORD`, - `--expand-user "{{REGISTRY_USERNAME}}:{{REGISTRY_PASSWORD}}"`, + `curl`, + `-u "${REGISTRY_USERNAME}:${REGISTRY_PASSWORD}"`, opts.getChartFqdn(pkgFile), `--output /dev/null`, `--silent -Iw '%{http_code}'`, @@ -293,48 +548,94 @@ func (h *Helm) doesChartExistOnRepo( Stdout(ctx) if err != nil { - return false, err + return c, false, err } httpCode = strings.TrimSpace(httpCode) if httpCode == "200" { // Chart exists - return true, nil + return c, true, nil } if httpCode == "404" { - return false, nil + return c, false, nil } - return false, fmt.Errorf("Server returned error code %s checking for chart existence on server.", httpCode) + return c, false, fmt.Errorf("Server returned error code %s checking for chart existence on server.", httpCode) } -func (h *Helm) hasMissingDependencies( +func (h *Helm) queryChartWithYq( // method call context ctx context.Context, // directory that contains the Helm Chart directory *dagger.Directory, -) bool { - _, err := h.createContainer(directory).WithExec([]string{"sh", "-c", "helm dep list | grep missing"}).Stdout(ctx) - return err == nil + yqQuery string, +) (string, error) { + c := h.createContainer(directory) + result, err := c.WithExec([]string{"sh", "-c", fmt.Sprintf(`helm show chart . | yq eval '%s' -`, yqQuery)}).Stdout(ctx) + if err != nil { + return "", err + } + + return strings.TrimSpace(result), nil } -func (h *Helm) dependencyUpdate( - // directory that contains the Helm Chart - directory *dagger.Directory, -) *dagger.Directory { - c := h.createContainer(directory) - return c.WithExec([]string{"sh", "-c", "helm dep update"}).Directory("charts") +func (h *Helm) hasMissingDependencies( + // method call context + ctx context.Context, + // container to run the command in + c *dagger.Container, +) (bool, error) { + output, err := c.With(InvalidatedCache).WithExec([]string{"sh", "-c", "helm dependency list"}).Stdout(ctx) + if err != nil { + return false, err + } + if strings.Contains(output, "missing") { + return true, nil + } + return false, nil +} + +// Must be called right after WithExec. Sets up the container to reuse directory changes. +func (h *Helm) retainDirectoryChanges( + c *dagger.Container, +) *dagger.Container { + h.Directory = c.Directory("/helm") + return c.With(h.reuseDirectoryChanges) } +// Must be called right after WithExec. Sets up the container to reuse directory changes. +func (h *Helm) reuseDirectoryChanges( + c *dagger.Container, +) *dagger.Container { + return c. + WithoutDirectory("/helm"). + WithDirectory("/helm", h.Directory, dagger.ContainerWithDirectoryOpts{Owner: "1001"}) +} + func (h *Helm) createContainer( // directory that contains the Helm Chart directory *dagger.Directory, ) *dagger.Container { - return dag.Container(). + c := dag.Container(). From(HELM_IMAGE). WithDirectory("/helm", directory, dagger.ContainerWithDirectoryOpts{Owner: "1001"}). WithWorkdir("/helm"). WithoutEntrypoint() + h.Directory = c.Directory("/helm") + return c +} + +// getRepoURLs returns a list of the repository URLs from the values string. +func (h *Helm) getRepoURLs( + valuesString string, +) []string { + resultList := []string{} + + valScanner := bufio.NewScanner(strings.NewReader(valuesString)) + for valScanner.Scan() { + resultList = append(resultList, valScanner.Text()) + } + return resultList } diff --git a/tests/charts.go b/tests/charts.go new file mode 100644 index 0000000..b349029 --- /dev/null +++ b/tests/charts.go @@ -0,0 +1,195 @@ +package main + +import ( + "bytes" + "dagger/go/internal/dagger" + "fmt" + "text/template" + "crypto/rand" + "context" +) + +const originalVersion = "0.1.1" +const originalAppVersion = "1.16.0" +const randomSuffixLength = 8 + +const mychartName = "dagger-module-helm-test" +const mychartDir = "./testdata/mychart/" +const mychartTemplateString = ` +apiVersion: v2 +name: {{ .Name }} +description: A Helm chart +type: application +version: {{ .Version }} +appVersion: "{{ .AppVersion }}" +` +const mydependentchartName = "dagger-module-helm-test-with-dependency" +const mydependentchartDir = "./testdata/mydependentchart/" +const mydependentchartTemplateString = mychartTemplateString + ` +dependencies: + - name: dependency-track + version: 1.8.1 + repository: https://puzzle.github.io/dependencytrack-helm/ +` + +// Chart represents one of our test Helm charts +type Chart struct{ + Name string + Version string + AppVersion string + DaggerDirectory *dagger.Directory + Directory string + ContentTemplate *template.Template + originalContent string + SessionKey string +} + +// Get the name of the chart +func (c Chart) GetName( +) string { + return c.Name +} + +// Get the version of the chart +func (c Chart) GetVersion( +) string { + return c.Version +} + +// Get the appVersion of the chart +func (c Chart) GetAppVersion( +) string { + return c.AppVersion +} + +// Modify the chart by changing only the version +func (c *Chart) WithVersion( + version string, +) *Chart { + c.Version = version + return c +} + +// Modify the chart by changing only the appVersion +func (c *Chart) WithAppVersion( + appVersion string, +) *Chart { + c.AppVersion = appVersion + return c +} + +// Modify the chart by appending a suffix to the original version +func (c *Chart) WithRandomOriginalVersionSuffix( +) *Chart { + return c.WithOriginalVersionSuffix(RandomString(randomSuffixLength)) +} + +// Modify the chart by appending a suffix to the original version +func (c *Chart) WithOriginalVersionSuffix( + suffix string, +) *Chart { + versionWithSuffix := fmt.Sprintf("%s-%s", originalVersion, suffix) + return c.WithVersion(versionWithSuffix) +} + +// Reset the chart to its original content +func (c *Chart) Reset( + ctx context.Context, +) *Chart { + c.DaggerDirectory. + WithoutFile("Chart.yaml"). + WithNewFile("Chart.yaml", c.originalContent). + Sync(ctx) + return c +} + +// Sync the directory with changes +func (c *Chart) Sync( + ctx context.Context, +) *Chart { + var buf bytes.Buffer + err := c.ContentTemplate.ExecuteTemplate(&buf, c.Name, c) + if err != nil { + panic(err) + } + + c.DaggerDirectory, err = c.DaggerDirectory. + WithoutFile("Chart.yaml"). + WithNewFile("Chart.yaml", buf.String()). + Sync(ctx) + + if err != nil { + panic(err) + } + + return c +} + +func newChart( + ctx context.Context, + name string, + directory string, + templateString string, + version string, + appVersion string, + sessionKey string, +) *Chart { + ourSessionKey := sessionKey + if (ourSessionKey == "") { + ourSessionKey = RandomString(10) + } + ourName := fmt.Sprintf("%s-%s", name, ourSessionKey) + c := Chart{ + Name: ourName, + Directory: directory, + ContentTemplate: template.Must(template.New(ourName).Parse(templateString)), + SessionKey: ourSessionKey, + Version: version, + AppVersion: appVersion, + } + + c.DaggerDirectory = dag.CurrentModule().Source().Directory(c.Directory) + return c.Sync(ctx) +} + +func NewMyChart( + ctx context.Context, +) *Chart { + return newChart(ctx, mychartName, mychartDir, mychartTemplateString, originalVersion, originalAppVersion, "") +} + + +func NewMyChartInSession( + ctx context.Context, + sessionKey string, +) *Chart { + return newChart(ctx, mychartName, mychartDir, mychartTemplateString, originalVersion, originalAppVersion, sessionKey) +} + +func NewMyDependentChart( + ctx context.Context, +) *Chart { + return newChart(ctx, mydependentchartName, mydependentchartDir, mydependentchartTemplateString, originalVersion, originalAppVersion, "") +} + +func NewMyDependentChartInSession( + ctx context.Context, + sessionKey string, +) *Chart { + return newChart(ctx, mydependentchartName, mydependentchartDir, mydependentchartTemplateString, originalVersion, originalAppVersion, sessionKey) +} + +// The payoff: A chart session that suffixes to the chart name a random SessionKey string for uniqueness of each test +func NewChartSession( + ctx context.Context, +) (*Chart, *Chart) { + Mychart := NewMyChart(ctx) + Mydependentchart := NewMyDependentChartInSession(ctx, Mychart.SessionKey) + return Mychart, Mydependentchart +} + +func RandomString(length int) string { + b := make([]byte, length+2) + rand.Read(b) + return fmt.Sprintf("%x", b)[2 : length+2] +} diff --git a/tests/main.go b/tests/main.go index db8ed65..872a8b9 100644 --- a/tests/main.go +++ b/tests/main.go @@ -4,8 +4,6 @@ import ( "context" "dagger/go/internal/dagger" "fmt" - "time" - "github.com/sourcegraph/conc/pool" ) @@ -15,26 +13,73 @@ type Go struct{} func (m *Go) All(ctx context.Context) error { p := pool.New().WithErrors().WithContext(ctx) + p.Go(m.HelmName) p.Go(m.HelmVersion) + p.Go(m.HelmAppVersion) p.Go(m.HelmTest) p.Go(m.HelmLint) p.Go(m.HelmLintWithArg) p.Go(m.HelmLintWithArgs) p.Go(m.HelmLintWithMissingDependencies) p.Go(m.HelmPackagePush) - p.Go(m.HelmPackagePushNonOci) + + // We have to simulate a non-OCI helm repository for this test to work... + //p.Go(m.HelmPackagePushNonOci) + p.Go(m.HelmPackagePushWithExistingChart) + p.Go(m.HelmPackagePushWithVersion) return p.Wait() } +func (m *Go) HelmName( + // method call context + ctx context.Context, +) error { + Mychart, _ := NewChartSession(ctx) + directory := Mychart.DaggerDirectory + expected := Mychart.GetName() + + version, err := dag.Helm().Name(ctx, directory) + if err != nil { + return err + } + + if version != expected { + return fmt.Errorf("expected %q, got %q", expected, version) + } + + return nil +} + func (m *Go) HelmVersion( // method call context ctx context.Context, ) error { - const expected = "0.1.1" - directory := dag.CurrentModule().Source().Directory("./testdata/mychart/") + Mychart, _ := NewChartSession(ctx) + directory := Mychart.DaggerDirectory + expected := Mychart.GetVersion() + version, err := dag.Helm().Version(ctx, directory) + if err != nil { + return err + } + + if version != expected { + return fmt.Errorf("expected %q, got %q", expected, version) + } + + return nil +} + +func (m *Go) HelmAppVersion( + // method call context + ctx context.Context, +) error { + Mychart, _ := NewChartSession(ctx) + directory := Mychart.DaggerDirectory + version, err := dag.Helm().AppVersion(ctx, directory) + expected := Mychart.GetAppVersion() if err != nil { return err @@ -60,9 +105,9 @@ func (m *Go) HelmPackagepush( // registry login password password *dagger.Secret, ) error { - randomString := fmt.Sprintf("%d", time.Now().UnixNano())[0:8] - // directory that contains the Helm Chart - directory := m.chartWithVersionSuffix(dag.CurrentModule().Source().Directory("./testdata/mychart/"), randomString) + Mychart, _ := NewChartSession(ctx) + directory := Mychart.DaggerDirectory + _, err := dag.Helm().PackagePush(ctx, directory, registry, repository, username, password) if err != nil { @@ -76,9 +121,11 @@ func (m *Go) HelmPackagePush( // method call context ctx context.Context, ) error { - // directory that contains the Helm Chart - directory := dag.CurrentModule().Source().Directory("./testdata/mychart/") - _, err := dag.Helm().PackagePush(ctx, directory, "ttl.sh", "helm", "username", dag.SetSecret("password", "secret")) + Mychart, _ := NewChartSession(ctx) + directory := Mychart.DaggerDirectory + _, err := dag. + Helm(). + PackagePush(ctx, directory, "ttl.sh", "helm", "username", dag.SetSecret("password", "secret")) if err != nil { return err @@ -91,14 +138,67 @@ func (m *Go) HelmPackagePushNonOci( // method call context ctx context.Context, ) error { - // directory that contains the Helm Chart - directory := dag.CurrentModule().Source().Directory("./testdata/mychart/") - _, err := dag.Helm().PackagePush(ctx, directory, "ttl.sh", "helm", "username", dag.SetSecret("password", "secret"), dagger.HelmPackagePushOpts{UseNonOciHelmRepo: true}) + Mychart, _ := NewChartSession(ctx) + directory := Mychart.DaggerDirectory + _, err := dag. + Helm(). + PackagePush(ctx, directory, "ttl.sh", "helm", "username", + dag.SetSecret("password", "secret"), + dagger.HelmPackagePushOpts{UseNonOciHelmRepo: true}) + + if err != nil { + return err + } + + return nil +} + +func (m *Go) HelmPackagePushWithVersion( + // method call context + ctx context.Context, +) error { + Mychart, _ := NewChartSession(ctx) + const differentVersion = "0.6.7" + directory := Mychart.DaggerDirectory + returnValue, err := dag. + Helm(). + PackagePush(ctx, directory, "ttl.sh", "helm", "username", + dag.SetSecret("password", "secret"), + dagger.HelmPackagePushOpts{SetVersionTo: differentVersion}) + + if err != nil { + return err + } + + if !returnValue { + return fmt.Errorf("should return true because chart does not exist") + } + + returnValue, err = dag. + Helm(). + PackagePush(ctx, directory, "ttl.sh", "helm", "username", + dag.SetSecret("password", "secret")) + if err != nil { + return err + } + if !returnValue { + return fmt.Errorf("should return true because chart does not exist") + } + + returnValue, err = dag. + Helm(). + PackagePush(ctx, directory, "ttl.sh", "helm", "username" + Mychart.SessionKey, + dag.SetSecret("password", "secret"), + dagger.HelmPackagePushOpts{SetVersionTo: differentVersion}) if err != nil { return err } + if returnValue { + return fmt.Errorf("should return false because chart already exists") + } + return nil } @@ -106,11 +206,13 @@ func (m *Go) HelmPackagePushWithExistingChart( // method call context ctx context.Context, ) error { - randomString := fmt.Sprintf("%d", time.Now().UnixNano())[0:8] - // directory that contains the Helm Chart - directory := m.chartWithVersionSuffix(dag.CurrentModule().Source().Directory("./testdata/mychart/"), randomString) + Mychart, _ := NewChartSession(ctx) + directory := Mychart.DaggerDirectory - returnValue, err := dag.Helm().PackagePush(ctx, directory, "ttl.sh", "helm", "username", dag.SetSecret("password", "secret")) + returnValue, err := dag. + Helm(). + PackagePush(ctx, directory, "ttl.sh", "helm", "username", + dag.SetSecret("password", "secret")) if err != nil { return err } @@ -118,7 +220,10 @@ func (m *Go) HelmPackagePushWithExistingChart( return fmt.Errorf("should return true because chart does not exists") } - returnValue, err = dag.Helm().PackagePush(ctx, directory, "ttl.sh", "helm", "username"+randomString, dag.SetSecret("password", "secret")) + returnValue, err = dag. + Helm(). + PackagePush(ctx, directory, "ttl.sh", "helm", "username"+RandomString(8), + dag.SetSecret("password", "secret")) if err != nil { return err } @@ -134,7 +239,8 @@ func (m *Go) HelmTest( ctx context.Context, ) error { args := []string{"."} - directory := dag.CurrentModule().Source().Directory("./testdata/mychart/") + Mychart, _ := NewChartSession(ctx) + directory := Mychart.DaggerDirectory _, err := dag.Helm().Test(ctx, directory, args) if err != nil { @@ -148,7 +254,8 @@ func (m *Go) HelmLint( // method call context ctx context.Context, ) error { - directory := dag.CurrentModule().Source().Directory("./testdata/mychart/") + Mychart, _ := NewChartSession(ctx) + directory := Mychart.DaggerDirectory _, err := dag.Helm().Lint(ctx, directory) if err != nil { @@ -163,7 +270,8 @@ func (m *Go) HelmLintWithArg( ctx context.Context, ) error { args := dagger.HelmLintOpts{Args: []string{"--quiet"}} - directory := dag.CurrentModule().Source().Directory("./testdata/mychart/") + Mychart, _ := NewChartSession(ctx) + directory := Mychart.DaggerDirectory _, err := dag.Helm().Lint(ctx, directory, args) if err != nil { @@ -178,7 +286,8 @@ func (m *Go) HelmLintWithArgs( ctx context.Context, ) error { args := dagger.HelmLintOpts{Args: []string{"--strict", "--quiet"}} - directory := dag.CurrentModule().Source().Directory("./testdata/mychart/") + Mychart, _ := NewChartSession(ctx) + directory := Mychart.DaggerDirectory _, err := dag.Helm().Lint(ctx, directory, args) if err != nil { @@ -192,7 +301,8 @@ func (m *Go) HelmLintWithMissingDependencies( // method call context ctx context.Context, ) error { - directory := dag.CurrentModule().Source().Directory("./testdata/mydependentchart/") + _, Mydependentchart := NewChartSession(ctx) + directory := Mydependentchart.DaggerDirectory _, err := dag.Helm().Lint(ctx, directory) if err != nil { @@ -202,22 +312,3 @@ func (m *Go) HelmLintWithMissingDependencies( return nil } -func (m *Go) chartWithVersionSuffix( - // directory that contains the Helm Chart - directory *dagger.Directory, - randomString string, -) *dagger.Directory { - // set name and version to arbitrary values - directory = directory. - WithoutFile("Chart.yaml"). - WithNewFile("Chart.yaml", -fmt.Sprintf(` -apiVersion: v2 -name: dagger-module-helm-test -description: A Helm chart -type: application -version: 0.1.1-%s -`, randomString)) - - return directory -} diff --git a/tests/testdata/mydependentchart/Chart.yaml b/tests/testdata/mydependentchart/Chart.yaml index 30cb06d..ebfdf72 100644 --- a/tests/testdata/mydependentchart/Chart.yaml +++ b/tests/testdata/mydependentchart/Chart.yaml @@ -1,5 +1,5 @@ apiVersion: v2 -name: dagger-module-helm-test +name: dagger-module-helm-test-with-dependency description: A Helm chart for Kubernetes # A chart can be either an 'application' or a 'library' chart.