func (l *LibvirtLXCBackend) Run(job *host.Job, runConfig *RunConfig) (err error) { g := grohl.NewContext(grohl.Data{"backend": "libvirt-lxc", "fn": "run", "job.id": job.ID}) g.Log(grohl.Data{"at": "start", "job.artifact.uri": job.Artifact.URI, "job.cmd": job.Config.Cmd}) if !job.Config.HostNetwork { <-l.networkConfigured } if _, ok := job.Config.Env["DISCOVERD"]; !ok { <-l.discoverdConfigured } if runConfig == nil { runConfig = &RunConfig{} } container := &libvirtContainer{ l: l, job: job, done: make(chan struct{}), } if !job.Config.HostNetwork { container.IP, err = l.ipalloc.RequestIP(l.bridgeNet, runConfig.IP) if err != nil { g.Log(grohl.Data{"at": "request_ip", "status": "error", "err": err}) return err } } defer func() { if err != nil { go container.cleanup() } }() g.Log(grohl.Data{"at": "pull_image"}) layers, err := l.pinkertonPull(job.Artifact.URI) if err != nil { g.Log(grohl.Data{"at": "pull_image", "status": "error", "err": err}) return err } imageID, err := pinkerton.ImageID(job.Artifact.URI) if err == pinkerton.ErrNoImageID && len(layers) > 0 { imageID = layers[len(layers)-1].ID } else if err != nil { g.Log(grohl.Data{"at": "image_id", "status": "error", "err": err}) return err } g.Log(grohl.Data{"at": "read_config"}) imageConfig, err := readDockerImageConfig(imageID) if err != nil { g.Log(grohl.Data{"at": "read_config", "status": "error", "err": err}) return err } g.Log(grohl.Data{"at": "checkout"}) rootPath, err := l.pinkerton.Checkout(job.ID, imageID) if err != nil { g.Log(grohl.Data{"at": "checkout", "status": "error", "err": err}) return err } container.RootPath = rootPath g.Log(grohl.Data{"at": "mount"}) if err := bindMount(l.InitPath, filepath.Join(rootPath, ".containerinit"), false, true); err != nil { g.Log(grohl.Data{"at": "mount", "file": ".containerinit", "status": "error", "err": err}) return err } if err := os.MkdirAll(filepath.Join(rootPath, "etc"), 0755); err != nil { g.Log(grohl.Data{"at": "mkdir", "dir": "etc", "status": "error", "err": err}) return err } if err := bindMount(l.resolvConf, filepath.Join(rootPath, "etc/resolv.conf"), false, true); err != nil { g.Log(grohl.Data{"at": "mount", "file": "resolv.conf", "status": "error", "err": err}) return err } if err := writeHostname(filepath.Join(rootPath, "etc/hosts"), job.ID); err != nil { g.Log(grohl.Data{"at": "write_hosts", "status": "error", "err": err}) return err } if err := os.MkdirAll(filepath.Join(rootPath, ".container-shared"), 0700); err != nil { g.Log(grohl.Data{"at": "mkdir", "dir": ".container-shared", "status": "error", "err": err}) return err } for i, m := range job.Config.Mounts { if err := os.MkdirAll(filepath.Join(rootPath, m.Location), 0755); err != nil { g.Log(grohl.Data{"at": "mkdir_mount", "dir": m.Location, "status": "error", "err": err}) return err } if m.Target == "" { m.Target = filepath.Join(l.VolPath, cluster.RandomJobID("")) job.Config.Mounts[i].Target = m.Target if err := os.MkdirAll(m.Target, 0755); err != nil { g.Log(grohl.Data{"at": "mkdir_vol", "dir": m.Target, "status": "error", "err": err}) return err } } if err := bindMount(m.Target, filepath.Join(rootPath, m.Location), m.Writeable, true); err != nil { g.Log(grohl.Data{"at": "mount", "target": m.Target, "location": m.Location, "status": "error", "err": err}) return err } } // apply volumes for _, v := range job.Config.Volumes { vol := l.vman.GetVolume(v.VolumeID) if vol == nil { err := fmt.Errorf("job %s required volume %s, but that volume does not exist", job.ID, v.VolumeID) g.Log(grohl.Data{"at": "volume", "volumeID": v.VolumeID, "status": "error", "err": err}) return err } if err := os.MkdirAll(filepath.Join(rootPath, v.Target), 0755); err != nil { g.Log(grohl.Data{"at": "volume_mkdir", "dir": v.Target, "status": "error", "err": err}) return err } if err != nil { g.Log(grohl.Data{"at": "volume_mount", "target": v.Target, "volumeID": v.VolumeID, "status": "error", "err": err}) return err } if err := bindMount(vol.Location(), filepath.Join(rootPath, v.Target), v.Writeable, true); err != nil { g.Log(grohl.Data{"at": "volume_mount2", "target": v.Target, "volumeID": v.VolumeID, "status": "error", "err": err}) return err } } if job.Config.Env == nil { job.Config.Env = make(map[string]string) } for i, p := range job.Config.Ports { if p.Proto != "tcp" && p.Proto != "udp" { return fmt.Errorf("unknown port proto %q", p.Proto) } if p.Port == 0 { job.Config.Ports[i].Port = 5000 + i } if i == 0 { job.Config.Env["PORT"] = strconv.Itoa(job.Config.Ports[i].Port) } job.Config.Env[fmt.Sprintf("PORT_%d", i)] = strconv.Itoa(job.Config.Ports[i].Port) } if !job.Config.HostNetwork { job.Config.Env["EXTERNAL_IP"] = container.IP.String() } config := &containerinit.Config{ TTY: job.Config.TTY, OpenStdin: job.Config.Stdin, WorkDir: job.Config.WorkingDir, Resources: job.Resources, } if !job.Config.HostNetwork { config.IP = container.IP.String() + "/24" config.Gateway = l.bridgeAddr.String() } if config.WorkDir == "" { config.WorkDir = imageConfig.WorkingDir } if job.Config.Uid > 0 { config.User = strconv.Itoa(job.Config.Uid) } else if imageConfig.User != "" { // TODO: check and lookup user from image config } if len(job.Config.Entrypoint) > 0 { config.Args = job.Config.Entrypoint config.Args = append(config.Args, job.Config.Cmd...) } else { config.Args = imageConfig.Entrypoint if len(job.Config.Cmd) > 0 { config.Args = append(config.Args, job.Config.Cmd...) } else { config.Args = append(config.Args, imageConfig.Cmd...) } } for _, port := range job.Config.Ports { config.Ports = append(config.Ports, port) } g.Log(grohl.Data{"at": "write_config"}) l.envMtx.RLock() err = writeContainerConfig(filepath.Join(rootPath, ".containerconfig"), config, map[string]string{ "PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "TERM": "xterm", "HOME": "/", }, l.defaultEnv, job.Config.Env, map[string]string{ "HOSTNAME": job.ID, }, ) l.envMtx.RUnlock() if err != nil { g.Log(grohl.Data{"at": "write_config", "status": "error", "err": err}) return err } l.state.AddJob(job, container.IP) domain := <.Domain{ Type: "lxc", Name: job.ID, Memory: lt.UnitInt{Value: 1, Unit: "GiB"}, OS: lt.OS{ Type: lt.OSType{Value: "exe"}, Init: "/.containerinit", }, Devices: lt.Devices{ Filesystems: []lt.Filesystem{{ Type: "mount", Source: lt.FSRef{Dir: rootPath}, Target: lt.FSRef{Dir: "/"}, }}, Consoles: []lt.Console{{Type: "pty"}}, }, OnPoweroff: "preserve", OnCrash: "preserve", } if spec, ok := job.Resources[resource.TypeMemory]; ok && spec.Limit != nil { domain.Memory = lt.UnitInt{Value: *spec.Limit, Unit: "bytes"} } if !job.Config.HostNetwork { domain.Devices.Interfaces = []lt.Interface{{ Type: "network", Source: lt.InterfaceSrc{Network: libvirtNetName}, }} } // attempt to run libvirt commands multiple times in case the libvirt daemon is // temporarily unavailable (e.g. it has restarted, which sometimes happens in CI) g.Log(grohl.Data{"at": "define_domain"}) var vd libvirt.VirDomain if err := l.withConnRetries(func() (err error) { vd, err = l.libvirt.DomainDefineXML(string(domain.XML())) return }); err != nil { g.Log(grohl.Data{"at": "define_domain", "status": "error", "err": err}) return err } g.Log(grohl.Data{"at": "create_domain"}) if err := l.withConnRetries(vd.Create); err != nil { g.Log(grohl.Data{"at": "create_domain", "status": "error", "err": err}) return err } uuid, err := vd.GetUUIDString() if err != nil { g.Log(grohl.Data{"at": "get_domain_uuid", "status": "error", "err": err}) return err } g.Log(grohl.Data{"at": "get_uuid", "uuid": uuid}) l.state.SetContainerID(job.ID, uuid) domainXML, err := vd.GetXMLDesc(0) if err != nil { g.Log(grohl.Data{"at": "get_domain_xml", "status": "error", "err": err}) return err } domain = <.Domain{} if err := xml.Unmarshal([]byte(domainXML), domain); err != nil { g.Log(grohl.Data{"at": "unmarshal_domain_xml", "status": "error", "err": err}) return err } go container.watch(nil) g.Log(grohl.Data{"at": "finish"}) return nil }
func (l *LibvirtLXCBackend) Run(job *host.Job) (err error) { g := grohl.NewContext(grohl.Data{"backend": "libvirt-lxc", "fn": "run", "job.id": job.ID}) g.Log(grohl.Data{"at": "start", "job.artifact.uri": job.Artifact.URI, "job.cmd": job.Config.Cmd}) container := &libvirtContainer{ l: l, job: job, done: make(chan struct{}), } if !job.Config.HostNetwork { ip, err := ipallocator.RequestIP(bridgeNet, nil) container.IP = *ip if err != nil { g.Log(grohl.Data{"at": "request_ip", "status": "error", "err": err}) return err } } defer func() { if err != nil { go container.cleanup() } }() g.Log(grohl.Data{"at": "pull_image"}) layers, err := l.pinkertonPull(job.Artifact.URI) if err != nil { g.Log(grohl.Data{"at": "pull_image", "status": "error", "err": err}) return err } imageID, err := pinkerton.ImageID(job.Artifact.URI) if err == pinkerton.ErrNoImageID && len(layers) > 0 { imageID = layers[len(layers)-1].ID } else if err != nil { g.Log(grohl.Data{"at": "image_id", "status": "error", "err": err}) return err } g.Log(grohl.Data{"at": "read_config"}) imageConfig, err := readDockerImageConfig(imageID) if err != nil { g.Log(grohl.Data{"at": "read_config", "status": "error", "err": err}) return err } g.Log(grohl.Data{"at": "checkout"}) rootPath, err := l.pinkerton.Checkout(job.ID, imageID) if err != nil { g.Log(grohl.Data{"at": "checkout", "status": "error", "err": err}) return err } container.RootPath = rootPath g.Log(grohl.Data{"at": "mount"}) if err := bindMount(l.InitPath, filepath.Join(rootPath, ".containerinit"), false, true); err != nil { g.Log(grohl.Data{"at": "mount", "file": ".containerinit", "status": "error", "err": err}) return err } if err := os.MkdirAll(filepath.Join(rootPath, "etc"), 0755); err != nil { g.Log(grohl.Data{"at": "mkdir", "dir": "etc", "status": "error", "err": err}) return err } if err := bindMount("/etc/flynn/resolv.conf", filepath.Join(rootPath, "etc/resolv.conf"), false, true); err != nil { g.Log(grohl.Data{"at": "mount", "file": "resolv.conf", "status": "error", "err": err}) return err } if err := writeHostname(filepath.Join(rootPath, "etc/hosts"), job.ID); err != nil { g.Log(grohl.Data{"at": "write_hosts", "status": "error", "err": err}) return err } if err := os.MkdirAll(filepath.Join(rootPath, ".container-shared"), 0700); err != nil { g.Log(grohl.Data{"at": "mkdir", "dir": ".container-shared", "status": "error", "err": err}) return err } for i, m := range job.Config.Mounts { if err := os.MkdirAll(filepath.Join(rootPath, m.Location), 0755); err != nil { g.Log(grohl.Data{"at": "mkdir_mount", "dir": m.Location, "status": "error", "err": err}) return err } if m.Target == "" { m.Target = filepath.Join(l.VolPath, cluster.RandomJobID("")) job.Config.Mounts[i].Target = m.Target if err := os.MkdirAll(m.Target, 0755); err != nil { g.Log(grohl.Data{"at": "mkdir_vol", "dir": m.Target, "status": "error", "err": err}) return err } } if err := bindMount(m.Target, filepath.Join(rootPath, m.Location), m.Writeable, true); err != nil { g.Log(grohl.Data{"at": "mount", "target": m.Target, "location": m.Location, "status": "error", "err": err}) return err } } if job.Config.Env == nil { job.Config.Env = make(map[string]string) } if !job.Config.HostNetwork { for i, p := range job.Config.Ports { if p.Proto != "tcp" && p.Proto != "udp" { return fmt.Errorf("unknown port proto %q", p.Proto) } if 0 < p.RangeEnd && p.RangeEnd < p.Port { return fmt.Errorf("port range end %d cannot be less than port %d", p.RangeEnd, p.Port) } var port uint16 if p.Port <= 0 { job.Config.Ports[i].RangeEnd = 0 port, err = l.ports[p.Proto].Get() } else if p.RangeEnd > p.Port { for j := p.RangeEnd; j >= p.Port; j-- { port, err = l.ports[p.Proto].GetPort(uint16(j)) if err != nil { break } } } else { port, err = l.ports[p.Proto].GetPort(uint16(p.Port)) } if err != nil { g.Log(grohl.Data{"at": "alloc_port", "status": "error", "err": err}) return err } job.Config.Ports[i].Port = int(port) if job.Config.Ports[i].RangeEnd == 0 { job.Config.Ports[i].RangeEnd = int(port) } if i == 0 { job.Config.Env["PORT"] = strconv.Itoa(int(port)) } job.Config.Env[fmt.Sprintf("PORT_%d", i)] = strconv.Itoa(int(port)) } } g.Log(grohl.Data{"at": "write_env"}) err = writeContainerEnv(filepath.Join(rootPath, ".containerenv"), map[string]string{ "PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "TERM": "xterm", "HOME": "/", }, job.Config.Env, map[string]string{ "HOSTNAME": job.ID, }, ) if err != nil { g.Log(grohl.Data{"at": "write_env", "status": "error", "err": err}) return err } var args []string if !job.Config.HostNetwork { args = append(args, "-i", container.IP.String()+"/24", "-g", bridgeAddr.String(), ) } if job.Config.TTY { args = append(args, "-tty") } if job.Config.Stdin { args = append(args, "-stdin") } if job.Config.WorkingDir != "" { args = append(args, "-w", job.Config.WorkingDir) } else if imageConfig.WorkingDir != "" { args = append(args, "-w", imageConfig.WorkingDir) } if job.Config.Uid > 0 { args = append(args, "-u", strconv.Itoa(job.Config.Uid)) } else if imageConfig.User != "" { // TODO: check and lookup user from image config } if len(job.Config.Entrypoint) > 0 { args = append(args, job.Config.Entrypoint...) args = append(args, job.Config.Cmd...) } else { args = append(args, imageConfig.Entrypoint...) if len(job.Config.Cmd) > 0 { args = append(args, job.Config.Cmd...) } else { args = append(args, imageConfig.Cmd...) } } l.state.AddJob(job) l.state.SetInternalIP(job.ID, container.IP.String()) domain := <.Domain{ Type: "lxc", Name: job.ID, Memory: lt.UnitInt{Value: 1, Unit: "GiB"}, VCPU: 1, OS: lt.OS{ Type: lt.OSType{Value: "exe"}, Init: "/.containerinit", InitArgs: args, }, Devices: lt.Devices{ Filesystems: []lt.Filesystem{{ Type: "mount", Source: lt.FSRef{Dir: rootPath}, Target: lt.FSRef{Dir: "/"}, }}, Consoles: []lt.Console{{Type: "pty"}}, }, OnPoweroff: "preserve", OnCrash: "preserve", } if !job.Config.HostNetwork { domain.Devices.Interfaces = []lt.Interface{{ Type: "network", Source: lt.InterfaceSrc{Network: libvirtNetName}, }} } g.Log(grohl.Data{"at": "define_domain"}) vd, err := l.libvirt.DomainDefineXML(string(domain.XML())) if err != nil { g.Log(grohl.Data{"at": "define_domain", "status": "error", "err": err}) return err } g.Log(grohl.Data{"at": "create_domain"}) if err := vd.Create(); err != nil { g.Log(grohl.Data{"at": "create_domain", "status": "error", "err": err}) return err } uuid, err := vd.GetUUIDString() if err != nil { g.Log(grohl.Data{"at": "get_domain_uuid", "status": "error", "err": err}) return err } g.Log(grohl.Data{"at": "get_uuid", "uuid": uuid}) l.state.SetContainerID(job.ID, uuid) domainXML, err := vd.GetXMLDesc(0) if err != nil { g.Log(grohl.Data{"at": "get_domain_xml", "status": "error", "err": err}) return err } domain = <.Domain{} if err := xml.Unmarshal([]byte(domainXML), domain); err != nil { g.Log(grohl.Data{"at": "unmarshal_domain_xml", "status": "error", "err": err}) return err } if !job.Config.HostNetwork { if len(domain.Devices.Interfaces) == 0 || domain.Devices.Interfaces[0].Target == nil || domain.Devices.Interfaces[0].Target.Dev == "" { err = errors.New("domain config missing interface") g.Log(grohl.Data{"at": "enable_hairpin", "status": "error", "err": err}) return err } iface := domain.Devices.Interfaces[0].Target.Dev if err := enableHairpinMode(iface); err != nil { g.Log(grohl.Data{"at": "enable_hairpin", "status": "error", "err": err}) return err } for _, p := range job.Config.Ports { if err := l.forwarder.Add(&net.TCPAddr{IP: container.IP, Port: p.Port}, p.RangeEnd, p.Proto); err != nil { g.Log(grohl.Data{"at": "forward_port", "port": p.Port, "status": "error", "err": err}) return err } } } go container.watch(nil) g.Log(grohl.Data{"at": "finish"}) return nil }