/
ecr.go
148 lines (120 loc) · 4.67 KB
/
ecr.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
package aws
import (
"context"
"encoding/json"
"errors"
"fmt"
"math/big"
"net/http"
"strings"
"time"
"github.com/open-policy-agent/opa/internal/version"
"github.com/open-policy-agent/opa/logging"
)
// Values taken from
// https://docs.aws.amazon.com/AmazonECR/latest/APIReference/API_GetAuthorizationToken.html
const (
ecrGetAuthorizationTokenTarget = "AmazonEC2ContainerRegistry_V20150921.GetAuthorizationToken"
ecrEndpointFmt = "https://ecr.%s.amazonaws.com/"
)
// ECR is used to request tokens from Elastic Container Registry.
type ECR struct {
// endpoint returns the region-specifc ECR endpoint.
// It can be overridden by tests.
endpoint func(region string) string
// client is used to send authorization tokens requests.
client *http.Client
logger logging.Logger
}
func NewECR(logger logging.Logger) *ECR {
return &ECR{
endpoint: func(region string) string {
return fmt.Sprintf(ecrEndpointFmt, region)
},
client: &http.Client{},
logger: logger,
}
}
// GetAuthorizationToken requests a token that can be used to authenticate image pull requests.
func (e *ECR) GetAuthorizationToken(ctx context.Context, creds Credentials, signatureVersion string) (ECRAuthorizationToken, error) {
endpoint := e.endpoint(creds.RegionName)
body := strings.NewReader("{}")
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, body)
if err != nil {
return ECRAuthorizationToken{}, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("X-Amz-Target", ecrGetAuthorizationTokenTarget)
req.Header.Set("Accept-Encoding", "identity")
req.Header.Set("Content-Type", "application/x-amz-json-1.1")
req.Header.Set("User-Agent", version.UserAgent)
e.logger.Debug("Signing ECR authorization token request")
if err := SignRequest(req, "ecr", creds, time.Now(), signatureVersion); err != nil {
return ECRAuthorizationToken{}, fmt.Errorf("failed to sign request: %w", err)
}
resp, err := DoRequestWithClient(req, e.client, "ecr get authorization token", e.logger)
if err != nil {
return ECRAuthorizationToken{}, err
}
var data struct {
AuthorizationData []struct {
AuthorizationToken string `json:"authorizationToken"`
ExpiresAt json.Number `json:"expiresAt"`
} `json:"authorizationData"`
}
if err := json.Unmarshal(resp, &data); err != nil {
return ECRAuthorizationToken{}, fmt.Errorf("failed to unmarshal response: %w", err)
}
if len(data.AuthorizationData) < 1 {
return ECRAuthorizationToken{}, errors.New("empty authorization data")
}
// The GetAuthorizationToken request returns a list of tokens for
// backwards compatibility reasons. We should only ever get one token back
// because we don't define any registryIDs in the request.
// See https://docs.aws.amazon.com/AmazonECR/latest/APIReference/API_GetAuthorizationToken.html#API_GetAuthorizationToken_ResponseSyntax
resultToken := data.AuthorizationData[0]
expiresAt, err := parseTimestamp(resultToken.ExpiresAt)
if err != nil {
return ECRAuthorizationToken{}, fmt.Errorf("failed to parse expiresAt: %w", err)
}
return ECRAuthorizationToken{
AuthorizationToken: resultToken.AuthorizationToken,
ExpiresAt: expiresAt,
}, nil
}
// ECRAuthorizationToken can sign requests to AWS ECR.
//
// It corresponds to data returned by the AWS GetAuthorizationToken API.
// See https://docs.aws.amazon.com/AmazonECR/latest/APIReference/API_AuthorizationData.html
type ECRAuthorizationToken struct {
AuthorizationToken string
ExpiresAt time.Time
}
// IsValid returns true if the token is set and not expired.
// It respects a margin of error for time handling and will mark it as expired early.
func (t *ECRAuthorizationToken) IsValid() bool {
const tokenExpirationMargin = 5 * time.Minute
expired := time.Now().Add(tokenExpirationMargin).After(t.ExpiresAt)
return t.AuthorizationToken != "" && !expired
}
var millisecondsFloat = new(big.Float).SetInt64(1e3)
// parseTimestamp parses the AWS format for timestamps.
// The time precision is in milliseconds.
//
// The logic is taken from
// https://github.com/aws/aws-sdk-go/blob/41717ba2c04d3fd03f94d09ea984a10899574935/private/protocol/json/jsonutil/unmarshal.go#L294-L302
func parseTimestamp(raw json.Number) (time.Time, error) {
s := raw.String()
float, ok := new(big.Float).SetString(s)
if !ok {
return time.Time{}, fmt.Errorf("not a float: %q", raw)
}
// The float is expected to be in second resolution with millisecond
// decimal places.
// Multiply by millisecondsFloat to obtain an integer in millisecond
// resolution
ms, _ := float.Mul(float, millisecondsFloat).Int64()
// Multiply again to obtain nanosecond resolution for time.Unix
ns := ms * 1e6
t := time.Unix(0, ns).UTC()
return t, nil
}