helm/pkg/registry/client_test.go

123 lines
3.8 KiB
Go
Raw Normal View History

/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package registry
import (
"io"
Prevent failing helm push on ghcr.io using standard GET auth token flow Fix GHCR auth by not forcing OAuth2 POST but also reset ForceAttemptOAuth2 after login. - Remove ForceAttemptOAuth2 in NewClient and only enable during Login ping and always restore to false. - Aligns with OCI Distribution auth (token via GET), avoiding GHCR 405 on POST /token. - Some tests Failures logs: ```sh ~/p/lifen/test/helm-f/quicktest ❯ ../../../helm/bin/helm push quicktest-0.1.0.tgz oci://ghcr.io/benoittgt/helm-charts --debug level=DEBUG msg=HEAD id=0 url=https://ghcr.io/v2/benoittgt/helm-charts/quicktest/manifests/sha256:af359fd8fb968ec1097afbd6e8e1dac9ee130861082e54dc2340d0c019407873 header=" \"Accept\": \"application/vnd.docker.distribution.manifest.v2+json, application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.oci.image.manifest.v1+json, application/vnd.oci.image.index.v1+json, application/vnd.oci.artifact.manifest.v1+json\"\n \"User-Agent\": \"Helm/4.0+unreleased\"" level=DEBUG msg=Resp id=0 status="401 Unauthorized" header=" \"Www-Authenticate\": \"Bearer realm=\\\"https://ghcr.io/token\\\",service=\\\"ghcr.io\\\",scope=\\\"repository:benoittgt/helm-charts/quicktest:pull\\\"\"\n \"Date\": \"Mon, 01 Sep 2025 13:56:35 GMT\"\n \"Content-Length\": \"73\"\n \"X-Github-Request-Id\": \"DC73:115F:2B40F2C:2BAB567:68B5A613\"\n \"Content-Type\": \"application/json\"" body=" Response body is empty" level=DEBUG msg=POST id=1 url=https://ghcr.io/token header=" \"Content-Type\": \"application/x-www-form-urlencoded\"\n \"User-Agent\": \"Helm/4.0+unreleased\"" level=DEBUG msg=Resp id=1 status="405 Method Not Allowed" header=" \"Docker-Distribution-Api-Version\": \"registry/2.0\"\n \"Strict-Transport-Security\": \"max-age=63072000; includeSubDomains; preload\"\n \"Date\": \"Mon, 01 Sep 2025 13:56:35 GMT\"\n \"Content-Length\": \"78\"\n \"X-Github-Request-Id\": \"DC73:115F:2B40F75:2BAB5C2:68B5A613\"\n \"Content-Type\": \"application/json\"" body="{\"errors\":[{\"code\":\"UNSUPPORTED\",\"message\":\"The operation is unsupported.\"}]}\n" Error: failed to perform "Exists" on destination: HEAD "https://ghcr.io/v2/benoittgt/helm-charts/quicktest/manifests/sha256:af359fd8fb968ec1097afbd6e8e1dac9ee130861082e54dc2340d0c019407873": POST "https://ghcr.io/token": response status code 405: unsupported: The operation is unsupported. ``` Signed-off-by: Benoit Tigeot <benoit.tigeot@lifen.fr>
2025-09-01 23:48:35 +08:00
"net/http"
"net/http/httptest"
"path/filepath"
"strings"
"testing"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/stretchr/testify/require"
"oras.land/oras-go/v2/content/memory"
)
// Inspired by oras test
// https://github.com/oras-project/oras-go/blob/05a2b09cbf2eab1df691411884dc4df741ec56ab/content_test.go#L1802
func TestTagManifestTransformsReferences(t *testing.T) {
memStore := memory.New()
client := &Client{out: io.Discard}
ctx := t.Context()
refWithPlus := "test-registry.io/charts/test:1.0.0+metadata"
expectedRef := "test-registry.io/charts/test:1.0.0_metadata" // + becomes _
configDesc := ocispec.Descriptor{MediaType: ConfigMediaType, Digest: "sha256:config", Size: 100}
layers := []ocispec.Descriptor{{MediaType: ChartLayerMediaType, Digest: "sha256:layer", Size: 200}}
parsedRef, err := newReference(refWithPlus)
require.NoError(t, err)
desc, err := client.tagManifest(ctx, memStore, configDesc, layers, nil, parsedRef)
require.NoError(t, err)
transformedDesc, err := memStore.Resolve(ctx, expectedRef)
require.NoError(t, err, "Should find the reference with _ instead of +")
require.Equal(t, desc.Digest, transformedDesc.Digest)
_, err = memStore.Resolve(ctx, refWithPlus)
require.Error(t, err, "Should NOT find the reference with the original +")
}
Prevent failing helm push on ghcr.io using standard GET auth token flow Fix GHCR auth by not forcing OAuth2 POST but also reset ForceAttemptOAuth2 after login. - Remove ForceAttemptOAuth2 in NewClient and only enable during Login ping and always restore to false. - Aligns with OCI Distribution auth (token via GET), avoiding GHCR 405 on POST /token. - Some tests Failures logs: ```sh ~/p/lifen/test/helm-f/quicktest ❯ ../../../helm/bin/helm push quicktest-0.1.0.tgz oci://ghcr.io/benoittgt/helm-charts --debug level=DEBUG msg=HEAD id=0 url=https://ghcr.io/v2/benoittgt/helm-charts/quicktest/manifests/sha256:af359fd8fb968ec1097afbd6e8e1dac9ee130861082e54dc2340d0c019407873 header=" \"Accept\": \"application/vnd.docker.distribution.manifest.v2+json, application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.oci.image.manifest.v1+json, application/vnd.oci.image.index.v1+json, application/vnd.oci.artifact.manifest.v1+json\"\n \"User-Agent\": \"Helm/4.0+unreleased\"" level=DEBUG msg=Resp id=0 status="401 Unauthorized" header=" \"Www-Authenticate\": \"Bearer realm=\\\"https://ghcr.io/token\\\",service=\\\"ghcr.io\\\",scope=\\\"repository:benoittgt/helm-charts/quicktest:pull\\\"\"\n \"Date\": \"Mon, 01 Sep 2025 13:56:35 GMT\"\n \"Content-Length\": \"73\"\n \"X-Github-Request-Id\": \"DC73:115F:2B40F2C:2BAB567:68B5A613\"\n \"Content-Type\": \"application/json\"" body=" Response body is empty" level=DEBUG msg=POST id=1 url=https://ghcr.io/token header=" \"Content-Type\": \"application/x-www-form-urlencoded\"\n \"User-Agent\": \"Helm/4.0+unreleased\"" level=DEBUG msg=Resp id=1 status="405 Method Not Allowed" header=" \"Docker-Distribution-Api-Version\": \"registry/2.0\"\n \"Strict-Transport-Security\": \"max-age=63072000; includeSubDomains; preload\"\n \"Date\": \"Mon, 01 Sep 2025 13:56:35 GMT\"\n \"Content-Length\": \"78\"\n \"X-Github-Request-Id\": \"DC73:115F:2B40F75:2BAB5C2:68B5A613\"\n \"Content-Type\": \"application/json\"" body="{\"errors\":[{\"code\":\"UNSUPPORTED\",\"message\":\"The operation is unsupported.\"}]}\n" Error: failed to perform "Exists" on destination: HEAD "https://ghcr.io/v2/benoittgt/helm-charts/quicktest/manifests/sha256:af359fd8fb968ec1097afbd6e8e1dac9ee130861082e54dc2340d0c019407873": POST "https://ghcr.io/token": response status code 405: unsupported: The operation is unsupported. ``` Signed-off-by: Benoit Tigeot <benoit.tigeot@lifen.fr>
2025-09-01 23:48:35 +08:00
// Verifies that Login always restores ForceAttemptOAuth2 to false on success.
func TestLogin_ResetsForceAttemptOAuth2_OnSuccess(t *testing.T) {
t.Parallel()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/v2/" {
// Accept either HEAD or GET
w.WriteHeader(http.StatusOK)
return
}
http.NotFound(w, r)
}))
defer srv.Close()
host := strings.TrimPrefix(srv.URL, "http://")
credFile := filepath.Join(t.TempDir(), "config.json")
c, err := NewClient(
ClientOptWriter(io.Discard),
ClientOptCredentialsFile(credFile),
)
if err != nil {
t.Fatalf("NewClient error: %v", err)
}
if c.authorizer == nil || c.authorizer.ForceAttemptOAuth2 {
t.Fatalf("expected ForceAttemptOAuth2 default to be false")
}
// Call Login with plain HTTP against our test server
if err := c.Login(host, LoginOptPlainText(true), LoginOptBasicAuth("u", "p")); err != nil {
t.Fatalf("Login error: %v", err)
}
if c.authorizer.ForceAttemptOAuth2 {
t.Errorf("ForceAttemptOAuth2 should be false after successful Login")
}
}
// Verifies that Login restores ForceAttemptOAuth2 to false even when ping fails.
func TestLogin_ResetsForceAttemptOAuth2_OnFailure(t *testing.T) {
t.Parallel()
// Start and immediately close, so connections will fail
srv := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {}))
host := strings.TrimPrefix(srv.URL, "http://")
srv.Close()
credFile := filepath.Join(t.TempDir(), "config.json")
c, err := NewClient(
ClientOptWriter(io.Discard),
ClientOptCredentialsFile(credFile),
)
if err != nil {
t.Fatalf("NewClient error: %v", err)
}
// Invoke Login, expect an error but ForceAttemptOAuth2 must end false
_ = c.Login(host, LoginOptPlainText(true), LoginOptBasicAuth("u", "p"))
if c.authorizer.ForceAttemptOAuth2 {
t.Errorf("ForceAttemptOAuth2 should be false after failed Login")
}
}