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 goauth binary for GOAUTH environment support #22

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
103 changes: 103 additions & 0 deletions cmd/goauth/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Copyright 2022 Google LLC All Rights Reserved.
//
// 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 main

import (
"context"
"encoding/base64"
"flag"
"fmt"
"log"
"os"
"strings"
"time"

"github.com/GoogleCloudPlatform/artifact-registry-go-tools/pkg/auth"
)

const help = `
Handle Go authentication with Google Cloud Artifact Registry Go Repositories.

Add to your GOAUTH environment variable:

export GOAUTH="sh -c 'GOPROXY=direct go run github.com/GoogleCloudPlatform/artifact-registry-go-tools/cmd/goauth@latest <location>'"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we can leave out sh -c. Without it the command should still work but not sure if sh -c works on Windows?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It won't work on Windows and I doubt sh -c is also needed.

Suggested change
export GOAUTH="sh -c 'GOPROXY=direct go run github.com/GoogleCloudPlatform/artifact-registry-go-tools/cmd/goauth@latest <location>'"
export GOAUTH="go run github.com/GoogleCloudPlatform/artifact-registry-go-tools/cmd/goauth@latest <location>"

must work allright

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It won't work on Windows and I doubt sh -c is also needed.

must work allright

No, it does not. Please feel free to do some testing and verification.

I already checked these scenarios:

export GOAUTH="go run github.com/smartpricer/artifact-registry-go-tools/cmd/goauth@latest europe-west1"

#--> recursive call

export GOAUTH="GOPROXY=direct go run github.com/smartpricer/artifact-registry-go-tools/cmd/goauth@latest europe-west1"

#--> 401, does not work

export GOAUTH="sh -c 'GOPROXY=direct go run github.com/smartpricer/artifact-registry-go-tools/cmd/goauth@latest europe-west1'"

#--> works fine

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding the Windows support: Feel free to suggest an example for Windows, after testing it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe the instructions must change to ask users to run go install first instead of go run

If you install the binary first like this:

GOPROXY=direct go install github.com/GoogleCloudPlatform/artifact-registry-go-tools/cmd/goauth@latest

Then you can use the installed binary directly in GOAUTH without having to use GOPROXY

GOAUTH="goauth europe-west1"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even better would be for this to be distributed with the gcloud SDK like the docker Auth does.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if going with @cagataygurturk , it needs to be renamed, e.g. go-auth-google-artifact-registry or go-auth-gar or sth. like this.

Besides that, I kind of agree with @owenhaynes , that this should become part of the Google Cloud SDK.

On our side, we solved this easily, because we run a mono repository, so we already have a place to put team-wide tooling.

Copy link
Member

@yihanzhen yihanzhen Feb 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even better would be for this to be distributed with the gcloud SDK like the docker Auth does.

I had the same thought - but the original rationale of implementing auth helpers in the same language as the repository format is that the users won't have a dependency on gcloud SDK in their build flow, and some customers do care about this mostly because gcloud is huge.

That being said I think implementing it within gcloud is indeed simpler; I'll go ahead and do that first to hopefully unblock folks. I would like to eventually add support for it in this auth tool, but I think it would require some design review among the team so I'll get back to it later.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI after spending a lot of time debugging what's described in golang/go#71889 I finally have made it to work locally. Need some extra time adding tests and stuff and this should be available very soon.


To support multiple locations, add the command multiple times to the GOAUTH variable (semicolon-separated).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add an example for using this together with a non Artifact Registry repo? e.g.,

To support an Artifact Registry repo together with other repos, add all commands to the GOAUTH variable. For example:

export GOAUTH="GOPROXY=direct go run github.com/GoogleCloudPlatform/artifact-registry-go-tools/cmd/goauth@latest <location>; another command"

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not against it, but your example won't work, see below. Please feel free to do some testing and verification on your side.


For more details, see https://pkg.go.dev/cmd/go@master#hdr-GOAUTH_environment_variable`

const defaultHostPattern = "%s-go.pkg.dev"

