@@ -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 {
300303type 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
484491func makeAbsPath (p string , base string ) string {
0 commit comments