diff --git a/cmd/control/cli.go b/cmd/control/cli.go index 54de0f45..b398a136 100755 --- a/cmd/control/cli.go +++ b/cmd/control/cli.go @@ -53,21 +53,31 @@ func Main() { HideHelp: true, Action: listServices, }, { - Name: "add, install, upgrade", + Name: "install", // TODO: add an --apply or --up ... // TODO: also support the repo-name prefix ShortName: "", Usage: "install/upgrade service / RancherOS", HideHelp: true, Action: service.Enable, + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "apply", + Usage: "Switch console/engine, or start service.", + }, + cli.BoolFlag{ + Name: "force", + Usage: "Don't ask questions.", + }, + }, }, { - Name: "remove, delete", + Name: "remove", ShortName: "", Usage: "remove service", HideHelp: true, Action: service.Del, }, { - Name: "logs, log", + Name: "logs", Usage: "View output from containers", //Before: verifyOneOrMoreServices, Action: composeApp.WithProject(factory, serviceApp.ProjectLog), @@ -238,21 +248,9 @@ func GetAllServices() map[string]map[string]*libcomposeConfig.ServiceConfigV1 { } for serviceType, serviceList := range services { for _, serviceLongName := range serviceList { - servicePath := fmt.Sprintf("%s/%s.yml", repoName, serviceLongName) - //log.Infof("loading %s", serviceLongName) - content, err := network.CacheLookup(servicePath) + p, err := service.LoadService(repoName, serviceLongName) if err != nil { - log.Errorf("Failed to load %s: %v", servicePath, err) - continue - } - if content, err = ComposeToCloudConfig(content); err != nil { - log.Errorf("Failed to convert compose to cloud-config syntax: %v", err) - continue - } - - p, err := config.ReadConfig(content, true) - if err != nil { - log.Errorf("Failed to load %s : %v", servicePath, err) + log.Errorf("Failed to load %s/%s : %v", repoName, serviceLongName, err) } // yes, the serviceLongName is really only the yml file name @@ -274,21 +272,6 @@ func GetAllServices() map[string]map[string]*libcomposeConfig.ServiceConfigV1 { return result } -//TODO: copied from cloudinitsave, move to config. -func ComposeToCloudConfig(bytes []byte) ([]byte, error) { - compose := make(map[interface{}]interface{}) - err := yaml.Unmarshal(bytes, &compose) - if err != nil { - return nil, err - } - - return yaml.Marshal(map[interface{}]interface{}{ - "rancher": map[interface{}]interface{}{ - "services": compose, - }, - }) -} - var originalCli = []cli.Command{ { Name: "config", diff --git a/cmd/control/console.go b/cmd/control/console.go old mode 100644 new mode 100755 index 883c0361..775273d7 --- a/cmd/control/console.go +++ b/cmd/control/console.go @@ -66,7 +66,7 @@ func consoleSwitch(c *cli.Context) error { 1. destroy the current console container 2. log you out 3. restart Docker`) - if !yes("Continue") { + if !util.Yes("Continue") { return nil } } @@ -111,6 +111,7 @@ func consoleEnable(c *cli.Context) error { cfg := config.LoadConfig() validateConsole(newConsole, cfg) + //TODO: why does default not need to be staged? if newConsole != "default" { if err := compose.StageServices(cfg, newConsole); err != nil { return err diff --git a/cmd/control/install.go b/cmd/control/install.go index d17d3820..4c272dd8 100755 --- a/cmd/control/install.go +++ b/cmd/control/install.go @@ -153,7 +153,7 @@ func installAction(c *cli.Context) error { return err } - if !kexec && reboot && (force || yes("Continue with reboot")) { + if !kexec && reboot && (force || util.Yes("Continue with reboot")) { log.Info("Rebooting") power.Reboot() } @@ -165,7 +165,7 @@ func runInstall(image, installType, cloudConfig, device, kappend string, force, fmt.Printf("Installing from %s\n", image) if !force { - if util.IsRunningInTty() && !yes("Continue") { + if util.IsRunningInTty() && !util.Yes("Continue") { log.Infof("Not continuing with installation due to user not saying 'yes'") os.Exit(1) } diff --git a/cmd/control/os.go b/cmd/control/os.go index d30a7f5b..23821236 100644 --- a/cmd/control/os.go +++ b/cmd/control/os.go @@ -22,6 +22,7 @@ import ( "github.com/rancher/os/compose" "github.com/rancher/os/config" "github.com/rancher/os/docker" + "github.com/rancher/os/util" ) type Images struct { @@ -226,7 +227,7 @@ func startUpgradeContainer(image string, stage, force, reboot, kexec bool, upgra if len(imageSplit) > 1 && imageSplit[1] == config.Version+config.Suffix { confirmation = fmt.Sprintf("Already at version %s. Continue anyway", imageSplit[1]) } - if !force && !yes(confirmation) { + if !force && !util.Yes(confirmation) { os.Exit(1) } @@ -276,7 +277,7 @@ func startUpgradeContainer(image string, stage, force, reboot, kexec bool, upgra return err } - if reboot && (force || yes("Continue with reboot")) { + if reboot && (force || util.Yes("Continue with reboot")) { log.Info("Rebooting") power.Reboot() } diff --git a/cmd/control/service/service.go b/cmd/control/service/service.go index fe805c49..68ed0980 100755 --- a/cmd/control/service/service.go +++ b/cmd/control/service/service.go @@ -1,11 +1,17 @@ package service import ( + "context" "fmt" "strings" + yaml "github.com/cloudfoundry-incubator/candiedyaml" "github.com/codegangsta/cli" dockerApp "github.com/docker/libcompose/cli/docker/app" + composeConfig "github.com/docker/libcompose/config" + + "github.com/docker/libcompose/project/options" + "github.com/docker/libcompose/project" "github.com/rancher/os/cmd/control/service/command" "github.com/rancher/os/compose" @@ -135,20 +141,125 @@ func Del(c *cli.Context) error { return nil } +//TODO: copied from cloudinitsave, move to config. +func ComposeToCloudConfig(bytes []byte) ([]byte, error) { + compose := make(map[interface{}]interface{}) + err := yaml.Unmarshal(bytes, &compose) + if err != nil { + return nil, err + } + + return yaml.Marshal(map[interface{}]interface{}{ + "rancher": map[interface{}]interface{}{ + "services": compose, + }, + }) +} + +// TODO: this should move to something like config/service.go? +// WARNING: this can contain more than one service - Josh and I aren't sure this is worth it +func LoadService(repoName, serviceLongName string) (*config.CloudConfig, error) { + servicePath := fmt.Sprintf("%s/%s.yml", repoName, serviceLongName) + //log.Infof("loading %s", serviceLongName) + content, err := network.CacheLookup(servicePath) + if err != nil { + return nil, fmt.Errorf("Failed to load %s: %v", servicePath, err) + } + if content, err = ComposeToCloudConfig(content); err != nil { + return nil, fmt.Errorf("Failed to convert compose to cloud-config syntax: %v", err) + } + + p, err := config.ReadConfig(content, true) + if err != nil { + return nil, fmt.Errorf("Failed to load %s : %v", servicePath, err) + } + return p, nil +} + +// TODO: this should move to something like config/service.go? +func IsConsole(serviceCfg *config.CloudConfig) bool { + //the service is called console, and has an io.rancher.os.console label. + for serviceName, service := range serviceCfg.Rancher.Services { + if serviceName == "console" { + for k := range service.Labels { + if k == "io.rancher.os.console" { + return true + } + } + } + } + return false +} + +// TODO: this should move to something like config/service.go? +func IsEngine(serviceCfg *config.CloudConfig) bool { + //the service is called docker, and the command is "ros user-docker" + for serviceName, service := range serviceCfg.Rancher.Services { + log.Infof("serviceName == %s", serviceName) + if serviceName == "docker" { + cmd := strings.Join(service.Command, " ") + log.Infof("service command == %s", cmd) + if cmd == "ros user-docker" { + log.Infof("yes, its a docker engine") + return true + } + } + } + return false +} + func Enable(c *cli.Context) error { cfg := config.LoadConfig() var enabledServices []string + var consoleService, engineService string + var errorServices []string + serviceMap := make(map[string]*config.CloudConfig) for _, service := range c.Args() { - validateService(service, cfg) + //validateService(service, cfg) + //log.Infof("start4") + // TODO: need to search for the service in all the repos. + // TODO: also need to deal with local file paths and URLs + serviceConfig, err := LoadService("core", service) + if err != nil { + log.Errorf("Failed to load %s: %s", service, err) + errorServices = append(errorServices, service) + continue + } + serviceMap[service] = serviceConfig + } + if len(serviceMap) == 0 { + log.Fatalf("No valid Services found") + } + if len(errorServices) > 0 { + if c.Bool("force") || !util.Yes("Some services failed to load, Continue?") { + log.Fatalf("Services failed to load: %v", errorServices) + } + } + for service, serviceConfig := range serviceMap { if val, ok := cfg.Rancher.ServicesInclude[service]; !ok || !val { if isLocal(service) && !strings.HasPrefix(service, "/var/lib/rancher/conf") { log.Fatalf("ERROR: Service should be in path /var/lib/rancher/conf") } - cfg.Rancher.ServicesInclude[service] = true + if IsConsole(serviceConfig) { + log.Debugf("Enabling the %s console", service) + if err := config.Set("rancher.console", service); err != nil { + log.Errorf("Failed to update 'rancher.console': %v", err) + } + consoleService = service + + } else if IsEngine(serviceConfig) { + log.Debugf("Enabling the %s user engine", service) + if err := config.Set("rancher.docker.engine", service); err != nil { + log.Errorf("Failed to update 'rancher.docker.engine': %v", err) + } + engineService = service + } else { + cfg.Rancher.ServicesInclude[service] = true + } enabledServices = append(enabledServices, service) } } @@ -163,6 +274,64 @@ func Enable(c *cli.Context) error { } } + //TODO: fix the case where the user is applying both a new console and a new docker engine + if consoleService != "" && c.Bool("apply") { + //ros console switch. + if !c.Bool("force") { + fmt.Println(`Switching consoles will +1. destroy the current console container +2. log you out +3. restart Docker`) + if !util.Yes("Continue") { + return nil + } + switchService, err := compose.CreateService(nil, "switch-console", &composeConfig.ServiceConfigV1{ + LogDriver: "json-file", + Privileged: true, + Net: "host", + Pid: "host", + Image: config.OsBase, + Labels: map[string]string{ + config.ScopeLabel: config.System, + }, + Command: []string{"/usr/bin/ros", "switch-console", consoleService}, + VolumesFrom: []string{"all-volumes"}, + }) + if err != nil { + log.Fatal(err) + } + + if err = switchService.Delete(context.Background(), options.Delete{}); err != nil { + log.Fatal(err) + } + if err = switchService.Up(context.Background(), options.Up{}); err != nil { + log.Fatal(err) + } + if err = switchService.Log(context.Background(), true); err != nil { + log.Fatal(err) + } + } + } + if engineService != "" && c.Bool("apply") { + log.Info("Starting the %s engine", engineService) + project, err := compose.GetProject(cfg, true, false) + if err != nil { + log.Fatal(err) + } + + if err = project.Stop(context.Background(), 10, "docker"); err != nil { + log.Fatal(err) + } + + if err = compose.LoadSpecialService(project, cfg, "docker", engineService); err != nil { + log.Fatal(err) + } + + if err = project.Up(context.Background(), options.Up{}, "docker"); err != nil { + log.Fatal(err) + } + } + return nil } diff --git a/cmd/control/util.go b/util/yes.go similarity index 84% rename from cmd/control/util.go rename to util/yes.go index 5e83803e..48dfeefc 100644 --- a/cmd/control/util.go +++ b/util/yes.go @@ -1,4 +1,4 @@ -package control +package util import ( "bufio" @@ -9,7 +9,7 @@ import ( "github.com/rancher/os/log" ) -func yes(question string) bool { +func Yes(question string) bool { fmt.Printf("%s [y/N]: ", question) in := bufio.NewReader(os.Stdin) line, err := in.ReadString('\n')