1
1
package main
2
2
3
3
import (
4
+ "context"
5
+ "encoding/base64"
6
+ "encoding/json"
4
7
"fmt"
8
+ "io/ioutil"
9
+ "net/http"
10
+ "net/url"
5
11
"os"
6
12
"strings"
7
13
14
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
15
+ "github.com/Azure/azure-sdk-for-go/sdk/azidentity"
8
16
"github.com/joho/godotenv"
17
+ "github.com/pkg/errors"
18
+ "github.com/sirupsen/logrus"
9
19
10
20
docker "github.com/drone-plugins/drone-buildx"
11
21
)
12
22
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
+
13
37
func main () {
14
38
// Load env-file if it exists first
15
39
if env := os .Getenv ("PLUGIN_ENV_FILE" ); env != "" {
@@ -19,15 +43,37 @@ func main() {
19
43
var (
20
44
repo = getenv ("PLUGIN_REPO" )
21
45
registry = getenv ("PLUGIN_REGISTRY" )
46
+
47
+ // If these credentials are provided, they will be directly used
48
+ // for docker login
22
49
username = getenv ("SERVICE_PRINCIPAL_CLIENT_ID" )
23
50
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" )
24
59
)
25
60
26
61
// default registry value
27
62
if registry == "" {
28
63
registry = "azurecr.io"
29
64
}
30
65
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
+
31
77
// must use the fully qualified repo name. If the
32
78
// repo name does not have the registry prefix we
33
79
// should prepend.
@@ -40,11 +86,167 @@ func main() {
40
86
os .Setenv ("DOCKER_USERNAME" , username )
41
87
os .Setenv ("DOCKER_PASSWORD" , password )
42
88
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
+ }
43
94
44
95
// invoke the base docker plugin binary
45
96
docker .Run ()
46
97
}
47
98
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
+
48
250
func getenv (key ... string ) (s string ) {
49
251
for _ , k := range key {
50
252
s = os .Getenv (k )
@@ -54,3 +256,9 @@ func getenv(key ...string) (s string) {
54
256
}
55
257
return
56
258
}
259
+
260
+ type subscriptionUrlResponse struct {
261
+ Value []struct {
262
+ ID string `json:"id"`
263
+ } `json:"value"`
264
+ }
0 commit comments