Skip to content

Commit

Permalink
add setup-uri command
Browse files Browse the repository at this point in the history
  • Loading branch information
malt3 committed Feb 22, 2025
1 parent 0c3d595 commit 0815c45
Show file tree
Hide file tree
Showing 10 changed files with 207 additions and 26 deletions.
2 changes: 1 addition & 1 deletion api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ type Cache interface {

// URISetupper is an optional interface that can be implemented by helpers to perform setup for a given URI.
type URISetupper interface {
SetupInstructionsForURI(uri string) string
SetupInstructionsForURI(ctx context.Context, uri string) string
}

var CacheMiss = errors.New("cache miss")
Expand Down
23 changes: 23 additions & 0 deletions authenticate/gcs/gcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package gcs
import (
"context"
"errors"
"fmt"
"net/url"
"time"

Expand All @@ -19,6 +20,28 @@ func (g *GCS) CacheKey(req api.GetCredentialsRequest) string {
return "https://storage.googleapis.com/"
}

func (g *GCS) SetupInstructionsForURI(ctx context.Context, uri string) string {
return fmt.Sprintf(`%s is a Google Cloud Storage (GCS) url.
IAM Setup:
In order to access data from a bucket, you need a Google Cloud user- or service account with read access to the objects you want to access (storage.objects.get).
No other permissions are needed. Refer to Google's documentation for more information: https://cloud.google.com/storage/docs/access-control/iam-permissions
Authentication Methods:
Option 1: Using gcloud CLI as a regular user (Recommended)
1. Install the Google Cloud SDK: https://cloud.google.com/sdk/docs/install
2. Run:
$ gcloud auth application-default login
3. Follow the browser prompts to authenticate
Option 2: Using a Service Account Key, OpenID Connect or other authentication mechanisms
1. Follow Google's documentation for choosing and setting up your method of choice: https://cloud.google.com/docs/authentication
2. Ensure your method of choice sets the Application Default Credentials (ADC) environment variable (GOOGLE_APPLICATION_CREDENTIALS): https://cloud.google.com/docs/authentication/provide-credentials-adc
3. Alternatively, check that the credentials file is in a well-known location ($HOME/.config/gcloud/application_default_credentials.json)`, uri)
}

func (GCS) Resolver(ctx context.Context) (api.Resolver, error) {
credentials, err := gauth.FindDefaultCredentials(ctx, "https://www.googleapis.com/auth/devstorage.read_only")
if err != nil {
Expand Down
29 changes: 29 additions & 0 deletions authenticate/github/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,35 @@ func (g *GitHub) Resolver(ctx context.Context) (api.Resolver, error) {
return &GitHubResolver{tokenSource: source}, nil
}

func (g *GitHub) SetupInstructionsForURI(ctx context.Context, uri string) string {
return fmt.Sprintf(`%s is a GitHub url.
The credential helper can be used to download any assets GitHub hosts, including:
- the git protocol via https
- raw code files (raw.githubusercontent.com/<org>/<repo>/<commit>/<file>)
- patches (github.com/<org>/<repo>/<commit>.patch)
- source tarballs (github.com/<org>/<repo>/archive/refs/tags/v1.2.3.tar.gz)
- release assets (github.com/<org>/<repo>/releases/download/v1.2.3/<file>)
- container images from ghcr.io (doc)
... and more.
With credentials, you are also less likely to be blocked by GitHub rate limits, even when accessing public repositories.
Authentication Methods:
Option 1: Using the GitHub CLI as a regular user (Recommended)
1. Install the GitHub CLI (gh): https://github.com/cli/cli#installation
2. Login via:
$ gh auth login
3. Follow the browser prompts to authenticate
Option 2: Authentication using a GitHub App, GitHub Actions Token or Personal Access Token (PAT)
1. Setup your authentication method of choice
2. Set the required environment variable (GH_TOKEN or GITHUB_TOKEN) when running Bazel (or other tools that access credential helpers)
3. Alternatively, add the secret to the system keyring under the gh:github.com key`, uri)
}

// CacheKey returns a cache key for the given request.
// For GitHub, the same token can be used for all requests.
func (g *GitHub) CacheKey(req api.GetCredentialsRequest) string {
Expand Down
1 change: 1 addition & 0 deletions bzl/private/source_files/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ release_files = [
"//cmd:all_files",
"//cmd/credential-helper:all_files",
"//cmd/installer:all_files",
"//cmd/internal/util:all_files",
"//cmd/root:all_files",
"//cmd/setup:all_files",
"//config:all_files",
Expand Down
19 changes: 19 additions & 0 deletions cmd/internal/util/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
load("@rules_go//go:def.bzl", "go_library")

go_library(
name = "util",
srcs = ["util.go"],
importpath = "github.com/tweag/credential-helper/cmd/internal/util",
visibility = ["//cmd:__subpackages__"],
deps = [
"//api",
"//config",
"//logging",
],
)

filegroup(
name = "all_files",
srcs = glob(["*"]),
visibility = ["//:__subpackages__"],
)
35 changes: 35 additions & 0 deletions cmd/internal/util/util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package util

import (
"context"

"github.com/tweag/credential-helper/api"
"github.com/tweag/credential-helper/config"
"github.com/tweag/credential-helper/logging"
)

func Configure(ctx context.Context, helperFactory api.HelperFactory, configReader config.ConfigReader, uri string) (context.Context, api.Helper) {
cfg, err := configReader.Read()
if err == nil {
logging.Debugf("found config file and choosing helper from it")
helperFactory = func(uri string) (api.Helper, error) {
helper, helperConfig, err := cfg.FindHelper(uri)
if err != nil {
return nil, err
}
if len(helperConfig) > 0 {
ctx = context.WithValue(ctx, api.HelperConfigKey, helperConfig)
}
return helper, nil
}
} else if err != config.ErrConfigNotFound {
logging.Fatalf("reading config: %v", err)
}

authenticator, err := helperFactory(uri)
if err != nil {
logging.Fatalf("%v", err)
}

return ctx, authenticator
}
1 change: 1 addition & 0 deletions cmd/root/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ go_library(
"//api",
"//cache",
"//cmd/installer",
"//cmd/internal/util",
"//cmd/setup",
"//config",
"//logging",
Expand Down
38 changes: 14 additions & 24 deletions cmd/root/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/tweag/credential-helper/api"
"github.com/tweag/credential-helper/cache"
"github.com/tweag/credential-helper/cmd/installer"
"github.com/tweag/credential-helper/cmd/internal/util"
"github.com/tweag/credential-helper/cmd/setup"
"github.com/tweag/credential-helper/config"
"github.com/tweag/credential-helper/logging"
Expand All @@ -25,6 +26,7 @@ const usage = `Usage: credential-helper [COMMAND] [ARGS...]
Commands:
get get credentials in the form of http headers for the uri provided on stdin and print result to stdout (see https://github.com/EngFlow/credential-helper-spec for more information)
setup-uri prints setup instructions for a given uri
setup-keyring stores a secret in the system keyring`

func Run(ctx context.Context, helperFactory api.HelperFactory, newCache api.NewCache, args []string) {
Expand All @@ -44,6 +46,8 @@ func Run(ctx context.Context, helperFactory api.HelperFactory, newCache api.NewC
switch command {
case "get":
clientProcess(ctx, helperFactory)
case "setup-uri":
setup.URIProcess(args[2:], helperFactory, config.OSReader{})
case "setup-keyring":
setup.KeyringProcess(args[2:])
case "agent-launch":
Expand All @@ -68,35 +72,15 @@ func Run(ctx context.Context, helperFactory api.HelperFactory, newCache api.NewC

// foreground immediately responds to the get command and exits.
// If possible, it sends the response to the agent for caching.
func foreground(ctx context.Context, helperFactory api.HelperFactory, cache api.Cache, configReader config.ConfigReader) {
func foreground(ctx context.Context, cache api.Cache, helperFactory api.HelperFactory, configReader config.ConfigReader) {
req := api.GetCredentialsRequest{}

err := json.NewDecoder(os.Stdin).Decode(&req)
if err != nil {
logging.Fatalf("%v", err)
}

cfg, err := configReader.Read()
if err == nil {
logging.Debugf("found config file and choosing helper from it")
helperFactory = func(uri string) (api.Helper, error) {
helper, helperConfig, err := cfg.FindHelper(uri)
if err != nil {
return nil, err
}
if len(helperConfig) > 0 {
ctx = context.WithValue(ctx, api.HelperConfigKey, helperConfig)
}
return helper, nil
}
} else if err != config.ErrConfigNotFound {
logging.Fatalf("reading config: %v", err)
}

authenticator, err := helperFactory(req.URI)
if err != nil {
logging.Fatalf("%v", err)
}
ctx, authenticator := util.Configure(ctx, helperFactory, configReader, req.URI)

cacheKey := authenticator.CacheKey(req)
if len(cacheKey) == 0 {
Expand Down Expand Up @@ -126,7 +110,13 @@ func foreground(ctx context.Context, helperFactory api.HelperFactory, cache api.

resp, err = resolver.Get(ctx, req)
if err != nil {
logging.Fatalf("%s", err)
var extraMessage string
_, canSetupViaAuthenticator := authenticator.(api.URISetupper)
_, canSetupViaResolver := resolver.(api.URISetupper)
if canSetupViaAuthenticator || canSetupViaResolver {
extraMessage = fmt.Sprintf("\n\nTip: try running the following command for setup instructions:\n $ %s setup-uri %s", os.Args[0], req.URI)
}
logging.Fatalf("%s%s", err, extraMessage)
}

err = json.NewEncoder(os.Stdout).Encode(resp)
Expand Down Expand Up @@ -178,7 +168,7 @@ func clientProcess(ctx context.Context, helperFactory api.HelperFactory) {
}
defer cleanup()

foreground(ctx, helperFactory, cache, config.OSReader{})
foreground(ctx, cache, helperFactory, config.OSReader{})
}

func clientCommandProcess(command string, r io.Reader) {
Expand Down
9 changes: 8 additions & 1 deletion cmd/setup/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,18 @@ load("@rules_go//go:def.bzl", "go_library")

go_library(
name = "setup",
srcs = ["keyring.go"],
srcs = [
"keyring.go",
"uri.go",
],
importpath = "github.com/tweag/credential-helper/cmd/setup",
visibility = ["//visibility:public"],
deps = [
"//agent/locate",
"//api",
"//cmd/internal/util",
"//config",
"//logging",
"@com_github_zalando_go_keyring//:go-keyring",
],
)
Expand Down
76 changes: 76 additions & 0 deletions cmd/setup/uri.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package setup

import (
"context"
"flag"
"fmt"
"os"

"github.com/tweag/credential-helper/api"
"github.com/tweag/credential-helper/cmd/internal/util"
"github.com/tweag/credential-helper/config"
"github.com/tweag/credential-helper/logging"
)

// URIProcess is the entry point for the setup-uri command.
func URIProcess(args []string, helperFactory api.HelperFactory, configReader config.ConfigReader) {
ctx := context.Background()

flagSet := flag.NewFlagSet("setup-uri", flag.ExitOnError)
flagSet.Usage = func() {
fmt.Fprintf(flagSet.Output(), "Prints setup instructions for a given uri.\n\n")
fmt.Fprintf(flagSet.Output(), "Usage: credential-helper setup-uri [uri]\n")
flagSet.PrintDefaults()
examples := []string{
"credential-helper setup-uri https://github.com/my-org/project/releases/download/v1.2.3/my-artifact.tar.gz",
"credential-helper setup-uri https://raw.githubusercontent.com/my-org/project/6012...a5de28/file.txt",
"credential-helper setup-uri https://storage.googleapis.com/bucket/path/to/object",
"credential-helper setup-uri https://my-bucket.s3.amazonaws.com/path/to/object",
"credential-helper setup-uri https://org-id.r2.cloudflarestorage.com/bucket/path/to/object",
"credential-helper setup-uri https://index.docker.io/v2/library/hello-world/blobs/sha256:d2c94e...7264ac5a",
}
fmt.Fprintf(flagSet.Output(), "\nExamples:\n")
for _, example := range examples {
fmt.Fprintf(flagSet.Output(), " $ %s\n", example)
}
os.Exit(1)
}

if err := flagSet.Parse(args); err != nil {
fatalFmt("parsing flags for setup-uri: %v", err)
}

if flagSet.NArg() != 1 {
flagSet.Usage()
}

uri := flagSet.Arg(0)

ctx, authenticator := util.Configure(ctx, helperFactory, configReader, uri)

var instructionGiver api.URISetupper

// first try to use the authenticator directly
instructionGiver, ok := authenticator.(api.URISetupper)

// as a fallback, try to use the resolver
if !ok {
resolver, err := authenticator.Resolver(ctx)
if err != nil {
logging.Fatalf("instantiating resolver: %s", err)
}

// check if either the resolver or the authenticator provides setup instructions
if resolverInstructionGiver, ok := resolver.(api.URISetupper); ok {
instructionGiver = resolverInstructionGiver
}
}

if !ok {
fmt.Printf("No setup instructions available for %s\nMaybe the config file is missing or incorrectly configured?", uri)
os.Exit(1)
}

setupInstructions := instructionGiver.SetupInstructionsForURI(ctx, uri)
fmt.Println(setupInstructions)
}

0 comments on commit 0815c45

Please sign in to comment.