Skip to content

Commit

Permalink
feat: support authenticated Helm repositories from helm repo add (#2196
Browse files Browse the repository at this point in the history
)

## Description

Support authenticated Helm repositories from helm repo add in zarf. Zarf
should be able to package and find images for any repo already on a
users system

## Related Issue

Fixes # 2189

## Type of change

- [ ] Bug fix (non-breaking change which fixes an issue)
- [X] New feature (non-breaking change which adds functionality)
- [ ] Other (security config, docs update, etc)

## Checklist before merging

- [ ] Test, docs, adr added or updated as needed
- [ ] [Contributor Guide
Steps](https://github.com/defenseunicorns/zarf/blob/main/CONTRIBUTING.md#developer-workflow)
followed

---------

Co-authored-by: Wayne Starr <[email protected]>
Co-authored-by: razzle <[email protected]>
  • Loading branch information
3 people authored Jan 24, 2024
1 parent b46898a commit e208cca
Show file tree
Hide file tree
Showing 6 changed files with 166 additions and 17 deletions.
6 changes: 6 additions & 0 deletions docs/3-create-a-zarf-package/2-zarf-components.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ Charts using the `url` key can be:
- A remote URL (oci://) to an OCI registry
- A remote URL (http/https) to a Helm repository

:::note

To use a private Helm repository the repo must be added to Helm. You can add a repo to Helm with the [`helm repo add`](https://helm.sh/docs/helm/helm_repo_add/) command or the internal [`zarf tools helm repo add`](../2-the-zarf-cli/100-cli-commands/zarf_tools_helm_repo_add.md) command.

:::

#### Chart Examples

<ExampleYAML src={require('../../examples/helm-charts/zarf.yaml')} component="demo-helm-charts" />
Expand Down
2 changes: 1 addition & 1 deletion examples/helm-charts/zarf.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ components:
localPath: chart
valuesFiles:
- values.yaml

- name: podinfo-oci
version: 6.4.0
namespace: podinfo-from-oci
Expand Down
24 changes: 22 additions & 2 deletions src/internal/packager/helm/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,15 @@ func (h *Helm) DownloadPublishedChart(cosignKeyPath string) error {
chartURL string
err error
)
repoFile, err := repo.LoadFile(pull.Settings.RepositoryConfig)

// Not returning the error here since the repo file is only needed if we are pulling from a repo that requires authentication
if err != nil {
message.Debugf("Unable to load the repo file at %q: %s", pull.Settings.RepositoryConfig, err.Error())
}

var username string
var password string

// Handle OCI registries
if registry.IsOCI(h.chart.URL) {
Expand All @@ -159,8 +168,18 @@ func (h *Helm) DownloadPublishedChart(cosignKeyPath string) error {
chartName = h.chart.RepoName
}

// Perform simple chart download
chartURL, err = repo.FindChartInRepoURL(h.chart.URL, chartName, h.chart.Version, pull.CertFile, pull.KeyFile, pull.CaFile, getter.All(pull.Settings))
if repoFile != nil {
// TODO: @AustinAbro321 Currently this selects the last repo with the same url
// We should introduce a new field in zarf to allow users to specify the local repo they want
for _, repo := range repoFile.Repositories {
if repo.URL == h.chart.URL {
username = repo.Username
password = repo.Password
}
}
}

chartURL, err = repo.FindChartInAuthRepoURL(h.chart.URL, username, password, chartName, h.chart.Version, pull.CertFile, pull.KeyFile, pull.CaFile, getter.All(pull.Settings))
if err != nil {
if strings.Contains(err.Error(), "not found") {
// Intentionally dogsled this error since this is just a nice to have helper
Expand All @@ -179,6 +198,7 @@ func (h *Helm) DownloadPublishedChart(cosignKeyPath string) error {
Getters: getter.All(pull.Settings),
Options: []getter.Option{
getter.WithInsecureSkipVerifyTLS(config.CommonOptions.Insecure),
getter.WithBasicAuth(username, password),
},
}

Expand Down
2 changes: 1 addition & 1 deletion src/test/external/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,5 @@ make test-external

``` bash
# If you are in the root folder of the repository and already have everything built (i.e., the binary, the init-package and the flux-test example package):
go test ./src/test/external-test/...
go test ./src/test/external/... -v
```
135 changes: 122 additions & 13 deletions src/test/external/ext_out_cluster_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,43 @@
package external

import (
"bytes"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"testing"

"github.com/defenseunicorns/zarf/src/pkg/utils"
"github.com/defenseunicorns/zarf/src/pkg/utils/exec"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"helm.sh/helm/v3/pkg/repo"
)

// Docker/k3d networking constants
const (
network = "k3d-k3s-external-test"
subnet = "172.31.0.0/16"
gateway = "172.31.0.1"
giteaIP = "172.31.0.99"
giteaHost = "gitea.localhost"
registryHost = "registry.localhost"
clusterName = "zarf-external-test"
network = "k3d-k3s-external-test"
subnet = "172.31.0.0/16"
gateway = "172.31.0.1"
giteaIP = "172.31.0.99"
giteaHost = "gitea.localhost"
registryHost = "registry.localhost"
clusterName = "zarf-external-test"
giteaUser = "git-user"
registryUser = "push-user"
commonPassword = "superSecurePassword"
)

var outClusterCredentialArgs = []string{
"--git-push-username=git-user",
"--git-push-password=superSecurePassword",
"--git-push-username=" + giteaUser,
"--git-push-password=" + commonPassword,
"--git-url=http://" + giteaHost + ":3000",
"--registry-push-username=push-user",
"--registry-push-password=superSecurePassword",
"--registry-push-username=" + registryUser,
"--registry-push-password=" + commonPassword,
"--registry-url=k3d-" + registryHost + ":5000"}

type ExtOutClusterTestSuite struct {
Expand All @@ -40,6 +50,7 @@ type ExtOutClusterTestSuite struct {
}

func (suite *ExtOutClusterTestSuite) SetupSuite() {

suite.Assertions = require.New(suite.T())

// Teardown any leftovers from previous tests
Expand Down Expand Up @@ -97,7 +108,7 @@ func (suite *ExtOutClusterTestSuite) Test_0_Mirror() {
suite.NoError(err, "unable to mirror the package with zarf")

// Check that the registry contains the images we want
regCatalogURL := fmt.Sprintf("http://push-user:superSecurePassword@k3d-%s:5000/v2/_catalog", registryHost)
regCatalogURL := fmt.Sprintf("http://%s:%s@k3d-%s:5000/v2/_catalog", registryUser, commonPassword, registryHost)
respReg, err := http.Get(regCatalogURL)
suite.NoError(err)
regBody, err := io.ReadAll(respReg.Body)
Expand All @@ -107,7 +118,7 @@ func (suite *ExtOutClusterTestSuite) Test_0_Mirror() {
suite.Contains(string(regBody), "stefanprodan/podinfo", "registry did not contain the expected image")

// Check that the git server contains the repos we want
gitRepoURL := fmt.Sprintf("http://git-user:superSecurePassword@%s:3000/api/v1/repos/search", giteaHost)
gitRepoURL := fmt.Sprintf("http://%s:%s@%s:3000/api/v1/repos/search", giteaUser, commonPassword, giteaHost)
respGit, err := http.Get(gitRepoURL)
suite.NoError(err)
gitBody, err := io.ReadAll(respGit.Body)
Expand Down Expand Up @@ -136,6 +147,104 @@ func (suite *ExtOutClusterTestSuite) Test_1_Deploy() {
suite.True(success, errorStr)
}

func (suite *ExtOutClusterTestSuite) Test_2_AuthToPrivateHelmChart() {
baseURL := fmt.Sprintf("http://%s:3000", giteaHost)

suite.createHelmChartInGitea(baseURL, giteaUser, commonPassword)
suite.makeGiteaUserPrivate(baseURL, giteaUser, commonPassword)

tempDir := suite.T().TempDir()
repoPath := filepath.Join(tempDir, "repositories.yaml")
os.Setenv("HELM_REPOSITORY_CONFIG", repoPath)
defer os.Unsetenv("HELM_REPOSITORY_CONFIG")

packagePath := filepath.Join("..", "packages", "external-helm-auth")
findImageArgs := []string{"dev", "find-images", packagePath}
err := exec.CmdWithPrint(zarfBinPath, findImageArgs...)
suite.Error(err, "Since auth has not been setup, this should fail")

repoFile := repo.NewFile()

chartURL := fmt.Sprintf("%s/api/packages/%s/helm", baseURL, giteaUser)
entry := &repo.Entry{
Name: "temp_entry",
Username: giteaUser,
Password: commonPassword,
URL: chartURL,
}
repoFile.Add(entry)
utils.WriteYaml(repoPath, repoFile, 0600)

err = exec.CmdWithPrint(zarfBinPath, findImageArgs...)
suite.NoError(err, "Unable to find images, helm auth likely failed")

packageCreateArgs := []string{"package", "create", packagePath, fmt.Sprintf("--output=%s", tempDir), "--confirm"}
err = exec.CmdWithPrint(zarfBinPath, packageCreateArgs...)
suite.NoError(err, "Unable to create package, helm auth likely failed")
}

func (suite *ExtOutClusterTestSuite) createHelmChartInGitea(baseURL string, username string, password string) {
tempDir := suite.T().TempDir()
podInfoVersion := "6.4.0"
podinfoChartPath := filepath.Join("..", "..", "..", "examples", "helm-charts", "chart")
err := exec.CmdWithPrint("helm", "package", podinfoChartPath, "--destination", tempDir)
podinfoTarballPath := filepath.Join(tempDir, fmt.Sprintf("podinfo-%s.tgz", podInfoVersion))
suite.NoError(err, "Unable to package chart")

utils.DownloadToFile(fmt.Sprintf("https://stefanprodan.github.io/podinfo/podinfo-%s.tgz", podInfoVersion), podinfoTarballPath, "")
url := fmt.Sprintf("%s/api/packages/%s/helm/api/charts", baseURL, username)

file, err := os.Open(podinfoTarballPath)
suite.NoError(err)
defer file.Close()

body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("file", podinfoTarballPath)
suite.NoError(err)
_, err = io.Copy(part, file)
suite.NoError(err)
writer.Close()

req, err := http.NewRequest("POST", url, body)
suite.NoError(err)

req.Header.Set("Content-Type", writer.FormDataContentType())
req.SetBasicAuth(username, password)

client := &http.Client{}

resp, err := client.Do(req)
suite.NoError(err)
resp.Body.Close()
}

func (suite *ExtOutClusterTestSuite) makeGiteaUserPrivate(baseURL string, username string, password string) {
url := fmt.Sprintf("%s/api/v1/admin/users/%s", baseURL, username)

userOption := map[string]interface{}{
"visibility": "private",
"login_name": username,
}

jsonData, err := json.Marshal(userOption)
suite.NoError(err)

req, err := http.NewRequest(http.MethodPatch, url, bytes.NewBuffer(jsonData))
suite.NoError(err)

req.Header.Set("Content-Type", "application/json")
req.SetBasicAuth(username, password)

client := &http.Client{}
resp, err := client.Do(req)
suite.NoError(err)
defer resp.Body.Close()

_, err = io.ReadAll(resp.Body)
suite.NoError(err)
}

func TestExtOurClusterTestSuite(t *testing.T) {
suite.Run(t, new(ExtOutClusterTestSuite))
}
14 changes: 14 additions & 0 deletions src/test/packages/external-helm-auth/zarf.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
kind: ZarfPackageConfig
metadata:
name: helm-auth
version: 0.0.1
components:
- name: private-chart
required: true
charts:
- name: podinfo
version: 6.4.0
url: http://gitea.localhost:3000/api/packages/git-user/helm
repoName: podinfo
namespace: podinfo-from-repo
releaseName: cool-release-name

0 comments on commit e208cca

Please sign in to comment.