Skip to content

Commit d56b49c

Browse files
committed
Rewrite Docker hosts parser
Signed-off-by: Maksym Pavlenko <[email protected]>
1 parent ddd4298 commit d56b49c

2 files changed

Lines changed: 160 additions & 161 deletions

File tree

remotes/docker/config/hosts.go

Lines changed: 156 additions & 149 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,12 @@ import (
3030
"strings"
3131
"time"
3232

33-
"github.com/BurntSushi/toml"
33+
"github.com/pelletier/go-toml"
34+
"github.com/pkg/errors"
35+
3436
"github.com/containerd/containerd/errdefs"
3537
"github.com/containerd/containerd/log"
3638
"github.com/containerd/containerd/remotes/docker"
37-
"github.com/pkg/errors"
3839
)
3940

4041
// UpdateClientFunc is a function that lets you to amend http Client behavior used by registry clients.
@@ -264,7 +265,7 @@ func loadHostDir(ctx context.Context, hostsDir string) ([]hostConfig, error) {
264265
return loadCertFiles(ctx, hostsDir)
265266
}
266267

267-
hosts, err := parseHostsFile(ctx, hostsDir, b)
268+
hosts, err := parseHostsFile(hostsDir, b)
268269
if err != nil {
269270
log.G(ctx).WithError(err).Error("failed to decode hosts.toml")
270271
// Fallback to checking certificate files
@@ -274,7 +275,9 @@ func loadHostDir(ctx context.Context, hostsDir string) ([]hostConfig, error) {
274275
return hosts, nil
275276
}
276277

277-
type hostFileConfig struct {
278+
// HostFileConfig describes a single host section within TOML file.
279+
// Note: This struct needs to be public in order to be properly deserialized by TOML library.
280+
type HostFileConfig struct {
278281
// Capabilities determine what operations a host is
279282
// capable of performing. Allowed values
280283
// - pull
@@ -283,14 +286,14 @@ type hostFileConfig struct {
283286
Capabilities []string `toml:"capabilities"`
284287

285288
// CACert can be a string or an array of strings
286-
CACert toml.Primitive `toml:"ca"`
289+
CACert interface{} `toml:"ca"`
287290

288291
// TODO: Make this an array (two key types, one for pairs (multiple files), one for single file?)
289-
Client toml.Primitive `toml:"client"`
292+
Client interface{} `toml:"client"`
290293

291294
SkipVerify *bool `toml:"skip_verify"`
292295

293-
Header map[string]toml.Primitive `toml:"header"`
296+
Header map[string]interface{} `toml:"header"`
294297

295298
// API (default: "docker")
296299
// API Version (default: "v2")
@@ -300,185 +303,189 @@ type hostFileConfig struct {
300303
type configFile struct {
301304
// hostConfig holds defaults for all hosts as well as
302305
// for the default server
303-
hostFileConfig
306+
HostFileConfig
304307

305308
// Server specifies the default server. When `host` is
306309
// also specified, those hosts are tried first.
307310
Server string `toml:"server"`
308311

309312
// HostConfigs store the per-host configuration
310-
HostConfigs map[string]hostFileConfig `toml:"host"`
313+
HostConfigs map[string]HostFileConfig `toml:"host"`
311314
}
312315

313-
func parseHostsFile(ctx context.Context, baseDir string, b []byte) ([]hostConfig, error) {
314-
var c configFile
315-
md, err := toml.Decode(string(b), &c)
316+
func parseHostsFile(baseDir string, b []byte) ([]hostConfig, error) {
317+
tree, err := toml.LoadBytes(b)
316318
if err != nil {
317-
return nil, err
319+
return nil, errors.Wrap(err, "failed to parse TOML")
318320
}
319321

320-
var orderedHosts []string
321-
for _, key := range md.Keys() {
322-
if len(key) >= 2 {
323-
if key[0] == "host" && (len(orderedHosts) == 0 || orderedHosts[len(orderedHosts)-1] != key[1]) {
324-
orderedHosts = append(orderedHosts, key[1])
325-
}
326-
}
322+
var (
323+
c configFile
324+
hosts []hostConfig
325+
)
326+
327+
if err := tree.Unmarshal(&c); err != nil {
328+
return nil, err
327329
}
328330

329-
if c.HostConfigs == nil {
330-
c.HostConfigs = map[string]hostFileConfig{}
331+
// Parse root host config
332+
parsed, err := parseHostConfig(c.Server, baseDir, c.HostFileConfig)
333+
if err != nil {
334+
return nil, err
331335
}
332-
if c.Server != "" {
333-
c.HostConfigs[c.Server] = c.hostFileConfig
334-
orderedHosts = append(orderedHosts, c.Server)
335-
} else if len(orderedHosts) == 0 {
336-
c.HostConfigs[""] = c.hostFileConfig
337-
orderedHosts = append(orderedHosts, "")
336+
hosts = append(hosts, parsed)
337+
338+
// Parse hosts array
339+
for host, config := range c.HostConfigs {
340+
parsed, err := parseHostConfig(host, baseDir, config)
341+
if err != nil {
342+
return nil, err
343+
}
344+
hosts = append(hosts, parsed)
338345
}
339-
hosts := make([]hostConfig, len(orderedHosts))
340-
for i, server := range orderedHosts {
341-
hostConfig := c.HostConfigs[server]
342346

343-
if server != "" {
344-
if !strings.HasPrefix(server, "http") {
345-
server = "https://" + server
346-
}
347-
u, err := url.Parse(server)
348-
if err != nil {
349-
return nil, errors.Errorf("unable to parse server %v", server)
350-
}
351-
hosts[i].scheme = u.Scheme
352-
hosts[i].host = u.Host
353-
354-
// TODO: Handle path based on registry protocol
355-
// Define a registry protocol type
356-
// OCI v1 - Always use given path as is
357-
// Docker v2 - Always ensure ends with /v2/
358-
if len(u.Path) > 0 {
359-
u.Path = path.Clean(u.Path)
360-
if !strings.HasSuffix(u.Path, "/v2") {
361-
u.Path = u.Path + "/v2"
362-
}
363-
} else {
364-
u.Path = "/v2"
365-
}
366-
hosts[i].path = u.Path
347+
return hosts, nil
348+
}
349+
350+
func parseHostConfig(server string, baseDir string, config HostFileConfig) (hostConfig, error) {
351+
var (
352+
result = hostConfig{}
353+
err error
354+
)
355+
356+
if server != "" {
357+
if !strings.HasPrefix(server, "http") {
358+
server = "https://" + server
367359
}
368-
hosts[i].skipVerify = hostConfig.SkipVerify
369-
370-
if len(hostConfig.Capabilities) > 0 {
371-
for _, c := range hostConfig.Capabilities {
372-
switch strings.ToLower(c) {
373-
case "pull":
374-
hosts[i].capabilities |= docker.HostCapabilityPull
375-
case "resolve":
376-
hosts[i].capabilities |= docker.HostCapabilityResolve
377-
case "push":
378-
hosts[i].capabilities |= docker.HostCapabilityPush
379-
default:
380-
return nil, errors.Errorf("unknown capability %v", c)
381-
}
360+
u, err := url.Parse(server)
361+
if err != nil {
362+
return hostConfig{}, errors.Wrapf(err, "unable to parse server %v", server)
363+
}
364+
result.scheme = u.Scheme
365+
result.host = u.Host
366+
// TODO: Handle path based on registry protocol
367+
// Define a registry protocol type
368+
// OCI v1 - Always use given path as is
369+
// Docker v2 - Always ensure ends with /v2/
370+
if len(u.Path) > 0 {
371+
u.Path = path.Clean(u.Path)
372+
if !strings.HasSuffix(u.Path, "/v2") {
373+
u.Path = u.Path + "/v2"
382374
}
383375
} else {
384-
hosts[i].capabilities = docker.HostCapabilityPull | docker.HostCapabilityResolve | docker.HostCapabilityPush
376+
u.Path = "/v2"
385377
}
378+
result.path = u.Path
379+
}
386380

387-
baseKey := []string{}
388-
if server != "" && server != c.Server {
389-
baseKey = append(baseKey, "host", server)
390-
}
391-
caKey := append(baseKey, "ca")
392-
if md.IsDefined(caKey...) {
393-
switch t := md.Type(caKey...); t {
394-
case "String":
395-
var caCert string
396-
if err := md.PrimitiveDecode(hostConfig.CACert, &caCert); err != nil {
397-
return nil, errors.Wrap(err, "failed to decode \"ca\"")
398-
}
399-
hosts[i].caCerts = []string{makeAbsPath(caCert, baseDir)}
400-
case "Array":
401-
var caCerts []string
402-
if err := md.PrimitiveDecode(hostConfig.CACert, &caCerts); err != nil {
403-
return nil, errors.Wrap(err, "failed to decode \"ca\"")
404-
}
405-
for i, p := range caCerts {
406-
caCerts[i] = makeAbsPath(p, baseDir)
407-
}
408-
409-
hosts[i].caCerts = caCerts
381+
result.skipVerify = config.SkipVerify
382+
383+
if len(config.Capabilities) > 0 {
384+
for _, c := range config.Capabilities {
385+
switch strings.ToLower(c) {
386+
case "pull":
387+
result.capabilities |= docker.HostCapabilityPull
388+
case "resolve":
389+
result.capabilities |= docker.HostCapabilityResolve
390+
case "push":
391+
result.capabilities |= docker.HostCapabilityPush
410392
default:
411-
return nil, errors.Errorf("invalid type %v for \"ca\"", t)
393+
return hostConfig{}, errors.Errorf("unknown capability %v", c)
412394
}
413395
}
396+
} else {
397+
result.capabilities = docker.HostCapabilityPull | docker.HostCapabilityResolve | docker.HostCapabilityPush
398+
}
414399

415-
clientKey := append(baseKey, "client")
416-
if md.IsDefined(clientKey...) {
417-
switch t := md.Type(clientKey...); t {
418-
case "String":
419-
var clientCert string
420-
if err := md.PrimitiveDecode(hostConfig.Client, &clientCert); err != nil {
421-
return nil, errors.Wrap(err, "failed to decode \"ca\"")
422-
}
423-
hosts[i].clientPairs = [][2]string{{makeAbsPath(clientCert, baseDir), ""}}
424-
case "Array":
425-
var clientCerts []interface{}
426-
if err := md.PrimitiveDecode(hostConfig.Client, &clientCerts); err != nil {
427-
return nil, errors.Wrap(err, "failed to decode \"ca\"")
428-
}
429-
for _, pairs := range clientCerts {
430-
switch p := pairs.(type) {
431-
case string:
432-
hosts[i].clientPairs = append(hosts[i].clientPairs, [2]string{makeAbsPath(p, baseDir), ""})
433-
case []interface{}:
434-
var pair [2]string
435-
if len(p) > 2 {
436-
return nil, errors.Errorf("invalid pair %v for \"client\"", p)
437-
}
438-
for pi, cp := range p {
439-
s, ok := cp.(string)
440-
if !ok {
441-
return nil, errors.Errorf("invalid type %T for \"client\"", cp)
442-
}
443-
pair[pi] = makeAbsPath(s, baseDir)
444-
}
445-
hosts[i].clientPairs = append(hosts[i].clientPairs, pair)
446-
default:
447-
return nil, errors.Errorf("invalid type %T for \"client\"", p)
448-
}
449-
}
450-
default:
451-
return nil, errors.Errorf("invalid type %v for \"client\"", t)
400+
if config.CACert != nil {
401+
switch cert := config.CACert.(type) {
402+
case string:
403+
result.caCerts = []string{makeAbsPath(cert, baseDir)}
404+
case []string:
405+
for _, p := range cert {
406+
result.caCerts = append(result.caCerts, makeAbsPath(p, baseDir))
452407
}
408+
case []interface{}:
409+
result.caCerts, err = makeStringSlice(cert, func(p string) string {
410+
return makeAbsPath(p, baseDir)
411+
})
412+
if err != nil {
413+
return hostConfig{}, err
414+
}
415+
default:
416+
return hostConfig{}, errors.Errorf("invalid type %v for \"ca\"", cert)
453417
}
418+
}
454419

455-
headerKey := append(baseKey, "header")
456-
if md.IsDefined(headerKey...) {
457-
header := http.Header{}
458-
for key, prim := range hostConfig.Header {
459-
switch t := md.Type(append(headerKey, key)...); t {
460-
case "String":
461-
var value string
462-
if err := md.PrimitiveDecode(prim, &value); err != nil {
463-
return nil, errors.Wrapf(err, "failed to decode header %q", key)
420+
if config.Client != nil {
421+
switch client := config.Client.(type) {
422+
case string:
423+
result.clientPairs = [][2]string{{makeAbsPath(client, baseDir), ""}}
424+
case []interface{}:
425+
// []string or [][2]string
426+
for _, pairs := range client {
427+
switch p := pairs.(type) {
428+
case string:
429+
result.clientPairs = append(result.clientPairs, [2]string{makeAbsPath(p, baseDir), ""})
430+
case []interface{}:
431+
slice, err := makeStringSlice(p, nil)
432+
if err != nil {
433+
return hostConfig{}, err
464434
}
465-
header[key] = []string{value}
466-
case "Array":
467-
var value []string
468-
if err := md.PrimitiveDecode(prim, &value); err != nil {
469-
return nil, errors.Wrapf(err, "failed to decode header %q", key)
435+
if len(slice) != 2 {
436+
return hostConfig{}, errors.Errorf("invalid pair %v for \"client\"", p)
470437
}
471438

472-
header[key] = value
439+
var pair [2]string
440+
copy(pair[:], slice)
441+
result.clientPairs = append(result.clientPairs, pair)
473442
default:
474-
return nil, errors.Errorf("invalid type %v for header %q", t, key)
443+
return hostConfig{}, errors.Errorf("invalid type %T for \"client\"", p)
475444
}
476445
}
477-
hosts[i].header = header
446+
default:
447+
return hostConfig{}, errors.Errorf("invalid type %v for \"client\"", client)
478448
}
479449
}
480450

481-
return hosts, nil
451+
if config.Header != nil {
452+
header := http.Header{}
453+
for key, ty := range config.Header {
454+
switch value := ty.(type) {
455+
case string:
456+
header[key] = []string{value}
457+
case []interface{}:
458+
header[key], err = makeStringSlice(value, nil)
459+
if err != nil {
460+
return hostConfig{}, err
461+
}
462+
default:
463+
return hostConfig{}, errors.Errorf("invalid type %v for header %q", ty, key)
464+
}
465+
}
466+
result.header = header
467+
}
468+
469+
return result, nil
470+
}
471+
472+
// makeStringSlice is a helper func to convert from []interface{} to []string.
473+
// Additionally an optional cb func may be passed to perform string mapping.
474+
func makeStringSlice(slice []interface{}, cb func(string) string) ([]string, error) {
475+
out := make([]string, len(slice))
476+
for i, value := range slice {
477+
str, ok := value.(string)
478+
if !ok {
479+
return nil, errors.Errorf("unable to cast %v to string", value)
480+
}
481+
482+
if cb != nil {
483+
out[i] = cb(str)
484+
} else {
485+
out[i] = str
486+
}
487+
}
488+
return out, nil
482489
}
483490

484491
func makeAbsPath(p string, base string) string {

0 commit comments

Comments
 (0)