diff --git a/cmd/control/cli.go b/cmd/control/cli.go index 1ba70fe6..bc7314f0 100644 --- a/cmd/control/cli.go +++ b/cmd/control/cli.go @@ -47,12 +47,6 @@ func Main() { HideHelp: true, Subcommands: serviceSubCommands(), }, - //{ - // Name: "reload", - // ShortName: "a", - // Usage: "reload configuration of a service and restart the container", - // Action: reload, - //}, { Name: "os", Usage: "operating system upgrade/downgrade", diff --git a/cmd/control/os.go b/cmd/control/os.go index 76d4f7cd..81453412 100644 --- a/cmd/control/os.go +++ b/cmd/control/os.go @@ -15,7 +15,9 @@ import ( dockerClient "github.com/fsouza/go-dockerclient" "github.com/codegangsta/cli" + "github.com/docker/libcompose/project" "github.com/rancherio/os/cmd/power" + "github.com/rancherio/os/compose" "github.com/rancherio/os/config" "github.com/rancherio/os/docker" ) @@ -147,7 +149,9 @@ func osUpgrade(c *cli.Context) { if c.Args().Present() { log.Fatalf("invalid arguments %v", c.Args()) } - startUpgradeContainer(image, c.Bool("stage"), c.Bool("force"), !c.Bool("no-reboot")) + if err := startUpgradeContainer(image, c.Bool("stage"), c.Bool("force"), !c.Bool("no-reboot")); err != nil { + log.Fatal(err) + } } func osVersion(c *cli.Context) { @@ -164,22 +168,28 @@ func yes(in *bufio.Reader, question string) bool { return strings.ToLower(line[0:1]) == "y" } -func startUpgradeContainer(image string, stage, force, reboot bool) { +func startUpgradeContainer(image string, stage, force, reboot bool) error { in := bufio.NewReader(os.Stdin) - container := docker.NewContainer(config.DOCKER_SYSTEM_HOST, &config.ContainerConfig{ - Cmd: "--name=os-upgrade " + - "--log-driver=json-file " + - "--rm " + - "--privileged " + - "--net=host " + - image + " " + - "-t rancher-upgrade " + - "-r " + config.VERSION, - }).Stage() + container, err := compose.CreateService(nil, "os-upgrade", &project.ServiceConfig{ + LogDriver: "json-file", + Privileged: true, + Net: "host", + Image: image, + Labels: project.NewSliceorMap(map[string]string{ + config.SCOPE: config.SYSTEM, + }), + Command: project.NewCommand( + "-t", "rancher-upgrade", + "-r", config.VERSION, + ), + }) + if err != nil { + return err + } - if container.Err != nil { - log.Fatal(container.Err) + if err := container.Pull(); err != nil { + return err } if !stage { @@ -191,46 +201,25 @@ func startUpgradeContainer(image string, stage, force, reboot bool) { } } - container.Start() - if container.Err != nil { - log.Fatal(container.Err) + if err := container.Start(); err != nil { + return err } - client, err := docker.NewClient(config.DOCKER_SYSTEM_HOST) - if err != nil { - log.Fatal(err) + if err := container.Log(); err != nil { + return err } - go func() { - client.Logs(dockerClient.LogsOptions{ - Container: container.Container.ID, - OutputStream: os.Stdout, - ErrorStream: os.Stderr, - Follow: true, - Stdout: true, - Stderr: true, - }) - }() - - exit, err := client.WaitContainer(container.Container.ID) - if err != nil { - log.Fatal(err) + if err := container.Up(); err != nil { + return err } - if container.Err != nil { - log.Fatal(container.Err) - } - - if exit == 0 { - if reboot && (force || yes(in, "Continue with reboot")) { - log.Info("Rebooting") - power.Reboot() - } - } else { - log.Error("Upgrade failed") - os.Exit(exit) + if reboot && (force || yes(in, "Continue with reboot")) { + log.Info("Rebooting") + power.Reboot() } } + + return nil } func parseBody(body []byte) (*Images, error) { diff --git a/cmd/control/reload.go b/cmd/control/reload.go deleted file mode 100644 index a34e3a6c..00000000 --- a/cmd/control/reload.go +++ /dev/null @@ -1,53 +0,0 @@ -package control - -import ( - log "github.com/Sirupsen/logrus" - - "github.com/codegangsta/cli" - "github.com/rancherio/os/config" - "github.com/rancherio/os/docker" -) - -//func parseContainers(cfg *config.Config) map[string]*docker.Container { -// result := map[string]*docker.Container{} -// -// for _, containerConfig := range cfg.SystemContainers { -// container := docker.NewContainer(config.DOCKER_SYSTEM_HOST, &containerConfig) -// if containerConfig.Id != "" { -// result[containerConfig.Id] = container -// } -// } -// -// return result -//} - -func reload(c *cli.Context) { - _, err := config.LoadConfig() - if err != nil { - log.Fatal(err) - } - - containers := map[string]*docker.Container{} //parseContainers(cfg) - toStart := make([]*docker.Container, 0, len(c.Args())) - - for _, id := range c.Args() { - if container, ok := containers[id]; ok { - toStart = append(toStart, container.Stage()) - } - } - - var firstErr error - for _, c := range toStart { - err := c.Start().Err - if err != nil { - log.Errorf("Failed to start %s : %v", c.ContainerCfg.Id, err) - if firstErr != nil { - firstErr = err - } - } - } - - if firstErr != nil { - log.Fatal(firstErr) - } -} diff --git a/cmd/control/service.go b/cmd/control/service.go index 9c46b9b7..2fade009 100644 --- a/cmd/control/service.go +++ b/cmd/control/service.go @@ -6,8 +6,8 @@ import ( "strings" "github.com/codegangsta/cli" + "github.com/rancherio/os/compose" "github.com/rancherio/os/config" - "github.com/rancherio/os/docker" "github.com/rancherio/os/util" ) @@ -93,7 +93,7 @@ func enable(c *cli.Context) { if strings.HasPrefix(service, "/") && !strings.HasPrefix(service, "/var/lib/rancher/conf") { log.Fatalf("ERROR: Service should be in path /var/lib/rancher/conf") } - if _, err := docker.LoadServiceResource(service, true, cfg); err != nil { + if _, err := compose.LoadServiceResource(service, true, cfg); err != nil { log.Fatalf("could not load service %s", service) } cfg.Rancher.ServicesInclude[service] = true diff --git a/compose/project.go b/compose/project.go new file mode 100644 index 00000000..08a2230f --- /dev/null +++ b/compose/project.go @@ -0,0 +1,162 @@ +package compose + +import ( + log "github.com/Sirupsen/logrus" + "github.com/docker/libcompose/cli/logger" + "github.com/docker/libcompose/docker" + "github.com/docker/libcompose/project" + "github.com/rancherio/os/config" + rosDocker "github.com/rancherio/os/docker" + "github.com/rancherio/os/util" +) + +func CreateService(cfg *config.CloudConfig, name string, serviceConfig *project.ServiceConfig) (project.Service, error) { + if cfg == nil { + var err error + cfg, err = config.LoadConfig() + if err != nil { + return nil, err + } + } + + p, err := RunServiceSet("once", cfg, map[string]*project.ServiceConfig{ + name: serviceConfig, + }) + if err != nil { + return nil, err + } + + return p.CreateService(name) +} + +func RunServiceSet(name string, cfg *config.CloudConfig, configs map[string]*project.ServiceConfig) (*project.Project, error) { + p, err := newProject(name, cfg) + if err != nil { + return nil, err + } + + addServices(p, cfg, map[string]string{}, configs) + + return p, p.Up() +} + +func RunServices(cfg *config.CloudConfig) error { + p, err := newCoreServiceProject(cfg) + if err != nil { + return err + } + + return p.Up() +} + +func newProject(name string, cfg *config.CloudConfig) (*project.Project, error) { + clientFactory, err := rosDocker.NewClientFactory(docker.ClientOpts{}) + if err != nil { + return nil, err + } + + serviceFactory := &rosDocker.ServiceFactory{ + Deps: map[string][]string{}, + } + context := &docker.Context{ + ClientFactory: clientFactory, + Context: project.Context{ + ProjectName: name, + EnvironmentLookup: rosDocker.NewConfigEnvironment(cfg), + ServiceFactory: serviceFactory, + Rebuild: true, + Log: cfg.Rancher.Log, + LoggerFactory: logger.NewColorLoggerFactory(), + }, + } + serviceFactory.Context = context + + return docker.NewProject(context) +} + +func addServices(p *project.Project, cfg *config.CloudConfig, enabled map[string]string, configs map[string]*project.ServiceConfig) { + // Note: we ignore errors while loading services + for name, serviceConfig := range cfg.Rancher.Services { + hash := project.GetServiceHash(name, *serviceConfig) + + if enabled[name] == hash { + continue + } + + if err := p.AddConfig(name, serviceConfig); err != nil { + log.Infof("Failed loading service %s", name) + continue + } + + enabled[name] = hash + } +} + +func newCoreServiceProject(cfg *config.CloudConfig) (*project.Project, error) { + network := false + projectEvents := make(chan project.ProjectEvent) + enabled := make(map[string]string) + + p, err := newProject("os", cfg) + if err != nil { + return nil, err + } + + p.AddListener(project.NewDefaultListener(p)) + p.AddListener(projectEvents) + + p.ReloadCallback = func() error { + err := cfg.Reload() + if err != nil { + return err + } + + for service, serviceEnabled := range cfg.Rancher.ServicesInclude { + if enabled[service] != "" || !serviceEnabled { + continue + } + + bytes, err := LoadServiceResource(service, network, cfg) + if err != nil { + if err == util.ErrNoNetwork { + log.Debugf("Can not load %s, networking not enabled", service) + } else { + log.Errorf("Failed to load %s : %v", service, err) + } + continue + } + + err = p.Load(bytes) + if err != nil { + log.Errorf("Failed to load %s : %v", service, err) + continue + } + + enabled[service] = service + } + + addServices(p, cfg, enabled, cfg.Rancher.Services) + + return nil + } + + go func() { + for event := range projectEvents { + if event.Event == project.CONTAINER_STARTED && event.ServiceName == "network" { + network = true + } + } + }() + + err = p.ReloadCallback() + if err != nil { + log.Errorf("Failed to reload os: %v", err) + return nil, err + } + + return p, nil +} + +func LoadServiceResource(name string, network bool, cfg *config.CloudConfig) ([]byte, error) { + return util.LoadResource(name, network, cfg.Rancher.Repositories.ToArray()) +} diff --git a/config/config.go b/config/config.go index 145de3fe..03c5cf13 100644 --- a/config/config.go +++ b/config/config.go @@ -5,8 +5,8 @@ import ( "strings" log "github.com/Sirupsen/logrus" + "github.com/docker/libcompose/project" "github.com/rancherio/os/util" - "github.com/rancherio/rancher-compose/librcompose/project" "gopkg.in/yaml.v2" ) @@ -139,18 +139,6 @@ func Dump(private, full bool) (string, error) { return string(bytes), err } -func (c *CloudConfig) configureConsole() error { - if console, ok := c.Rancher.Services[CONSOLE_CONTAINER]; ok { - if c.Rancher.Console.Persistent { - console.Labels.MapParts()[REMOVE] = "false" - } else { - console.Labels.MapParts()[REMOVE] = "true" - } - } - - return nil -} - func (c *CloudConfig) amendNils() error { if c.Rancher.Environment == nil { c.Rancher.Environment = map[string]string{} @@ -173,7 +161,6 @@ func (c *CloudConfig) amendNils() error { func (c *CloudConfig) readGlobals() error { return util.ShortCircuit( c.readCmdline, - c.configureConsole, // TODO: this smells (it is a write hidden inside a read) ) } @@ -216,27 +203,6 @@ func (c *CloudConfig) Set(key string, value interface{}) error { return c.Reload() } -func (d *DockerConfig) BridgeConfig() (string, string) { - var name, cidr string - - args := append(d.Args, d.ExtraArgs...) - for i, opt := range args { - if opt == "-b" && i < len(args)-1 { - name = args[i+1] - } - - if opt == "--fixed-cidr" && i < len(args)-1 { - cidr = args[i+1] - } - } - - if name == "" || name == "none" { - return "", "" - } else { - return name, cidr - } -} - func (r Repositories) ToArray() []string { result := make([]string, 0, len(r)) for _, repo := range r { diff --git a/config/types.go b/config/types.go index eb08cf7b..7bc88e88 100644 --- a/config/types.go +++ b/config/types.go @@ -2,12 +2,11 @@ package config import ( "github.com/coreos/coreos-cloudinit/config" + "github.com/docker/libcompose/project" "github.com/rancher/netconf" - "github.com/rancherio/rancher-compose/librcompose/project" ) const ( - CONSOLE_CONTAINER = "console" DOCKER_BIN = "/usr/bin/docker" ROS_BIN = "/usr/bin/ros" SYSINIT_BIN = "/usr/bin/ros-sysinit" @@ -24,7 +23,6 @@ const ( HASH = "io.rancher.os.hash" ID = "io.rancher.os.id" DETACH = "io.rancher.os.detach" - REMOVE = "io.rancher.os.remove" CREATE_ONLY = "io.rancher.os.createonly" RELOAD_CONFIG = "io.rancher.os.reloadconfig" SCOPE = "io.rancher.os.scope" @@ -73,8 +71,8 @@ type RancherConfig struct { Autoformat map[string]*project.ServiceConfig `yaml:"autoformat,omitempty"` BootstrapDocker DockerConfig `yaml:"bootstrap_docker,omitempty"` CloudInit CloudInit `yaml:"cloud_init,omitempty"` - Console ConsoleConfig `yaml:"console,omitempty"` Debug bool `yaml:"debug,omitempty"` + Log bool `yaml:"log,omitempty"` Disable []string `yaml:"disable,omitempty"` ServicesInclude map[string]bool `yaml:"services_include,omitempty"` Modules []string `yaml:"modules,omitempty"` @@ -84,15 +82,9 @@ type RancherConfig struct { State StateConfig `yaml:"state,omitempty"` SystemDocker DockerConfig `yaml:"system_docker,omitempty"` Upgrade UpgradeConfig `yaml:"upgrade,omitempty"` - UserContainers []ContainerConfig `yaml:"user_containers,omitempty"` UserDocker DockerConfig `yaml:"user_docker,omitempty"` } -type ConsoleConfig struct { - Tail bool `yaml:"tail,omitempty"` - Persistent bool `yaml:"persistent,omitempty"` -} - type UpgradeConfig struct { Url string `yaml:"url,omitempty"` Image string `yaml:"image,omitempty"` diff --git a/docker/client.go b/docker/client.go index 7e1750c4..0df4885b 100644 --- a/docker/client.go +++ b/docker/client.go @@ -1,10 +1,6 @@ package docker import ( - "time" - - log "github.com/Sirupsen/logrus" - dockerClient "github.com/fsouza/go-dockerclient" "github.com/rancherio/os/config" ) @@ -28,25 +24,10 @@ func NewClient(endpoint string) (*dockerClient.Client, error) { return nil, err } - retry := false - for i := 0; i < (MAX_WAIT / INTERVAL); i++ { - _, err = client.Info() - if err == nil { - break - } + err = ClientOK(endpoint, func() bool { + _, err := client.Info() + return err == nil + }) - retry = true - - log.Infof("Waiting for Docker at %s", endpoint) - time.Sleep(INTERVAL * time.Millisecond) - } - - if err != nil { - return nil, err - } - - if retry { - log.Infof("Connected to Docker at %s", endpoint) - } - return client, nil + return client, err } diff --git a/docker/client_factory.go b/docker/client_factory.go new file mode 100644 index 00000000..0c0726ea --- /dev/null +++ b/docker/client_factory.go @@ -0,0 +1,94 @@ +package docker + +import ( + "fmt" + "sync" + + "github.com/docker/libcompose/docker" + "github.com/docker/libcompose/project" + "github.com/docker/machine/log" + "github.com/rancherio/os/config" + "github.com/rancherio/os/util" + "github.com/samalba/dockerclient" +) + +type ClientFactory struct { + userClient dockerclient.Client + systemClient dockerclient.Client + userOnce sync.Once + systemOnce sync.Once +} + +func NewClientFactory(opts docker.ClientOpts) (docker.ClientFactory, error) { + userOpts := opts + systemOpts := opts + + userOpts.Host = config.DOCKER_HOST + systemOpts.Host = config.DOCKER_SYSTEM_HOST + + userClient, err := docker.CreateClient(userOpts) + if err != nil { + return nil, err + } + + systemClient, err := docker.CreateClient(systemOpts) + if err != nil { + return nil, err + } + + return &ClientFactory{ + userClient: userClient, + systemClient: systemClient, + }, nil +} + +func (c *ClientFactory) Create(service project.Service) dockerclient.Client { + if IsSystemContainer(service.Config()) { + waitFor(&c.systemOnce, c.systemClient, config.DOCKER_SYSTEM_HOST) + return c.systemClient + } + + waitFor(&c.userOnce, c.userClient, config.DOCKER_HOST) + return c.userClient +} + +func waitFor(once *sync.Once, client dockerclient.Client, endpoint string) { + once.Do(func() { + err := ClientOK(endpoint, func() bool { + _, err := client.Info() + return err == nil + }) + if err != nil { + panic(err.Error()) + } + }) +} + +func ClientOK(endpoint string, test func() bool) error { + backoff := util.Backoff{} + defer backoff.Close() + + var err error + retry := false + for ok := range backoff.Start() { + if !ok { + err = fmt.Errorf("Timeout waiting for Docker at %s", endpoint) + break + } + if test() { + break + } + retry = true + log.Infof("Waiting for Docker at %s", endpoint) + } + + if err != nil { + return err + } + + if retry { + log.Infof("Connected to Docker at %s", endpoint) + } + + return nil +} diff --git a/docker/container.go b/docker/container.go deleted file mode 100644 index 3f244446..00000000 --- a/docker/container.go +++ /dev/null @@ -1,632 +0,0 @@ -package docker - -import ( - "crypto/sha1" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "io" - "os" - "reflect" - "sort" - "strings" - - log "github.com/Sirupsen/logrus" - flag "github.com/docker/docker/pkg/mflag" - "github.com/docker/docker/pkg/parsers" - "github.com/docker/docker/runconfig" - shlex "github.com/flynn/go-shlex" - dockerClient "github.com/fsouza/go-dockerclient" - "github.com/rancherio/os/config" - "github.com/rancherio/os/util" - "github.com/rancherio/rancher-compose/librcompose/docker" - "github.com/rancherio/rancher-compose/librcompose/project" -) - -type Container struct { - Err error - Name string - remove bool - detach bool - Config *runconfig.Config - HostConfig *runconfig.HostConfig - dockerHost string - Container *dockerClient.Container - ContainerCfg *config.ContainerConfig -} - -type ByCreated []dockerClient.APIContainers - -func (c ByCreated) Len() int { return len(c) } -func (c ByCreated) Swap(i, j int) { c[i], c[j] = c[j], c[i] } -func (c ByCreated) Less(i, j int) bool { return c[j].Created < c[i].Created } - -func getHash(containerCfg *config.ContainerConfig) string { - hash := sha1.New() - - io.WriteString(hash, fmt.Sprintln(containerCfg.Id)) - io.WriteString(hash, fmt.Sprintln(containerCfg.Cmd)) - io.WriteString(hash, fmt.Sprintln(containerCfg.MigrateVolumes)) - io.WriteString(hash, fmt.Sprintln(containerCfg.ReloadConfig)) - io.WriteString(hash, fmt.Sprintln(containerCfg.CreateOnly)) - - if containerCfg.Service != nil { - //Get values of Service through reflection - val := reflect.ValueOf(containerCfg.Service).Elem() - - //Create slice to sort the keys in Service Config, which allow constant hash ordering - serviceKeys := []string{} - - //Create a data structure of map of values keyed by a string - unsortedKeyValue := make(map[string]interface{}) - - //Get all keys and values in Service Configuration - for i := 0; i < val.NumField(); i++ { - valueField := val.Field(i) - keyField := val.Type().Field(i) - - serviceKeys = append(serviceKeys, keyField.Name) - unsortedKeyValue[keyField.Name] = valueField.Interface() - } - - //Sort serviceKeys alphabetically - sort.Strings(serviceKeys) - - //Go through keys and write hash - for _, serviceKey := range serviceKeys { - serviceValue := unsortedKeyValue[serviceKey] - - io.WriteString(hash, fmt.Sprintf("\n %v: ", serviceKey)) - - switch s := serviceValue.(type) { - case project.SliceorMap: - sliceKeys := []string{} - for lkey := range s.MapParts() { - if lkey != "io.rancher.os.hash" { - sliceKeys = append(sliceKeys, lkey) - } - } - sort.Strings(sliceKeys) - - for _, sliceKey := range sliceKeys { - io.WriteString(hash, fmt.Sprintf("%s=%v, ", sliceKey, s.MapParts()[sliceKey])) - } - case project.MaporEqualSlice: - sliceKeys := s.Slice() - // do not sort keys as the order matters - - for _, sliceKey := range sliceKeys { - io.WriteString(hash, fmt.Sprintf("%s, ", sliceKey)) - } - case project.MaporColonSlice: - sliceKeys := s.Slice() - // do not sort keys as the order matters - - for _, sliceKey := range sliceKeys { - io.WriteString(hash, fmt.Sprintf("%s, ", sliceKey)) - } - case project.MaporSpaceSlice: - sliceKeys := s.Slice() - // do not sort keys as the order matters - - for _, sliceKey := range sliceKeys { - io.WriteString(hash, fmt.Sprintf("%s, ", sliceKey)) - } - case project.Command: - sliceKeys := s.Slice() - // do not sort keys as the order matters - - for _, sliceKey := range sliceKeys { - io.WriteString(hash, fmt.Sprintf("%s, ", sliceKey)) - } - case project.Stringorslice: - sliceKeys := s.Slice() - sort.Strings(sliceKeys) - - for _, sliceKey := range sliceKeys { - io.WriteString(hash, fmt.Sprintf("%s, ", sliceKey)) - } - case []string: - sliceKeys := s - sort.Strings(sliceKeys) - - for _, sliceKey := range sliceKeys { - io.WriteString(hash, fmt.Sprintf("%s, ", sliceKey)) - } - default: - io.WriteString(hash, fmt.Sprintf("%v", serviceValue)) - } - } - } - - return hex.EncodeToString(hash.Sum(nil)) -} - -func StartAndWait(dockerHost string, containerCfg *config.ContainerConfig) error { - container := NewContainer(dockerHost, containerCfg).start(false, true) - return container.Err -} - -func NewContainerFromService(dockerHost string, name string, service *project.ServiceConfig) *Container { - c := &Container{ - Name: name, - dockerHost: dockerHost, - ContainerCfg: &config.ContainerConfig{ - Id: name, - Service: service, - }, - } - - return c.Parse() -} - -func NewContainer(dockerHost string, containerCfg *config.ContainerConfig) *Container { - c := &Container{ - dockerHost: dockerHost, - ContainerCfg: containerCfg, - } - return c.Parse() -} - -func (c *Container) returnErr(err error) *Container { - c.Err = err - return c -} - -func getByLabel(client *dockerClient.Client, key, value string) (*dockerClient.APIContainers, error) { - containers, err := client.ListContainers(dockerClient.ListContainersOptions{ - All: true, - Filters: map[string][]string{ - config.LABEL: {fmt.Sprintf("%s=%s", key, value)}, - }, - }) - - if err != nil { - return nil, err - } - - if len(containers) == 0 { - return nil, nil - } - - sort.Sort(ByCreated(containers)) - return &containers[0], nil -} - -func (c *Container) Lookup() *Container { - c.Parse() - - if c.Err != nil || (c.Container != nil && c.Container.HostConfig != nil) { - return c - } - - hash := getHash(c.ContainerCfg) - - client, err := NewClient(c.dockerHost) - if err != nil { - return c.returnErr(err) - } - - containers, err := client.ListContainers(dockerClient.ListContainersOptions{ - All: true, - Filters: map[string][]string{ - config.LABEL: {fmt.Sprintf("%s=%s", config.HASH, hash)}, - }, - }) - if err != nil { - return c.returnErr(err) - } - - if len(containers) == 0 { - return c - } - - c.Container, c.Err = inspect(client, containers[0].ID) - - return c -} - -func inspect(client *dockerClient.Client, id string) (*dockerClient.Container, error) { - c, err := client.InspectContainer(id) - if err != nil { - return nil, err - } - - if strings.HasPrefix(c.Name, "/") { - c.Name = c.Name[1:] - } - - return c, err -} - -func (c *Container) Exists() bool { - c.Lookup() - return c.Container != nil -} - -func (c *Container) Reset() *Container { - c.Config = nil - c.HostConfig = nil - c.Container = nil - c.Err = nil - - return c -} - -func (c *Container) requiresSyslog() bool { - return (c.ContainerCfg.Service.LogDriver == "" || c.ContainerCfg.Service.LogDriver == "syslog") -} - -func (c *Container) requiresUserDocker() bool { - if c.dockerHost == config.DOCKER_HOST { - return true - } - - return false -} - -func (c *Container) hasLink(link string) bool { - return util.Contains(c.ContainerCfg.Service.Links.Slice(), link) -} - -func (c *Container) addLink(link string) { - if c.hasLink(link) { - return - } - - log.Debugf("Adding %s link to %s", link, c.Name) - c.ContainerCfg.Service.Links = project.NewMaporColonSlice(append(c.ContainerCfg.Service.Links.Slice(), link)) -} - -func (c *Container) parseService() { - if c.requiresSyslog() { - c.addLink("syslog") - } - - if c.requiresUserDocker() { - c.addLink("dockerwait") - } else if c.ContainerCfg.Service.Image != "" { - client, err := NewClient(c.dockerHost) - if err != nil { - c.Err = err - return - } - - i, _ := client.InspectImage(c.ContainerCfg.Service.Image) - if i == nil { - c.addLink("network") - } - } - - cfg, hostConfig, err := docker.Convert(c.ContainerCfg.Service) - if err != nil { - c.Err = err - return - } - - c.Config = cfg - c.HostConfig = hostConfig - - c.detach = c.Config.Labels[config.DETACH] != "false" - c.remove = c.Config.Labels[config.REMOVE] != "false" - c.ContainerCfg.CreateOnly = c.Config.Labels[config.CREATE_ONLY] == "true" - c.ContainerCfg.ReloadConfig = c.Config.Labels[config.RELOAD_CONFIG] == "true" -} - -func (c *Container) parseCmd() { - flags := flag.NewFlagSet("run", flag.ExitOnError) - - flRemove := flags.Bool([]string{"#rm", "-rm"}, false, "") - flDetach := flags.Bool([]string{"d", "-detach"}, false, "") - flName := flags.String([]string{"#name", "-name"}, "", "") - - args, err := shlex.Split(c.ContainerCfg.Cmd) - if err != nil { - c.Err = err - return - } - - log.Debugf("Parsing [%s]", strings.Join(args, ",")) - c.Config, c.HostConfig, _, c.Err = runconfig.Parse(flags, args) - - c.Name = *flName - c.detach = *flDetach - c.remove = *flRemove -} - -func (c *Container) Parse() *Container { - if c.Config != nil || c.Err != nil { - return c - } - - if len(c.ContainerCfg.Cmd) > 0 { - c.parseCmd() - } else if c.ContainerCfg.Service != nil { - c.parseService() - } else { - c.Err = errors.New("Cmd or Service must be set") - return c - } - - if c.ContainerCfg.Id == "" { - c.ContainerCfg.Id = c.Name - } - - return c -} - -func (c *Container) Create() *Container { - return c.start(true, false) -} - -func (c *Container) Start() *Container { - return c.start(false, false) -} - -func (c *Container) StartAndWait() *Container { - return c.start(false, true) -} - -func (c *Container) Stage() *Container { - c.Parse() - - if c.Err != nil { - return c - } - - client, err := NewClient(c.dockerHost) - if err != nil { - c.Err = err - return c - } - - _, err = client.InspectImage(c.Config.Image) - if err == dockerClient.ErrNoSuchImage { - toPull := c.Config.Image - _, tag := parsers.ParseRepositoryTag(toPull) - if tag == "" { - toPull += ":latest" - } - c.Err = client.PullImage(dockerClient.PullImageOptions{ - Repository: toPull, - OutputStream: os.Stdout, - }, dockerClient.AuthConfiguration{}) - } else if err != nil { - log.Errorf("Failed to stage: %s: %v", c.Config.Image, err) - c.Err = err - } - - return c -} - -func (c *Container) Delete() *Container { - c.Parse() - c.Stage() - c.Lookup() - - if c.Err != nil { - return c - } - - if !c.Exists() { - return c - } - - client, err := NewClient(c.dockerHost) - if err != nil { - return c.returnErr(err) - } - - err = client.RemoveContainer(dockerClient.RemoveContainerOptions{ - ID: c.Container.ID, - Force: true, - }) - if err != nil { - return c.returnErr(err) - } - - return c -} - -func (c *Container) renameCurrent(client *dockerClient.Client) error { - if c.Name == "" { - return nil - } - - if c.Name == c.Container.Name { - return nil - } - - err := client.RenameContainer(dockerClient.RenameContainerOptions{ID: c.Container.ID, Name: c.Name}) - if err != nil { - return err - } - - c.Container, err = inspect(client, c.Container.ID) - return err -} - -func (c *Container) renameOld(client *dockerClient.Client, opts *dockerClient.CreateContainerOptions) error { - if len(opts.Name) == 0 { - return nil - } - - existing, err := inspect(client, opts.Name) - if _, ok := err.(*dockerClient.NoSuchContainer); ok { - return nil - } - - if err != nil { - return nil - } - - if c.Container != nil && existing.ID == c.Container.ID { - return nil - } - - var newName string - if label, ok := existing.Config.Labels[config.HASH]; ok { - newName = fmt.Sprintf("%s-%s", existing.Name, label) - } else { - newName = fmt.Sprintf("%s-unknown-%s", existing.Name, util.RandSeq(12)) - } - - if existing.State.Running { - err := client.StopContainer(existing.ID, 2) - if err != nil { - return err - } - - _, err = client.WaitContainer(existing.ID) - if err != nil { - return err - } - } - - log.Debugf("Renaming %s to %s", existing.Name, newName) - return client.RenameContainer(dockerClient.RenameContainerOptions{ID: existing.ID, Name: newName}) -} - -func (c *Container) getCreateOpts(client *dockerClient.Client) (*dockerClient.CreateContainerOptions, error) { - bytes, err := json.Marshal(c) - if err != nil { - log.Errorf("Failed to marshall: %v", c) - return nil, err - } - - var opts dockerClient.CreateContainerOptions - - err = json.Unmarshal(bytes, &opts) - if err != nil { - log.Errorf("Failed to unmarshall: %s", string(bytes)) - return nil, err - } - - if opts.Config.Labels == nil { - opts.Config.Labels = make(map[string]string) - } - - hash := getHash(c.ContainerCfg) - - opts.Config.Labels[config.HASH] = hash - opts.Config.Labels[config.ID] = c.ContainerCfg.Id - - return &opts, nil -} - -func appendVolumesFrom(client *dockerClient.Client, containerCfg *config.ContainerConfig, opts *dockerClient.CreateContainerOptions) error { - if !containerCfg.MigrateVolumes { - return nil - } - - container, err := getByLabel(client, config.ID, containerCfg.Id) - if err != nil || container == nil { - return err - } - - if opts.HostConfig.VolumesFrom == nil { - opts.HostConfig.VolumesFrom = []string{container.ID} - } else { - opts.HostConfig.VolumesFrom = append(opts.HostConfig.VolumesFrom, container.ID) - } - - return nil -} - -func (c *Container) start(createOnly, wait bool) *Container { - log.Debugf("Container: STARTING '%v', createOnly: %v, !detach: %v, wait: %v", c.Name, createOnly, !c.detach, wait) - c.Lookup() - c.Stage() - - if c.Err != nil { - return c - } - - client, err := NewClient(c.dockerHost) - if err != nil { - return c.returnErr(err) - } - - created := false - - opts, err := c.getCreateOpts(client) - if err != nil { - log.Errorf("Failed to create container create options: %v", err) - return c.returnErr(err) - } - - if c.Exists() && c.remove { - log.Debugf("Deleting container %s", c.Container.ID) - c.Delete() - - if c.Err != nil { - return c - } - - c.Reset().Lookup() - if c.Err != nil { - return c - } - } - - if !c.Exists() { - err = c.renameOld(client, opts) - if err != nil { - return c.returnErr(err) - } - - err := appendVolumesFrom(client, c.ContainerCfg, opts) - if err != nil { - return c.returnErr(err) - } - - c.Container, err = client.CreateContainer(*opts) - created = true - if err != nil { - return c.returnErr(err) - } - } - - hostConfig := c.Container.HostConfig - if created { - hostConfig = opts.HostConfig - } - - if createOnly { - return c - } - - if !c.Container.State.Running { - if !created { - err = c.renameOld(client, opts) - if err != nil { - return c.returnErr(err) - } - } - - err = c.renameCurrent(client) - if err != nil { - return c.returnErr(err) - } - - err = client.StartContainer(c.Container.ID, hostConfig) - if err != nil { - log.Errorf("Error from Docker %s", err) - return c.returnErr(err) - } - } - - log.Debugf("Container: WAIT? '%v' !c.detach && wait: %v", c.Name, !c.detach && wait) - if !c.detach && wait { - var exitCode int - exitCode, c.Err = client.WaitContainer(c.Container.ID) - log.Debugf("Container: FINISHED '%v', exitCode: %v", c.Name, exitCode) - if exitCode != 0 { - c.Err = errors.New(fmt.Sprintf("Container %s exited with code %d", c.Name, exitCode)) - } - return c - } - - return c -} diff --git a/docker/container_test.go b/docker/container_test.go deleted file mode 100644 index 06c613f6..00000000 --- a/docker/container_test.go +++ /dev/null @@ -1,307 +0,0 @@ -package docker - -import ( - "fmt" - "strings" - "testing" - - "github.com/rancherio/os/config" - "github.com/rancherio/rancher-compose/librcompose/project" - "github.com/stretchr/testify/require" - - dockerClient "github.com/fsouza/go-dockerclient" - "os" -) - -func testDockerHost(t *testing.T) { - assert := require.New(t) - assert.Equal(os.Getenv("DOCKER_HOST"), config.DOCKER_HOST) -} - -func TestHash(t *testing.T) { - assert := require.New(t) - - hash := getHash(&config.ContainerConfig{ - Id: "id", - Cmd: "1 2 3", - }) - - hash2 := getHash(&config.ContainerConfig{ - Id: "id2", - Cmd: "1 2 3", - }) - - hash3 := getHash(&config.ContainerConfig{ - Id: "id3", - Cmd: "1 2 3 4", - }) - - assert.Equal("d601444333c7fb4cb955bcca36c5ed59b6fa8c3f", hash, "") - assert.NotEqual(hash, hash2, "") - assert.NotEqual(hash2, hash3, "") - assert.NotEqual(hash, hash3, "") -} - -func TestHash2(t *testing.T) { - assert := require.New(t) - - cfg := &config.ContainerConfig{ - Id: "docker-volumes", - Cmd: "", - MigrateVolumes: false, - ReloadConfig: false, - CreateOnly: true, - Service: &project.ServiceConfig{ - CapAdd: nil, - CapDrop: nil, - CpuShares: 0, - Command: project.NewCommand(), - Detach: "", - Dns: project.NewStringorslice(), - DnsSearch: project.NewStringorslice(), - DomainName: "", - Entrypoint: project.NewCommand(), - EnvFile: project.NewStringorslice(), - Environment: project.NewMaporEqualSlice([]string{}), - Hostname: "", - Image: "state", - Labels: project.NewSliceorMap(map[string]string{ - "io.rancher.os.createonly": "true", - "io.rancher.os.scope": "system"}), - Links: project.NewMaporColonSlice(nil), - LogDriver: "json-file", - MemLimit: 0, - Name: "", - Net: "none", - Pid: "", - Ipc: "", - Ports: nil, - Privileged: true, - Restart: "", - ReadOnly: true, - StdinOpen: false, - Tty: false, - User: "", - Volumes: []string{ - "/var/lib/docker:/var/lib/docker", - "/var/lib/rancher/conf:/var/lib/rancher/conf", - "/var/lib/system-docker:/var/lib/system-docker"}, - VolumesFrom: nil, - WorkingDir: "", - Expose: nil, - ExternalLinks: nil}, - } - - for i := 0; i < 1000; i++ { - assert.Equal(getHash(cfg), getHash(cfg), fmt.Sprintf("Failed at iteration: %v", i)) - } -} - -func TestBool2String(t *testing.T) { - assert := require.New(t) - assert.Equal("true", fmt.Sprint(true), "") -} - -func TestParse(t *testing.T) { - assert := require.New(t) - - cfg := &config.ContainerConfig{ - Cmd: "--name c1 " + - "-d " + - "--rm " + - "--privileged " + - "test/image " + - "arg1 " + - "arg2 ", - } - - c := NewContainer("", cfg).Parse() - - assert.NoError(c.Err, "") - assert.Equal(cfg.Id, "c1", "Id doesn't match") - assert.Equal(c.Name, "c1", "Name doesn't match") - assert.True(c.remove, "Remove doesn't match") - assert.True(c.detach, "Detach doesn't match") - assert.Equal(c.Config.Cmd.Len(), 2, "Args doesn't match") - assert.Equal(c.Config.Cmd.Slice()[0], "arg1", "Arg1 doesn't match") - assert.Equal(c.Config.Cmd.Slice()[1], "arg2", "Arg2 doesn't match") - assert.True(c.HostConfig.Privileged, "Privileged doesn't match") -} - -func TestIdFromName(t *testing.T) { - assert := require.New(t) - - cfg := &config.ContainerConfig{ - Cmd: "--name foo -v /test busybox echo hi", - } - - assert.Equal("", cfg.Id) - NewContainer(config.DOCKER_HOST, cfg) - assert.Equal("foo", cfg.Id) -} - -func testMigrateVolumes(t *testing.T) { - assert := require.New(t) - - c := NewContainer(config.DOCKER_HOST, &config.ContainerConfig{ - Cmd: "--name foo -v /test busybox echo hi", - }).Parse().Start().Lookup() - - assert.NoError(c.Err, "") - - test_path, ok := c.Container.Volumes["/test"] - assert.True(ok, "") - - c2 := NewContainer(config.DOCKER_HOST, &config.ContainerConfig{ - MigrateVolumes: true, - Cmd: "--name foo -v /test2 busybox echo hi", - }).Parse().Start().Lookup() - - assert.NoError(c2.Err, "") - - assert.True(c2.Container != nil) - - _, ok = c2.Container.Volumes["/test2"] - assert.True(ok, "") - assert.Equal(test_path, c2.Container.Volumes["/test"]) - - c.Delete() - c2.Delete() -} - -func testRollback(t *testing.T) { - assert := require.New(t) - - c := NewContainer(config.DOCKER_HOST, &config.ContainerConfig{ - Cmd: "--name rollback busybox echo hi", - }).Parse().Start().Lookup() - - assert.NoError(c.Err, "") - assert.Equal("rollback", c.Container.Name) - - c2 := NewContainer(config.DOCKER_HOST, &config.ContainerConfig{ - Cmd: "--name rollback busybox echo bye", - }).Parse().Start().Lookup() - - assert.Equal("rollback", c2.Container.Name) - assert.NoError(c2.Err, "") - assert.NotEqual(c.Container.ID, c2.Container.ID) - - c3 := NewContainer(config.DOCKER_HOST, &config.ContainerConfig{ - Cmd: "--name rollback busybox echo hi", - }).Parse().Start().Lookup() - - assert.NoError(c3.Err, "") - assert.Equal(c.Container.ID, c3.Container.ID) - assert.Equal("rollback", c3.Container.Name) - - c2.Reset().Lookup() - assert.NoError(c2.Err, "") - assert.True(strings.HasPrefix(c2.Container.Name, "rollback-")) - - c.Delete() - c2.Delete() -} - -func testStart(t *testing.T) { - assert := require.New(t) - - c := NewContainer(config.DOCKER_HOST, &config.ContainerConfig{ - Cmd: "--pid=host --privileged --rm busybox echo hi", - }).Parse().Start().Lookup() - - assert.NoError(c.Err, "") - - assert.True(c.HostConfig.Privileged, "") - assert.True(c.Container.HostConfig.Privileged, "") - assert.Equal("host", c.Container.HostConfig.PidMode, "") - - c.Delete() -} - -func testLookup(t *testing.T) { - assert := require.New(t) - - cfg := &config.ContainerConfig{ - Cmd: "--rm busybox echo hi", - } - c := NewContainer(config.DOCKER_HOST, cfg).Parse().Start() - - cfg2 := &config.ContainerConfig{ - Cmd: "--rm busybox echo hi2", - } - c2 := NewContainer(config.DOCKER_HOST, cfg2).Parse().Start() - - assert.NoError(c.Err, "") - assert.NoError(c2.Err, "") - - c1Lookup := NewContainer(config.DOCKER_HOST, cfg).Lookup() - c2Lookup := NewContainer(config.DOCKER_HOST, cfg2).Lookup() - - assert.NoError(c1Lookup.Err, "") - assert.NoError(c2Lookup.Err, "") - - assert.Equal(c.Container.ID, c1Lookup.Container.ID, "") - assert.Equal(c2.Container.ID, c2Lookup.Container.ID, "") - - c.Delete() - c2.Delete() -} - -func testDelete(t *testing.T) { - assert := require.New(t) - - c := NewContainer(config.DOCKER_HOST, &config.ContainerConfig{ - Cmd: "--rm busybox echo hi", - }).Parse() - - assert.False(c.Exists()) - assert.NoError(c.Err, "") - - c.Start() - assert.NoError(c.Err, "") - c.Reset() - assert.NoError(c.Err, "") - - assert.True(c.Exists()) - assert.NoError(c.Err, "") - - c.Delete() - assert.NoError(c.Err, "") - - c.Reset() - assert.False(c.Exists()) - assert.NoError(c.Err, "") -} - -func testDockerClientNames(t *testing.T) { - assert := require.New(t) - client, err := dockerClient.NewClient(config.DOCKER_HOST) - - assert.NoError(err, "") - - c, err := client.CreateContainer(dockerClient.CreateContainerOptions{ - Name: "foo", - Config: &dockerClient.Config{ - Image: "ubuntu", - }, - }) - - assert.NoError(err, "") - assert.Equal("foo", c.Name) - - c2, err := client.InspectContainer(c.ID) - - assert.NoError(err, "") - assert.Equal("/foo", c2.Name) - - c2, err = inspect(client, c.ID) - - assert.NoError(err, "") - assert.Equal("foo", c2.Name) - - client.RemoveContainer(dockerClient.RemoveContainerOptions{ - ID: c2.ID, - Force: true, - }) -} diff --git a/docker/env.go b/docker/env.go new file mode 100644 index 00000000..ac60546e --- /dev/null +++ b/docker/env.go @@ -0,0 +1,55 @@ +package docker + +import ( + "fmt" + "strings" + + "github.com/docker/libcompose/project" + "github.com/rancherio/os/config" +) + +type ConfigEnvironment struct { + cfg *config.CloudConfig +} + +func NewConfigEnvironment(cfg *config.CloudConfig) *ConfigEnvironment { + return &ConfigEnvironment{ + cfg: cfg, + } +} + +func appendEnv(array []string, key, value string) []string { + parts := strings.SplitN(key, "/", 2) + if len(parts) == 2 { + key = parts[1] + } + + return append(array, fmt.Sprintf("%s=%s", key, value)) +} + +func lookupKeys(cfg *config.CloudConfig, keys ...string) []string { + for _, key := range keys { + if strings.HasSuffix(key, "*") { + result := []string{} + for envKey, envValue := range cfg.Rancher.Environment { + keyPrefix := key[:len(key)-1] + if strings.HasPrefix(envKey, keyPrefix) { + result = appendEnv(result, envKey, envValue) + } + } + + if len(result) > 0 { + return result + } + } else if value, ok := cfg.Rancher.Environment[key]; ok { + return appendEnv([]string{}, key, value) + } + } + + return []string{} +} + +func (c *ConfigEnvironment) Lookup(key, serviceName string, serviceConfig *project.ServiceConfig) []string { + fullKey := fmt.Sprintf("%s/%s", serviceName, key) + return lookupKeys(c.cfg, fullKey, key) +} diff --git a/docker/factory.go b/docker/factory.go deleted file mode 100644 index 5d394fd0..00000000 --- a/docker/factory.go +++ /dev/null @@ -1,103 +0,0 @@ -package docker - -import ( - log "github.com/Sirupsen/logrus" - - "github.com/rancherio/os/config" - "github.com/rancherio/os/util" - "github.com/rancherio/rancher-compose/librcompose/project" -) - -type ContainerFactory struct { - cfg *config.CloudConfig -} - -type containerBasedService struct { - project.EmptyService - name string - project *project.Project - container *Container - serviceConfig *project.ServiceConfig - cfg *config.CloudConfig -} - -func NewContainerFactory(cfg *config.CloudConfig) *ContainerFactory { - return &ContainerFactory{ - cfg: cfg, - } -} - -func (c *containerBasedService) Up() error { - container := c.container - containerCfg := c.container.ContainerCfg - - fakeCreate := false - create := containerCfg.CreateOnly - - if util.Contains(c.cfg.Rancher.Disable, c.name) { - fakeCreate = true - } - - var event project.Event - - c.project.Notify(project.CONTAINER_STARTING, c.name, map[string]string{}) - - if fakeCreate { - event = project.CONTAINER_CREATED - } else if create { - container.Create() - event = project.CONTAINER_CREATED - } else { - container.StartAndWait() - event = project.CONTAINER_STARTED - } - - if container.Err != nil { - log.Errorf("Failed to run %v: %v", containerCfg.Id, container.Err) - } - - if container.Err == nil && containerCfg.ReloadConfig { - return project.ErrRestart - } - - if container.Container != nil { - c.project.Notify(event, c.name, map[string]string{ - project.CONTAINER_ID: container.Container.ID, - }) - } - - return container.Err -} - -func (c *containerBasedService) Config() *project.ServiceConfig { - return c.serviceConfig -} - -func (c *containerBasedService) Name() string { - return c.name -} - -func isSystemService(serviceConfig *project.ServiceConfig) bool { - return serviceConfig.Labels.MapParts()[config.SCOPE] == config.SYSTEM -} - -func (c *ContainerFactory) Create(project *project.Project, name string, serviceConfig *project.ServiceConfig) (project.Service, error) { - host := config.DOCKER_HOST - if isSystemService(serviceConfig) { - host = config.DOCKER_SYSTEM_HOST - } - - container := NewContainerFromService(host, name, serviceConfig) - - if container.Err != nil { - return nil, container.Err - } - - return &containerBasedService{ - name: name, - project: project, - container: container, - serviceConfig: serviceConfig, - cfg: c.cfg, - }, nil -} diff --git a/docker/service.go b/docker/service.go new file mode 100644 index 00000000..62cb8204 --- /dev/null +++ b/docker/service.go @@ -0,0 +1,159 @@ +package docker + +import ( + "fmt" + + "github.com/docker/libcompose/docker" + "github.com/docker/libcompose/project" + "github.com/docker/machine/log" + "github.com/rancherio/os/config" + "github.com/samalba/dockerclient" +) + +type Service struct { + *docker.Service + deps map[string][]string + context *docker.Context +} + +func NewService(factory *ServiceFactory, name string, serviceConfig *project.ServiceConfig, context *docker.Context) *Service { + return &Service{ + Service: docker.NewService(name, serviceConfig, context), + deps: factory.Deps, + context: context, + } +} + +func (s *Service) DependentServices() []project.ServiceRelationship { + rels := s.Service.DependentServices() + for _, dep := range s.deps[s.Name()] { + rels = appendLink(rels, dep, true) + } + + if s.requiresSyslog() { + rels = appendLink(rels, "syslog", false) + } + + if s.requiresUserDocker() { + // Linking to cloud-init is a hack really. The problem is we need to link to something + // that will trigger a reload + rels = appendLink(rels, "cloud-init", false) + } else if s.missingImage() { + rels = appendLink(rels, "network", false) + } + return rels +} + +func (s *Service) missingImage() bool { + image := s.Config().Image + if image == "" { + return false + } + client := s.context.ClientFactory.Create(s) + i, err := client.InspectImage(s.Config().Image) + return err != nil || i == nil +} + +func (s *Service) requiresSyslog() bool { + return s.Config().LogDriver == "syslog" +} + +func (s *Service) requiresUserDocker() bool { + return s.Config().Labels.MapParts()[config.SCOPE] != config.SYSTEM +} + +func appendLink(deps []project.ServiceRelationship, name string, optional bool) []project.ServiceRelationship { + rel := project.NewServiceRelationship(name, project.REL_TYPE_LINK) + rel.Optional = optional + return append(deps, rel) +} + +func (s *Service) Up() error { + labels := s.Config().Labels.MapParts() + + if err := s.Service.Create(); err != nil { + return err + } + if err := s.rename(); err != nil { + return err + } + if labels[config.CREATE_ONLY] == "true" { + return s.checkReload(labels) + } + if err := s.Service.Up(); err != nil { + return err + } + if labels[config.DETACH] == "false" { + if err := s.wait(); err != nil { + return err + } + } + + return s.checkReload(labels) +} + +func (s *Service) checkReload(labels map[string]string) error { + if labels[config.RELOAD_CONFIG] == "true" { + return project.ErrRestart + } + return nil +} + +func (s *Service) Create() error { + if err := s.Service.Create(); err != nil { + return err + } + return s.rename() +} + +func (s *Service) getContainer() (dockerclient.Client, *dockerclient.ContainerInfo, error) { + containers, err := s.Service.Containers() + if err != nil { + return nil, nil, err + } + + if len(containers) == 0 { + return nil, nil, nil + } + + id, err := containers[0].Id() + if err != nil { + return nil, nil, err + } + + client := s.context.ClientFactory.Create(s) + info, err := client.InspectContainer(id) + return client, info, err +} + +func (s *Service) wait() error { + client, info, err := s.getContainer() + if err != nil || info == nil { + return err + } + + status := <-client.Wait(info.Id) + if status.Error != nil { + return status.Error + } + + if status.ExitCode == 0 { + return nil + } else { + return fmt.Errorf("ExitCode %d", status.ExitCode) + } +} + +func (s *Service) rename() error { + client, info, err := s.getContainer() + if err != nil || info == nil { + return err + } + + if len(info.Name) > 0 && info.Name[1:] != s.Name() { + log.Debugf("Renaming container %s => %s", info.Name[1:], s.Name()) + return client.RenameContainer(info.Name[1:], s.Name()) + } else { + return nil + } +} diff --git a/docker/service_factory.go b/docker/service_factory.go new file mode 100644 index 00000000..cedcef0c --- /dev/null +++ b/docker/service_factory.go @@ -0,0 +1,27 @@ +package docker + +import ( + "github.com/docker/libcompose/docker" + "github.com/docker/libcompose/project" + "github.com/rancherio/os/util" +) + +type ServiceFactory struct { + Context *docker.Context + Deps map[string][]string +} + +func (s *ServiceFactory) Create(project *project.Project, name string, serviceConfig *project.ServiceConfig) (project.Service, error) { + if after := serviceConfig.Labels.MapParts()["io.rancher.os.after"]; after != "" { + for _, dep := range util.TrimSplit(after, ",") { + s.Deps[name] = append(s.Deps[name], dep) + } + } + if before := serviceConfig.Labels.MapParts()["io.rancher.os.before"]; before != "" { + for _, dep := range util.TrimSplit(before, ",") { + s.Deps[dep] = append(s.Deps[dep], name) + } + } + + return NewService(s, name, serviceConfig, s.Context), nil +} diff --git a/docker/services.go b/docker/services.go deleted file mode 100644 index babeebac..00000000 --- a/docker/services.go +++ /dev/null @@ -1,137 +0,0 @@ -package docker - -import ( - "fmt" - "strings" - - log "github.com/Sirupsen/logrus" - "github.com/rancherio/os/config" - "github.com/rancherio/os/util" - "github.com/rancherio/rancher-compose/librcompose/project" -) - -type configEnvironment struct { - cfg *config.CloudConfig -} - -func appendEnv(array []string, key, value string) []string { - parts := strings.SplitN(key, "/", 2) - if len(parts) == 2 { - key = parts[1] - } - - return append(array, fmt.Sprintf("%s=%s", key, value)) -} - -func lookupKeys(cfg *config.CloudConfig, keys ...string) []string { - for _, key := range keys { - if strings.HasSuffix(key, "*") { - result := []string{} - for envKey, envValue := range cfg.Rancher.Environment { - keyPrefix := key[:len(key)-1] - if strings.HasPrefix(envKey, keyPrefix) { - result = appendEnv(result, envKey, envValue) - } - } - - if len(result) > 0 { - return result - } - } else if value, ok := cfg.Rancher.Environment[key]; ok { - return appendEnv([]string{}, key, value) - } - } - - return []string{} -} - -func (c *configEnvironment) Lookup(key, serviceName string, serviceConfig *project.ServiceConfig) []string { - fullKey := fmt.Sprintf("%s/%s", serviceName, key) - return lookupKeys(c.cfg, fullKey, key) -} - -func RunServices(name string, cfg *config.CloudConfig, configs map[string]*project.ServiceConfig) error { - network := false - projectEvents := make(chan project.ProjectEvent) - p := project.NewProject(name, NewContainerFactory(cfg)) - p.EnvironmentLookup = &configEnvironment{cfg: cfg} - p.AddListener(projectEvents) - enabled := make(map[string]bool) - - for name, serviceConfig := range configs { - if err := p.AddConfig(name, serviceConfig); err != nil { - log.Infof("Failed loading service %s", name) - continue - } - enabled[name] = true - } - - p.ReloadCallback = func() error { - if p.Name != "system-init" { - return nil - } - - if err := cfg.Reload(); err != nil { - return err - } - - for service, serviceEnabled := range cfg.Rancher.ServicesInclude { - if !serviceEnabled { - continue - } - - if en, ok := enabled[service]; ok && en { - continue - } - - bytes, err := LoadServiceResource(service, network, cfg) - if err != nil { - if err == util.ErrNoNetwork { - log.Debugf("Can not load %s, networking not enabled", service) - } else { - log.Errorf("Failed to load %s : %v", service, err) - } - continue - } - - if err := p.Load(bytes); err != nil { - log.Errorf("Failed to load %s : %v", service, err) - continue - } - - enabled[service] = true - } - - for service, config := range cfg.Rancher.Services { - if en, ok := enabled[service]; ok && en { - continue - } - - if err := p.AddConfig(service, config); err != nil { - log.Errorf("Failed to load %s : %v", service, err) - continue - } - enabled[service] = true - } - - return nil - } - - go func() { - for event := range projectEvents { - if event.Event == project.CONTAINER_STARTED && event.ServiceName == "network" { - network = true - } - } - }() - - if err := p.ReloadCallback(); err != nil { - log.Errorf("Failed to reload %s : %v", name, err) - return err - } - return p.Up() -} - -func LoadServiceResource(name string, network bool, cfg *config.CloudConfig) ([]byte, error) { - return util.LoadResource(name, network, cfg.Rancher.Repositories.ToArray()) -} diff --git a/docker/util.go b/docker/util.go new file mode 100644 index 00000000..d2079c72 --- /dev/null +++ b/docker/util.go @@ -0,0 +1,11 @@ +package docker + +import ( + "github.com/docker/libcompose/project" + "github.com/rancherio/os/config" +) + +func IsSystemContainer(serviceConfig *project.ServiceConfig) bool { + return serviceConfig.Labels.MapParts()[config.SCOPE] == config.SYSTEM + +} diff --git a/init/bootstrap.go b/init/bootstrap.go index 1ed20b73..3a8288b4 100644 --- a/init/bootstrap.go +++ b/init/bootstrap.go @@ -8,11 +8,11 @@ import ( "strings" log "github.com/Sirupsen/logrus" + "github.com/docker/libcompose/project" "github.com/rancher/docker-from-scratch" + "github.com/rancherio/os/compose" "github.com/rancherio/os/config" - "github.com/rancherio/os/docker" "github.com/rancherio/os/util" - "github.com/rancherio/rancher-compose/librcompose/project" ) func autoformat(cfg *config.CloudConfig) error { @@ -23,16 +23,17 @@ func autoformat(cfg *config.CloudConfig) error { FORMATZERO := "FORMATZERO=" + fmt.Sprint(cfg.Rancher.State.FormatZero) cfg.Rancher.Autoformat["autoformat"].Environment = project.NewMaporEqualSlice([]string{AUTOFORMAT, FORMATZERO}) log.Info("Running Autoformat services") - err := docker.RunServices("autoformat", cfg, cfg.Rancher.Autoformat) + _, err := compose.RunServiceSet("autoformat", cfg, cfg.Rancher.Autoformat) return err } func runBootstrapContainers(cfg *config.CloudConfig) error { log.Info("Running Bootstrap services") - return docker.RunServices("bootstrap", cfg, cfg.Rancher.BootstrapContainers) + _, err := compose.RunServiceSet("bootstrap", cfg, cfg.Rancher.BootstrapContainers) + return err } -func startDocker(cfg *config.Config) (chan interface{}, error) { +func startDocker(cfg *config.CloudConfig) (chan interface{}, error) { launchConfig, args := getLaunchConfig(cfg, &cfg.Rancher.BootstrapDocker) launchConfig.Fork = true diff --git a/init/init.go b/init/init.go index deb28a70..43bc801a 100644 --- a/init/init.go +++ b/init/init.go @@ -30,7 +30,7 @@ var ( } ) -func loadModules(cfg *config.Config) error { +func loadModules(cfg *config.CloudConfig) error { mounted := map[string]bool{} f, err := os.Open("/proc/modules") @@ -44,7 +44,7 @@ func loadModules(cfg *config.Config) error { mounted[strings.SplitN(reader.Text(), " ", 2)[0]] = true } - for _, module := range cfg.Modules { + for _, module := range cfg.Rancher.Modules { if mounted[module] { continue } @@ -58,7 +58,7 @@ func loadModules(cfg *config.Config) error { return nil } -func sysInit(cfg *config.Config) error { +func sysInit(cfg *config.CloudConfig) error { args := append([]string{config.SYSINIT_BIN}, os.Args[1:]...) cmd := &exec.Cmd{ @@ -83,18 +83,18 @@ func MainInit() { } } -func mountState(cfg *config.Config) error { +func mountState(cfg *config.CloudConfig) error { var err error - if cfg.State.Dev == "" { + if cfg.Rancher.State.Dev == "" { return nil } - dev := util.ResolveDevice(cfg.State.Dev) + dev := util.ResolveDevice(cfg.Rancher.State.Dev) if dev == "" { - return fmt.Errorf("Could not resolve device %q", cfg.State.Dev) + return fmt.Errorf("Could not resolve device %q", cfg.Rancher.State.Dev) } - fsType := cfg.State.FsType + fsType := cfg.Rancher.State.FsType if fsType == "auto" { fsType, err = util.GetFsType(dev) } @@ -108,7 +108,7 @@ func mountState(cfg *config.Config) error { return util.Mount(dev, STATE, fsType, "") } -func tryMountState(cfg *config.Config) error { +func tryMountState(cfg *config.CloudConfig) error { if mountState(cfg) == nil { return nil } @@ -121,8 +121,8 @@ func tryMountState(cfg *config.Config) error { return mountState(cfg) } -func tryMountAndBootstrap(cfg *config.Config) error { - if err := tryMountState(cfg); !cfg.State.Required && err != nil { +func tryMountAndBootstrap(cfg *config.CloudConfig) error { + if err := tryMountState(cfg); !cfg.Rancher.State.Required && err != nil { return nil } else if err != nil { return err @@ -132,15 +132,15 @@ func tryMountAndBootstrap(cfg *config.Config) error { return switchRoot(STATE) } -func getLaunchConfig(cfg *config.Config, dockerCfg *config.DockerConfig) (*dockerlaunch.Config, []string) { +func getLaunchConfig(cfg *config.CloudConfig, dockerCfg *config.DockerConfig) (*dockerlaunch.Config, []string) { var launchConfig dockerlaunch.Config args := dockerlaunch.ParseConfig(&launchConfig, append(dockerCfg.Args, dockerCfg.ExtraArgs...)...) - launchConfig.DnsConfig.Nameservers = cfg.Network.Dns.Nameservers - launchConfig.DnsConfig.Search = cfg.Network.Dns.Search + launchConfig.DnsConfig.Nameservers = cfg.Rancher.Network.Dns.Nameservers + launchConfig.DnsConfig.Search = cfg.Rancher.Network.Dns.Search - if !cfg.Debug { + if !cfg.Rancher.Debug { launchConfig.LogFile = config.SYSTEM_DOCKER_LOG } @@ -148,17 +148,17 @@ func getLaunchConfig(cfg *config.Config, dockerCfg *config.DockerConfig) (*docke } func RunInit() error { - var cfg config.Config + var cfg config.CloudConfig os.Setenv("PATH", "/sbin:/usr/sbin:/usr/bin") // Magic setting to tell Docker to do switch_root and not pivot_root os.Setenv("DOCKER_RAMDISK", "true") initFuncs := []config.InitFunc{ - func(cfg *config.Config) error { + func(cfg *config.CloudConfig) error { return dockerlaunch.PrepareFs(&mountConfig) }, - func(cfg *config.Config) error { + func(cfg *config.CloudConfig) error { newCfg, err := config.LoadConfig() if err == nil { newCfg, err = config.LoadConfig() @@ -167,7 +167,7 @@ func RunInit() error { *cfg = *newCfg } - if cfg.Debug { + if cfg.Rancher.Debug { cfgString, _ := config.Dump(false, true) if cfgString != "" { log.Debugf("Config: %s", cfgString) @@ -178,7 +178,7 @@ func RunInit() error { }, loadModules, tryMountAndBootstrap, - func(cfg *config.Config) error { + func(cfg *config.CloudConfig) error { return cfg.Reload() }, loadModules, @@ -189,7 +189,7 @@ func RunInit() error { return err } - launchConfig, args := getLaunchConfig(&cfg, &cfg.SystemDocker) + launchConfig, args := getLaunchConfig(&cfg, &cfg.Rancher.SystemDocker) log.Info("Launching System Docker") _, err := dockerlaunch.LaunchDocker(launchConfig, config.DOCKER_BIN, args...) diff --git a/init/root.go b/init/root.go index 241bd2a4..86aad6fb 100644 --- a/init/root.go +++ b/init/root.go @@ -44,15 +44,14 @@ func copyMoveRoot(rootfs string) error { filename := path.Join("/", file.Name()) if filename == rootfs { + log.Debugf("Skipping Deleting %s", filename) continue } log.Debugf("Deleting %s", filename) - //if err := os.Remove(filename); err != nil { if err := os.RemoveAll(filename); err != nil { return err } - //} } return nil @@ -90,7 +89,7 @@ func switchRoot(rootfs string) error { } log.Debugf("Successfully moved to new root at %s", rootfs) - os.Setenv("DOCKER_RAMDISK", "false") + os.Unsetenv("DOCKER_RAMDISK") return nil } diff --git a/init/sysinit.go b/init/sysinit.go index 40af7591..52b82438 100644 --- a/init/sysinit.go +++ b/init/sysinit.go @@ -7,6 +7,7 @@ import ( log "github.com/Sirupsen/logrus" dockerClient "github.com/fsouza/go-dockerclient" + "github.com/rancherio/os/compose" "github.com/rancherio/os/config" "github.com/rancherio/os/docker" ) @@ -88,42 +89,6 @@ func loadImages(cfg *config.CloudConfig) error { return nil } -func runContainers(cfg *config.CloudConfig) error { - return docker.RunServices("system-init", cfg, cfg.Rancher.Services) -} - -func tailConsole(cfg *config.CloudConfig) error { - if !cfg.Rancher.Console.Tail { - return nil - } - - client, err := docker.NewSystemClient() - if err != nil { - return err - } - - console, ok := cfg.Rancher.Services[config.CONSOLE_CONTAINER] - if !ok { - log.Error("Console not found") - return nil - } - - c := docker.NewContainerFromService(config.DOCKER_SYSTEM_HOST, config.CONSOLE_CONTAINER, console) - if c.Err != nil { - return c.Err - } - - log.Infof("Tailing console : %s", c.Name) - return client.Logs(dockerClient.LogsOptions{ - Container: c.Name, - Stdout: true, - Stderr: true, - Follow: true, - OutputStream: os.Stdout, - ErrorStream: os.Stderr, - }) -} - func SysInit() error { cfg, err := config.LoadConfig() if err != nil { @@ -132,7 +97,9 @@ func SysInit() error { initFuncs := []config.InitFunc{ loadImages, - runContainers, + func(cfg *config.CloudConfig) error { + return compose.RunServices(cfg) + }, func(cfg *config.CloudConfig) error { syscall.Sync() return nil @@ -141,7 +108,6 @@ func SysInit() error { log.Infof("RancherOS %s started", config.VERSION) return nil }, - tailConsole, } return config.RunInitFuncs(cfg, initFuncs) diff --git a/main.go b/main.go index 3b8546af..15a8bd81 100644 --- a/main.go +++ b/main.go @@ -6,6 +6,7 @@ import ( log "github.com/Sirupsen/logrus" "github.com/docker/docker/pkg/reexec" + dockerlaunchMain "github.com/rancher/docker-from-scratch/main" "github.com/rancherio/os/cmd/cloudinit" "github.com/rancherio/os/cmd/control" "github.com/rancherio/os/cmd/network" @@ -39,6 +40,7 @@ func registerCmd(cmd string, mainFunc func()) { func main() { registerCmd("/init", osInit.MainInit) registerCmd(config.SYSINIT_BIN, sysinit.Main) + registerCmd("/usr/bin/dockerlaunch", dockerlaunchMain.Main) registerCmd("/usr/bin/system-docker", systemdocker.Main) registerCmd("/sbin/poweroff", power.PowerOff) registerCmd("/sbin/reboot", power.Reboot) diff --git a/os-config.yml b/os-config.yml index 206aecfa..ea67bd1f 100644 --- a/os-config.yml +++ b/os-config.yml @@ -27,8 +27,7 @@ rancher: labels: io.rancher.os.detach: false io.rancher.os.scope: system - links: - - autoformat + io.rancher.os.after: autoformat log_driver: json-file net: host uts: host @@ -89,10 +88,7 @@ rancher: io.rancher.os.detach: false io.rancher.os.reloadconfig: true io.rancher.os.scope: system - links: - - preload-user-images - - cloud-init-pre - - network + io.rancher.os.after: cloud-init-pre,network net: host uts: host privileged: true @@ -107,8 +103,7 @@ rancher: io.rancher.os.detach: false io.rancher.os.reloadconfig: true io.rancher.os.scope: system - links: - - preload-system-images + io.rancher.os.after: preload-system-images net: host uts: host privileged: true @@ -136,15 +131,13 @@ rancher: - /usr/bin/ros:/usr/bin/respawn:ro - /usr/bin/ros:/usr/bin/system-docker:ro - /usr/bin/ros:/usr/sbin/wait-for-docker:ro - - /lib/modules:/lib/modules + - /usr/bin/ros:/usr/sbin/dockerlaunch:ro - /usr/bin/docker:/usr/bin/docker:ro console: image: rancher/os-console:v0.4.0-dev labels: - io.rancher.os.remove: true io.rancher.os.scope: system - links: - - cloud-init + io.rancher.os.after: cloud-init net: host uts: host pid: host @@ -157,8 +150,7 @@ rancher: image: rancher/os-docker:v0.4.0-dev labels: io.rancher.os.scope: system - links: - - network + io.rancher.os.after: network net: host uts: host pid: host @@ -177,27 +169,14 @@ rancher: privileged: true read_only: true volumes: - - /var/lib/rancher/conf:/var/lib/rancher/conf - /var/lib/docker:/var/lib/docker - /var/lib/system-docker:/var/lib/system-docker - dockerwait: - image: rancher/os-dockerwait:v0.4.0-dev - labels: - io.rancher.os.detach: false - io.rancher.os.scope: system - links: - - docker - net: host - uts: host - volumes_from: - - all-volumes network: image: rancher/os-network:v0.4.0-dev labels: io.rancher.os.detach: false io.rancher.os.scope: system - links: - - cloud-init-pre + io.rancher.os.after: cloud-init-pre net: host uts: host privileged: true @@ -208,9 +187,7 @@ rancher: image: rancher/os-ntp:v0.4.0-dev labels: io.rancher.os.scope: system - links: - - cloud-init - - network + io.rancher.os.after: cloud-init, network net: host uts: host privileged: true @@ -231,9 +208,6 @@ rancher: image: rancher/os-preload:v0.4.0-dev labels: io.rancher.os.detach: false - io.rancher.os.scope: system - links: - - dockerwait privileged: true volumes: - /var/run/docker.sock:/var/run/docker.sock @@ -263,7 +237,7 @@ rancher: read_only: true volumes: - /dev:/host/dev - - /os-config.yml:/os-config.yml + - /usr/share/ros/os-config.yml:/usr/share/ros/os-config.yml - /var/lib/rancher:/var/lib/rancher - /var/lib/rancher/conf:/var/lib/rancher/conf - /etc/ssl/certs/ca-certificates.crt:/etc/ssl/certs/ca-certificates.crt.rancher diff --git a/scripts/run b/scripts/run index b233d586..1fd16a62 100755 --- a/scripts/run +++ b/scripts/run @@ -99,7 +99,7 @@ else done fi -KERNEL_ARGS="rancher.password=rancher console=ttyS0 ${QEMU_APPEND}" +KERNEL_ARGS="rancher.password=rancher rancher.modules=[9p,9pnet_virtio] console=ttyS0 ${QEMU_APPEND}" if [ "$UNAME" == "Darwin" ] && [ -x $(which xhyve) ]; then diff --git a/util/backoff.go b/util/backoff.go new file mode 100644 index 00000000..aa4adce7 --- /dev/null +++ b/util/backoff.go @@ -0,0 +1,54 @@ +package util + +import "time" + +type Backoff struct { + StartMillis, MaxIntervalMillis, MaxMillis int + c chan bool + done chan bool +} + +func (b *Backoff) Start() <-chan bool { + b.c = make(chan bool) + b.done = make(chan bool) + go b.backoff() + return b.c +} + +func (b *Backoff) Close() error { + b.done <- true + return nil +} + +func (b *Backoff) backoff() { + if b.StartMillis == 0 && b.MaxIntervalMillis == 0 { + b.StartMillis = 100 + b.MaxIntervalMillis = 2000 + b.MaxMillis = 300000 + } + + start := time.Now() + currentMillis := b.StartMillis + + for { + writeVal := true + if time.Now().Sub(start) > (time.Duration(b.MaxMillis) * time.Millisecond) { + b.c <- false + } + + select { + case <-b.done: + close(b.done) + close(b.c) + return + case b.c <- writeVal: + } + + time.Sleep(time.Duration(currentMillis) * time.Millisecond) + + currentMillis *= 2 + if currentMillis > b.MaxIntervalMillis { + currentMillis = b.MaxIntervalMillis + } + } +} diff --git a/util/util.go b/util/util.go index c9e5be03..e4482b75 100644 --- a/util/util.go +++ b/util/util.go @@ -345,3 +345,16 @@ func KVPairs2Map(kvs []string) map[string]string { } return r } + +func TrimSplitN(str, sep string, count int) []string { + result := []string{} + for _, part := range strings.SplitN(strings.TrimSpace(str), sep, count) { + result = append(result, strings.TrimSpace(part)) + } + + return result +} + +func TrimSplit(str, sep string) []string { + return TrimSplitN(str, sep, -1) +}