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 "strings"
713
14+ "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
15+ "github.com/Azure/azure-sdk-for-go/sdk/azidentity"
816 "github.com/joho/godotenv"
17+ "github.com/pkg/errors"
18+ "github.com/sirupsen/logrus"
919
1020 docker "github.com/drone-plugins/drone-buildx"
1121)
1222
23+ const (
24+ acrCertPath = "/tmp/acr-cert.pem"
25+ azSubscriptionApiVersion = "2021-04-01"
26+ azSubscriptionBaseUrl = "https://management.azure.com/subscriptions/"
27+ basePublicUrl = "https://portal.azure.com/#view/Microsoft_Azure_ContainerRegistries/TagMetadataBlade/registryId/"
28+ defaultUsername = "00000000-0000-0000-0000-000000000000"
29+
30+ // Environment variable names for Azure Environment Credential
31+ clientIdEnv = "AZURE_CLIENT_ID"
32+ clientSecretKeyEnv = "AZURE_CLIENT_SECRET"
33+ tenantKeyEnv = "AZURE_TENANT_ID"
34+ certPathEnv = "AZURE_CLIENT_CERTIFICATE_PATH"
35+ )
36+
1337func main () {
1438 // Load env-file if it exists first
1539 if env := os .Getenv ("PLUGIN_ENV_FILE" ); env != "" {
@@ -19,15 +43,37 @@ func main() {
1943 var (
2044 repo = getenv ("PLUGIN_REPO" )
2145 registry = getenv ("PLUGIN_REGISTRY" )
46+
47+ // If these credentials are provided, they will be directly used
48+ // for docker login
2249 username = getenv ("SERVICE_PRINCIPAL_CLIENT_ID" )
2350 password = getenv ("SERVICE_PRINCIPAL_CLIENT_SECRET" )
51+
52+ // Service principal credentials
53+ clientId = getenv ("CLIENT_ID" )
54+ clientSecret = getenv ("CLIENT_SECRET" )
55+ clientCert = getenv ("CLIENT_CERTIFICATE" )
56+ tenantId = getenv ("TENANT_ID" )
57+ subscriptionId = getenv ("SUBSCRIPTION_ID" )
58+ publicUrl = getenv ("DAEMON_REGISTRY" )
2459 )
2560
2661 // default registry value
2762 if registry == "" {
2863 registry = "azurecr.io"
2964 }
3065
66+ // Get auth if username and password is not specified
67+ if username == "" && password == "" {
68+ // docker login credentials are not provided
69+ var err error
70+ username = defaultUsername
71+ password , publicUrl , err = getAuth (clientId , clientSecret , clientCert , tenantId , subscriptionId , registry )
72+ if err != nil {
73+ logrus .Fatal (err )
74+ }
75+ }
76+
3177 // must use the fully qualified repo name. If the
3278 // repo name does not have the registry prefix we
3379 // should prepend.
@@ -40,11 +86,167 @@ func main() {
4086 os .Setenv ("DOCKER_USERNAME" , username )
4187 os .Setenv ("DOCKER_PASSWORD" , password )
4288 os .Setenv ("PLUGIN_REGISTRY_TYPE" , "ACR" )
89+ if publicUrl != "" {
90+ // Set this env variable if public URL for artifact is available
91+ // If not, we will fall back to registry url
92+ os .Setenv ("ARTIFACT_REGISTRY" , publicUrl )
93+ }
4394
4495 // invoke the base docker plugin binary
4596 docker .Run ()
4697}
4798
99+ func getAuth (clientId , clientSecret , clientCert , tenantId , subscriptionId , registry string ) (string , string , error ) {
100+ // Verify inputs
101+ if tenantId == "" {
102+ return "" , "" , fmt .Errorf ("tenantId cannot be empty for AAD authentication" )
103+ }
104+ if clientId == "" {
105+ return "" , "" , fmt .Errorf ("clientId cannot be empty for AAD authentication" )
106+ }
107+ if clientSecret == "" && clientCert == "" {
108+ return "" , "" , fmt .Errorf ("one of client secret or client cert should be defined" )
109+ }
110+
111+ // Setup cert
112+ if clientCert != "" {
113+ err := setupACRCert (clientCert , acrCertPath )
114+ if err != nil {
115+ errors .Wrap (err , "failed to push setup cert file" )
116+ }
117+ }
118+
119+ // Get AZ env
120+ if err := os .Setenv (clientIdEnv , clientId ); err != nil {
121+ return "" , "" , errors .Wrap (err , "failed to set env variable client Id" )
122+ }
123+ if err := os .Setenv (clientSecretKeyEnv , clientSecret ); err != nil {
124+ return "" , "" , errors .Wrap (err , "failed to set env variable client secret" )
125+ }
126+ if err := os .Setenv (tenantKeyEnv , tenantId ); err != nil {
127+ return "" , "" , errors .Wrap (err , "failed to set env variable tenant Id" )
128+ }
129+ if err := os .Setenv (certPathEnv , acrCertPath ); err != nil {
130+ return "" , "" , errors .Wrap (err , "failed to set env variable cert path" )
131+ }
132+ env , err := azidentity .NewEnvironmentCredential (nil )
133+ if err != nil {
134+ return "" , "" , errors .Wrap (err , "failed to get env credentials from azure" )
135+ }
136+ os .Unsetenv (clientIdEnv )
137+ os .Unsetenv (clientSecretKeyEnv )
138+ os .Unsetenv (tenantKeyEnv )
139+ os .Unsetenv (certPathEnv )
140+
141+ // Fetch AAD token
142+ policy := policy.TokenRequestOptions {
143+ Scopes : []string {"https://management.azure.com/.default" },
144+ }
145+ aadToken , err := env .GetToken (context .Background (), policy )
146+ if err != nil {
147+ return "" , "" , errors .Wrap (err , "failed to fetch access token" )
148+ }
149+
150+ // Get public URL for artifacts
151+ publicUrl , err := getPublicUrl (aadToken .Token , registry , subscriptionId )
152+ if err != nil {
153+ // execution should not fail because of this error
154+ fmt .Fprintf (os .Stderr , "failed to get public url with error: %s\n " , err )
155+ }
156+
157+ // Fetch token
158+ ACRToken , err := fetchACRToken (tenantId , aadToken .Token , registry )
159+ if err != nil {
160+ return "" , "" , errors .Wrap (err , "failed to fetch ACR token" )
161+ }
162+ return ACRToken , publicUrl , nil
163+ }
164+
165+ func fetchACRToken (tenantId , token , registry string ) (string , error ) {
166+ // oauth exchange
167+ formData := url.Values {
168+ "grant_type" : {"access_token" },
169+ "service" : {registry },
170+ "tenant" : {tenantId },
171+ "access_token" : {token },
172+ }
173+ jsonResponse , err := http .PostForm (fmt .Sprintf ("https://%s/oauth2/exchange" , registry ), formData )
174+ if err != nil || jsonResponse == nil {
175+ return "" , errors .Wrap (err , "failed to fetch ACR token" )
176+ }
177+
178+ // fetch token from response
179+ var response map [string ]interface {}
180+ err = json .NewDecoder (jsonResponse .Body ).Decode (& response )
181+ if err != nil {
182+ return "" , errors .Wrap (err , "failed to decode oauth exchange response" )
183+ }
184+
185+ // Parse the refresh_token from the response
186+ if t , found := response ["refresh_token" ]; found {
187+ if refreshToken , ok := t .(string ); ok {
188+ return refreshToken , nil
189+ }
190+ return "" , errors .New ("failed to cast refresh token from acr" )
191+ }
192+ return "" , errors .Wrap (err , "refresh token not found in response of oauth exchange call" )
193+ }
194+
195+ func setupACRCert (cert , certPath string ) error {
196+ decoded , err := base64 .StdEncoding .DecodeString (cert )
197+ if err != nil {
198+ return errors .Wrap (err , "failed to base64 decode ACR certificate" )
199+ }
200+ err = ioutil .WriteFile (certPath , decoded , 0644 )
201+ if err != nil {
202+ return errors .Wrap (err , "failed to write ACR certificate" )
203+ }
204+ return nil
205+ }
206+
207+ func getPublicUrl (token , registryUrl , subscriptionId string ) (string , error ) {
208+ if len (subscriptionId ) == 0 || registryUrl == "" {
209+ return "" , nil
210+ }
211+
212+ registry := strings .Split (registryUrl , "." )[0 ]
213+ filter := fmt .Sprintf ("resourceType eq 'Microsoft.ContainerRegistry/registries' and name eq '%s'" , registry )
214+ params := url.Values {}
215+ params .Add ("$filter" , filter )
216+ params .Add ("api-version" , azSubscriptionApiVersion )
217+ params .Add ("$select" , "id" )
218+ url := azSubscriptionBaseUrl + subscriptionId + "/resources?" + params .Encode ()
219+
220+ client := & http.Client {}
221+ req , err := http .NewRequest ("GET" , url , nil )
222+ if err != nil {
223+ fmt .Println (err )
224+ return "" , errors .Wrap (err , "failed to create request for getting container registry setting" )
225+ }
226+
227+ req .Header .Add ("Authorization" , "Bearer " + token )
228+ res , err := client .Do (req )
229+ if err != nil {
230+ fmt .Println (err )
231+ return "" , errors .Wrap (err , "failed to send request for getting container registry setting" )
232+ }
233+ defer res .Body .Close ()
234+
235+ var response subscriptionUrlResponse
236+ err = json .NewDecoder (res .Body ).Decode (& response )
237+ if err != nil {
238+ return "" , errors .Wrap (err , "failed to send request for getting container registry setting" )
239+ }
240+ if len (response .Value ) == 0 {
241+ return "" , errors .New ("no id present for base url" )
242+ }
243+ return basePublicUrl + encodeParam (response .Value [0 ].ID ), nil
244+ }
245+
246+ func encodeParam (s string ) string {
247+ return url .QueryEscape (s )
248+ }
249+
48250func getenv (key ... string ) (s string ) {
49251 for _ , k := range key {
50252 s = os .Getenv (k )
@@ -54,3 +256,9 @@ func getenv(key ...string) (s string) {
54256 }
55257 return
56258}
259+
260+ type subscriptionUrlResponse struct {
261+ Value []struct {
262+ ID string `json:"id"`
263+ } `json:"value"`
264+ }
0 commit comments