Skip to content

Commit c4ff01c

Browse files
committed
Updated cmd/drone-acr/main.go and the dependencies
1 parent 44157a2 commit c4ff01c

File tree

3 files changed

+275
-21
lines changed

3 files changed

+275
-21
lines changed

cmd/drone-acr/main.go

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,46 @@
11
package main
22

33
import (
4+
"context"
5+
"encoding/base64"
6+
"encoding/json"
47
"fmt"
8+
"io/ioutil"
9+
"net/http"
10+
"net/url"
511
"os"
612
"os/exec"
713
"strings"
814

15+
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
16+
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
917
"github.com/joho/godotenv"
18+
"github.com/pkg/errors"
1019
"github.com/sirupsen/logrus"
1120

1221
docker "github.com/drone-plugins/drone-docker"
1322
)
1423

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+
1544
func main() {
1645
// Load env-file if it exists first
1746
if env := os.Getenv("PLUGIN_ENV_FILE"); env != "" {
@@ -21,15 +50,37 @@ func main() {
2150
var (
2251
repo = getenv("PLUGIN_REPO")
2352
registry = getenv("PLUGIN_REGISTRY")
53+
54+
// If these credentials are provided, they will be directly used
55+
// for docker login
2456
username = getenv("SERVICE_PRINCIPAL_CLIENT_ID")
2557
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")
2666
)
2767

2868
// default registry value
2969
if registry == "" {
3070
registry = "azurecr.io"
3171
}
3272

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+
3384
// must use the fully qualified repo name. If the
3485
// repo name does not have the registry prefix we
3586
// should prepend.
@@ -42,6 +93,11 @@ func main() {
4293
os.Setenv("DOCKER_USERNAME", username)
4394
os.Setenv("DOCKER_PASSWORD", password)
4495
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+
}
45101

46102
// invoke the base docker plugin binary
47103
cmd := exec.Command(docker.GetDroneDockerExecCmd())
@@ -53,6 +109,157 @@ func main() {
53109
}
54110
}
55111

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+
56263
func getenv(key ...string) (s string) {
57264
for _, k := range key {
58265
s = os.Getenv(k)

go.mod

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
module github.com/drone-plugins/drone-docker
22

33
require (
4+
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1
5+
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2
46
github.com/aws/aws-sdk-go v1.26.7
57
github.com/coreos/go-semver v0.3.0
68
github.com/dchest/uniuri v1.2.0
79
github.com/drone-plugins/drone-plugin-lib v0.4.1
810
github.com/drone/drone-go v1.7.1
911
github.com/inhies/go-bytesize v0.0.0-20210819104631-275770b98743
1012
github.com/joho/godotenv v1.3.0
13+
github.com/pkg/errors v0.9.1
1114
github.com/sirupsen/logrus v1.9.0
12-
github.com/stretchr/testify v1.8.1
15+
github.com/stretchr/testify v1.10.0
1316
github.com/urfave/cli v1.22.2
1417
golang.org/x/oauth2 v0.13.0
1518
google.golang.org/api v0.146.0
@@ -18,22 +21,27 @@ require (
1821
require (
1922
cloud.google.com/go/compute v1.23.1 // indirect
2023
cloud.google.com/go/compute/metadata v0.2.3 // indirect
24+
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
25+
github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3 // indirect
2126
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
2227
github.com/davecgh/go-spew v1.1.1 // indirect
28+
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
2329
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
2430
github.com/golang/protobuf v1.5.3 // indirect
2531
github.com/google/s2a-go v0.1.7 // indirect
26-
github.com/google/uuid v1.3.1 // indirect
32+
github.com/google/uuid v1.6.0 // indirect
2733
github.com/googleapis/enterprise-certificate-proxy v0.3.1 // indirect
2834
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
2935
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af // indirect
36+
github.com/kylelemons/godebug v1.1.0 // indirect
37+
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
3038
github.com/pmezard/go-difflib v1.0.0 // indirect
3139
github.com/russross/blackfriday/v2 v2.1.0 // indirect
3240
go.opencensus.io v0.24.0 // indirect
33-
golang.org/x/crypto v0.14.0 // indirect
34-
golang.org/x/net v0.17.0 // indirect
35-
golang.org/x/sys v0.13.0 // indirect
36-
golang.org/x/text v0.13.0 // indirect
41+
golang.org/x/crypto v0.36.0 // indirect
42+
golang.org/x/net v0.37.0 // indirect
43+
golang.org/x/sys v0.31.0 // indirect
44+
golang.org/x/text v0.23.0 // indirect
3745
google.golang.org/appengine v1.6.8 // indirect
3846
google.golang.org/genproto/googleapis/rpc v0.0.0-20231016165738-49dd2c1f3d0b // indirect
3947
google.golang.org/grpc v1.59.0 // indirect
@@ -42,4 +50,6 @@ require (
4250
gopkg.in/yaml.v3 v3.0.1 // indirect
4351
)
4452

45-
go 1.22.4
53+
go 1.23.0
54+
55+
toolchain go1.23.7

0 commit comments

Comments
 (0)