github.com/mgoltzsche/ctnr@v0.7.1-alpha/model/compose/dctransform.go (about)

     1  package compose
     2  
     3  import (
     4  	"fmt"
     5  	"io/ioutil"
     6  	"os"
     7  	"path/filepath"
     8  	"strings"
     9  
    10  	"github.com/docker/cli/cli/compose/loader"
    11  	"github.com/docker/cli/cli/compose/types"
    12  	"github.com/mgoltzsche/ctnr/model"
    13  	exterrors "github.com/mgoltzsche/ctnr/pkg/errors"
    14  	"github.com/mgoltzsche/ctnr/pkg/log"
    15  	"github.com/mgoltzsche/ctnr/pkg/sliceutils"
    16  	"github.com/pkg/errors"
    17  )
    18  
    19  //
    20  // Currently only Docker Compose 3 schema is supported.
    21  // Let's hope soon the older schema versions are supported as well
    22  // when this is merged: https://github.com/docker/cli/pull/573
    23  // and containers/image updated their github.com/docker/docker dependency
    24  //
    25  
    26  func Load(file, cwd string, env map[string]string, warn log.Logger) (r *model.CompoundServices, err error) {
    27  	defer exterrors.Wrapd(&err, "load docker compose file")
    28  	absCwd := cwd
    29  	if absCwd, err = filepath.Abs(cwd); err != nil {
    30  		return
    31  	}
    32  	b, err := ioutil.ReadFile(file)
    33  	if err != nil {
    34  		return
    35  	}
    36  	dcyml, err := loader.ParseYAML(b)
    37  	if err != nil {
    38  		return
    39  	}
    40  	cfg, err := loader.Load(types.ConfigDetails{
    41  		WorkingDir:  cwd,
    42  		ConfigFiles: []types.ConfigFile{types.ConfigFile{file, dcyml}},
    43  		Environment: env,
    44  	})
    45  	if err != nil {
    46  		return
    47  	}
    48  	return transform(cfg, absCwd, warn)
    49  }
    50  
    51  func GetEnv() map[string]string {
    52  	r := map[string]string{}
    53  	for _, entry := range os.Environ() {
    54  		s := strings.SplitN(entry, "=", 2)
    55  		if len(s) == 2 {
    56  			r[s[0]] = s[1]
    57  		} else {
    58  			r[s[0]] = ""
    59  		}
    60  	}
    61  	return r
    62  }
    63  
    64  func transform(cfg *types.Config, cwd string, warn log.Logger) (r *model.CompoundServices, err error) {
    65  	services, err := toServices(cfg.Services)
    66  	if err != nil {
    67  		return
    68  	}
    69  	r = &model.CompoundServices{
    70  		Dir:      cwd,
    71  		Volumes:  toVolumes(cfg.Volumes, warn),
    72  		Services: services,
    73  		// TODO: map networks, secrets
    74  	}
    75  	return
    76  }
    77  
    78  func toVolumes(vols map[string]types.VolumeConfig, warn log.Logger) map[string]model.Volume {
    79  	r := map[string]model.Volume{}
    80  	for name, vol := range vols {
    81  		v := model.Volume{}
    82  		if vol.External.External {
    83  			v.External = vol.Name
    84  			if vol.External.Name != "" {
    85  				v.External = vol.External.Name
    86  			}
    87  		} else {
    88  			warn.Printf("adding unsupported volume %v as temporary volume", vol)
    89  		}
    90  		r[name] = v
    91  	}
    92  	return r
    93  }
    94  
    95  func toServices(services []types.ServiceConfig) (r map[string]model.Service, err error) {
    96  	r = map[string]model.Service{}
    97  	for _, service := range services {
    98  		if r[service.Name], err = toService(service); err != nil {
    99  			return
   100  		}
   101  	}
   102  	return
   103  }
   104  
   105  func toService(s types.ServiceConfig) (r model.Service, err error) {
   106  	r = model.NewService(s.Name)
   107  	r.Build = toBuild(s.Build)
   108  	r.CapAdd = s.CapAdd
   109  	r.CapDrop = s.CapDrop
   110  	// s.CgroupParent
   111  	r.Command = []string(s.Command)
   112  	// TODO:
   113  	// DependsOn
   114  	// CredentialSpec
   115  	// Deploy
   116  	// Devices
   117  	r.Dns = []string(s.DNS)
   118  	r.DnsSearch = []string(s.DNSSearch)
   119  	r.Domainname = s.DomainName
   120  	r.Entrypoint = []string(s.Entrypoint)
   121  	r.Environment = toStringMap(s.Environment)
   122  	// EnvFile
   123  	r.Expose = []string(s.Expose)
   124  	// ExternalLinks
   125  	if r.ExtraHosts, err = toExtraHosts(s.ExtraHosts); err != nil {
   126  		return
   127  	}
   128  	r.Hostname = s.ContainerName
   129  	// Healthcheck
   130  	r.Image = "docker://" + s.Image
   131  	// Ipc
   132  	// Labels
   133  	// Links
   134  	// Logging
   135  	// MacAddress
   136  	// NetworkMode
   137  	// Pid
   138  	if r.Ports, err = toPorts(s.Ports); err != nil {
   139  		return
   140  	}
   141  	// Privileged
   142  	r.ReadOnly = s.ReadOnly
   143  	// Restart
   144  	// Secrets
   145  	// SecurityOpt
   146  	r.StdinOpen = s.StdinOpen
   147  	r.StopGracePeriod = s.StopGracePeriod
   148  	r.StopSignal = s.StopSignal
   149  	// Tmpfs
   150  	r.Tty = s.Tty
   151  	// Ulimits
   152  	r.User = toUser(s.User)
   153  	r.Volumes = toVolumeMounts(s.Volumes)
   154  	r.Cwd = s.WorkingDir
   155  	// Isolation
   156  	return
   157  }
   158  
   159  func toBuild(s types.BuildConfig) (r *model.ImageBuild) {
   160  	if s.Context != "" || s.Dockerfile != "" {
   161  		r = &model.ImageBuild{
   162  			Context:    s.Context,
   163  			Dockerfile: s.Dockerfile,
   164  			Args:       toStringMap(s.Args),
   165  		}
   166  	}
   167  	return
   168  }
   169  
   170  func toStringMap(m types.MappingWithEquals) map[string]string {
   171  	r := map[string]string{}
   172  	for k, v := range (map[string]*string)(m) {
   173  		if v == nil {
   174  			r[k] = ""
   175  		} else {
   176  			r[k] = *v
   177  		}
   178  	}
   179  	return r
   180  }
   181  
   182  func toExtraHosts(hl types.HostsList) ([]model.ExtraHost, error) {
   183  	l := []string(hl)
   184  	r := make([]model.ExtraHost, 0, len(l))
   185  	for _, h := range hl {
   186  		he := strings.SplitN(h, ":", 2)
   187  		if len(he) != 2 {
   188  			return nil, errors.Errorf("invalid extra_hosts entry: expected format host:ip but was %q", h)
   189  		}
   190  		r = append(r, model.ExtraHost{
   191  			Name: he[0],
   192  			Ip:   he[1],
   193  		})
   194  	}
   195  	return r, nil
   196  }
   197  
   198  func toPorts(ports []types.ServicePortConfig) (r []model.PortBinding, err error) {
   199  	r = make([]model.PortBinding, 0, len(ports))
   200  	for _, p := range ports {
   201  		if p.Target > 65535 {
   202  			return nil, errors.Errorf("invalid target port %d exceeded range", p.Target)
   203  		}
   204  		if p.Published > 65535 {
   205  			return nil, errors.Errorf("invalid published port %d exceeded range", p.Published)
   206  		}
   207  		r = append(r, model.PortBinding{
   208  			Protocol:  p.Protocol,
   209  			Target:    uint16(p.Target),
   210  			Published: uint16(p.Published),
   211  			// TODO: compose parser that supports host IP mapping
   212  		})
   213  		// TODO: checkout p.Mode
   214  	}
   215  	return
   216  }
   217  
   218  func toUser(s string) (u *model.User) {
   219  	if s == "" {
   220  		return nil
   221  	}
   222  	ug := strings.SplitN(s, ":", 2)
   223  	if len(ug) == 2 {
   224  		u = &model.User{ug[0], ug[1], nil}
   225  	} else {
   226  		u = &model.User{ug[0], ug[0], nil}
   227  	}
   228  	return
   229  }
   230  
   231  func toVolumeMounts(vols []types.ServiceVolumeConfig) []model.VolumeMount {
   232  	r := []model.VolumeMount{}
   233  	for _, vol := range vols {
   234  		var opts []string
   235  		if vol.Bind != nil && vol.Bind.Propagation != "" {
   236  			opts = strings.Split(vol.Bind.Propagation, ":")
   237  		}
   238  		if vol.ReadOnly {
   239  			sliceutils.AddToSet(&opts, "ro")
   240  		}
   241  		if vol.Tmpfs != nil {
   242  			opts = append(opts, fmt.Sprintf("size=%d", vol.Tmpfs.Size))
   243  		}
   244  		// TODO: Consistency
   245  		r = append(r, model.VolumeMount{
   246  			Type:    model.MountType(vol.Type), // 'volume', 'bind' or 'tmpfs'
   247  			Source:  vol.Source,
   248  			Target:  vol.Target,
   249  			Options: opts,
   250  		})
   251  	}
   252  	return r
   253  }