forked from containers/image
/
system_registries_v2.go
483 lines (422 loc) · 15.6 KB
/
system_registries_v2.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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
package sysregistriesv2
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strings"
"sync"
"github.com/BurntSushi/toml"
"github.com/containers/image/types"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/containers/image/docker/reference"
)
// systemRegistriesConfPath is the path to the system-wide registry
// configuration file and is used to add/subtract potential registries for
// obtaining images. You can override this at build time with
// -ldflags '-X github.com/containers/image/sysregistries.systemRegistriesConfPath=$your_path'
var systemRegistriesConfPath = builtinRegistriesConfPath
// builtinRegistriesConfPath is the path to the registry configuration file.
// DO NOT change this, instead see systemRegistriesConfPath above.
const builtinRegistriesConfPath = "/etc/containers/registries.conf"
// Endpoint describes a remote location of a registry.
type Endpoint struct {
// The endpoint's remote location.
Location string `toml:"location,omitempty"`
// If true, certs verification will be skipped and HTTP (non-TLS)
// connections will be allowed.
Insecure bool `toml:"insecure,omitempty"`
}
// rewriteReference will substitute the provided reference `prefix` to the
// endpoints `location` from the `ref` and creates a new named reference from it.
// The function errors if the newly created reference is not parsable.
func (e *Endpoint) rewriteReference(ref reference.Named, prefix string) (reference.Named, error) {
refString := ref.String()
if !refMatchesPrefix(refString, prefix) {
return nil, fmt.Errorf("invalid prefix '%v' for reference '%v'", prefix, refString)
}
newNamedRef := strings.Replace(refString, prefix, e.Location, 1)
newParsedRef, err := reference.ParseNamed(newNamedRef)
if err != nil {
return nil, errors.Wrapf(err, "error rewriting reference")
}
logrus.Debugf("reference rewritten from '%v' to '%v'", refString, newParsedRef.String())
return newParsedRef, nil
}
// Registry represents a registry.
type Registry struct {
// Prefix is used for matching images, and to translate one namespace to
// another. If `Prefix="example.com/bar"`, `location="example.com/foo/bar"`
// and we pull from "example.com/bar/myimage:latest", the image will
// effectively be pulled from "example.com/foo/bar/myimage:latest".
// If no Prefix is specified, it defaults to the specified location.
Prefix string `toml:"prefix"`
// A registry is an Endpoint too
Endpoint
// The registry's mirrors.
Mirrors []Endpoint `toml:"mirror,omitempty"`
// If true, pulling from the registry will be blocked.
Blocked bool `toml:"blocked,omitempty"`
// If true, mirrors will only be used for digest pulls. Pulling images by
// tag can potentially yield different images, depending on which endpoint
// we pull from. Forcing digest-pulls for mirrors avoids that issue.
MirrorByDigestOnly bool `toml:"mirror-by-digest-only,omitempty"`
}
// PullSource consists of an Endpoint and a Reference. Note that the reference is
// rewritten according to the registries prefix and the Endpoint's location.
type PullSource struct {
Endpoint Endpoint
Reference reference.Named
}
// PullSourcesFromReference returns a slice of PullSource's based on the passed
// reference.
func (r *Registry) PullSourcesFromReference(ref reference.Named) ([]PullSource, error) {
var endpoints []Endpoint
if r.MirrorByDigestOnly {
// Only use mirrors when the reference is a digest one.
if _, isDigested := ref.(reference.Canonical); isDigested {
endpoints = append(r.Mirrors, r.Endpoint)
} else {
endpoints = []Endpoint{r.Endpoint}
}
} else {
endpoints = append(r.Mirrors, r.Endpoint)
}
sources := []PullSource{}
for _, ep := range endpoints {
rewritten, err := ep.rewriteReference(ref, r.Prefix)
if err != nil {
return nil, err
}
sources = append(sources, PullSource{Endpoint: ep, Reference: rewritten})
}
return sources, nil
}
// V1TOMLregistries is for backwards compatibility to sysregistries v1
type V1TOMLregistries struct {
Registries []string `toml:"registries"`
}
// V1TOMLConfig is for backwards compatibility to sysregistries v1
type V1TOMLConfig struct {
Search V1TOMLregistries `toml:"search"`
Insecure V1TOMLregistries `toml:"insecure"`
Block V1TOMLregistries `toml:"block"`
}
// V1RegistriesConf is the sysregistries v1 configuration format.
type V1RegistriesConf struct {
V1TOMLConfig `toml:"registries"`
}
// Nonempty returns true if config contains at least one configuration entry.
func (config *V1RegistriesConf) Nonempty() bool {
return (len(config.V1TOMLConfig.Search.Registries) != 0 ||
len(config.V1TOMLConfig.Insecure.Registries) != 0 ||
len(config.V1TOMLConfig.Block.Registries) != 0)
}
// V2RegistriesConf is the sysregistries v2 configuration format.
type V2RegistriesConf struct {
Registries []Registry `toml:"registry"`
// An array of host[:port] (not prefix!) entries to use for resolving unqualified image references
UnqualifiedSearchRegistries []string `toml:"unqualified-search-registries"`
}
// Nonempty returns true if config contains at least one configuration entry.
func (config *V2RegistriesConf) Nonempty() bool {
return (len(config.Registries) != 0 ||
len(config.UnqualifiedSearchRegistries) != 0)
}
// tomlConfig is the data type used to unmarshal the toml config.
type tomlConfig struct {
V2RegistriesConf
V1RegistriesConf // for backwards compatibility with sysregistries v1
}
// InvalidRegistries represents an invalid registry configurations. An example
// is when "registry.com" is defined multiple times in the configuration but
// with conflicting security settings.
type InvalidRegistries struct {
s string
}
// Error returns the error string.
func (e *InvalidRegistries) Error() string {
return e.s
}
// parseLocation parses the input string, performs some sanity checks and returns
// the sanitized input string. An error is returned if the input string is
// empty or if contains an "http{s,}://" prefix.
func parseLocation(input string) (string, error) {
trimmed := strings.TrimRight(input, "/")
if trimmed == "" {
return "", &InvalidRegistries{s: "invalid location: cannot be empty"}
}
if strings.HasPrefix(trimmed, "http://") || strings.HasPrefix(trimmed, "https://") {
msg := fmt.Sprintf("invalid location '%s': URI schemes are not supported", input)
return "", &InvalidRegistries{s: msg}
}
return trimmed, nil
}
// ConvertToV2 returns a v2 config corresponding to a v1 one.
func (config *V1RegistriesConf) ConvertToV2() (*V2RegistriesConf, error) {
regMap := make(map[string]*Registry)
// The order of the registries is not really important, but make it deterministic (the same for the same config file)
// to minimize behavior inconsistency and not contribute to difficult-to-reproduce situations.
registryOrder := []string{}
getRegistry := func(location string) (*Registry, error) { // Note: _pointer_ to a long-lived object
var err error
location, err = parseLocation(location)
if err != nil {
return nil, err
}
reg, exists := regMap[location]
if !exists {
reg = &Registry{
Endpoint: Endpoint{Location: location},
Mirrors: []Endpoint{},
Prefix: location,
}
regMap[location] = reg
registryOrder = append(registryOrder, location)
}
return reg, nil
}
for _, blocked := range config.V1TOMLConfig.Block.Registries {
reg, err := getRegistry(blocked)
if err != nil {
return nil, err
}
reg.Blocked = true
}
for _, insecure := range config.V1TOMLConfig.Insecure.Registries {
reg, err := getRegistry(insecure)
if err != nil {
return nil, err
}
reg.Insecure = true
}
res := &V2RegistriesConf{
UnqualifiedSearchRegistries: config.V1TOMLConfig.Search.Registries,
}
for _, location := range registryOrder {
reg := regMap[location]
res.Registries = append(res.Registries, *reg)
}
return res, nil
}
// anchoredDomainRegexp is an internal implementation detail of postProcess, defining the valid values of elements of UnqualifiedSearchRegistries.
var anchoredDomainRegexp = regexp.MustCompile("^" + reference.DomainRegexp.String() + "$")
// postProcess checks the consistency of all the configuration, looks for conflicts,
// and normalizes the configuration (e.g., sets the Prefix to Location if not set).
func (config *V2RegistriesConf) postProcess() error {
regMap := make(map[string][]*Registry)
for i := range config.Registries {
reg := &config.Registries[i]
// make sure Location and Prefix are valid
var err error
reg.Location, err = parseLocation(reg.Location)
if err != nil {
return err
}
if reg.Prefix == "" {
reg.Prefix = reg.Location
} else {
reg.Prefix, err = parseLocation(reg.Prefix)
if err != nil {
return err
}
}
// make sure mirrors are valid
for _, mir := range reg.Mirrors {
mir.Location, err = parseLocation(mir.Location)
if err != nil {
return err
}
}
regMap[reg.Location] = append(regMap[reg.Location], reg)
}
// Given a registry can be mentioned multiple times (e.g., to have
// multiple prefixes backed by different mirrors), we need to make sure
// there are no conflicts among them.
//
// Note: we need to iterate over the registries array to ensure a
// deterministic behavior which is not guaranteed by maps.
for _, reg := range config.Registries {
others, _ := regMap[reg.Location]
for _, other := range others {
if reg.Insecure != other.Insecure {
msg := fmt.Sprintf("registry '%s' is defined multiple times with conflicting 'insecure' setting", reg.Location)
return &InvalidRegistries{s: msg}
}
if reg.Blocked != other.Blocked {
msg := fmt.Sprintf("registry '%s' is defined multiple times with conflicting 'blocked' setting", reg.Location)
return &InvalidRegistries{s: msg}
}
}
}
for i := range config.UnqualifiedSearchRegistries {
registry, err := parseLocation(config.UnqualifiedSearchRegistries[i])
if err != nil {
return err
}
if !anchoredDomainRegexp.MatchString(registry) {
return &InvalidRegistries{fmt.Sprintf("Invalid unqualified-search-registries entry %#v", registry)}
}
config.UnqualifiedSearchRegistries[i] = registry
}
return nil
}
// ConfigPath returns the path to the system-wide registry configuration file.
func ConfigPath(ctx *types.SystemContext) string {
confPath := systemRegistriesConfPath
if ctx != nil {
if ctx.SystemRegistriesConfPath != "" {
confPath = ctx.SystemRegistriesConfPath
} else if ctx.RootForImplicitAbsolutePaths != "" {
confPath = filepath.Join(ctx.RootForImplicitAbsolutePaths, systemRegistriesConfPath)
}
}
return confPath
}
// configMutex is used to synchronize concurrent accesses to configCache.
var configMutex = sync.Mutex{}
// configCache caches already loaded configs with config paths as keys and is
// used to avoid redudantly parsing configs. Concurrent accesses to the cache
// are synchronized via configMutex.
var configCache = make(map[string]*V2RegistriesConf)
// InvalidateCache invalidates the registry cache. This function is meant to be
// used for long-running processes that need to reload potential changes made to
// the cached registry config files.
func InvalidateCache() {
configMutex.Lock()
defer configMutex.Unlock()
configCache = make(map[string]*V2RegistriesConf)
}
// getConfig returns the config object corresponding to ctx, loading it if it is not yet cached.
func getConfig(ctx *types.SystemContext) (*V2RegistriesConf, error) {
configPath := ConfigPath(ctx)
configMutex.Lock()
// if the config has already been loaded, return the cached registries
if config, inCache := configCache[configPath]; inCache {
configMutex.Unlock()
return config, nil
}
configMutex.Unlock()
return TryUpdatingCache(ctx)
}
// TryUpdatingCache loads the configuration from the provided `SystemContext`
// without using the internal cache. On success, the loaded configuration will
// be added into the internal registry cache.
func TryUpdatingCache(ctx *types.SystemContext) (*V2RegistriesConf, error) {
configPath := ConfigPath(ctx)
configMutex.Lock()
defer configMutex.Unlock()
// load the config
config, err := loadRegistryConf(configPath)
if err != nil {
// Return an empty []Registry if we use the default config,
// which implies that the config path of the SystemContext
// isn't set. Note: if ctx.SystemRegistriesConfPath points to
// the default config, we will still return an error.
if os.IsNotExist(err) && (ctx == nil || ctx.SystemRegistriesConfPath == "") {
return &V2RegistriesConf{Registries: []Registry{}}, nil
}
return nil, err
}
v2Config := &config.V2RegistriesConf
// backwards compatibility for v1 configs
if config.V1RegistriesConf.Nonempty() {
if config.V2RegistriesConf.Nonempty() {
return nil, &InvalidRegistries{s: "mixing sysregistry v1/v2 is not supported"}
}
v2, err := config.V1RegistriesConf.ConvertToV2()
if err != nil {
return nil, err
}
v2Config = v2
}
if err := v2Config.postProcess(); err != nil {
return nil, err
}
// populate the cache
configCache[configPath] = v2Config
return v2Config, nil
}
// GetRegistries loads and returns the registries specified in the config.
// Note the parsed content of registry config files is cached. For reloading,
// use `InvalidateCache` and re-call `GetRegistries`.
func GetRegistries(ctx *types.SystemContext) ([]Registry, error) {
config, err := getConfig(ctx)
if err != nil {
return nil, err
}
return config.Registries, nil
}
// UnqualifiedSearchRegistries returns a list of host[:port] entries to try
// for unqualified image search, in the returned order)
func UnqualifiedSearchRegistries(ctx *types.SystemContext) ([]string, error) {
config, err := getConfig(ctx)
if err != nil {
return nil, err
}
return config.UnqualifiedSearchRegistries, nil
}
// refMatchesPrefix returns true iff ref,
// which is a registry, repository namespace, repository or image reference (as formatted by
// reference.Domain(), reference.Named.Name() or reference.Reference.String()
// — note that this requires the name to start with an explicit hostname!),
// matches a Registry.Prefix value.
// (This is split from the caller primarily to make testing easier.)
func refMatchesPrefix(ref, prefix string) bool {
switch {
case len(ref) < len(prefix):
return false
case len(ref) == len(prefix):
return ref == prefix
case len(ref) > len(prefix):
if !strings.HasPrefix(ref, prefix) {
return false
}
c := ref[len(prefix)]
// This allows "example.com:5000" to match "example.com",
// which is unintended; that will get fixed eventually, DON'T RELY
// ON THE CURRENT BEHAVIOR.
return c == ':' || c == '/' || c == '@'
default:
panic("Internal error: impossible comparison outcome")
}
}
// FindRegistry returns the Registry with the longest prefix for ref,
// which is a registry, repository namespace repository or image reference (as formatted by
// reference.Domain(), reference.Named.Name() or reference.Reference.String()
// — note that this requires the name to start with an explicit hostname!).
// If no Registry prefixes the image, nil is returned.
func FindRegistry(ctx *types.SystemContext, ref string) (*Registry, error) {
config, err := getConfig(ctx)
if err != nil {
return nil, err
}
reg := Registry{}
prefixLen := 0
for _, r := range config.Registries {
if refMatchesPrefix(ref, r.Prefix) {
length := len(r.Prefix)
if length > prefixLen {
reg = r
prefixLen = length
}
}
}
if prefixLen != 0 {
return ®, nil
}
return nil, nil
}
// Loads the registry configuration file from the filesystem and then unmarshals
// it. Returns the unmarshalled object.
func loadRegistryConf(configPath string) (*tomlConfig, error) {
config := &tomlConfig{}
configBytes, err := ioutil.ReadFile(configPath)
if err != nil {
return nil, err
}
err = toml.Unmarshal(configBytes, &config)
return config, err
}