This commit is contained in:
Copilot 2025-10-26 10:16:52 +08:00 committed by GitHub
commit 8c55dc7c01
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 177 additions and 2 deletions

View File

@ -1819,7 +1819,16 @@ repositories:
oci: true oci: true
``` ```
It is important not to include a scheme for the URL as helm requires that these are not present for OCI registries It is important not to include a scheme for the URL as helm requires that these are not present for OCI registries.
The URL can optionally include an organization or repository path. Helmfile will automatically extract the registry hostname for authentication:
```yaml
repositories:
- name: myOCIRegistry
url: quay.io/my-organization
oci: true
```
Secondly the credentials for the OCI registry can either be specified within `helmfile.yaml` similar to Secondly the credentials for the OCI registry can either be specified within `helmfile.yaml` similar to

View File

@ -30,6 +30,7 @@ type DiffKey struct {
type Helm struct { type Helm struct {
Charts []string Charts []string
Repo []string Repo []string
RegistryLoginHost string // Captures the registry host for OCI login
Releases []Release Releases []Release
Deleted []Release Deleted []Release
Linted []Release Linted []Release
@ -104,6 +105,7 @@ func (helm *Helm) UpdateRepo() error {
return nil return nil
} }
func (helm *Helm) RegistryLogin(name, username, password, caFile, certFile, keyFile string, skipTLSVerify bool) error { func (helm *Helm) RegistryLogin(name, username, password, caFile, certFile, keyFile string, skipTLSVerify bool) error {
helm.RegistryLoginHost = name
return nil return nil
} }
func (helm *Helm) SyncRelease(context helmexec.HelmContext, name, chart, namespace string, flags ...string) error { func (helm *Helm) SyncRelease(context helmexec.HelmContext, name, chart, namespace string, flags ...string) error {

View File

@ -592,7 +592,10 @@ func (st *HelmState) SyncRepos(helm RepoUpdater, shouldSkip map[string]bool) ([]
username, password := gatherUsernamePassword(repo.Name, repo.Username, repo.Password) username, password := gatherUsernamePassword(repo.Name, repo.Username, repo.Password)
var err error var err error
if repo.OCI { if repo.OCI {
err = helm.RegistryLogin(repo.URL, username, password, repo.CaFile, repo.CertFile, repo.KeyFile, repo.SkipTLSVerify) // For OCI registries, extract just the registry host for login
// helm registry login expects "registry.io" not "registry.io/org/path"
registryHost := extractRegistryHost(repo.URL)
err = helm.RegistryLogin(registryHost, username, password, repo.CaFile, repo.CertFile, repo.KeyFile, repo.SkipTLSVerify)
} else { } else {
err = helm.AddRepo(repo.Name, repo.URL, repo.CaFile, repo.CertFile, repo.KeyFile, username, password, repo.Managed, repo.PassCredentials, repo.SkipTLSVerify) err = helm.AddRepo(repo.Name, repo.URL, repo.CaFile, repo.CertFile, repo.KeyFile, username, password, repo.Managed, repo.PassCredentials, repo.SkipTLSVerify)
} }
@ -607,6 +610,23 @@ func (st *HelmState) SyncRepos(helm RepoUpdater, shouldSkip map[string]bool) ([]
return updated, nil return updated, nil
} }
// extractRegistryHost extracts the registry hostname (and optional port) from a URL.
// For OCI registries, helm registry login requires just the host, not the full path.
// Examples:
// - "quay.io/org/repo" -> "quay.io"
// - "registry:443/helm-charts" -> "registry:443"
// - "myregistry.azurecr.io" -> "myregistry.azurecr.io"
func extractRegistryHost(repoURL string) string {
// Find the first slash after the initial part
slashIndex := strings.Index(repoURL, "/")
if slashIndex == -1 {
// No slash, return the whole URL
return repoURL
}
// Return everything before the first slash
return repoURL[:slashIndex]
}
func gatherUsernamePassword(repoName string, username string, password string) (string, string) { func gatherUsernamePassword(repoName string, username string, password string) (string, string) {
var user, pass string var user, pass string

View File

@ -1248,6 +1248,103 @@ func TestHelmState_SyncRepos(t *testing.T) {
} }
} }
func TestHelmState_SyncRepos_OCI(t *testing.T) {
tests := []struct {
name string
repos []RepositorySpec
envs map[string]string
wantRegistryHost string
}{
{
name: "OCI registry with organization path",
repos: []RepositorySpec{
{
Name: "ociregistry",
URL: "quay.io/ORG_NAME",
OCI: true,
Username: "testuser",
Password: "testpass",
},
},
wantRegistryHost: "quay.io",
},
{
name: "OCI registry with multiple path segments",
repos: []RepositorySpec{
{
Name: "myregistry",
URL: "registry.example.com/org/team/repo",
OCI: true,
Username: "user",
Password: "pass",
},
},
wantRegistryHost: "registry.example.com",
},
{
name: "OCI registry without path",
repos: []RepositorySpec{
{
Name: "simpleregistry",
URL: "myregistry.azurecr.io",
OCI: true,
Username: "azureuser",
Password: "azurepass",
},
},
wantRegistryHost: "myregistry.azurecr.io",
},
{
name: "OCI registry with port and path",
repos: []RepositorySpec{
{
Name: "localregistry",
URL: "localhost:5000/charts",
OCI: true,
Username: "local",
Password: "local",
},
},
wantRegistryHost: "localhost:5000",
},
{
name: "OCI registry with environment credentials",
repos: []RepositorySpec{
{
Name: "envregistry",
URL: "registry.io/org",
OCI: true,
},
},
envs: map[string]string{
"ENVREGISTRY_USERNAME": "envuser",
"ENVREGISTRY_PASSWORD": "envpass",
},
wantRegistryHost: "registry.io",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
for k, v := range tt.envs {
t.Setenv(k, v)
}
helm := &exectest.Helm{}
state := &HelmState{
ReleaseSetSpec: ReleaseSetSpec{
Repositories: tt.repos,
},
}
_, err := state.SyncRepos(helm, map[string]bool{})
if err != nil {
t.Errorf("HelmState.SyncRepos() error = %v", err)
}
if helm.RegistryLoginHost != tt.wantRegistryHost {
t.Errorf("HelmState.SyncRepos() RegistryLoginHost = %q, want %q", helm.RegistryLoginHost, tt.wantRegistryHost)
}
})
}
}
func TestHelmState_SyncReleases(t *testing.T) { func TestHelmState_SyncReleases(t *testing.T) {
postRenderer := "foo.sh" postRenderer := "foo.sh"
tests := []struct { tests := []struct {
@ -3095,6 +3192,53 @@ func TestReverse(t *testing.T) {
} }
} }
func Test_extractRegistryHost(t *testing.T) {
tests := []struct {
name string
repoURL string
expected string
}{
{
name: "URL with organization and repository path",
repoURL: "quay.io/ORG_NAME",
expected: "quay.io",
},
{
name: "URL with registry port and path",
repoURL: "registry:443/helm-charts",
expected: "registry:443",
},
{
name: "URL with multiple path segments",
repoURL: "registry.io/org/repo/subpath",
expected: "registry.io",
},
{
name: "URL with registry only, no path",
repoURL: "myregistry.azurecr.io",
expected: "myregistry.azurecr.io",
},
{
name: "URL with registry and port only",
repoURL: "localhost:5000",
expected: "localhost:5000",
},
{
name: "Docker Hub style URL",
repoURL: "registry-1.docker.io/library",
expected: "registry-1.docker.io",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := extractRegistryHost(tt.repoURL)
if got != tt.expected {
t.Errorf("extractRegistryHost(%q) = %q, want %q", tt.repoURL, got, tt.expected)
}
})
}
}
func Test_gatherUsernamePassword(t *testing.T) { func Test_gatherUsernamePassword(t *testing.T) {
type args struct { type args struct {
repoName string repoName string