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