File: shortnames.go

package info (click to toggle)
golang-github-containers-image 5.28.0-4
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 5,104 kB
  • sloc: sh: 194; makefile: 73
file content (350 lines) | stat: -rw-r--r-- 11,572 bytes parent folder | download | duplicates (2)
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
package sysregistriesv2

import (
	"fmt"
	"os"
	"path/filepath"
	"reflect"
	"strings"

	"github.com/BurntSushi/toml"
	"github.com/containers/image/v5/docker/reference"
	"github.com/containers/image/v5/internal/rootless"
	"github.com/containers/image/v5/types"
	"github.com/containers/storage/pkg/homedir"
	"github.com/containers/storage/pkg/lockfile"
	"github.com/sirupsen/logrus"
	"golang.org/x/exp/maps"
)

// defaultShortNameMode is the default mode of registries.conf files if the
// corresponding field is left empty.
const defaultShortNameMode = types.ShortNameModePermissive

// userShortNamesFile is the user-specific config file to store aliases.
var userShortNamesFile = filepath.FromSlash("containers/short-name-aliases.conf")

// shortNameAliasesConfPath returns the path to the machine-generated
// short-name-aliases.conf file.
func shortNameAliasesConfPath(ctx *types.SystemContext) (string, error) {
	if ctx != nil && len(ctx.UserShortNameAliasConfPath) > 0 {
		return ctx.UserShortNameAliasConfPath, nil
	}

	if rootless.GetRootlessEUID() == 0 {
		// Root user or in a non-conforming user NS
		return filepath.Join("/var/cache", userShortNamesFile), nil
	}

	// Rootless user
	cacheRoot, err := homedir.GetCacheHome()
	if err != nil {
		return "", err
	}

	return filepath.Join(cacheRoot, userShortNamesFile), nil
}

// shortNameAliasConf is a subset of the `V2RegistriesConf` format.  It's used in the
// software-maintained `userShortNamesFile`.
type shortNameAliasConf struct {
	// A map for aliasing short names to their fully-qualified image
	// reference counter parts.
	// Note that Aliases is niled after being loaded from a file.
	Aliases map[string]string `toml:"aliases"`

	// If you add any field, make sure to update nonempty() below.
}

// nonempty returns true if config contains at least one configuration entry.
func (c *shortNameAliasConf) nonempty() bool {
	copy := *c // A shallow copy
	if copy.Aliases != nil && len(copy.Aliases) == 0 {
		copy.Aliases = nil
	}
	return !reflect.DeepEqual(copy, shortNameAliasConf{})
}

// alias combines the parsed value of an alias with the config file it has been
// specified in.  The config file is crucial for an improved user experience
// such that users are able to resolve potential pull errors.
type alias struct {
	// The parsed value of an alias.  May be nil if set to "" in a config.
	value reference.Named
	// The config file the alias originates from.
	configOrigin string
}

// shortNameAliasCache is the result of parsing shortNameAliasConf,
// pre-processed for faster usage.
type shortNameAliasCache struct {
	// Note that an alias value may be nil iff it's set as an empty string
	// in the config.
	namedAliases map[string]alias
}

// ResolveShortNameAlias performs an alias resolution of the specified name.
// The user-specific short-name-aliases.conf has precedence over aliases in the
// assembled registries.conf.  It returns the possibly resolved alias or nil, a
// human-readable description of the config where the alias is specified, and
// an error. The origin of the config file is crucial for an improved user
// experience such that users are able to resolve potential pull errors.
// Almost all callers should use pkg/shortnames instead.
//
// Note that it’s the caller’s responsibility to pass only a repository
// (reference.IsNameOnly) as the short name.
func ResolveShortNameAlias(ctx *types.SystemContext, name string) (reference.Named, string, error) {
	if err := validateShortName(name); err != nil {
		return nil, "", err
	}
	confPath, lock, err := shortNameAliasesConfPathAndLock(ctx)
	if err != nil {
		return nil, "", err
	}

	// Acquire the lock as a reader to allow for multiple routines in the
	// same process space to read simultaneously.
	lock.RLock()
	defer lock.Unlock()

	_, aliasCache, err := loadShortNameAliasConf(confPath)
	if err != nil {
		return nil, "", err
	}

	// First look up the short-name-aliases.conf.  Note that a value may be
	// nil iff it's set as an empty string in the config.
	alias, resolved := aliasCache.namedAliases[name]
	if resolved {
		return alias.value, alias.configOrigin, nil
	}

	config, err := getConfig(ctx)
	if err != nil {
		return nil, "", err
	}
	alias, resolved = config.aliasCache.namedAliases[name]
	if resolved {
		return alias.value, alias.configOrigin, nil
	}
	return nil, "", nil
}