func main() {
jsonKey := flag.String("json_key", "", "path to the json key of the service account used for this location. Leave empty to use the oauth token instead.")
Copy link
Member

@yihanzhen yihanzhen Feb 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was wondering if we could reuse the same program entry point. The main motivation is to avoid duplicate code (the two flags here), or at least make it harder for future developers to miss one another when adding more features to goauth or netrc.

maybe

export GOAUTH="GOPROXY=direct go run github.com/GoogleCloudPlatform/artifact-registry-go-tools/cmd/auth@latest goauth ---location"

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's better to have it separate, because the application behavior is dictated by Go and will have to be different from the auth command behavior: Things like writing and reading from stdin/out and getting an additional commandline argument.

Even if they seem to be similar now, they probably will divert more in the future.

hostPattern := flag.String("host_pattern", defaultHostPattern, "Artifact Registry server host pattern, where %s will be replaced by a location string.")

flag.Parse()

location := flag.Arg(0)
if location == "" {
fmt.Fprintln(os.Stderr, help)
return
}
if strings.HasPrefix(location, "https://") {
log.Println("Location has to be a Google Cloud region, e.g. 'us-central1'.")
os.Exit(2)
}

// generate the authentication header
urlLine := locationURL(location, *hostPattern)
authHeader, err := keyAuthHeader(*jsonKey)
if err != nil {
log.Println(err)
os.Exit(3)
}

// send the Go authentication information
fmt.Printf("%s\n\nAuthorization: %s\n\n", urlLine, authHeader)
}

func locationURL(location string, hostPattern string) string {
host := fmt.Sprintf(hostPattern, location)

return fmt.Sprintf("https://%s", host)
}

func keyAuthHeader(jsonKeyPath string) (string, error) {
ctx := context.Background()
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()

if jsonKeyPath != "" {
key, err := auth.EncodeJsonKey(jsonKeyPath)
if err != nil {
return "", fmt.Errorf("failed to encode JSON key: %w", err)
}

return basicAuthHeader("_json_key_base64", key), nil
} else {
token, err := auth.Token(ctx)
if err != nil {
return "", fmt.Errorf("failed to get oauth token: %w", err)
}

return basicAuthHeader("oauth2accesstoken", token), nil
}
}

func basicAuthHeader(username, password string) string {
a := fmt.Sprintf("%s:%s", username, password)
b := base64.StdEncoding.EncodeToString([]byte(a))

return fmt.Sprintf("Basic %s", b)
}
63 changes: 63 additions & 0 deletions cmd/goauth/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package main

import (
"encoding/base64"
"strings"
"testing"
)

func TestLocationURL(t *testing.T) {
url := locationURL("us-central1", defaultHostPattern)

if url != "https://us-central1-go.pkg.dev" {
t.Fatalf("unexpected url: %s", url)
}
}

func TestAuthHeader_DefaultCredentials(t *testing.T) {
header, err := keyAuthHeader("")
if err != nil {
t.Fatal(err)
}

t.Logf("header: %s", header)

if !strings.HasPrefix(header, "Basic ") {
t.Fatal(err)
}

decoded, err := base64.StdEncoding.DecodeString(header[6:])
if err != nil {
t.Fatal(err)
}

t.Logf("decoded: %s", decoded)

if !strings.HasPrefix(string(decoded), "oauth2accesstoken:") {
t.Fatal(err)
}
}

func TestAuthHeader_JSONKey(t *testing.T) {
header, err := keyAuthHeader("./test/dummy.json")
if err != nil {
t.Fatal(err)
}

t.Logf("header: %s", header)

if !strings.HasPrefix(header, "Basic ") {
t.Fatal(err)
}

decoded, err := base64.StdEncoding.DecodeString(header[6:])
if err != nil {
t.Fatal(err)
}

t.Logf("decoded: %s", decoded)

if !strings.HasPrefix(string(decoded), "_json_key_base64:") {
t.Fatal(err)
}
}
1 change: 1 addition & 0 deletions cmd/goauth/test/dummy.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{ "comment": "the content of this file is irrelevant" }