From 1543444adad0ee7a0f648115fa3436c6eb05097a Mon Sep 17 00:00:00 2001 From: Aleksa Sarai Date: Mon, 5 Sep 2016 22:29:03 +1000 Subject: [PATCH] contrib: add recvtty proof-of-concept This is a proof-of-concept for the --console-socket API. It just acts as a dumb input-output copy process (nowhere near as good as the internal runC one since it doesn't handle console resizes or signals). It also provides a test-friendly mode that will be used in the bats integration tests. This patch is part of the console rewrite patchset. Signed-off-by: Aleksa Sarai --- .gitignore | 1 + Makefile | 24 +++- contrib/cmd/recvtty/recvtty.go | 247 +++++++++++++++++++++++++++++++++ 3 files changed, 266 insertions(+), 6 deletions(-) create mode 100644 contrib/cmd/recvtty/recvtty.go diff --git a/.gitignore b/.gitignore index fecde9caf..dab07a180 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ vendor/pkg /runc +contrib/cmd/recvtty/recvtty Godeps/_workspace/src/github.com/opencontainers/runc man/man8 release diff --git a/Makefile b/Makefile index c728fd58d..7ec779c82 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,8 @@ -.PHONY: dbuild man \ +.PHONY: all dbuild man \ localtest localunittest localintegration \ test unittest integration +SOURCES := $(shell find . 2>&1 | grep -E '.*\.(c|h|go)$$') PREFIX := $(DESTDIR)/usr/local BINDIR := $(PREFIX)/sbin GIT_BRANCH := $(shell git rev-parse --abbrev-ref HEAD 2>/dev/null) @@ -26,13 +27,23 @@ VERSION := ${shell cat ./VERSION} SHELL := $(shell command -v bash 2>/dev/null) -all: $(RUNC_LINK) +.DEFAULT: runc + +runc: $(SOURCES) | $(RUNC_LINK) go build -i -ldflags "-X main.gitCommit=${COMMIT} -X main.version=${VERSION}" -tags "$(BUILDTAGS)" -o runc . -static: $(RUNC_LINK) - CGO_ENABLED=1 go build -i -tags "$(BUILDTAGS) cgo static_build" -ldflags "-w -extldflags -static -X main.gitCommit=${COMMIT} -X main.version=${VERSION}" -o runc . +all: runc recvtty -release: $(RUNC_LINK) +recvtty: contrib/cmd/recvtty/recvtty + +contrib/cmd/recvtty/recvtty: $(SOURCES) | $(RUNC_LINK) + go build -i -ldflags "-X main.gitCommit=${COMMIT} -X main.version=${VERSION}" -tags "$(BUILDTAGS)" -o contrib/cmd/recvtty/recvtty ./contrib/cmd/recvtty + +static: $(SOURCES) | $(RUNC_LINK) + CGO_ENABLED=1 go build -i -tags "$(BUILDTAGS) cgo static_build" -ldflags "-w -extldflags -static -X main.gitCommit=${COMMIT} -X main.version=${VERSION}" -o runc . + CGO_ENABLED=1 go build -i -tags "$(BUILDTAGS) cgo static_build" -ldflags "-w -extldflags -static -X main.gitCommit=${COMMIT} -X main.version=${VERSION}" -o contrib/cmd/recvtty/recvtty ./contrib/cmd/recvtty + +release: $(RUNC_LINK) | $(RUNC_LINK) @flag_list=(seccomp selinux apparmor static ambient); \ unset expression; \ for flag in "$${flag_list[@]}"; do \ @@ -62,7 +73,7 @@ $(RUNC_LINK): ln -sfn $(CURDIR) $(RUNC_LINK) dbuild: runcimage - docker run --rm -v $(CURDIR):/go/src/$(PROJECT) --privileged $(RUNC_IMAGE) make + docker run --rm -v $(CURDIR):/go/src/$(PROJECT) --privileged $(RUNC_IMAGE) make clean all lint: go vet ./... @@ -113,6 +124,7 @@ uninstall-man: clean: rm -f runc + rm -f contrib/cmd/recvtty/recvtty rm -f $(RUNC_LINK) rm -rf $(GOPATH)/pkg rm -rf $(RELEASE_DIR) diff --git a/contrib/cmd/recvtty/recvtty.go b/contrib/cmd/recvtty/recvtty.go new file mode 100644 index 000000000..196b32836 --- /dev/null +++ b/contrib/cmd/recvtty/recvtty.go @@ -0,0 +1,247 @@ +/* + * Copyright 2016 SUSE LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package main + +import ( + "fmt" + "io" + "io/ioutil" + "net" + "os" + "strings" + + "github.com/opencontainers/runc/libcontainer" + "github.com/opencontainers/runc/libcontainer/utils" + "github.com/urfave/cli" +) + +// version will be populated by the Makefile, read from +// VERSION file of the source code. +var version = "" + +// gitCommit will be the hash that the binary was built from +// and will be populated by the Makefile +var gitCommit = "" + +const ( + usage = `Open Container Initiative contrib/cmd/recvtty + +recvtty is a reference implementation of a consumer of runC's --console-socket +API. It has two main modes of operation: + + * single: Only permit one terminal to be sent to the socket, which is + then hooked up to the stdio of the recvtty process. This is useful + for rudimentary shell management of a container. + + * null: Permit as many terminals to be sent to the socket, but they + are read to /dev/null. This is used for testing, and imitates the + old runC API's --console=/dev/pts/ptmx hack which would allow for a + similar trick. This is probably not what you want to use, unless + you're doing something like our bats integration tests. + +To use recvtty, just specify a socket path at which you want to receive +terminals: + + $ recvtty [--mode ] socket.sock +` +) + +func bail(err error) { + fmt.Fprintf(os.Stderr, "[recvtty] fatal error: %v\n", err) + os.Exit(1) +} + +func handleSingle(path string) error { + // Open a socket. + ln, err := net.Listen("unix", path) + if err != nil { + return err + } + defer ln.Close() + + // We only accept a single connection, since we can only really have + // one reader for os.Stdin. Plus this is all a PoC. + conn, err := ln.Accept() + if err != nil { + return err + } + defer conn.Close() + + // Close ln, to allow for other instances to take over. + ln.Close() + + // Get the fd of the connection. + unixconn, ok := conn.(*net.UnixConn) + if !ok { + return fmt.Errorf("failed to cast to unixconn") + } + + socket, err := unixconn.File() + if err != nil { + return err + } + defer socket.Close() + + // Get the master file descriptor from runC. + master, err := utils.RecvFd(socket) + if err != nil { + return err + } + + // Print the file descriptor tag. + ti, err := libcontainer.GetTerminalInfo(master.Name()) + if err != nil { + return err + } + fmt.Printf("[recvtty] received masterfd: container '%s'\n", ti.ContainerID) + + // Copy from our stdio to the master fd. + quitChan := make(chan struct{}) + go func() { + io.Copy(os.Stdout, master) + quitChan <- struct{}{} + }() + go func() { + io.Copy(master, os.Stdin) + quitChan <- struct{}{} + }() + + // Only close the master fd once we've stopped copying. + <-quitChan + master.Close() + return nil +} + +func handleNull(path string) error { + // Open a socket. + ln, err := net.Listen("unix", path) + if err != nil { + return err + } + defer ln.Close() + + // As opposed to handleSingle we accept as many connections as we get, but + // we don't interact with Stdin at all (and we copy stdout to /dev/null). + for { + conn, err := ln.Accept() + if err != nil { + return err + } + go func(conn net.Conn) { + // Don't leave references lying around. + defer conn.Close() + + // Get the fd of the connection. + unixconn, ok := conn.(*net.UnixConn) + if !ok { + return + } + + socket, err := unixconn.File() + if err != nil { + return + } + defer socket.Close() + + // Get the master file descriptor from runC. + master, err := utils.RecvFd(socket) + if err != nil { + return + } + + // Print the file descriptor tag. + ti, err := libcontainer.GetTerminalInfo(master.Name()) + if err != nil { + bail(err) + } + fmt.Printf("[recvtty] received masterfd: container '%s'\n", ti.ContainerID) + + // Just do a dumb copy to /dev/null. + devnull, err := os.OpenFile("/dev/null", os.O_RDWR, 0) + if err != nil { + // TODO: Handle this nicely. + return + } + + io.Copy(devnull, master) + devnull.Close() + }(conn) + } +} + +func main() { + app := cli.NewApp() + app.Name = "recvtty" + app.Usage = usage + + // Set version to be the same as runC. + var v []string + if version != "" { + v = append(v, version) + } + if gitCommit != "" { + v = append(v, fmt.Sprintf("commit: %s", gitCommit)) + } + app.Version = strings.Join(v, "\n") + + // Set the flags. + app.Flags = []cli.Flag{ + cli.StringFlag{ + Name: "mode, m", + Value: "single", + Usage: "Mode of operation (single or null)", + }, + cli.StringFlag{ + Name: "pid-file", + Value: "", + Usage: "Path to write daemon process ID to", + }, + } + + app.Action = func(ctx *cli.Context) error { + args := ctx.Args() + if len(args) != 1 { + return fmt.Errorf("need to specify a single socket path") + } + path := ctx.Args()[0] + + pidPath := ctx.String("pid-file") + if pidPath != "" { + pid := fmt.Sprintf("%d\n", os.Getpid()) + if err := ioutil.WriteFile(pidPath, []byte(pid), 0644); err != nil { + return err + } + } + + switch ctx.String("mode") { + case "single": + if err := handleSingle(path); err != nil { + return err + } + case "null": + if err := handleNull(path); err != nil { + return err + } + default: + return fmt.Errorf("need to select a valid mode: %s", ctx.String("mode")) + } + return nil + } + if err := app.Run(os.Args); err != nil { + bail(err) + } +}