// editShortNameAlias loads the aliases.conf file and changes it. If value is
// set, it adds the name-value pair as a new alias. Otherwise, it will remove
// name from the config.
func editShortNameAlias(ctx *types.SystemContext, name string, value *string) error {
	if err := validateShortName(name); err != nil {
		return err
	}
	if value != nil {
		if _, err := parseShortNameValue(*value); err != nil {
			return err
		}
	}

	confPath, lock, err := shortNameAliasesConfPathAndLock(ctx)
	if err != nil {
		return err
	}

	// Acquire the lock as a writer to prevent data corruption.
	lock.Lock()
	defer lock.Unlock()

	// Load the short-name-alias.conf, add the specified name-value pair,
	// and write it back to the file.
	conf, _, err := loadShortNameAliasConf(confPath)
	if err != nil {
		return err
	}

	if conf.Aliases == nil { // Ensure we have a map to update.
		conf.Aliases = make(map[string]string)
	}
	if value != nil {
		conf.Aliases[name] = *value
	} else {
		// If the name does not exist, throw an error.
		if _, exists := conf.Aliases[name]; !exists {
			return fmt.Errorf("short-name alias %q not found in %q: please check registries.conf files", name, confPath)
		}

		delete(conf.Aliases, name)
	}

	f, err := os.OpenFile(confPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
	if err != nil {
		return err
	}
	defer f.Close()

	encoder := toml.NewEncoder(f)
	return encoder.Encode(conf)
}

// AddShortNameAlias adds the specified name-value pair as a new alias to the
// user-specific aliases.conf.  It may override an existing alias for `name`.
//
// Note that it’s the caller’s responsibility to pass only a repository
// (reference.IsNameOnly) as the short name.
func AddShortNameAlias(ctx *types.SystemContext, name string, value string) error {
	return editShortNameAlias(ctx, name, &value)
}

// RemoveShortNameAlias clears the alias for the specified name.  It throws an
// error in case name does not exist in the machine-generated
// short-name-alias.conf.  In such case, the alias must be specified in one of
// the registries.conf files, which is the users' responsibility.
//
// Note that it’s the caller’s responsibility to pass only a repository
// (reference.IsNameOnly) as the short name.
func RemoveShortNameAlias(ctx *types.SystemContext, name string) error {
	return editShortNameAlias(ctx, name, nil)
}

// parseShortNameValue parses the specified alias into a reference.Named.  The alias is
// expected to not be tagged or carry a digest and *must* include a
// domain/registry.
//
// Note that the returned reference is always normalized.
func parseShortNameValue(alias string) (reference.Named, error) {
	ref, err := reference.Parse(alias)
	if err != nil {
		return nil, fmt.Errorf("parsing alias %q: %w", alias, err)
	}

	if _, ok := ref.(reference.Digested); ok {
		return nil, fmt.Errorf("invalid alias %q: must not contain digest", alias)
	}

	if _, ok := ref.(reference.Tagged); ok {
		return nil, fmt.Errorf("invalid alias %q: must not contain tag", alias)
	}

	named, ok := ref.(reference.Named)
	if !ok {
		return nil, fmt.Errorf("invalid alias %q: must contain registry and repository", alias)
	}

	registry := reference.Domain(named)
	if !(strings.ContainsAny(registry, ".:") || registry == "localhost") {
		return nil, fmt.Errorf("invalid alias %q: must contain registry and repository", alias)
	}

	// A final parse to make sure that docker.io references are correctly
	// normalized (e.g., docker.io/alpine to docker.io/library/alpine.
	named, err = reference.ParseNormalizedNamed(alias)
	return named, err
}

// validateShortName parses the specified `name` of an alias (i.e., the left-hand
// side) and checks if it's a short name and does not include a tag or digest.
func validateShortName(name string) error {
	repo, err := reference.Parse(name)
	if err != nil {
		return fmt.Errorf("cannot parse short name: %q: %w", name, err)
	}

	if _, ok := repo.(reference.Digested); ok {
		return fmt.Errorf("invalid short name %q: must not contain digest", name)
	}

	if _, ok := repo.(reference.Tagged); ok {
		return fmt.Errorf("invalid short name %q: must not contain tag", name)
	}

	named, ok := repo.(reference.Named)
	if !ok {
		return fmt.Errorf("invalid short name %q: no name", name)
	}

	registry := reference.Domain(named)
	if strings.ContainsAny(registry, ".:") || registry == "localhost" {
		return fmt.Errorf("invalid short name %q: must not contain registry", name)
	}
	return nil
}

// newShortNameAliasCache parses shortNameAliasConf and returns the corresponding internal
// representation.
func newShortNameAliasCache(path string, conf *shortNameAliasConf) (*shortNameAliasCache, error) {
	res := shortNameAliasCache{
		namedAliases: make(map[string]alias),
	}
	errs := []error{}
	for name, value := range conf.Aliases {
		if err := validateShortName(name); err != nil {
			errs = append(errs, err)
		}

		// Empty right-hand side values in config files allow to reset
		// an alias in a previously loaded config. This way, drop-in
		// config files from registries.conf.d can reset potentially
		// malconfigured aliases.
		if value == "" {
			res.namedAliases[name] = alias{nil, path}
			continue
		}

		named, err := parseShortNameValue(value)
		if err != nil {
			// We want to report *all* malformed entries to avoid a
			// whack-a-mole for the user.
			errs = append(errs, err)
		} else {
			res.namedAliases[name] = alias{named, path}
		}
	}
	if len(errs) > 0 {
		err := errs[0]
		for i := 1; i < len(errs); i++ {
			err = fmt.Errorf("%v\n: %w", errs[i], err)
		}
		return nil, err
	}
	return &res, nil
}

// updateWithConfigurationFrom updates c with configuration from updates.
// In case of conflict, updates is preferred.
func (c *shortNameAliasCache) updateWithConfigurationFrom(updates *shortNameAliasCache) {
	maps.Copy(c.namedAliases, updates.namedAliases)
}

func loadShortNameAliasConf(confPath string) (*shortNameAliasConf, *shortNameAliasCache, error) {
	conf := shortNameAliasConf{}

	meta, err := toml.DecodeFile(confPath, &conf)
	if err != nil && !os.IsNotExist(err) {
		// It's okay if the config doesn't exist.  Other errors are not.
		return nil, nil, fmt.Errorf("loading short-name aliases config file %q: %w", confPath, err)
	}
	if keys := meta.Undecoded(); len(keys) > 0 {
		logrus.Debugf("Failed to decode keys %q from %q", keys, confPath)
	}

	// Even if we don’t always need the cache, doing so validates the machine-generated config.  The
	// file could still be corrupted by another process or user.
	cache, err := newShortNameAliasCache(confPath, &conf)
	if err != nil {
		return nil, nil, fmt.Errorf("loading short-name aliases config file %q: %w", confPath, err)
	}

	return &conf, cache, nil
}

func shortNameAliasesConfPathAndLock(ctx *types.SystemContext) (string, *lockfile.LockFile, error) {
	shortNameAliasesConfPath, err := shortNameAliasesConfPath(ctx)
	if err != nil {
		return "", nil, err
	}
	// Make sure the path to file exists.
	if err := os.MkdirAll(filepath.Dir(shortNameAliasesConfPath), 0700); err != nil {
		return "", nil, err
	}

	lockPath := shortNameAliasesConfPath + ".lock"
	locker, err := lockfile.GetLockFile(lockPath)
	return shortNameAliasesConfPath, locker, err
}