Fix OCI registry authentication with URL paths

Extract registry host from URL before login to support paths like quay.io/org

Co-authored-by: yxxhero <11087727+yxxhero@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2025-10-11 11:10:07 +00:00
parent 85dbd5da9a
commit a90252631b
3 changed files with 167 additions and 1 deletions

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