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"
13
+ "path/filepath"
7
14
"strings"
8
15
16
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
17
+ "github.com/Azure/azure-sdk-for-go/sdk/azidentity"
9
18
"github.com/joho/godotenv"
19
+ "github.com/pkg/errors"
10
20
"github.com/sirupsen/logrus"
11
21
12
22
docker "github.com/drone-plugins/drone-docker"
13
23
)
14
24
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
+
15
49
func main () {
16
50
// Load env-file if it exists first
17
51
if env := os .Getenv ("PLUGIN_ENV_FILE" ); env != "" {
@@ -21,15 +55,37 @@ func main() {
21
55
var (
22
56
repo = getenv ("PLUGIN_REPO" )
23
57
registry = getenv ("PLUGIN_REGISTRY" )
58
+
59
+ // If these credentials are provided, they will be directly used
60
+ // for docker login
24
61
username = getenv ("SERVICE_PRINCIPAL_CLIENT_ID" )
25
62
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" )
26
71
)
27
72
28
73
// default registry value
29
74
if registry == "" {
30
75
registry = "azurecr.io"
31
76
}
32
77
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
+
33
89
// must use the fully qualified repo name. If the
34
90
// repo name does not have the registry prefix we
35
91
// should prepend.
@@ -42,6 +98,11 @@ func main() {
42
98
os .Setenv ("DOCKER_USERNAME" , username )
43
99
os .Setenv ("DOCKER_PASSWORD" , password )
44
100
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
+ }
45
106
46
107
// invoke the base docker plugin binary
47
108
cmd := exec .Command (docker .GetDroneDockerExecCmd ())
@@ -53,6 +114,157 @@ func main() {
53
114
}
54
115
}
55
116
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
+
56
268
func getenv (key ... string ) (s string ) {
57
269
for _ , k := range key {
58
270
s = os .Getenv (k )
0 commit comments