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