Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add more tests for docker hub and fix cloudflare R2 downloads #31

Merged
merged 4 commits into from
Feb 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ module(

bazel_dep(name = "platforms", version = "0.0.10")
bazel_dep(name = "bazel_skylib", version = "1.7.1")
bazel_dep(name = "rules_go", version = "0.50.1")
bazel_dep(name = "rules_go", version = "0.53.0")
bazel_dep(name = "gazelle", version = "0.40.0")

go_deps = use_extension("@gazelle//:extensions.bzl", "go_deps")
Expand Down
5 changes: 5 additions & 0 deletions authenticate/s3/s3.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,11 @@ func (s *S3Resolver) Get(ctx context.Context, req api.GetCredentialsRequest) (ap
return api.GetCredentialsResponse{}, err
}

if parsedURL.Query().Has("X-Amz-Expires") {
// This is a presigned URL, no need to sign it again.
return api.GetCredentialsResponse{}, nil
}

if parsedURL.Scheme != "https" {
return api.GetCredentialsResponse{}, errors.New("only https is supported")
}
Expand Down
2 changes: 1 addition & 1 deletion examples/customized/MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ local_path_override(
path = "../..",
)

bazel_dep(name = "rules_go", version = "0.50.1")
bazel_dep(name = "rules_go", version = "0.53.0")
bazel_dep(name = "gazelle", version = "0.40.0")

go_sdk = use_extension("@rules_go//go:extensions.bzl", "go_sdk")
Expand Down
2 changes: 1 addition & 1 deletion examples/full/MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,4 @@ oci.pull(
image = "hello-world",
platforms = ["linux/amd64"],
)
use_repo(oci, "docker_hub_hello_world_linux_amd64", "ghcr_oci")
use_repo(oci, "docker_hub_hello_world", "docker_hub_hello_world_linux_amd64", "ghcr_oci")
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require (
github.com/aws/aws-sdk-go-v2 v1.32.5
github.com/aws/aws-sdk-go-v2/config v1.28.5
github.com/aws/aws-sdk-go-v2/credentials v1.17.46
github.com/bazelbuild/rules_go v0.53.0
github.com/stretchr/testify v1.10.0
github.com/zalando/go-keyring v0.2.6
golang.org/x/oauth2 v0.24.0
Expand All @@ -29,6 +30,6 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/sys v0.26.0 // indirect
golang.org/x/sys v0.28.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.33.1 h1:6SZUVRQNvExYlMLbHdlKB48x0fLb
github.com/aws/aws-sdk-go-v2/service/sts v1.33.1/go.mod h1:GqWyYCwLXnlUB1lOAXQyNSPqPLQJvmo8J0DWBzp9mtg=
github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro=
github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
github.com/bazelbuild/rules_go v0.53.0 h1:u160DT+RRb+Xb2aSO4piN8xhs4aZvWz2UDXCq48F4ao=
github.com/bazelbuild/rules_go v0.53.0/go.mod h1:xB1jfsYHWlnZyPPxzlOSst4q2ZAwS251Mp9Iw6TPuBc=
github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0=
github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
Expand All @@ -48,8 +50,8 @@ github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8u
github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI=
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
Expand Down
2 changes: 1 addition & 1 deletion helperfactory/fallback/fallback_factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func FallbackHelperFactory(rawURL string) (api.Helper, error) {
return &authenticateGitHub.GitHub{}, nil
case strings.EqualFold(u.Host, "ghcr.io"):
return authenticateGitHub.GitHubContainerRegistry(), nil
case strings.HasSuffix(strings.ToLower(u.Host), ".r2.cloudflarestorage.com"):
case strings.HasSuffix(strings.ToLower(u.Host), ".r2.cloudflarestorage.com") && !u.Query().Has("X-Amz-Expires"):
return &authenticateS3.R2{}, nil
// container registries using the default OCI resolver
case strings.EqualFold(u.Host, "index.docker.io"):
Expand Down
15 changes: 15 additions & 0 deletions integration/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
load("@rules_go//go:def.bzl", "go_test")

go_test(
name = "integration_test",
srcs = [
"framework_test.go",
"oci_test.go",
],
data = ["//:tweag-credential-helper"],
env = {"CREDENTIAL_HELPER_LOGGING": "debug"},
deps = [
"//api",
"@rules_go//go/runfiles",
],
)
193 changes: 193 additions & 0 deletions integration/framework_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
package integration

import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"io"
"net/http"
"os"
"os/exec"
"testing"

"github.com/bazelbuild/rules_go/go/runfiles"
"github.com/tweag/credential-helper/api"
)

type response struct{}

type TestBuilder struct {
credentialHelperBinary string
t *testing.T
}

func (builder *TestBuilder) WithHelperBinary(credentialHelperBinary string) *TestBuilder {
builder.credentialHelperBinary = credentialHelperBinary
return builder
}

func (builder *TestBuilder) WithDefaultHelperBinary() *TestBuilder {
if os.Getenv("BAZEL_TEST") == "1" {
runfileLocation, err := runfiles.Rlocation("_main/tweag-credential-helper_/tweag-credential-helper")
if err != nil {
panic(err)
}
builder.credentialHelperBinary = runfileLocation
} else {
builder.credentialHelperBinary = "tweag-credential-helper"
}
return builder
}

func (builder *TestBuilder) WithT(t *testing.T) *TestBuilder {
builder.t = t
return builder
}

func (builder *TestBuilder) Build() *CredentialHelperTest {
return &CredentialHelperTest{
credentialHelperBinary: builder.credentialHelperBinary,
t: builder.t,
}
}

type CredentialHelperTest struct {
credentialHelperBinary string
t *testing.T
}

func (test *CredentialHelperTest) invoke(ctx context.Context, stdin []byte, args ...string) ([]byte, error) {
cmd := exec.CommandContext(ctx, test.credentialHelperBinary, args...)
cmd.Stdin = bytes.NewReader(stdin)
cmd.Stderr = os.Stderr
out, err := cmd.Output()
return out, err
}

func (test *CredentialHelperTest) getCommand(ctx context.Context, uri string) (api.GetCredentialsResponse, error) {
stdin, err := json.Marshal(api.GetCredentialsRequest{URI: uri})
if err != nil {
return api.GetCredentialsResponse{}, err
}

var response api.GetCredentialsResponse
stdout, err := test.invoke(ctx, stdin, "get")
if err != nil {
return api.GetCredentialsResponse{}, err
}
if err := json.Unmarshal(stdout, &response); err != nil {
return api.GetCredentialsResponse{}, err
}
return response, nil
}

func (test *CredentialHelperTest) Fetch(ctx context.Context, uri string) (*FetchResult, error) {
helperResp, err := test.getCommand(ctx, uri)
if err != nil {
return nil, err
}

req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, http.NoBody)
if err != nil {
return nil, err
}

for key, values := range helperResp.Headers {
for _, value := range values {
req.Header.Add(key, value)
}
}

client := http.Client{}

httpResp, err := client.Do(req)
if err != nil {
return nil, err
}

defer httpResp.Body.Close()

body, err := io.ReadAll(httpResp.Body)
if err != nil {
return nil, err
}

return &FetchResult{
helperResp: &helperResp,
httpResp: httpResp,
body: body,
t: test.t,
}, nil
}

type FetchResult struct {
helperResp *api.GetCredentialsResponse
httpResp *http.Response
body []byte
t *testing.T
}

func (result *FetchResult) ExpectStatusCode(expected int) {
if result.httpResp == nil {
result.t.Error("http response is nil")
return
}
if result.httpResp.StatusCode != expected {
result.t.Errorf("expected status code %d, got %d", expected, result.httpResp.StatusCode)
}
}

func (result *FetchResult) ExpectHeader(key, value string) {
if result.httpResp == nil {
result.t.Error("http response is nil")
return
}
actual := result.httpResp.Header.Get(key)
if actual != value {
result.t.Errorf("expected header %q to be %q, got %q", key, value, actual)
}
}

func (result *FetchResult) ExpectHelperToReturnHeader(headerName string, validations ...func([]string) error) {
if result.helperResp == nil {
result.t.Error("helper response is nil")
return
}
actual := result.helperResp.Headers[headerName]
for _, validation := range validations {
if err := validation(actual); err != nil {
result.t.Error(err)
}
}
}

func (result *FetchResult) ExpectBody(expected []byte) {
if !bytes.Equal(result.body, expected) {
result.t.Errorf("expected body to match %v, got %v", string(expected), string(result.body))
}
}

func (result *FetchResult) ExpectBodySHA256(expected [sha256.Size]byte) {
if result.httpResp == nil {
result.t.Error("http response is nil")
return
}
hash := sha256.New()
hash.Write(result.body)
actual := hash.Sum(nil)
if !bytes.Equal(actual, expected[:]) {
result.t.Errorf("expected body sha256 to match %x, got %x", expected, actual)
}
}

func HexToSHA256(t *testing.T, hexStr string) [sha256.Size]byte {
hash, err := hex.DecodeString(hexStr)
if err != nil {
t.Fatalf("failed to decode hex: %v", err)
}
var hash32 [sha256.Size]byte
copy(hash32[:], hash)
return hash32
}
28 changes: 28 additions & 0 deletions integration/oci_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package integration

import (
"context"
"strings"
"testing"
)

func TestDockerHub(t *testing.T) {
builder := &TestBuilder{}
test := builder.WithDefaultHelperBinary().WithT(t).Build()
ctx := context.Background()
fetchResult, err := test.Fetch(ctx, "https://index.docker.io/v2/library/hello-world/blobs/sha256:d2c94e258dcb3c5ac2798d32e1249e42ef01cba4841c2234249495f87264ac5a")
if err != nil {
t.Fatalf("failed to fetch credentials: %v", err)
}
fetchResult.ExpectHelperToReturnHeader("Authorization", func(s []string) error {
if len(s) != 1 {
t.Errorf("expected exactly one Authorization header, got %d", len(s))
}
if len(s) == 1 && !strings.HasPrefix(s[0], "Bearer ") {
t.Errorf("expected Authorization header to start with 'Bearer '")
}
return nil
})
fetchResult.ExpectStatusCode(200)
fetchResult.ExpectBodySHA256(HexToSHA256(t, "d2c94e258dcb3c5ac2798d32e1249e42ef01cba4841c2234249495f87264ac5a"))
}
Loading