Skip to content
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
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,26 @@ steps:
from_secret: gcr_json_key
```

### GAR (Google Artifact Registry) using workload identity (OIDC)

```yaml
steps:
- name: push-to-gar
image: plugins/gcr
pull: never
settings:
tag: latest
repo: project-id/repo/image-name
registry_type: GAR
location: europe
project_number: project-number
pool_id: workload identity pool id
provider_id: workload identity provider id
service_account_email: service account email
oidc_token_id:
from_secret: token
```

## Developer Notes

- When updating the base image, you will need to update for each architecture and OS.
Expand Down
16 changes: 11 additions & 5 deletions cmd/drone-docker/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,11 @@ func main() {
Usage: "registry type",
EnvVar: "PLUGIN_REGISTRY_TYPE",
},
cli.StringFlag{
Name: "access-token",
Usage: "access token",
EnvVar: "ACCESS_TOKEN",
},
}

if err := app.Run(os.Args); err != nil {
Expand All @@ -309,11 +314,12 @@ func run(c *cli.Context) error {
Dryrun: c.Bool("dry-run"),
Cleanup: c.BoolT("docker.purge"),
Login: docker.Login{
Registry: c.String("docker.registry"),
Username: c.String("docker.username"),
Password: c.String("docker.password"),
Email: c.String("docker.email"),
Config: c.String("docker.config"),
Registry: c.String("docker.registry"),
Username: c.String("docker.username"),
Password: c.String("docker.password"),
Email: c.String("docker.email"),
Config: c.String("docker.config"),
AccessToken: c.String("access-token"),
},
CardPath: c.String("drone-card-path"),
ArtifactFile: c.String("artifact-file"),
Expand Down
121 changes: 99 additions & 22 deletions cmd/drone-gcr/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,15 @@ import (
"strconv"
"strings"

docker "github.com/drone-plugins/drone-docker"

"github.com/joho/godotenv"
"github.com/sirupsen/logrus"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"

docker "github.com/drone-plugins/drone-docker"
"google.golang.org/api/iamcredentials/v1"
"google.golang.org/api/option"
"google.golang.org/api/sts/v1"
)

type Config struct {
Expand All @@ -25,11 +29,21 @@ type Config struct {
WorkloadIdentity bool
Username string
RegistryType string
AccessToken string
}

type staticTokenSource struct {
token *oauth2.Token
}

func (s *staticTokenSource) Token() (*oauth2.Token, error) {
return s.token, nil
}

func loadConfig() Config {
// Default username
username := "_json_key"
var config Config

// Load env-file if it exists
if env := os.Getenv("PLUGIN_ENV_FILE"); env != "" {
Expand All @@ -38,18 +52,36 @@ func loadConfig() Config {
}
}

idToken := getenv("PLUGIN_OIDC_TOKEN_ID")
projectId := getenv("PLUGIN_PROJECT_NUMBER")
poolId := getenv("PLUGIN_POOL_ID")
providerId := getenv("PLUGIN_PROVIDER_ID")
serviceAccountEmail := getenv("PLUGIN_SERVICE_ACCOUNT_EMAIL")

if idToken != "" && projectId != "" && poolId != "" && providerId != "" && serviceAccountEmail != "" {
federalToken, err := getFederalToken(idToken, projectId, poolId, providerId)
if err != nil {
logrus.Fatalf("Error (getFederalToken): %s", err)
}
accessToken, err := getGoogleCloudAccessToken(federalToken, serviceAccountEmail)
if err != nil {
logrus.Fatalf("Error (getGoogleCloudAccessToken): %s", err)
}
config.AccessToken = accessToken
} else {
password := getenv(
"PLUGIN_JSON_KEY",
"GCR_JSON_KEY",
"GOOGLE_CREDENTIALS",
"TOKEN",
)
config.WorkloadIdentity = parseBoolOrDefault(false, getenv("PLUGIN_WORKLOAD_IDENTITY"))
config.Username, config.Password = setUsernameAndPassword(username, password, config.WorkloadIdentity)
}

location := getenv("PLUGIN_LOCATION")
repo := getenv("PLUGIN_REPO")

password := getenv(
"PLUGIN_JSON_KEY",
"GCR_JSON_KEY",
"GOOGLE_CREDENTIALS",
"TOKEN",
)
workloadIdentity := parseBoolOrDefault(false, getenv("PLUGIN_WORKLOAD_IDENTITY"))
username, password = setUsernameAndPassword(username, password, workloadIdentity)

registryType := getenv("PLUGIN_REGISTRY_TYPE")
if registryType == "" {
registryType = "GCR"
Expand All @@ -73,24 +105,23 @@ func loadConfig() Config {
if !strings.HasPrefix(repo, registry) {
repo = path.Join(registry, repo)
}

return Config{
Repo: repo,
Registry: registry,
Password: password,
WorkloadIdentity: workloadIdentity,
Username: username,
RegistryType: registryType,
}
config.Repo = repo
config.Registry = registry
config.RegistryType = registryType
return config
}

func main() {
config := loadConfig()
if config.AccessToken != "" {
os.Setenv("ACCESS_TOKEN", config.AccessToken)
} else if config.Username != "" && config.Password != "" {
os.Setenv("DOCKER_USERNAME", config.Username)
os.Setenv("DOCKER_PASSWORD", config.Password)
}

os.Setenv("PLUGIN_REPO", config.Repo)
os.Setenv("PLUGIN_REGISTRY", config.Registry)
os.Setenv("DOCKER_USERNAME", config.Username)
os.Setenv("DOCKER_PASSWORD", config.Password)
os.Setenv("PLUGIN_REGISTRY_TYPE", config.RegistryType)

// invoke the base docker plugin binary
Expand Down Expand Up @@ -152,3 +183,49 @@ func getenv(key ...string) (s string) {
}
return
}

func getFederalToken(idToken, projectNumber, poolId, providerId string) (string, error) {
ctx := context.Background()
stsService, err := sts.NewService(ctx, option.WithoutAuthentication())
if err != nil {
return "", err
}
audience := fmt.Sprintf("//iam.googleapis.com/projects/%s/locations/global/workloadIdentityPools/%s/providers/%s", projectNumber, poolId, providerId)
tokenRequest := &sts.GoogleIdentityStsV1ExchangeTokenRequest{
GrantType: "urn:ietf:params:oauth:grant-type:token-exchange",
SubjectToken: idToken,
Audience: audience,
Scope: "https://www.googleapis.com/auth/cloud-platform",
RequestedTokenType: "urn:ietf:params:oauth:token-type:access_token",
SubjectTokenType: "urn:ietf:params:oauth:token-type:id_token",
}
tokenResponse, err := stsService.V1.Token(tokenRequest).Do()
if err != nil {
return "", err
}

return tokenResponse.AccessToken, nil
}

func getGoogleCloudAccessToken(federatedToken string, serviceAccountEmail string) (string, error) {
ctx := context.Background()
tokenSource := &staticTokenSource{
token: &oauth2.Token{AccessToken: federatedToken},
}
service, err := iamcredentials.NewService(ctx, option.WithTokenSource(tokenSource))
if err != nil {
return "", err
}

name := "projects/-/serviceAccounts/" + serviceAccountEmail
rb := &iamcredentials.GenerateAccessTokenRequest{
Scope: []string{"https://www.googleapis.com/auth/cloud-platform"},
}

resp, err := service.Projects.ServiceAccounts.GenerateAccessToken(name, rb).Do()
if err != nil {
return "", err
}

return resp.AccessToken, nil
}
38 changes: 31 additions & 7 deletions docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,12 @@ type (

// Login defines Docker login parameters.
Login struct {
Registry string // Docker registry address
Username string // Docker registry username
Password string // Docker registry password
Email string // Docker registry email
Config string // Docker Auth Config
Registry string // Docker registry address
Username string // Docker registry username
Password string // Docker registry password
Email string // Docker registry email
Config string // Docker Auth Config
AccessToken string // External Access Token
}

// Build defines Docker build parameters.
Expand Down Expand Up @@ -113,7 +114,6 @@ type (

// Exec executes the plugin step
func (p Plugin) Exec() error {

// start the Docker daemon server
if !p.Daemon.Disabled {
p.startDaemon()
Expand Down Expand Up @@ -143,6 +143,8 @@ func (p Plugin) Exec() error {
fmt.Println("Detected registry credentials")
case p.Login.Config != "":
fmt.Println("Detected registry credentials file")
case p.Login.AccessToken != "":
fmt.Println("Detected access token")
default:
fmt.Println("Registry credentials or Docker config not provided. Guest mode enabled.")
}
Expand All @@ -166,7 +168,18 @@ func (p Plugin) Exec() error {
out := string(raw)
out = strings.Replace(out, "WARNING! Using --password via the CLI is insecure. Use --password-stdin.", "", -1)
fmt.Println(out)
return fmt.Errorf("Error authenticating: exit status 1")
return fmt.Errorf("error authenticating: exit status 1")
}
} else if p.Login.AccessToken != "" {
cmd := commandLoginAccessToken(p.Login, p.Login.AccessToken)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("error logging in to Docker registry: %s", err)
}
if strings.Contains(string(output), "Login Succeeded") {
fmt.Println("Login successful")
} else {
return fmt.Errorf("login did not succeed")
}
}

Expand Down Expand Up @@ -270,6 +283,17 @@ func commandLogin(login Login) *exec.Cmd {
)
}

func commandLoginAccessToken(login Login, accessToken string) *exec.Cmd {
cmd := exec.Command(dockerExe,
"login",
"-u",
"oauth2accesstoken",
"--password-stdin",
login.Registry)
cmd.Stdin = strings.NewReader(accessToken)
return cmd
}

// helper to check if args match "docker pull <image>"
func isCommandPull(args []string) bool {
return len(args) > 2 && args[1] == "pull"
Expand Down
26 changes: 19 additions & 7 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,31 @@ require (
github.com/joho/godotenv v1.3.0
github.com/sirupsen/logrus v1.9.0
github.com/urfave/cli v1.22.2
golang.org/x/oauth2 v0.8.0
golang.org/x/oauth2 v0.13.0
)

require (
cloud.google.com/go/compute/metadata v0.2.0 // indirect
cloud.google.com/go/compute v1.23.1 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/s2a-go v0.1.7 // indirect
github.com/google/uuid v1.3.1 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.1 // indirect
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/sys v0.8.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.28.0 // indirect
go.opencensus.io v0.24.0 // indirect
golang.org/x/crypto v0.14.0 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
google.golang.org/api v0.146.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b // indirect
google.golang.org/grpc v1.59.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/yaml.v2 v2.2.8 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
Expand Down
Loading