Merge 6cf2e0b522 into daebbfb0ad
				
					
				
			This commit is contained in:
		
						commit
						8c55dc7c01
					
				|  | @ -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 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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 { | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue