mirror of
https://github.com/drone-plugins/drone-docker.git
synced 2026-06-04 18:24:24 +08:00
Add support for AAD auth for docker-acr (#395)
* Add support for AAD auth for docker-acr * Update go version --------- Co-authored-by: TP Honey <tp@harness.io>
This commit is contained in:
@@ -1,17 +1,40 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
|
||||
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
docker "github.com/drone-plugins/drone-docker"
|
||||
)
|
||||
|
||||
const (
|
||||
acrCertPath = "/tmp/acr-cert.pem"
|
||||
azSubscriptionApiVersion = "2021-04-01"
|
||||
azSubscriptionBaseUrl = "https://management.azure.com/subscriptions/"
|
||||
basePublicUrl = "https://portal.azure.com/#view/Microsoft_Azure_ContainerRegistries/TagMetadataBlade/registryId/"
|
||||
defaultUsername = "00000000-0000-0000-0000-000000000000"
|
||||
|
||||
// Environment variable names for Azure Environment Credential
|
||||
clientIdEnv = "AZURE_CLIENT_ID"
|
||||
clientSecretKeyEnv = "AZURE_CLIENT_SECRET"
|
||||
tenantKeyEnv = "AZURE_TENANT_ID"
|
||||
certPathEnv = "AZURE_CLIENT_CERTIFICATE_PATH"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Load env-file if it exists first
|
||||
if env := os.Getenv("PLUGIN_ENV_FILE"); env != "" {
|
||||
@@ -21,8 +44,19 @@ func main() {
|
||||
var (
|
||||
repo = getenv("PLUGIN_REPO")
|
||||
registry = getenv("PLUGIN_REGISTRY")
|
||||
|
||||
// If these credentials are provided, they will be directly used
|
||||
// for docker login
|
||||
username = getenv("SERVICE_PRINCIPAL_CLIENT_ID")
|
||||
password = getenv("SERVICE_PRINCIPAL_CLIENT_SECRET")
|
||||
|
||||
// Service principal credentials
|
||||
clientId = getenv("CLIENT_ID")
|
||||
clientSecret = getenv("CLIENT_SECRET")
|
||||
clientCert = getenv("CLIENT_CERTIFICATE")
|
||||
tenantId = getenv("TENANT_ID")
|
||||
subscriptionId = getenv("SUBSCRIPTION_ID")
|
||||
publicUrl = getenv("DAEMON_REGISTRY")
|
||||
)
|
||||
|
||||
// default registry value
|
||||
@@ -30,6 +64,17 @@ func main() {
|
||||
registry = "azurecr.io"
|
||||
}
|
||||
|
||||
// Get auth if username and password is not specified
|
||||
if username == "" && password == "" {
|
||||
// docker login credentials are not provided
|
||||
var err error
|
||||
username = defaultUsername
|
||||
password, publicUrl, err = getAuth(clientId, clientSecret, clientCert, tenantId, subscriptionId, registry)
|
||||
if err != nil {
|
||||
logrus.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// must use the fully qualified repo name. If the
|
||||
// repo name does not have the registry prefix we
|
||||
// should prepend.
|
||||
@@ -42,6 +87,11 @@ func main() {
|
||||
os.Setenv("DOCKER_USERNAME", username)
|
||||
os.Setenv("DOCKER_PASSWORD", password)
|
||||
os.Setenv("PLUGIN_REGISTRY_TYPE", "ACR")
|
||||
if publicUrl != "" {
|
||||
// Set this env variable if public URL for artifact is available
|
||||
// If not, we will fall back to registry url
|
||||
os.Setenv("ARTIFACT_REGISTRY", publicUrl)
|
||||
}
|
||||
|
||||
// invoke the base docker plugin binary
|
||||
cmd := exec.Command(docker.GetDroneDockerExecCmd())
|
||||
@@ -53,6 +103,157 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
func getAuth(clientId, clientSecret, clientCert, tenantId, subscriptionId, registry string) (string, string, error) {
|
||||
// Verify inputs
|
||||
if tenantId == "" {
|
||||
return "", "", fmt.Errorf("tenantId cannot be empty for AAD authentication")
|
||||
}
|
||||
if clientId == "" {
|
||||
return "", "", fmt.Errorf("clientId cannot be empty for AAD authentication")
|
||||
}
|
||||
if clientSecret == "" && clientCert == "" {
|
||||
return "", "", fmt.Errorf("one of client secret or client cert should be defined")
|
||||
}
|
||||
|
||||
// Setup cert
|
||||
if clientCert != "" {
|
||||
err := setupACRCert(clientCert, acrCertPath)
|
||||
if err != nil {
|
||||
errors.Wrap(err, "failed to push setup cert file")
|
||||
}
|
||||
}
|
||||
|
||||
// Get AZ env
|
||||
if err := os.Setenv(clientIdEnv, clientId); err != nil {
|
||||
return "", "", errors.Wrap(err, "failed to set env variable client Id")
|
||||
}
|
||||
if err := os.Setenv(clientSecretKeyEnv, clientSecret); err != nil {
|
||||
return "", "", errors.Wrap(err, "failed to set env variable client secret")
|
||||
}
|
||||
if err := os.Setenv(tenantKeyEnv, tenantId); err != nil {
|
||||
return "", "", errors.Wrap(err, "failed to set env variable tenant Id")
|
||||
}
|
||||
if err := os.Setenv(certPathEnv, acrCertPath); err != nil {
|
||||
return "", "", errors.Wrap(err, "failed to set env variable cert path")
|
||||
}
|
||||
env, err := azidentity.NewEnvironmentCredential(nil)
|
||||
if err != nil {
|
||||
return "", "", errors.Wrap(err, "failed to get env credentials from azure")
|
||||
}
|
||||
os.Unsetenv(clientIdEnv)
|
||||
os.Unsetenv(clientSecretKeyEnv)
|
||||
os.Unsetenv(tenantKeyEnv)
|
||||
os.Unsetenv(certPathEnv)
|
||||
|
||||
// Fetch AAD token
|
||||
policy := policy.TokenRequestOptions{
|
||||
Scopes: []string{"https://management.azure.com/.default"},
|
||||
}
|
||||
aadToken, err := env.GetToken(context.Background(), policy)
|
||||
if err != nil {
|
||||
return "", "", errors.Wrap(err, "failed to fetch access token")
|
||||
}
|
||||
|
||||
// Get public URL for artifacts
|
||||
publicUrl, err := getPublicUrl(aadToken.Token, registry, subscriptionId)
|
||||
if err != nil {
|
||||
// execution should not fail because of this error
|
||||
fmt.Fprintf(os.Stderr, "failed to get public url with error: %s\n", err)
|
||||
}
|
||||
|
||||
// Fetch token
|
||||
ACRToken, err := fetchACRToken(tenantId, aadToken.Token, registry)
|
||||
if err != nil {
|
||||
return "", "", errors.Wrap(err, "failed to fetch ACR token")
|
||||
}
|
||||
return ACRToken, publicUrl, nil
|
||||
}
|
||||
|
||||
func fetchACRToken(tenantId, token, registry string) (string, error) {
|
||||
// oauth exchange
|
||||
formData := url.Values{
|
||||
"grant_type": {"access_token"},
|
||||
"service": {registry},
|
||||
"tenant": {tenantId},
|
||||
"access_token": {token},
|
||||
}
|
||||
jsonResponse, err := http.PostForm(fmt.Sprintf("https://%s/oauth2/exchange", registry), formData)
|
||||
if err != nil || jsonResponse == nil {
|
||||
return "", errors.Wrap(err, "failed to fetch ACR token")
|
||||
}
|
||||
|
||||
// fetch token from response
|
||||
var response map[string]interface{}
|
||||
err = json.NewDecoder(jsonResponse.Body).Decode(&response)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to decode oauth exchange response")
|
||||
}
|
||||
|
||||
// Parse the refresh_token from the response
|
||||
if t, found := response["refresh_token"]; found {
|
||||
if refreshToken, ok := t.(string); ok {
|
||||
return refreshToken, nil
|
||||
}
|
||||
return "", errors.New("failed to cast refresh token from acr")
|
||||
}
|
||||
return "", errors.Wrap(err, "refresh token not found in response of oauth exchange call")
|
||||
}
|
||||
|
||||
func setupACRCert(cert, certPath string) error {
|
||||
decoded, err := base64.StdEncoding.DecodeString(cert)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to base64 decode ACR certificate")
|
||||
}
|
||||
err = ioutil.WriteFile(certPath, decoded, 0644)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to write ACR certificate")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getPublicUrl(token, registryUrl, subscriptionId string) (string, error) {
|
||||
if len(subscriptionId) == 0 || registryUrl == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
registry := strings.Split(registryUrl, ".")[0]
|
||||
filter := fmt.Sprintf("resourceType eq 'Microsoft.ContainerRegistry/registries' and name eq '%s'", registry)
|
||||
params := url.Values{}
|
||||
params.Add("$filter", filter)
|
||||
params.Add("api-version", azSubscriptionApiVersion)
|
||||
params.Add("$select", "id")
|
||||
url := azSubscriptionBaseUrl + subscriptionId + "/resources?" + params.Encode()
|
||||
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return "", errors.Wrap(err, "failed to create request for getting container registry setting")
|
||||
}
|
||||
|
||||
req.Header.Add("Authorization", "Bearer "+token)
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return "", errors.Wrap(err, "failed to send request for getting container registry setting")
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
var response subscriptionUrlResponse
|
||||
err = json.NewDecoder(res.Body).Decode(&response)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to send request for getting container registry setting")
|
||||
}
|
||||
if len(response.Value) == 0 {
|
||||
return "", errors.New("no id present for base url")
|
||||
}
|
||||
return basePublicUrl + encodeParam(response.Value[0].ID), nil
|
||||
}
|
||||
|
||||
func encodeParam(s string) string {
|
||||
return url.QueryEscape(s)
|
||||
}
|
||||
|
||||
func getenv(key ...string) (s string) {
|
||||
for _, k := range key {
|
||||
s = os.Getenv(k)
|
||||
@@ -62,3 +263,9 @@ func getenv(key ...string) (s string) {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type subscriptionUrlResponse struct {
|
||||
Value []struct {
|
||||
ID string `json:"id"`
|
||||
} `json:"value"`
|
||||
}
|
||||
|
||||
+21
-14
@@ -112,6 +112,12 @@ func main() {
|
||||
Usage: "don't start the docker daemon",
|
||||
EnvVar: "PLUGIN_DAEMON_OFF",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "artifact.registry",
|
||||
Usage: "artifact registry",
|
||||
Value: "https://index.docker.io/v1/",
|
||||
EnvVar: "ARTIFACT_REGISTRY,PLUGIN_REGISTRY,DOCKER_REGISTRY",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "dockerfile",
|
||||
Usage: "build dockerfile",
|
||||
@@ -346,20 +352,21 @@ func run(c *cli.Context) error {
|
||||
SSHAgentKey: c.String("ssh-agent-key"),
|
||||
},
|
||||
Daemon: docker.Daemon{
|
||||
Registry: c.String("docker.registry"),
|
||||
Mirror: c.String("daemon.mirror"),
|
||||
StorageDriver: c.String("daemon.storage-driver"),
|
||||
StoragePath: c.String("daemon.storage-path"),
|
||||
Insecure: c.Bool("daemon.insecure"),
|
||||
Disabled: c.Bool("daemon.off"),
|
||||
IPv6: c.Bool("daemon.ipv6"),
|
||||
Debug: c.Bool("daemon.debug"),
|
||||
Bip: c.String("daemon.bip"),
|
||||
DNS: c.StringSlice("daemon.dns"),
|
||||
DNSSearch: c.StringSlice("daemon.dns-search"),
|
||||
MTU: c.String("daemon.mtu"),
|
||||
Experimental: c.Bool("daemon.experimental"),
|
||||
RegistryType: registryType,
|
||||
Registry: c.String("docker.registry"),
|
||||
Mirror: c.String("daemon.mirror"),
|
||||
StorageDriver: c.String("daemon.storage-driver"),
|
||||
StoragePath: c.String("daemon.storage-path"),
|
||||
Insecure: c.Bool("daemon.insecure"),
|
||||
Disabled: c.Bool("daemon.off"),
|
||||
IPv6: c.Bool("daemon.ipv6"),
|
||||
Debug: c.Bool("daemon.debug"),
|
||||
Bip: c.String("daemon.bip"),
|
||||
DNS: c.StringSlice("daemon.dns"),
|
||||
DNSSearch: c.StringSlice("daemon.dns-search"),
|
||||
MTU: c.String("daemon.mtu"),
|
||||
Experimental: c.Bool("daemon.experimental"),
|
||||
RegistryType: registryType,
|
||||
ArtifactRegistry: c.String("artifact.registry"),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user