11package main
22
33import (
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+
1549func 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+
56268func getenv (key ... string ) (s string ) {
57269 for _ , k := range key {
58270 s = os .Getenv (k )
0 commit comments