Skip to content

Commit 78fc38d

Browse files
authored
feat: [CI-16478]: Add Azure Container Registry (ACR) authentication support (#480)
* Updated cmd/drone-acr/main.go and the dependencies * Updated cmd/drone-acr/main.go * Import path/filepath
1 parent 57234fc commit 78fc38d

File tree

3 files changed

+280
-21
lines changed

3 files changed

+280
-21
lines changed

cmd/drone-acr/main.go

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,51 @@
11
package main
22

33
import (
4+
"context"
5+
"encoding/base64"
6+
"encoding/json"
47
"fmt"
8+
"io/ioutil"
9+
"net/http"
10+
"net/url"
511
"os"
612
"os/exec"
13+
"path/filepath"
714
"strings"
815

16+
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
17+
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
918
"github.com/joho/godotenv"
19+
"github.com/pkg/errors"
1020
"github.com/sirupsen/logrus"
1121

1222
docker "github.com/drone-plugins/drone-docker"
1323
)
1424

25+
type subscriptionUrlResponse struct {
26+
Value []struct {
27+
ID string `json:"id"`
28+
} `json:"value"`
29+
}
30+
31+
const (
32+
acrCertFile = "acr-cert.pem"
33+
azSubscriptionApiVersion = "2021-04-01"
34+
azSubscriptionBaseUrl = "https://management.azure.com/subscriptions/"
35+
basePublicUrl = "https://portal.azure.com/#view/Microsoft_Azure_ContainerRegistries/TagMetadataBlade/registryId/"
36+
defaultUsername = "00000000-0000-0000-0000-000000000000"
37+
38+
// Environment variable names for Azure Environment Credential
39+
clientIdEnv = "AZURE_CLIENT_ID"
40+
clientSecretKeyEnv = "AZURE_CLIENT_SECRET"
41+
tenantKeyEnv = "AZURE_TENANT_ID"
42+
certPathEnv = "AZURE_CLIENT_CERTIFICATE_PATH"
43+
)
44+
45+
var (
46+
acrCertPath = filepath.Join(os.TempDir(), acrCertFile)
47+
)
48+
1549
func main() {
1650
// Load env-file if it exists first
1751
if env := os.Getenv("PLUGIN_ENV_FILE"); env != "" {
@@ -21,15 +55,37 @@ func main() {
2155
var (
2256
repo = getenv("PLUGIN_REPO")
2357
registry = getenv("PLUGIN_REGISTRY")
58+
59+
// If these credentials are provided, they will be directly used
60+
// for docker login
2461
username = getenv("SERVICE_PRINCIPAL_CLIENT_ID")
2562
password = getenv("SERVICE_PRINCIPAL_CLIENT_SECRET")
63+
64+
// Service principal credentials
65+
clientId = getenv("CLIENT_ID")
66+
clientSecret = getenv("CLIENT_SECRET")
67+
clientCert = getenv("CLIENT_CERTIFICATE")
68+
tenantId = getenv("TENANT_ID")
69+
subscriptionId = getenv("SUBSCRIPTION_ID")
70+
publicUrl = getenv("DAEMON_REGISTRY")
2671
)
2772

2873
// default registry value
2974
if registry == "" {
3075
registry = "azurecr.io"
3176
}
3277

78+
// Get auth if username and password is not specified
79+
if username == "" && password == "" {
80+
// docker login credentials are not provided
81+
var err error
82+
username = defaultUsername
83+
password, publicUrl, err = getAuth(clientId, clientSecret, clientCert, tenantId, subscriptionId, registry)
84+
if err != nil {
85+
logrus.Fatal(err)
86+
}
87+
}
88+
3389
// must use the fully qualified repo name. If the
3490
// repo name does not have the registry prefix we
3591
// should prepend.
@@ -42,6 +98,11 @@ func main() {
4298
os.Setenv("DOCKER_USERNAME", username)
4399
os.Setenv("DOCKER_PASSWORD", password)
44100
os.Setenv("PLUGIN_REGISTRY_TYPE", "ACR")
101+
if publicUrl != "" {
102+
// Set this env variable if public URL for artifact is available
103+
// If not, we will fall back to registry url
104+
os.Setenv("ARTIFACT_REGISTRY", publicUrl)
105+
}
45106

46107
// invoke the base docker plugin binary
47108
cmd := exec.Command(docker.GetDroneDockerExecCmd())
@@ -53,6 +114,157 @@ func main() {
53114
}
54115
}
55116

117+
func getAuth(clientId, clientSecret, clientCert, tenantId, subscriptionId, registry string) (string, string, error) {
118+
// Verify inputs
119+
if tenantId == "" {
120+
return "", "", fmt.Errorf("tenantId cannot be empty for AAD authentication")
121+
}
122+
if clientId == "" {
123+
return "", "", fmt.Errorf("clientId cannot be empty for AAD authentication")
124+
}
125+
if clientSecret == "" && clientCert == "" {
126+
return "", "", fmt.Errorf("one of client secret or client cert should be defined")
127+
}
128+
129+
// Setup cert
130+
if clientCert != "" {
131+
err := setupACRCert(clientCert, acrCertPath)
132+
if err != nil {
133+
errors.Wrap(err, "failed to push setup cert file")
134+
}
135+
}
136+
137+
// Get AZ env
138+
if err := os.Setenv(clientIdEnv, clientId); err != nil {
139+
return "", "", errors.Wrap(err, "failed to set env variable client Id")
140+
}
141+
if err := os.Setenv(clientSecretKeyEnv, clientSecret); err != nil {
142+
return "", "", errors.Wrap(err, "failed to set env variable client secret")
143+
}
144+
if err := os.Setenv(tenantKeyEnv, tenantId); err != nil {
145+
return "", "", errors.Wrap(err, "failed to set env variable tenant Id")
146+
}
147+
if err := os.Setenv(certPathEnv, acrCertPath); err != nil {
148+
return "", "", errors.Wrap(err, "failed to set env variable cert path")
149+
}
150+
env, err := azidentity.NewEnvironmentCredential(nil)
151+
if err != nil {
152+
return "", "", errors.Wrap(err, "failed to get env credentials from azure")
153+
}
154+
os.Unsetenv(clientIdEnv)
155+
os.Unsetenv(clientSecretKeyEnv)
156+
os.Unsetenv(tenantKeyEnv)
157+
os.Unsetenv(certPathEnv)
158+
159+
// Fetch AAD token
160+
policy := policy.TokenRequestOptions{
161+
Scopes: []string{"https://management.azure.com/.default"},
162+
}
163+
aadToken, err := env.GetToken(context.Background(), policy)
164+
if err != nil {
165+
return "", "", errors.Wrap(err, "failed to fetch access token")
166+
}
167+
168+
// Get public URL for artifacts
169+
publicUrl, err := getPublicUrl(aadToken.Token, registry, subscriptionId)
170+
if err != nil {
171+
// execution should not fail because of this error
172+
fmt.Fprintf(os.Stderr, "failed to get public url with error: %s\n", err)
173+
}
174+
175+
// Fetch token
176+
ACRToken, err := fetchACRToken(tenantId, aadToken.Token, registry)
177+
if err != nil {
178+
return "", "", errors.Wrap(err, "failed to fetch ACR token")
179+
}
180+
return ACRToken, publicUrl, nil
181+
}
182+
183+
func fetchACRToken(tenantId, token, registry string) (string, error) {
184+
// oauth exchange
185+
formData := url.Values{
186+
"grant_type": {"access_token"},
187+
"service": {registry},
188+
"tenant": {tenantId},
189+
"access_token": {token},
190+
}
191+
jsonResponse, err := http.PostForm(fmt.Sprintf("https://%s/oauth2/exchange", registry), formData)
192+
if err != nil || jsonResponse == nil {
193+
return "", errors.Wrap(err, "failed to fetch ACR token")
194+
}
195+
196+
// fetch token from response
197+
var response map[string]interface{}
198+
err = json.NewDecoder(jsonResponse.Body).Decode(&response)
199+
if err != nil {
200+
return "", errors.Wrap(err, "failed to decode oauth exchange response")
201+
}
202+
203+
// Parse the refresh_token from the response
204+
if t, found := response["refresh_token"]; found {
205+
if refreshToken, ok := t.(string); ok {
206+
return refreshToken, nil
207+
}
208+
return "", errors.New("failed to cast refresh token from acr")
209+
}
210+
return "", errors.Wrap(err, "refresh token not found in response of oauth exchange call")
211+
}
212+
213+
func setupACRCert(cert, certPath string) error {
214+
decoded, err := base64.StdEncoding.DecodeString(cert)
215+
if err != nil {
216+
return errors.Wrap(err, "failed to base64 decode ACR certificate")
217+
}
218+
err = ioutil.WriteFile(certPath, decoded, 0644)
219+
if err != nil {
220+
return errors.Wrap(err, "failed to write ACR certificate")
221+
}
222+
return nil
223+
}
224+
225+
func getPublicUrl(token, registryUrl, subscriptionId string) (string, error) {
226+
if len(subscriptionId) == 0 || registryUrl == "" {
227+
return "", nil
228+
}
229+
230+
registry := strings.Split(registryUrl, ".")[0]
231+
filter := fmt.Sprintf("resourceType eq 'Microsoft.ContainerRegistry/registries' and name eq '%s'", registry)
232+
params := url.Values{}
233+
params.Add("$filter", filter)
234+
params.Add("api-version", azSubscriptionApiVersion)
235+
params.Add("$select", "id")
236+
url := azSubscriptionBaseUrl + subscriptionId + "/resources?" + params.Encode()
237+
238+
client := &http.Client{}
239+
req, err := http.NewRequest("GET", url, nil)
240+
if err != nil {
241+
fmt.Println(err)
242+
return "", errors.Wrap(err, "failed to create request for getting container registry setting")
243+
}
244+
245+
req.Header.Add("Authorization", "Bearer "+token)
246+
res, err := client.Do(req)
247+
if err != nil {
248+
fmt.Println(err)
249+
return "", errors.Wrap(err, "failed to send request for getting container registry setting")
250+
}
251+
defer res.Body.Close()
252+
253+
var response subscriptionUrlResponse
254+
err = json.NewDecoder(res.Body).Decode(&response)
255+
if err != nil {
256+
return "", errors.Wrap(err, "failed to send request for getting container registry setting")
257+
}
258+
if len(response.Value) == 0 {
259+
return "", errors.New("no id present for base url")
260+
}
261+
return basePublicUrl + encodeParam(response.Value[0].ID), nil
262+
}
263+
264+
func encodeParam(s string) string {
265+
return url.QueryEscape(s)
266+
}
267+
56268
func getenv(key ...string) (s string) {
57269
for _, k := range key {
58270
s = os.Getenv(k)

go.mod

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
module github.com/drone-plugins/drone-docker
22

33
require (
4+
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1
5+
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2
46
github.com/aws/aws-sdk-go v1.26.7
57
github.com/coreos/go-semver v0.3.0
68
github.com/dchest/uniuri v1.2.0
79
github.com/drone-plugins/drone-plugin-lib v0.4.1
810
github.com/drone/drone-go v1.7.1
911
github.com/inhies/go-bytesize v0.0.0-20210819104631-275770b98743
1012
github.com/joho/godotenv v1.3.0
13+
github.com/pkg/errors v0.9.1
1114
github.com/sirupsen/logrus v1.9.0
12-
github.com/stretchr/testify v1.8.1
15+
github.com/stretchr/testify v1.10.0
1316
github.com/urfave/cli v1.22.2
1417
golang.org/x/oauth2 v0.13.0
1518
google.golang.org/api v0.146.0
@@ -18,22 +21,27 @@ require (
1821
require (
1922
cloud.google.com/go/compute v1.23.1 // indirect
2023
cloud.google.com/go/compute/metadata v0.2.3 // indirect
24+
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
25+
github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3 // indirect
2126
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
2227
github.com/davecgh/go-spew v1.1.1 // indirect
28+
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
2329
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
2430
github.com/golang/protobuf v1.5.3 // indirect
2531
github.com/google/s2a-go v0.1.7 // indirect
26-
github.com/google/uuid v1.3.1 // indirect
32+
github.com/google/uuid v1.6.0 // indirect
2733
github.com/googleapis/enterprise-certificate-proxy v0.3.1 // indirect
2834
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
2935
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af // indirect
36+
github.com/kylelemons/godebug v1.1.0 // indirect
37+
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
3038
github.com/pmezard/go-difflib v1.0.0 // indirect
3139
github.com/russross/blackfriday/v2 v2.1.0 // indirect
3240
go.opencensus.io v0.24.0 // indirect
33-
golang.org/x/crypto v0.14.0 // indirect
34-
golang.org/x/net v0.17.0 // indirect
35-
golang.org/x/sys v0.13.0 // indirect
36-
golang.org/x/text v0.13.0 // indirect
41+
golang.org/x/crypto v0.36.0 // indirect
42+
golang.org/x/net v0.37.0 // indirect
43+
golang.org/x/sys v0.31.0 // indirect
44+
golang.org/x/text v0.23.0 // indirect
3745
google.golang.org/appengine v1.6.8 // indirect
3846
google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b // indirect
3947
google.golang.org/grpc v1.59.0 // indirect
@@ -42,4 +50,6 @@ require (
4250
gopkg.in/yaml.v3 v3.0.1 // indirect
4351
)
4452

45-
go 1.22.4
53+
go 1.23.0
54+
55+
toolchain go1.23.7

0 commit comments

Comments
 (0)