feat: Migrate from Helm 3 to Helm 4 (#28076)

Signed-off-by: reggie-k <[email protected]>
This commit is contained in:
Regina Voloshin
2026-06-09 14:59:06 +03:00
committed by GitHub
parent c606368825
commit eec8d2e6e5
29 changed files with 448 additions and 46 deletions
+3 -1
View File
@@ -208,7 +208,9 @@ func NewRepoAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Command {
errors.Fatal(errors.ErrorGeneric, "Must specify --name for repos of type 'helm'")
}
if repoOpts.Repo.Type == "oci" && repoOpts.InsecureOCIForceHTTP {
if repoOpts.InsecureOCIForceHTTP {
err := cmdutil.ValidateInsecureOCIForceHTTP(repoOpts.InsecureOCIForceHTTP, repoOpts.Repo.Type, repoOpts.Repo.EnableOCI)
errors.CheckError(err)
repoOpts.Repo.InsecureOCIForceHttp = repoOpts.InsecureOCIForceHTTP
}
+4
View File
@@ -180,6 +180,9 @@ func NewRepoCredsAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comma
err = cmdutil.ValidateBearerTokenForHTTPSRepoOnly(repo.BearerToken, git.IsHTTPSURL(repo.URL))
errors.CheckError(err)
err = cmdutil.ValidateInsecureOCIForceHTTP(repo.InsecureOCIForceHttp, repo.Type, repo.EnableOCI)
errors.CheckError(err)
repoCreateReq := repocredspkg.RepoCredsCreateRequest{
Creds: &repo,
Upsert: upsert,
@@ -202,6 +205,7 @@ func NewRepoCredsAddCommand(clientOpts *argocdclient.ClientOptions) *cobra.Comma
command.Flags().StringVar(&repo.GitHubAppEnterpriseBaseURL, "github-app-enterprise-base-url", "", "base url to use when using GitHub Enterprise (e.g. https://ghe.example.com/api/v3")
command.Flags().BoolVar(&upsert, "upsert", false, "Override an existing repository with the same name even if the spec differs")
command.Flags().BoolVar(&repo.EnableOCI, "enable-oci", false, "Specifies whether helm-oci support should be enabled for this repo")
command.Flags().BoolVar(&repo.InsecureOCIForceHttp, "insecure-oci-force-http", false, "Use http when accessing an OCI repository")
command.Flags().StringVar(&repo.Type, "type", common.DefaultRepoType, "type of the repository, \"git\" or \"helm\"")
command.Flags().StringVar(&gcpServiceAccountKeyPath, "gcp-service-account-key-path", "", "service account key for the Google Cloud Platform")
command.Flags().BoolVar(&repo.ForceHttpBasicAuth, "force-http-basic-auth", false, "whether to force basic auth when connecting via HTTP")
+10
View File
@@ -37,3 +37,13 @@ func ValidateBearerTokenAndPasswordCombo(bearerToken string, password string) er
}
return nil
}
// ValidateInsecureOCIForceHTTP returns an error if --insecure-oci-force-http is set
// on a repo that is not OCI-capable. Only type=oci repos, or type=helm repos with
// --enable-oci, are OCI-capable; --enable-oci is meaningful only for helm repos.
func ValidateInsecureOCIForceHTTP(insecureOCIForceHTTP bool, repoType string, enableOCI bool) error {
if insecureOCIForceHTTP && repoType != "oci" && (repoType != "helm" || !enableOCI) {
return stderrors.New("--insecure-oci-force-http requires --type oci or --enable-oci")
}
return nil
}
+70
View File
@@ -153,3 +153,73 @@ func TestValidateBearerTokenForHTTPSRepoOnly(t *testing.T) {
})
}
}
func TestValidateInsecureOCIForceHTTP(t *testing.T) {
tests := []struct {
name string
insecureOCIForceHTTP bool
repoType string
enableOCI bool
expectError bool
}{
{
name: "type=oci with insecure-oci-force-http",
insecureOCIForceHTTP: true,
repoType: "oci",
enableOCI: false,
expectError: false,
},
{
name: "type=helm with enable-oci and insecure-oci-force-http",
insecureOCIForceHTTP: true,
repoType: "helm",
enableOCI: true,
expectError: false,
},
{
name: "type=helm without enable-oci and insecure-oci-force-http",
insecureOCIForceHTTP: true,
repoType: "helm",
enableOCI: false,
expectError: true,
},
{
name: "type=git with insecure-oci-force-http",
insecureOCIForceHTTP: true,
repoType: "git",
enableOCI: false,
expectError: true,
},
{
name: "type=git with enable-oci and insecure-oci-force-http",
insecureOCIForceHTTP: true,
repoType: "git",
enableOCI: true,
expectError: true,
},
{
name: "insecure-oci-force-http not set on git repo",
insecureOCIForceHTTP: false,
repoType: "git",
enableOCI: false,
expectError: false,
},
{
name: "insecure-oci-force-http not set on helm repo",
insecureOCIForceHTTP: false,
repoType: "helm",
enableOCI: false,
expectError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateInsecureOCIForceHTTP(tt.insecureOCIForceHTTP, tt.repoType, tt.enableOCI)
if tt.expectError {
require.ErrorContains(t, err, "--insecure-oci-force-http requires --type oci or --enable-oci")
} else {
require.NoError(t, err)
}
})
}
}
+43
View File
@@ -2,6 +2,47 @@
## Breaking Changes
### Helm was upgraded to `4.2.0`.
In Helm v4, the OCI implementation is more strict: if the registry doesn't speak TLS, it is required to explicitly use `--plain-http` upon `helm push`, `helm registry login` and upon `helm dependency build`.
This is a breaking change for Argo CD users with plain HTTP OCI registries. Two actions may be required:
#### Set --insecure-oci-force-http on existing OCI repos
For any existing OCI repository that uses plain HTTP (not HTTPS), add the flag via CLI (CLI version 3.5): `repo add ... --insecure-oci-force-http --upsert`
Or set it directly in the Kubernetes secret:
```yaml
stringData:
insecureOCIForceHttp: "true"
```
#### Explicitly register plain HTTP dependency repos
If your Helm charts have OCI dependencies (Chart.yaml dependencies with repository: `oci://...`) hosted on plain HTTP registries, those dependency repos must now be explicitly added to Argo CD with the `--insecure-oci-force-http` flag, the same way as described above. With Helm v3 this was not required — plain HTTP OCI dependency registries were accessed transparently without registration. With Helm v4, Argo CD needs to know which dependency registries use plain HTTP in order to pass `--plain-http` to `helm dependency build`.
#### Known limitation — conflicting TLS flags (Helm v4)
For Argo CD OCI repo, setting both `--insecure-skip-server-verification` (passed to Helm as `--insecure-skip-tls-verify`) and `--insecure-oci-force-http` (passed to Helm as `--plain-http`) causes Helm v4 to silently drop `--plain-http`, as `--insecure-skip-tls-verify` takes internal precedence. Plain HTTP registry operations then fail with `http: server gave HTTP response to HTTPS client`. This affects:
- Argo CD repos with `type=helm` with `--enable-oci`: having both flags on the same repo causes the chart pull to fail.
- Argo CD repos with `type=oci` and `type=helm` with dependencies: `--insecure-skip-server-verification` on the main repo combined with `--insecure-oci-force-http` on any dependency repo causes dependency build to fail.
There is no workaround when both configurations are legitimately required in the same chain.
#### Applications with spec.source.helm.version setting
If you historically have the following setting on your Helm application, the setting is ignored and Argo CD will use Helm v4 only to render your charts:
```yaml
spec:
source:
helm:
version: v3
```
You are not required to update or remove this field, and can leave this setting as is.
### UI extensions must externalize `react/jsx-runtime`
The Argo CD UI was upgraded from React 16 to React 19 in 3.5. UI extensions built against an older Argo CD UI may fail to load with a `TypeError` until they are rebuilt to externalize `react/jsx-runtime`.
@@ -130,6 +171,8 @@ Consume structured result from `sourceIntegrityResult` field instead.
## Helm Upgraded
Helm was upgraded to `4.2.0`, resulting in some breaking changes described in the `Breaking changes` section of this doc.
## Custom Healthchecks Added
## Other Changes
+1
View File
@@ -56,6 +56,7 @@ argocd repocreds add REPOURL [flags]
--github-app-installation-id int installation id of the GitHub Application (optional, will be auto-discovered if not provided)
--github-app-private-key-path string private key of the GitHub Application
-h, --help help for add
--insecure-oci-force-http Use http when accessing an OCI repository
--password string password to the repository
--proxy-url string If provided, this URL will be used to connect via proxy
--ssh-private-key-path string path to the private ssh key (e.g. ~/.ssh/id_rsa)
+8 -9
View File
@@ -689,18 +689,14 @@ repoServer:
chmod -R 777 $HELM_DATA_HOME;
```
## Helm Version
## Helm Version
Argo CD will assume that the Helm chart is v3 (even if the apiVersion field in the chart is Helm v2), unless v2 is explicitly specified within the Argo CD Application (see below).
This field was used in the past, during the transition period from Helm 2 to Helm 3.
Before Helm 2 became EOL, Argo CD was shipped with both Helm binaries (v2 and v3) and users could specify which Helm binary Argo CD should use to render their charts, by setting this field.
If needed, it is possible to specifically set the Helm version to template with by setting the `helm-version` flag on the cli (either v2 or v3):
```bash
argocd app set helm-guestbook --helm-version v3
```
Or using declarative syntax:
Since Helm 2 became EOL, this field does not need to be configured anymore. It exists for backwards-compatibility only. The only Helm binary used to render charts in Argo CD (starting with version 3.5) is v4.
If you historically have the following setting on your Helm applications:
```yaml
spec:
source:
@@ -708,6 +704,9 @@ spec:
version: v3
```
you are not required to update or remove this field, and can leave this setting as is.
## Helm `--pass-credentials`
Helm, [starting with v3.6.1](https://github.com/helm/helm/releases/tag/v3.6.1),
@@ -0,0 +1 @@
1376ea697140e4db316736e760d5a47d12afc1524dce704476ef06fd7fdeddc6 helm-v4.2.0-darwin-amd64.tar.gz
@@ -0,0 +1 @@
f13f959015447b6bc309f9fd506509926543988a39035c088b52522ec95e2acb helm-v4.2.0-darwin-arm64.tar.gz
@@ -0,0 +1 @@
97dbeb971be4ac4b27e3839976d9564c0fb35c6f3b1da89dd1e292d236af4096 helm-v4.2.0-linux-amd64.tar.gz
@@ -0,0 +1 @@
1f8de130dfbd04de64978e7b852a7a547be1404956a366608276d2520b678670 helm-v4.2.0-linux-arm64.tar.gz
@@ -0,0 +1 @@
48f0637b93247717b725e8d4a8d2cf8df0e2fdea91bdd0e36e2426c2d5c76e4e helm-v4.2.0-linux-ppc64le.tar.gz
@@ -0,0 +1 @@
328e9ed27904f9910026240c4311bb1b0bf91c6fde1634f212097694507a702f helm-v4.2.0-linux-s390x.tar.gz
+2 -2
View File
@@ -3,9 +3,9 @@ set -eux -o pipefail
. "$(dirname "$0")"/../tool-versions.sh
export TARGET_FILE=helm-v${helm3_version}-${INSTALL_OS}-${ARCHITECTURE}.tar.gz
export TARGET_FILE=helm-v${helm4_version}-${INSTALL_OS}-${ARCHITECTURE}.tar.gz
[ -e "$DOWNLOADS/${TARGET_FILE}" ] || curl -sLf --retry 3 -o "$DOWNLOADS/${TARGET_FILE}" "https://get.helm.sh/helm-v${helm3_version}-$INSTALL_OS-$ARCHITECTURE.tar.gz"
[ -e "$DOWNLOADS/${TARGET_FILE}" ] || curl -sLf --retry 3 -o "$DOWNLOADS/${TARGET_FILE}" "https://get.helm.sh/helm-v${helm4_version}-$INSTALL_OS-$ARCHITECTURE.tar.gz"
"$(dirname "$0")"/compare-chksum.sh
mkdir -p /tmp/helm && tar -C /tmp/helm -xf "$DOWNLOADS/${TARGET_FILE}"
sudo install -m 0755 "/tmp/helm/$INSTALL_OS-$ARCHITECTURE/helm" "$BIN/helm"
+1 -1
View File
@@ -11,7 +11,7 @@
# Use ./hack/installers/checksums/add-helm-checksums.sh and
# add-kustomize-checksums.sh to help download checksums.
###############################################################################
helm3_version=3.21.0
helm4_version=4.2.0
kustomize5_version=5.8.1
protoc_version=29.3
oras_version=1.2.0
@@ -13,6 +13,7 @@ metadata:
app: argocd-redis-ha
secrets:
- name: argocd-redis
---
# Source: redis-ha/charts/redis-ha/templates/redis-haproxy-serviceaccount.yaml
apiVersion: v1
@@ -25,6 +26,7 @@ metadata:
release: argocd
chart: redis-ha-4.35.10
app: argocd-redis-ha
---
# Source: redis-ha/charts/redis-ha/templates/redis-ha-configmap.yaml
apiVersion: v1
@@ -846,6 +848,7 @@ data:
done
echo "Failover successful"
fi
---
# Source: redis-ha/charts/redis-ha/templates/redis-ha-health-configmap.yaml
apiVersion: v1
@@ -921,6 +924,7 @@ data:
exit 1
fi
echo "response=$response"
---
# Source: redis-ha/charts/redis-ha/templates/redis-ha-role.yaml
apiVersion: rbac.authorization.k8s.io/v1
@@ -940,6 +944,7 @@ rules:
- endpoints
verbs:
- get
---
# Source: redis-ha/charts/redis-ha/templates/redis-haproxy-role.yaml
apiVersion: rbac.authorization.k8s.io/v1
@@ -960,6 +965,7 @@ rules:
- endpoints
verbs:
- get
---
# Source: redis-ha/charts/redis-ha/templates/redis-ha-rolebinding.yaml
apiVersion: rbac.authorization.k8s.io/v1
@@ -979,6 +985,7 @@ roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: argocd-redis-ha
---
# Source: redis-ha/charts/redis-ha/templates/redis-haproxy-rolebinding.yaml
apiVersion: rbac.authorization.k8s.io/v1
@@ -999,6 +1006,7 @@ roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: argocd-redis-ha-haproxy
---
# Source: redis-ha/charts/redis-ha/templates/redis-ha-announce-service.yaml
apiVersion: v1
@@ -1086,6 +1094,7 @@ spec:
release: argocd
app: redis-ha
"statefulset.kubernetes.io/pod-name": argocd-redis-ha-server-2
---
# Source: redis-ha/charts/redis-ha/templates/redis-ha-service.yaml
apiVersion: v1
@@ -1114,6 +1123,7 @@ spec:
selector:
release: argocd
app: redis-ha
---
# Source: redis-ha/charts/redis-ha/templates/redis-haproxy-service.yaml
apiVersion: v1
@@ -1142,6 +1152,7 @@ spec:
selector:
release: argocd
app: redis-ha-haproxy
---
# Source: redis-ha/charts/redis-ha/templates/redis-haproxy-deployment.yaml
kind: Deployment
@@ -1279,6 +1290,7 @@ spec:
- name: data
emptyDir:
{}
---
# Source: redis-ha/charts/redis-ha/templates/redis-ha-statefulset.yaml
apiVersion: apps/v1
+13 -3
View File
@@ -1140,6 +1140,7 @@ func getHelmRepos(appPath string, repositories []*v1alpha1.Repository, helmRepoC
repo.TLSClientCertData = repositoryCredential.TLSClientCertData
repo.TLSClientCertKey = repositoryCredential.TLSClientCertKey
repo.UseAzureWorkloadIdentity = repositoryCredential.UseAzureWorkloadIdentity
repo.InsecureOCIForceHttp = repositoryCredential.InsecureOCIForceHttp
} else if repo.EnableOCI {
// finally if repo is OCI and no credentials found, use the first OCI credential matching by hostname
// see https://github.com/argoproj/argo-cd/issues/14636
@@ -1154,12 +1155,13 @@ func getHelmRepos(appPath string, repositories []*v1alpha1.Repository, helmRepoC
repo.Password = cred.Password
repo.UseAzureWorkloadIdentity = cred.UseAzureWorkloadIdentity
repo.EnableOCI = true
repo.InsecureOCIForceHttp = cred.InsecureOCIForceHttp
break
}
}
}
}
repos = append(repos, helm.HelmRepository{Name: repo.Name, Repo: repo.Repo, Creds: repo.GetHelmCreds(), EnableOci: repo.EnableOCI})
repos = append(repos, helm.HelmRepository{Name: repo.Name, Repo: repo.Repo, Creds: repo.GetHelmCreds(), EnableOci: repo.EnableOCI || repo.Type == "oci", InsecureOCIForceHttp: repo.InsecureOCIForceHttp})
}
return repos, nil
}
@@ -2879,7 +2881,11 @@ func (s *Service) newOCIClientResolveRevision(ctx context.Context, repo *v1alpha
func (s *Service) newHelmClientResolveRevision(repo *v1alpha1.Repository, revision string, chart string, noRevisionCache bool) (helm.Client, string, error) {
enableOCI := repo.EnableOCI || helm.IsHelmOciRepo(repo.Repo)
helmClient := s.newHelmClient(repo.Repo, repo.GetHelmCreds(), enableOCI, repo.Proxy, repo.NoProxy, helm.WithIndexCache(s.cache), helm.WithChartPaths(s.chartPaths))
opts := []helm.ClientOpts{helm.WithIndexCache(s.cache), helm.WithChartPaths(s.chartPaths)}
if repo.InsecureOCIForceHttp {
opts = append(opts, helm.WithPlainHTTP())
}
helmClient := s.newHelmClient(repo.Repo, repo.GetHelmCreds(), enableOCI, repo.Proxy, repo.NoProxy, opts...)
// Note: This check runs the risk of returning a version which is not found in the helm registry.
if versions.IsVersion(revision) {
@@ -3084,7 +3090,11 @@ func (s *Service) TestRepository(ctx context.Context, q *apiclient.TestRepositor
if !helm.IsHelmOciRepo(repo.Repo) {
return errors.New("OCI Helm repository URL should include hostname and port only")
}
_, err := helm.NewClient(repo.Repo, repo.GetHelmCreds(), repo.EnableOCI, repo.Proxy, repo.NoProxy).TestHelmOCI()
clientOpts := []helm.ClientOpts{}
if repo.InsecureOCIForceHttp {
clientOpts = append(clientOpts, helm.WithPlainHTTP())
}
_, err := helm.NewClient(repo.Repo, repo.GetHelmCreds(), repo.EnableOCI, repo.Proxy, repo.NoProxy, clientOpts...).TestHelmOCI()
return err
}
_, err := helm.NewClient(repo.Repo, repo.GetHelmCreds(), repo.EnableOCI, repo.Proxy, repo.NoProxy).GetIndex(false, s.initConstants.HelmRegistryMaxIndexSize)
+38
View File
@@ -5894,3 +5894,41 @@ func TestGenerateManifest_OCISourceSkipsGitClient(t *testing.T) {
// verify that newGitClient was never invoked
assert.False(t, gitCalled, "GenerateManifest should not invoke Git for OCI sources")
}
func TestGetHelmRepos_InsecureOCIForceHttpPropagatedFromRepo(t *testing.T) {
q := apiclient.ManifestRequest{
Repos: []*v1alpha1.Repository{{
Repo: "oci://example.com",
Username: "test",
Password: "test",
Type: "oci",
InsecureOCIForceHttp: true,
}},
HelmRepoCreds: []*v1alpha1.RepoCreds{},
}
helmRepos, err := getHelmRepos("./testdata/oci-dependencies", q.Repos, q.HelmRepoCreds)
require.NoError(t, err)
require.Len(t, helmRepos, 1)
assert.True(t, helmRepos[0].InsecureOCIForceHttp)
}
func TestGetHelmRepos_InsecureOCIForceHttpPropagatedFromRepoCreds(t *testing.T) {
q := apiclient.ManifestRequest{
Repos: []*v1alpha1.Repository{},
HelmRepoCreds: []*v1alpha1.RepoCreds{{
URL: "oci://example.com",
Username: "test",
Password: "test",
Type: "oci",
InsecureOCIForceHttp: true,
}},
}
helmRepos, err := getHelmRepos("./testdata/oci-dependencies", q.Repos, q.HelmRepoCreds)
require.NoError(t, err)
require.Len(t, helmRepos, 1)
assert.True(t, helmRepos[0].InsecureOCIForceHttp)
}
+7 -3
View File
@@ -156,6 +156,7 @@ func AddHelmOCIRepo(t *testing.T, name string) {
"--type", "helm",
"--name", name,
"--enable-oci",
"--insecure-oci-force-http",
}
errors.NewHandler(t).FailOnErr(fixture.RunCli(args...))
}
@@ -212,7 +213,7 @@ func AddHelmoOCICredentialsWithoutUserPass(t *testing.T) {
t.Helper()
args := []string{
"repocreds", "add", fixture.RepoURL(fixture.RepoURLTypeHelmOCI),
"--enable-oci", "--type", "helm",
"--enable-oci", "--type", "helm", "--insecure-oci-force-http",
}
errors.NewHandler(t).FailOnErr(fixture.RunCli(args...))
}
@@ -238,13 +239,14 @@ func PushChartToOCIRegistry(t *testing.T, chartPathName, chartName, chartVersion
chartAbsPath, err2 := filepath.Abs("./" + chartPathName)
require.NoError(t, err2)
errors.NewHandler(t).FailOnErr(fixture.Run("", "helm", "dependency", "build", chartAbsPath))
errors.NewHandler(t).FailOnErr(fixture.Run("", "helm", "dependency", "build", "--plain-http", chartAbsPath))
errors.NewHandler(t).FailOnErr(fixture.Run("", "helm", "package", chartAbsPath, "--destination", tempDest))
_ = os.RemoveAll(fmt.Sprintf("%s/%s", chartAbsPath, "charts"))
errors.NewHandler(t).FailOnErr(fixture.Run(
"",
"helm",
"push",
"--plain-http",
fmt.Sprintf("%s/%s-%s.tgz", tempDest, chartName, chartVersion),
"oci://"+fixture.HelmOCIRegistryURL,
))
@@ -261,7 +263,7 @@ func PushChartToAuthenticatedOCIRegistry(t *testing.T, chartPathName, chartName,
chartAbsPath, err2 := filepath.Abs("./" + chartPathName)
require.NoError(t, err2)
errors.NewHandler(t).FailOnErr(fixture.Run("", "helm", "dependency", "build", chartAbsPath))
errors.NewHandler(t).FailOnErr(fixture.Run("", "helm", "dependency", "build", "--plain-http", chartAbsPath))
errors.NewHandler(t).FailOnErr(fixture.Run("", "helm", "package", chartAbsPath, "--destination", tempDest))
_ = os.RemoveAll(fmt.Sprintf("%s/%s", chartAbsPath, "charts"))
@@ -270,6 +272,7 @@ func PushChartToAuthenticatedOCIRegistry(t *testing.T, chartPathName, chartName,
"helm",
"registry",
"login",
"--plain-http",
"--username", fixture.GitUsername,
"--password", fixture.GitPassword,
"localhost:5001",
@@ -279,6 +282,7 @@ func PushChartToAuthenticatedOCIRegistry(t *testing.T, chartPathName, chartName,
"",
"helm",
"push",
"--plain-http",
fmt.Sprintf("%s/%s-%s.tgz", tempDest, chartName, chartVersion),
"oci://"+fixture.HelmAuthenticatedOCIRegistryURL,
))
+18
View File
@@ -676,3 +676,21 @@ func TestTemplatesHelmOCIWithDependencies(t *testing.T) {
Expect(HealthIs(health.HealthStatusHealthy)).
Expect(SyncStatusIs(SyncStatusCodeSynced))
}
func TestHelmOCIRegistryWithConstraintRevision(t *testing.T) {
Given(t).
PushChartToOCIRegistry("testdata/helm-values", "helm-values", "1.0.0").
HelmOCIRepoAdded("myrepo").
RepoURLType(fixture.RepoURLTypeHelmOCI).
Chart("helm-values").
Revision(">=1.0.0").
When().
CreateApp().
Then().
When().
Sync().
Then().
Expect(OperationPhaseIs(OperationSucceeded)).
Expect(HealthIs(health.HealthStatusHealthy)).
Expect(SyncStatusIs(SyncStatusCodeSynced))
}
+1
View File
@@ -41,6 +41,7 @@ func TestOCIWithOCIHelmRegistryDependencies(t *testing.T) {
PushImageToOCIRegistry("testdata/helm-oci-with-dependencies", "1.0.0").
OCIRegistry(fixture.OCIHostURL).
OCIRepoAdded("helm-oci-with-dependencies", "helm-oci-with-dependencies").
OCIRepoAdded("helm-values", "myrepo").
OCIRegistryPath("helm-oci-with-dependencies").
Revision("1.0.0").
Path(".").
@@ -1,5 +1,4 @@
#!/usr/bin/env bash
export HELM_EXPERIMENTAL_OCI=1
docker run -p 5001:5000 --rm --name authed-registry -v "$(pwd)"/test/fixture/testrepos/.oci-htpasswd:/etc/docker/registry/auth.htpasswd \
-e REGISTRY_AUTH="{htpasswd: {realm: localhost, path: /etc/docker/registry/auth.htpasswd}}" \
registry
@@ -1,3 +1,2 @@
#!/usr/bin/env bash
export HELM_EXPERIMENTAL_OCI=1
docker run -p 5000:5000 --rm --name registry registry
+12 -3
View File
@@ -88,6 +88,13 @@ func WithUserAgent(userAgent string) ClientOpts {
}
}
// WithPlainHTTP configures the client to use plain HTTP instead of HTTPS for OCI registry operations.
func WithPlainHTTP() ClientOpts {
return func(c *nativeHelmChart) {
c.plainHTTP = true
}
}
func NewClient(repoURL string, creds Creds, enableOci bool, proxy string, noProxy string, opts ...ClientOpts) Client {
return NewClientWithLock(repoURL, creds, globalLock, enableOci, proxy, noProxy, opts...)
}
@@ -116,6 +123,7 @@ type nativeHelmChart struct {
creds Creds
repoLock sync.KeyLock
enableOci bool
plainHTTP bool
indexCache indexCache
proxy string
noProxy string
@@ -217,7 +225,7 @@ func (c *nativeHelmChart) ExtractChart(chart string, version string, passCredent
return "", nil, fmt.Errorf("failed to get password for helm registry: %w", err)
}
if helmPassword != "" && c.creds.GetUsername() != "" {
_, err = helmCmd.RegistryLogin(c.repoURL, c.creds)
_, err = helmCmd.RegistryLogin(c.repoURL, c.creds, c.plainHTTP)
if err != nil {
_ = os.RemoveAll(tempDir)
return "", nil, fmt.Errorf("error logging into OCI registry: %w", err)
@@ -229,7 +237,7 @@ func (c *nativeHelmChart) ExtractChart(chart string, version string, passCredent
}
// 'helm pull' ensures that chart is downloaded into temp directory
_, err = helmCmd.PullOCI(c.repoURL, chart, version, tempDest, c.creds)
_, err = helmCmd.PullOCI(c.repoURL, chart, version, tempDest, c.creds, c.plainHTTP)
if err != nil {
_ = os.RemoveAll(tempDir)
return "", nil, fmt.Errorf("error pulling OCI chart: %w", err)
@@ -328,7 +336,7 @@ func (c *nativeHelmChart) TestHelmOCI() (bool, error) {
return false, fmt.Errorf("failed to get password for helm registry: %w", err)
}
if c.creds.GetUsername() != "" && helmPassword != "" {
_, err = helmCmd.RegistryLogin(c.repoURL, c.creds)
_, err = helmCmd.RegistryLogin(c.repoURL, c.creds, c.plainHTTP)
if err != nil {
return false, fmt.Errorf("error logging into OCI registry: %w", err)
}
@@ -483,6 +491,7 @@ func (c *nativeHelmChart) GetTags(chart string, noCache bool) ([]string, error)
if err != nil {
return nil, fmt.Errorf("failed to initialize repository: %w", err)
}
repo.PlainHTTP = c.plainHTTP
tlsConf, err := newTLSConfig(c.creds)
if err != nil {
return nil, fmt.Errorf("failed setup tlsConfig: %w", err)
+12
View File
@@ -769,3 +769,15 @@ entries: {}
t.Logf("Default User-Agent sent: %s", receivedUserAgent)
})
}
func TestWithPlainHTTP(t *testing.T) {
t.Run("not set by default", func(t *testing.T) {
c := NewClient("my.registry.com", HelmCreds{}, true, "", "")
assert.False(t, c.(*nativeHelmChart).plainHTTP)
})
t.Run("set via WithPlainHTTP option", func(t *testing.T) {
c := NewClient("my.registry.com", HelmCreds{}, true, "", "", WithPlainHTTP())
assert.True(t, c.(*nativeHelmChart).plainHTTP)
})
}
+20 -6
View File
@@ -34,11 +34,16 @@ type Cmd struct {
func NewCmd(workDir string, version string, proxy string, noProxy string) (*Cmd, error) {
switch version {
// If v3 is specified (or by default, if no value is specified) then use v3
case "", "v3":
// There was a point in time when we supported both v2 and v3, and at some point we stopped
// supporting v2.
// We now support v4 only, but from Helm docs v4 is backwards compatible with v3 charts.
// Removing the v3 value would break existing applications that specify v3.
// There is also no reason for users to specify v4 explicitly, but in case someone does it, we support the value.
// TODO: Remove support for v3 value in a major release.
case "", "v3", "v4":
return NewCmdWithVersion(workDir, false, proxy, noProxy)
}
return nil, fmt.Errorf("helm chart version '%s' is not supported", version)
return nil, fmt.Errorf("helm version '%s' is not supported", version)
}
func NewCmdWithVersion(workDir string, isHelmOci bool, proxy string, noProxy string) (*Cmd, error) {
@@ -79,13 +84,16 @@ func (c Cmd) run(ctx context.Context, args ...string) (string, string, error) {
return out, fullCommand, nil
}
func (c *Cmd) RegistryLogin(repo string, creds Creds) (string, error) {
func (c *Cmd) RegistryLogin(repo string, creds Creds, plainHTTP bool) (string, error) {
args := []string{"registry", "login"}
registry, err := c.getHelmRegistry(repo)
if err != nil {
return "", fmt.Errorf("failed to parse registry URL: %w", err)
}
args = append(args, registry)
if plainHTTP {
args = append(args, "--plain-http")
}
if creds.GetUsername() != "" {
args = append(args, "--username", creds.GetUsername())
@@ -288,7 +296,7 @@ func (c *Cmd) Fetch(repo, chartName, version, destination string, creds Creds, p
return out, nil
}
func (c *Cmd) PullOCI(repo string, chart string, version string, destination string, creds Creds) (string, error) {
func (c *Cmd) PullOCI(repo string, chart string, version string, destination string, creds Creds, plainHTTP bool) (string, error) {
args := []string{
"pull", fmt.Sprintf("oci://%s/%s", repo, chart), "--version",
version,
@@ -320,6 +328,9 @@ func (c *Cmd) PullOCI(repo string, chart string, version string, destination str
if creds.GetInsecureSkipVerify() {
args = append(args, "--insecure-skip-tls-verify")
}
if plainHTTP {
args = append(args, "--plain-http")
}
out, _, err := c.run(context.Background(), args...)
if err != nil {
return "", fmt.Errorf("failed to pull OCI chart: %w", err)
@@ -327,11 +338,14 @@ func (c *Cmd) PullOCI(repo string, chart string, version string, destination str
return out, nil
}
func (c *Cmd) dependencyBuild(insecure bool) (string, error) {
func (c *Cmd) dependencyBuild(insecure bool, plainHTTP bool) (string, error) {
args := []string{"dependency", "build"}
if insecure {
args = append(args, "--insecure-skip-tls-verify")
}
if plainHTTP {
args = append(args, "--plain-http")
}
out, _, err := c.run(context.Background(), args...)
if err != nil {
return "", fmt.Errorf("failed to build dependencies: %w", err)
+86 -11
View File
@@ -42,7 +42,7 @@ func TestCmd_template_noApiVersionsInError(t *testing.T) {
func TestNewCmd_helmInvalidVersion(t *testing.T) {
_, err := NewCmd(".", "abcd", "", "")
log.Println(err)
assert.EqualError(t, err, "helm chart version 'abcd' is not supported")
assert.EqualError(t, err, "helm version 'abcd' is not supported")
}
func TestNewCmd_withProxy(t *testing.T) {
@@ -57,6 +57,7 @@ func TestRegistryLogin(t *testing.T) {
name string
repo string
creds *HelmCreds
plainHTTP bool
execErr error
expectedErr error
expectedOut string
@@ -114,6 +115,20 @@ func TestRegistryLogin(t *testing.T) {
},
expectedOut: "helm registry login my.registry.com:5000 --username u --password p --ca-file /ca --insecure",
},
{
name: "plain-http",
repo: "my.registry.com/repo",
creds: &HelmCreds{Username: "user", Password: "pass"},
plainHTTP: true,
expectedOut: "helm registry login my.registry.com --plain-http --username user --password pass",
},
{
name: "insecure and plain-http both set",
repo: "my.registry.com/repo",
creds: &HelmCreds{InsecureSkipVerify: true},
plainHTTP: true,
expectedOut: "helm registry login my.registry.com --plain-http --insecure",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
@@ -124,7 +139,7 @@ func TestRegistryLogin(t *testing.T) {
return strings.Join(cmd.Args, " "), nil
})
require.NoError(t, err)
out, err := c.RegistryLogin(tc.repo, tc.creds)
out, err := c.RegistryLogin(tc.repo, tc.creds, tc.plainHTTP)
assert.Equal(t, tc.expectedOut, out)
if tc.expectedErr != nil {
require.EqualError(t, err, tc.expectedErr.Error())
@@ -135,21 +150,36 @@ func TestRegistryLogin(t *testing.T) {
}
}
func TestDependencyBuild(t *testing.T) {
func TestPullOCI(t *testing.T) {
tests := []struct {
name string
insecure bool
creds HelmCreds
plainHTTP bool
expectedOut string
}{
{
name: "without insecure",
insecure: false,
expectedOut: "helm dependency build",
name: "without flags",
creds: HelmCreds{},
plainHTTP: false,
expectedOut: "helm pull oci://my.registry.com/myrepo/mychart --version 1.0.0 --destination /tmp/dest",
},
{
name: "with insecure",
insecure: true,
expectedOut: "helm dependency build --insecure-skip-tls-verify",
name: "insecure skip verify",
creds: HelmCreds{InsecureSkipVerify: true},
plainHTTP: false,
expectedOut: "helm pull oci://my.registry.com/myrepo/mychart --version 1.0.0 --destination /tmp/dest --insecure-skip-tls-verify",
},
{
name: "plain-http",
creds: HelmCreds{},
plainHTTP: true,
expectedOut: "helm pull oci://my.registry.com/myrepo/mychart --version 1.0.0 --destination /tmp/dest --plain-http",
},
{
name: "insecure and plain-http both set",
creds: HelmCreds{InsecureSkipVerify: true},
plainHTTP: true,
expectedOut: "helm pull oci://my.registry.com/myrepo/mychart --version 1.0.0 --destination /tmp/dest --insecure-skip-tls-verify --plain-http",
},
}
for _, tc := range tests {
@@ -158,7 +188,52 @@ func TestDependencyBuild(t *testing.T) {
return strings.Join(cmd.Args, " "), nil
})
require.NoError(t, err)
out, err := c.dependencyBuild(tc.insecure)
out, err := c.PullOCI("my.registry.com/myrepo", "mychart", "1.0.0", "/tmp/dest", &tc.creds, tc.plainHTTP)
require.NoError(t, err)
assert.Equal(t, tc.expectedOut, out)
})
}
}
func TestDependencyBuild(t *testing.T) {
tests := []struct {
name string
insecure bool
plainHTTP bool
expectedOut string
}{
{
name: "without flags",
insecure: false,
plainHTTP: false,
expectedOut: "helm dependency build",
},
{
name: "with insecure",
insecure: true,
plainHTTP: false,
expectedOut: "helm dependency build --insecure-skip-tls-verify",
},
{
name: "with plain-http",
insecure: false,
plainHTTP: true,
expectedOut: "helm dependency build --plain-http",
},
{
name: "with insecure and plain-http both set",
insecure: true,
plainHTTP: true,
expectedOut: "helm dependency build --insecure-skip-tls-verify --plain-http",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
c, err := newCmdWithVersion(".", false, "", "", func(cmd *exec.Cmd, _ func(_ string) string) (string, error) {
return strings.Join(cmd.Args, " "), nil
})
require.NoError(t, err)
out, err := c.dependencyBuild(tc.insecure, tc.plainHTTP)
require.NoError(t, err)
assert.Equal(t, tc.expectedOut, out)
})
+18 -5
View File
@@ -25,9 +25,10 @@ const (
type HelmRepository struct {
Creds
Name string
Repo string
EnableOci bool
Name string
Repo string
EnableOci bool
InsecureOCIForceHttp bool
}
// Helm provides wrapper functionality around the `helm` command.
@@ -91,7 +92,7 @@ func (h *helm) DependencyBuild() error {
return fmt.Errorf("failed to get password for helm registry: %w", err)
}
if repo.GetUsername() != "" && helmPassword != "" {
_, err := h.cmd.RegistryLogin(repo.Repo, repo.Creds)
_, err := h.cmd.RegistryLogin(repo.Repo, repo.Creds, repo.InsecureOCIForceHttp)
defer func() {
_, _ = h.cmd.RegistryLogout(repo.Repo, repo.Creds)
@@ -108,8 +109,20 @@ func (h *helm) DependencyBuild() error {
}
}
}
// Check if any dependent repository has insecureOCIForceHttp set to true
// If so, set the command to use plain HTTP, as helm dependency build command doesn't support mixed TLS and non TLS dependencies
// Please note that the fact we logged in earlier to each dependent repo with it's own InsecureOCIForceHttp either enabled or disabled
// is unrelated to how we perform helm dependency build, which does not have an option to set --plain-http per repo
plainHTTP := false
for i := range h.repos {
if h.repos[i].InsecureOCIForceHttp {
plainHTTP = true
break
}
}
h.repos = nil
_, err := h.cmd.dependencyBuild(h.insecure)
_, err := h.cmd.dependencyBuild(h.insecure, plainHTTP)
if err != nil {
return fmt.Errorf("failed to build helm dependencies: %w", err)
}
+63
View File
@@ -1,7 +1,9 @@
package helm
import (
"os/exec"
"path/filepath"
"slices"
"testing"
"github.com/stretchr/testify/require"
@@ -276,3 +278,64 @@ func TestSkipTests(t *testing.T) {
require.NoError(t, err)
require.Empty(t, objs)
}
func TestDependencyBuild_PlainHTTPFromDependencyRepo(t *testing.T) {
// dependency build has no per-repo --plain-http; if ANY dependency repo is
// plain-http, the whole build must use --plain-http (see helm.DependencyBuild).
tests := []struct {
name string
depInsecureHTTP []bool // one entry per dependency repo
expectPlainHTTP bool
}{
{
name: "single https dep — no plain-http",
depInsecureHTTP: []bool{false},
expectPlainHTTP: false,
},
{
name: "single plain-http dep — plain-http",
depInsecureHTTP: []bool{true},
expectPlainHTTP: true,
},
{
name: "mixed deps — any plain-http dep forces plain-http for the whole build",
depInsecureHTTP: []bool{false, true},
expectPlainHTTP: true,
},
{
name: "all https deps — no plain-http",
depInsecureHTTP: []bool{false, false},
expectPlainHTTP: false,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
var capturedArgs []string
c, err := newCmdWithVersion(".", false, "", "", func(cmd *exec.Cmd, _ func(string) string) (string, error) {
capturedArgs = cmd.Args
return "", nil
})
require.NoError(t, err)
repos := make([]HelmRepository, len(tc.depInsecureHTTP))
for i, forceHTTP := range tc.depInsecureHTTP {
repos[i] = HelmRepository{
Repo: "oci://localhost:5000/myrepo",
EnableOci: true,
InsecureOCIForceHttp: forceHTTP,
Creds: HelmCreds{},
}
}
h := &helm{
cmd: *c,
repos: repos,
}
err = h.DependencyBuild()
require.NoError(t, err)
require.Equal(t, tc.expectPlainHTTP, slices.Contains(capturedArgs, "--plain-http"))
})
}
}