1
2
3
4
5
6
7 package kauth
8
9 import (
10 "context"
11 "encoding/json"
12 "fmt"
13 "net"
14 "net/url"
15 "path/filepath"
16 "sort"
17 "strings"
18
19 "github.com/google/go-containerregistry/pkg/authn"
20 "github.com/google/go-containerregistry/pkg/logs"
21 corev1 "k8s.io/api/core/v1"
22 k8serrors "k8s.io/apimachinery/pkg/api/errors"
23 "k8s.io/client-go/rest"
24 "sigs.k8s.io/controller-runtime/pkg/client"
25 )
26
27 const (
28
29
30
31
32 NoServiceAccount = "no service account"
33 )
34
35
36 type Options struct {
37
38
39
40 Namespace string
41
42
43
44
45
46
47 ServiceAccountName string
48
49
50
51 ImagePullSecrets []string
52
53
54
55
56
57 UseMountSecrets bool
58 }
59
60
61
62 func New(ctx context.Context, c client.Client, opt Options) (authn.Keychain, error) {
63 if opt.Namespace == "" {
64 opt.Namespace = "default"
65 }
66 if opt.ServiceAccountName == "" {
67 opt.ServiceAccountName = "default"
68 }
69
70
71
72
73
74
75
76
77 var pullSecrets []corev1.Secret
78 for _, name := range opt.ImagePullSecrets {
79 ps := &corev1.Secret{}
80 err := c.Get(ctx, client.ObjectKey{
81 Name: name, Namespace: opt.Namespace,
82 }, ps)
83 if k8serrors.IsNotFound(err) {
84 logs.Warn.Printf("secret %s/%s not found; ignoring", opt.Namespace, name)
85 continue
86 } else if err != nil {
87 return nil, err
88 }
89 pullSecrets = append(pullSecrets, *ps)
90 }
91
92
93
94
95
96 if opt.ServiceAccountName != NoServiceAccount {
97 sa := &corev1.ServiceAccount{}
98 err := c.Get(ctx, client.ObjectKey{
99 Name: opt.ServiceAccountName, Namespace: opt.Namespace,
100 }, sa)
101 switch {
102 case k8serrors.IsNotFound(err):
103 logs.Warn.Printf("serviceaccount %s/%s not found; ignoring", opt.Namespace, opt.ServiceAccountName)
104 case err != nil:
105 return nil, err
106 case err == nil:
107 for _, localObj := range sa.ImagePullSecrets {
108 ps := &corev1.Secret{}
109 err := c.Get(ctx, client.ObjectKey{
110 Name: localObj.Name, Namespace: opt.Namespace,
111 }, ps)
112
113 if k8serrors.IsNotFound(err) {
114 logs.Warn.Printf("secret %s/%s not found; ignoring", opt.Namespace, localObj.Name)
115 continue
116 } else if err != nil {
117 return nil, err
118 }
119 pullSecrets = append(pullSecrets, *ps)
120 }
121
122 if opt.UseMountSecrets {
123 for _, obj := range sa.Secrets {
124 s := &corev1.Secret{}
125 err := c.Get(ctx, client.ObjectKey{
126 Name: obj.Name, Namespace: opt.Namespace,
127 }, s)
128 if k8serrors.IsNotFound(err) {
129 logs.Warn.Printf("secret %s/%s not found; ignoring", opt.Namespace, obj.Name)
130 continue
131 } else if err != nil {
132 return nil, err
133 }
134 pullSecrets = append(pullSecrets, *s)
135 }
136 }
137 }
138 }
139
140 return NewFromPullSecrets(pullSecrets)
141 }
142
143
144
145
146 func NewInCluster(ctx context.Context, opt Options) (authn.Keychain, error) {
147 clusterConfig, err := rest.InClusterConfig()
148 if err != nil {
149 return nil, err
150 }
151 c, err := client.New(clusterConfig, client.Options{})
152 if err != nil {
153 return nil, err
154 }
155 return New(ctx, c, opt)
156 }
157
158 type dockerConfigJSON struct {
159 Auths map[string]authn.AuthConfig
160 }
161
162
163
164 func NewFromPullSecrets(secrets []corev1.Secret) (authn.Keychain, error) {
165 keyring := &keyring{
166 index: make([]string, 0),
167 creds: make(map[string][]authn.AuthConfig),
168 }
169
170 var cfg dockerConfigJSON
171
172
173 for _, secret := range secrets {
174 if b, exists := secret.Data[corev1.DockerConfigJsonKey]; secret.Type == corev1.SecretTypeDockerConfigJson && exists && len(b) > 0 {
175 if err := json.Unmarshal(b, &cfg); err != nil {
176 return nil, err
177 }
178 }
179 if b, exists := secret.Data[corev1.DockerConfigKey]; secret.Type == corev1.SecretTypeDockercfg && exists && len(b) > 0 {
180 if err := json.Unmarshal(b, &cfg.Auths); err != nil {
181 return nil, err
182 }
183 }
184
185 for registry, v := range cfg.Auths {
186 value := registry
187 if !strings.HasPrefix(value, "https://") && !strings.HasPrefix(value, "http://") {
188 value = "https://" + value
189 }
190 parsed, err := url.Parse(value)
191 if err != nil {
192 return nil, fmt.Errorf("Entry %q in dockercfg invalid (%w)", value, err)
193 }
194
195
196
197
198
199
200
201 effectivePath := parsed.Path
202 if strings.HasPrefix(effectivePath, "/v2/") || strings.HasPrefix(effectivePath, "/v1/") {
203 effectivePath = effectivePath[3:]
204 }
205 var key string
206 if (len(effectivePath) > 0) && (effectivePath != "/") {
207 key = parsed.Host + effectivePath
208 } else {
209 key = parsed.Host
210 }
211
212 if _, ok := keyring.creds[key]; !ok {
213 keyring.index = append(keyring.index, key)
214 }
215
216 keyring.creds[key] = append(keyring.creds[key], v)
217 }
218
219
220
221 sort.Sort(sort.Reverse(sort.StringSlice(keyring.index)))
222 }
223 return keyring, nil
224 }
225
226 type keyring struct {
227 index []string
228 creds map[string][]authn.AuthConfig
229 }
230
231 func (keyring *keyring) Resolve(target authn.Resource) (authn.Authenticator, error) {
232 image := target.String()
233 auths := []authn.AuthConfig{}
234
235 for _, k := range keyring.index {
236
237
238 if matched, _ := urlsMatchStr(k, image); matched {
239 auths = append(auths, keyring.creds[k]...)
240 }
241 }
242
243 if len(auths) == 0 {
244 return authn.Anonymous, nil
245 }
246
247 return toAuthenticator(auths)
248 }
249
250
251 func urlsMatchStr(glob string, target string) (bool, error) {
252 globURL, err := parseSchemelessURL(glob)
253 if err != nil {
254 return false, err
255 }
256 targetURL, err := parseSchemelessURL(target)
257 if err != nil {
258 return false, err
259 }
260 return urlsMatch(globURL, targetURL)
261 }
262
263
264
265
266 func parseSchemelessURL(schemelessURL string) (*url.URL, error) {
267 parsed, err := url.Parse("https://" + schemelessURL)
268 if err != nil {
269 return nil, err
270 }
271
272 parsed.Scheme = ""
273 return parsed, nil
274 }
275
276
277 func splitURL(url *url.URL) (parts []string, port string) {
278 host, port, err := net.SplitHostPort(url.Host)
279 if err != nil {
280
281 host, port = url.Host, ""
282 }
283 return strings.Split(host, "."), port
284 }
285
286
287
288
289
290
291
292
293
294
295 func urlsMatch(globURL *url.URL, targetURL *url.URL) (bool, error) {
296 globURLParts, globPort := splitURL(globURL)
297 targetURLParts, targetPort := splitURL(targetURL)
298 if globPort != targetPort {
299
300 return false, nil
301 }
302 if len(globURLParts) != len(targetURLParts) {
303
304 return false, nil
305 }
306 if !strings.HasPrefix(targetURL.Path, globURL.Path) {
307
308 return false, nil
309 }
310 for k, globURLPart := range globURLParts {
311 targetURLPart := targetURLParts[k]
312 matched, err := filepath.Match(globURLPart, targetURLPart)
313 if err != nil {
314 return false, err
315 }
316 if !matched {
317
318 return false, nil
319 }
320 }
321
322 return true, nil
323 }
324
325 func toAuthenticator(configs []authn.AuthConfig) (authn.Authenticator, error) {
326 cfg := configs[0]
327
328 if cfg.Auth != "" {
329 cfg.Auth = ""
330 }
331
332 return authn.FromConfig(cfg), nil
333 }
334
View as plain text