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
```
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

View File

@ -30,6 +30,7 @@ type DiffKey struct {
type Helm struct {
Charts []string
Repo []string
RegistryLoginHost string // Captures the registry host for OCI login
Releases []Release
Deleted []Release
Linted []Release
@ -104,6 +105,7 @@ func (helm *Helm) UpdateRepo() error {
return nil
}
func (helm *Helm) RegistryLogin(name, username, password, caFile, certFile, keyFile string, skipTLSVerify bool) error {
helm.RegistryLoginHost = name
return nil
}
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)
var err error
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 {
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
}
// 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) {
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) {
postRenderer := "foo.sh"
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) {
type args struct {
repoName string