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 }