From 719d255636ef74f82c412eea6c2002fb13c4e329 Mon Sep 17 00:00:00 2001 From: Josh Curl Date: Mon, 6 Jun 2016 15:13:15 -0700 Subject: [PATCH] First class consoles --- cmd/control/cli.go | 6 + cmd/control/console.go | 104 ++++++++++++++++++ cmd/control/os.go | 10 -- cmd/control/util.go | 19 ++++ cmd/power/power.go | 42 +------ cmd/switchconsole/switch_console.go | 41 +++++++ compose/project.go | 49 +-------- compose/reload.go | 73 ++++++++++++ config/types.go | 1 + main.go | 2 + os-config.tpl.yml | 1 + .../assets/test_03/cloud-config.yml | 3 +- .../assets/test_05/cloud-config.yml | 3 +- .../assets/test_18/cloud-config.yml | 3 +- util/network/network.go | 10 +- util/util.go | 38 +++++++ 16 files changed, 302 insertions(+), 103 deletions(-) create mode 100644 cmd/control/console.go create mode 100644 cmd/control/util.go create mode 100644 cmd/switchconsole/switch_console.go create mode 100644 compose/reload.go diff --git a/cmd/control/cli.go b/cmd/control/cli.go index d8c19eb2..f5d7b833 100644 --- a/cmd/control/cli.go +++ b/cmd/control/cli.go @@ -31,6 +31,12 @@ func Main() { HideHelp: true, Subcommands: configSubcommands(), }, + { + Name: "console", + Usage: "console container commands", + HideHelp: true, + Subcommands: consoleSubcommands(), + }, { Name: "dev", ShortName: "d", diff --git a/cmd/control/console.go b/cmd/control/console.go new file mode 100644 index 00000000..aa1792f7 --- /dev/null +++ b/cmd/control/console.go @@ -0,0 +1,104 @@ +package control + +import ( + "bufio" + "fmt" + "os" + + "golang.org/x/net/context" + + log "github.com/Sirupsen/logrus" + "github.com/codegangsta/cli" + composeConfig "github.com/docker/libcompose/config" + "github.com/docker/libcompose/project/options" + "github.com/rancher/os/compose" + "github.com/rancher/os/config" + "github.com/rancher/os/docker" + "github.com/rancher/os/util" + "github.com/rancher/os/util/network" +) + +func consoleSubcommands() []cli.Command { + return []cli.Command{ + { + Name: "switch", + Usage: "switch currently running console", + Action: consoleSwitch, + }, + { + Name: "list", + Usage: "list available consoles", + Action: consoleList, + }, + } +} + +func consoleSwitch(c *cli.Context) error { + if len(c.Args()) != 1 { + log.Fatal("Must specify exactly one existing container") + } + newConsole := c.Args()[0] + + in := bufio.NewReader(os.Stdin) + question := fmt.Sprintf("Switching consoles will destroy the current console container and restart Docker. Continue") + if !yes(in, question) { + return nil + } + + cfg := config.LoadConfig() + + if err := compose.StageServices(cfg, newConsole); err != nil { + return err + } + + client, err := docker.NewSystemClient() + if err != nil { + return err + } + + currentContainerId, err := util.GetCurrentContainerId() + if err != nil { + return err + } + + currentContainer, err := client.ContainerInspect(context.Background(), currentContainerId) + if err != nil { + return err + } + + service, err := compose.CreateService(nil, "switch-console", &composeConfig.ServiceConfigV1{ + LogDriver: "json-file", + Privileged: true, + Net: "host", + Pid: "host", + Image: currentContainer.Config.Image, + Labels: map[string]string{ + config.SCOPE: config.SYSTEM, + }, + Command: []string{"/usr/bin/switch-console", newConsole}, + VolumesFrom: []string{"all-volumes"}, + }) + if err != nil { + return err + } + + if err = service.Delete(context.Background(), options.Delete{}); err != nil { + return err + } + return service.Up(context.Background(), options.Up{}) +} + +func consoleList(c *cli.Context) error { + cfg := config.LoadConfig() + + consoles, err := network.GetConsoles(cfg.Rancher.Repositories.ToArray()) + if err != nil { + return err + } + + for _, console := range consoles { + fmt.Println(console) + } + + return nil +} diff --git a/cmd/control/os.go b/cmd/control/os.go index 31e67368..84d91ddc 100644 --- a/cmd/control/os.go +++ b/cmd/control/os.go @@ -177,16 +177,6 @@ func osVersion(c *cli.Context) error { return nil } -func yes(in *bufio.Reader, question string) bool { - fmt.Printf("%s [y/N]: ", question) - line, err := in.ReadString('\n') - if err != nil { - log.Fatal(err) - } - - return strings.ToLower(line[0:1]) == "y" -} - func startUpgradeContainer(image string, stage, force, reboot, kexec bool, upgradeConsole bool, kernelArgs string) error { in := bufio.NewReader(os.Stdin) diff --git a/cmd/control/util.go b/cmd/control/util.go new file mode 100644 index 00000000..6ab95b67 --- /dev/null +++ b/cmd/control/util.go @@ -0,0 +1,19 @@ +package control + +import ( + "bufio" + "fmt" + "strings" + + log "github.com/Sirupsen/logrus" +) + +func yes(in *bufio.Reader, question string) bool { + fmt.Printf("%s [y/N]: ", question) + line, err := in.ReadString('\n') + if err != nil { + log.Fatal(err) + } + + return strings.ToLower(line[0:1]) == "y" +} diff --git a/cmd/power/power.go b/cmd/power/power.go index ab849e01..632e8aed 100644 --- a/cmd/power/power.go +++ b/cmd/power/power.go @@ -1,7 +1,6 @@ package power import ( - "bufio" "errors" "os" "path/filepath" @@ -17,10 +16,7 @@ import ( "github.com/docker/engine-api/types/filters" "github.com/rancher/os/docker" -) - -const ( - DOCKER_CGROUPS_FILE = "/proc/self/cgroup" + "github.com/rancher/os/util" ) func runDocker(name string) error { @@ -51,7 +47,7 @@ func runDocker(name string) error { } } - currentContainerId, err := getCurrentContainerId() + currentContainerId, err := util.GetCurrentContainerId() if err != nil { return err } @@ -185,7 +181,7 @@ func shutDownContainers() error { return err } - currentContainerId, err := getCurrentContainerId() + currentContainerId, err := util.GetCurrentContainerId() if err != nil { return err } @@ -222,35 +218,3 @@ func shutDownContainers() error { return nil } - -func getCurrentContainerId() (string, error) { - file, err := os.Open(DOCKER_CGROUPS_FILE) - - if err != nil { - return "", err - } - - fileReader := bufio.NewScanner(file) - if !fileReader.Scan() { - return "", errors.New("Empty file /proc/self/cgroup") - } - line := fileReader.Text() - parts := strings.Split(line, "/") - - for len(parts) != 3 { - if !fileReader.Scan() { - return "", errors.New("Found no docker cgroups") - } - line = fileReader.Text() - parts = strings.Split(line, "/") - if len(parts) == 3 { - if strings.HasSuffix(parts[1], "docker") { - break - } else { - parts = nil - } - } - } - - return parts[len(parts)-1:][0], nil -} diff --git a/cmd/switchconsole/switch_console.go b/cmd/switchconsole/switch_console.go new file mode 100644 index 00000000..644ea3cd --- /dev/null +++ b/cmd/switchconsole/switch_console.go @@ -0,0 +1,41 @@ +package switchconsole + +import ( + "os" + + log "github.com/Sirupsen/logrus" + "github.com/docker/libcompose/project/options" + "github.com/rancher/os/compose" + "github.com/rancher/os/config" + "golang.org/x/net/context" +) + +func Main() { + if len(os.Args) != 2 { + log.Fatal("Must specify exactly one existing container") + } + newConsole := os.Args[1] + + cfg := config.LoadConfig() + + project, err := compose.GetProject(cfg, true) + if err != nil { + log.Fatal(err) + } + + if err = compose.LoadService(project, cfg, true, newConsole); err != nil { + log.Fatal(err) + } + + if err = project.Up(context.Background(), options.Up{}, "console"); err != nil { + log.Fatal(err) + } + + if err = project.Restart(context.Background(), 10, "docker"); err != nil { + log.Errorf("Failed to restart Docker: %v", err) + } + + if err = config.Set("rancher.console", newConsole); err != nil { + log.Errorf("Failed to update 'rancher.console': %v", err) + } +} diff --git a/compose/project.go b/compose/project.go index e2edb60d..c115ff40 100644 --- a/compose/project.go +++ b/compose/project.go @@ -183,9 +183,6 @@ func adjustContainerNames(m map[interface{}]interface{}) map[interface{}]interfa } func newCoreServiceProject(cfg *config.CloudConfig, useNetwork bool) (*project.Project, error) { - projectEvents := make(chan events.Event) - enabled := map[interface{}]interface{}{} - environmentLookup := rosDocker.NewConfigEnvironment(cfg) authLookup := rosDocker.NewConfigAuthLookup(cfg) @@ -194,53 +191,11 @@ func newCoreServiceProject(cfg *config.CloudConfig, useNetwork bool) (*project.P return nil, err } + projectEvents := make(chan events.Event) p.AddListener(project.NewDefaultListener(p)) p.AddListener(projectEvents) - p.ReloadCallback = func() error { - cfg = config.LoadConfig() - - environmentLookup.SetConfig(cfg) - authLookup.SetConfig(cfg) - - enabled = addServices(p, enabled, cfg.Rancher.Services) - - for service, serviceEnabled := range cfg.Rancher.ServicesInclude { - if _, ok := enabled[service]; ok || !serviceEnabled { - continue - } - - bytes, err := network.LoadServiceResource(service, useNetwork, cfg) - if err != nil { - if err == network.ErrNoNetwork { - log.Debugf("Can not load %s, networking not enabled", service) - } else { - log.Errorf("Failed to load %s : %v", service, err) - } - continue - } - - m := map[interface{}]interface{}{} - if err := yaml.Unmarshal(bytes, &m); err != nil { - log.Errorf("Failed to parse YAML configuration: %s : %v", service, err) - continue - } - bytes, err = yaml.Marshal(adjustContainerNames(m)) - if err != nil { - log.Errorf("Failed to marshal YAML configuration: %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 - } - - return nil - } + p.ReloadCallback = projectReload(p, &useNetwork, environmentLookup, authLookup) go func() { for event := range projectEvents { diff --git a/compose/reload.go b/compose/reload.go new file mode 100644 index 00000000..b2278664 --- /dev/null +++ b/compose/reload.go @@ -0,0 +1,73 @@ +package compose + +import ( + "fmt" + + log "github.com/Sirupsen/logrus" + yaml "github.com/cloudfoundry-incubator/candiedyaml" + "github.com/docker/libcompose/project" + "github.com/rancher/os/config" + "github.com/rancher/os/docker" + "github.com/rancher/os/util/network" +) + +func LoadService(p *project.Project, cfg *config.CloudConfig, useNetwork bool, service string) error { + bytes, err := network.LoadServiceResource(service, useNetwork, cfg) + if err != nil { + return err + } + + m := map[interface{}]interface{}{} + if err = yaml.Unmarshal(bytes, &m); err != nil { + return fmt.Errorf("Failed to parse YAML configuration for %s: %v", service, err) + } + + m = adjustContainerNames(m) + + bytes, err = yaml.Marshal(m) + if err != nil { + return fmt.Errorf("Failed to marshal YAML configuration for %s: %v", service, err) + } + + if err = p.Load(bytes); err != nil { + return fmt.Errorf("Failed to load %s: %v", service, err) + } + + return nil +} + +func projectReload(p *project.Project, useNetwork *bool, environmentLookup *docker.ConfigEnvironment, authLookup *docker.ConfigAuthLookup) func() error { + enabled := map[interface{}]interface{}{} + return func() error { + cfg := config.LoadConfig() + + environmentLookup.SetConfig(cfg) + authLookup.SetConfig(cfg) + + enabled = addServices(p, enabled, cfg.Rancher.Services) + + for service, serviceEnabled := range cfg.Rancher.ServicesInclude { + if _, ok := enabled[service]; ok || !serviceEnabled { + continue + } + + if err := LoadService(p, cfg, *useNetwork, service); err != nil { + if err != network.ErrNoNetwork { + log.Error(err) + } + continue + } + + enabled[service] = service + } + + if cfg.Rancher.Console != "" { + err := LoadService(p, cfg, *useNetwork, cfg.Rancher.Console) + if err != nil && err != network.ErrNoNetwork { + log.Error(err) + } + } + + return nil + } +} diff --git a/config/types.go b/config/types.go index 4d0c94a9..946d8660 100644 --- a/config/types.go +++ b/config/types.go @@ -85,6 +85,7 @@ type CloudConfig struct { } type RancherConfig struct { + Console string `yaml:"console,omitempty"` Environment map[string]string `yaml:"environment,omitempty"` Services map[string]*composeConfig.ServiceConfigV1 `yaml:"services,omitempty"` BootstrapContainers map[string]*composeConfig.ServiceConfigV1 `yaml:"bootstrap,omitempty"` diff --git a/main.go b/main.go index bf27db8e..dee27724 100644 --- a/main.go +++ b/main.go @@ -9,6 +9,7 @@ import ( "github.com/rancher/os/cmd/network" "github.com/rancher/os/cmd/power" "github.com/rancher/os/cmd/respawn" + "github.com/rancher/os/cmd/switchconsole" "github.com/rancher/os/cmd/sysinit" "github.com/rancher/os/cmd/systemdocker" "github.com/rancher/os/cmd/userdocker" @@ -28,6 +29,7 @@ var entrypoints = map[string]func(){ "respawn": respawn.Main, "ros-sysinit": sysinit.Main, "shutdown": power.Main, + "switch-console": switchconsole.Main, "system-docker": systemdocker.Main, "user-docker": userdocker.Main, "wait-for-docker": wait.Main, diff --git a/os-config.tpl.yml b/os-config.tpl.yml index a2fa2761..ccd6b4f7 100644 --- a/os-config.tpl.yml +++ b/os-config.tpl.yml @@ -154,6 +154,7 @@ rancher: - /usr/bin/ros:/usr/bin/cloud-init:ro - /usr/bin/ros:/usr/sbin/netconf:ro - /usr/bin/ros:/usr/sbin/wait-for-docker:ro + - /usr/bin/ros:/usr/bin/switch-console:ro console: image: {{.OS_REPO}}/os-console:{{.VERSION}}{{.SUFFIX}} labels: diff --git a/tests/integration/assets/test_03/cloud-config.yml b/tests/integration/assets/test_03/cloud-config.yml index 43d347f9..839f7782 100644 --- a/tests/integration/assets/test_03/cloud-config.yml +++ b/tests/integration/assets/test_03/cloud-config.yml @@ -1,6 +1,5 @@ #cloud-config rancher: - services_include: - debian-console: true + console: debian ssh_authorized_keys: - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDUlsWAL5Rf0Wis/A7k7Tlqx0fZS60VzCZrPZYbP/wkL95jv0XzCx8bd1rZHeybblHPDNpND3BLv4qPY5DxRyexF4seGuzcJI/pOvGUGjQondeMPgDTFEo5w939gSdeTZcfXzQ0wAVhzwDbgH4zPfMzbdoo8Aiu9jkKljXw8IFju0gh+t6iKkGZCIjKT9o7zza1vGfkodhvi2V3VzPdNO28gaxZaRNtmBYUoVnGyR6nXN1Q3CJaVuh5o6GPCOqrhHNbYOFZKBpDiHbxPhVpxHQD2+8yUSGTG7WW75FfZePja5y8d0c/O5L37ZYx4AZAd3KgQYDBT2XCEJGQNawNbfpt diff --git a/tests/integration/assets/test_05/cloud-config.yml b/tests/integration/assets/test_05/cloud-config.yml index 43d347f9..839f7782 100644 --- a/tests/integration/assets/test_05/cloud-config.yml +++ b/tests/integration/assets/test_05/cloud-config.yml @@ -1,6 +1,5 @@ #cloud-config rancher: - services_include: - debian-console: true + console: debian ssh_authorized_keys: - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDUlsWAL5Rf0Wis/A7k7Tlqx0fZS60VzCZrPZYbP/wkL95jv0XzCx8bd1rZHeybblHPDNpND3BLv4qPY5DxRyexF4seGuzcJI/pOvGUGjQondeMPgDTFEo5w939gSdeTZcfXzQ0wAVhzwDbgH4zPfMzbdoo8Aiu9jkKljXw8IFju0gh+t6iKkGZCIjKT9o7zza1vGfkodhvi2V3VzPdNO28gaxZaRNtmBYUoVnGyR6nXN1Q3CJaVuh5o6GPCOqrhHNbYOFZKBpDiHbxPhVpxHQD2+8yUSGTG7WW75FfZePja5y8d0c/O5L37ZYx4AZAd3KgQYDBT2XCEJGQNawNbfpt diff --git a/tests/integration/assets/test_18/cloud-config.yml b/tests/integration/assets/test_18/cloud-config.yml index 8b240b65..155a1d98 100644 --- a/tests/integration/assets/test_18/cloud-config.yml +++ b/tests/integration/assets/test_18/cloud-config.yml @@ -1,11 +1,10 @@ #cloud-config rancher: + console: debian services: missing_image: image: tianon/true labels: io.rancher.os.scope: system - services_include: - debian-console: true ssh_authorized_keys: - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC85w9stZyiLQp/DkVO6fqwiShYcj1ClKdtCqgHtf+PLpJkFReSFu8y21y+ev09gsSMRRrjF7yt0pUHV6zncQhVeqsZtgc5WbELY2DOYUGmRn/CCvPbXovoBrQjSorqlBmpuPwsStYLr92Xn+VVsMNSUIegHY22DphGbDKG85vrKB8HxUxGIDxFBds/uE8FhSy+xsoyT/jUZDK6pgq2HnGl6D81ViIlKecpOpWlW3B+fea99ADNyZNVvDzbHE5pcI3VRw8u59WmpWOUgT6qacNVACl8GqpBvQk8sw7O/X9DSZHCKafeD9G5k+GYbAUz92fKWrx/lOXfUXPS3+c8dRIF diff --git a/util/network/network.go b/util/network/network.go index dbe22ebd..af3fbd45 100644 --- a/util/network/network.go +++ b/util/network/network.go @@ -21,6 +21,14 @@ var ( ) func GetServices(urls []string) ([]string, error) { + return getServices(urls, "services") +} + +func GetConsoles(urls []string) ([]string, error) { + return getServices(urls, "consoles") +} + +func getServices(urls []string, key string) ([]string, error) { result := []string{} for _, url := range urls { @@ -38,7 +46,7 @@ func GetServices(urls []string) ([]string, error) { continue } - if list, ok := services["services"]; ok { + if list, ok := services[key]; ok { result = append(result, list...) } } diff --git a/util/util.go b/util/util.go index 3f2c995e..b75b7898 100644 --- a/util/util.go +++ b/util/util.go @@ -1,7 +1,9 @@ package util import ( + "bufio" "bytes" + "errors" "fmt" "io/ioutil" "os" @@ -13,6 +15,10 @@ import ( log "github.com/Sirupsen/logrus" ) +const ( + DOCKER_CGROUPS_FILE = "/proc/self/cgroup" +) + type AnyMap map[interface{}]interface{} func Contains(values []string, value string) bool { @@ -187,3 +193,35 @@ func TrimSplitN(str, sep string, count int) []string { func TrimSplit(str, sep string) []string { return TrimSplitN(str, sep, -1) } + +func GetCurrentContainerId() (string, error) { + file, err := os.Open(DOCKER_CGROUPS_FILE) + + if err != nil { + return "", err + } + + fileReader := bufio.NewScanner(file) + if !fileReader.Scan() { + return "", errors.New("Empty file /proc/self/cgroup") + } + line := fileReader.Text() + parts := strings.Split(line, "/") + + for len(parts) != 3 { + if !fileReader.Scan() { + return "", errors.New("Found no docker cgroups") + } + line = fileReader.Text() + parts = strings.Split(line, "/") + if len(parts) == 3 { + if strings.HasSuffix(parts[1], "docker") { + break + } else { + parts = nil + } + } + } + + return parts[len(parts)-1:][0], nil +}