├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── cmd ├── admin.go ├── cmd.go ├── environ.go ├── envy.go ├── http.go ├── session.go ├── system.go ├── user.go └── util.go ├── data ├── environ │ ├── Dockerfile │ └── envyrc ├── home │ └── .bashrc ├── id_host └── id_host.pub ├── envy.go ├── go.mod ├── go.sum ├── pkg └── hterm │ ├── assets.go │ ├── assets │ ├── hterm.html │ └── hterm.js │ └── hterm.go └── tests ├── Dockerfile └── tests.bash /.gitignore: -------------------------------------------------------------------------------- 1 | envy.tgz 2 | dind.tgz 3 | alpine.tgz 4 | ubuntu.tgz 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM gliderlabs/alpine:3.8 2 | RUN apk --update add bash curl go git mercurial musl-dev 3 | 4 | RUN curl -Ls https://github.com/progrium/execd/releases/download/v0.1.0/execd_0.1.0_Linux_x86_64.tgz \ 5 | | tar -zxC /bin \ 6 | && curl -Ls https://github.com/progrium/entrykit/releases/download/v0.2.0/entrykit_0.2.0_Linux_x86_64.tgz \ 7 | | tar -zxC /bin \ 8 | && curl -sL https://get.docker.com/builds/Linux/x86_64/docker-1.7.1 > /bin/docker \ 9 | && chmod +x /bin/docker \ 10 | && entrykit --symlink 11 | 12 | ADD ./data /tmp/data 13 | 14 | ENV GOPATH /go 15 | COPY . /go/src/github.com/progrium/envy 16 | WORKDIR /go/src/github.com/progrium/envy 17 | RUN go get && CGO_ENABLED=0 go build -a -buildmode exe -installsuffix cgo -o /bin/envy \ 18 | && ln -s /bin/envy /bin/enter \ 19 | && ln -s /bin/envy /bin/auth \ 20 | && ln -s /bin/envy /bin/serve 21 | 22 | VOLUME /envy 23 | EXPOSE 22 80 24 | ENTRYPOINT ["codep", "/bin/execd -e -k /tmp/data/id_host /bin/auth /bin/enter", "/bin/serve"] 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Jeff Lindsay 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | build: hterm 3 | docker build -t progrium/envy . 4 | docker tag progrium/envy progrium/envy:local 5 | 6 | dev: 7 | docker build -t progrium/envy:dev -f Dockerfile . 8 | docker run --rm --name envy.dev \ 9 | -v /tmp/envy:/envy \ 10 | -v /var/run/docker.sock:/var/run/docker.sock \ 11 | -p 8000:80 \ 12 | -p 2222:22 \ 13 | -e HOST_ROOT=/tmp/envy \ 14 | progrium/envy:dev 15 | 16 | 17 | test: 18 | test -f tests/envy.tgz || docker save progrium/envy > tests/envy.tgz 19 | test -f tests/dind.tgz || docker save progrium/dind > tests/dind.tgz 20 | test -f tests/alpine.tgz || docker save alpine > tests/alpine.tgz 21 | test -f tests/ubuntu.tgz || docker save ubuntu > tests/ubuntu.tgz 22 | docker rm -f envy.test &> /dev/null || true 23 | docker build -t envy-tests -f tests/Dockerfile . 24 | docker run -d --name envy.test --privileged envy-tests 25 | docker exec envy.test make test2 26 | 27 | test2: 28 | #docker build -t progrium/envy . 29 | docker load -i /envy/tests/envy.tgz 30 | docker load -i /envy/tests/dind.tgz 31 | docker load -i /envy/tests/ubuntu.tgz 32 | docker load -i /envy/tests/alpine.tgz 33 | docker run -d \ 34 | --net=host \ 35 | -v /tmp/envy:/envy \ 36 | -v /var/run/docker.sock:/var/run/docker.sock \ 37 | -e ALLOWALL=true \ 38 | -e HOST_ROOT=/tmp/envy \ 39 | progrium/envy 40 | basht /envy/tests/*.bash 41 | 42 | clean: 43 | docker rmi progrium/envy:local &> /dev/null || true 44 | rm -f tests/envy.tgz 45 | rm -f tests/dind.tgz 46 | rm -f tests/alpine.tgz 47 | rm -f tests/ubuntu.tgz 48 | 49 | hterm: 50 | cd pkg/hterm && go generate 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # envy 2 | 3 | Development environment manager. Work in progress, but join the process! 4 | 5 | ## Running Envy 6 | ``` 7 | $ docker run -d --name envy \ 8 | -v /mnt/envy:/envy \ 9 | -v /var/run/docker.sock:/var/run/docker.sock \ 10 | -p 80:80 \ 11 | -p 22:22 \ 12 | -e HOST_ROOT=/mnt/envy \ 13 | progrium/envy 14 | ``` 15 | 16 | ## Using Envy 17 | 18 | You connect to Envy via SSH or HTTP. [See this screencast for a demo.](https://vimeo.com/131329120) 19 | 20 | Users are authenticated against GitHub using HTTP auth (user,pass or user,token) or SSH keys. 21 | 22 | ## Concepts 23 | 24 | * **environment** This refers to a Docker image defining an environment 25 | * An environment is a Docker image 26 | * It can also refer to the directory used to build the Docker image 27 | * Each environment comes with a Docker-in-Docker instance 28 | * **session** This is an active shell in a Docker container instance for an environment 29 | 30 | ## Envy Commands 31 | 32 | All sessions have access to the `envy` binary and can run management commands. Some of the commands 33 | are only available to users with admin privileges. Here are current commands: 34 | ``` 35 | Usage: envy [options] [arguments] 36 | 37 | Commands: 38 | 39 | admin ls list admin users 40 | admin rm remove admin user 41 | admin add add admin user 42 | environ rebuild rebuild environment image 43 | session reload reload session from environment image 44 | session commit commit session changes to environment image 45 | session switch switch session to different environment 46 | 47 | Run 'envy help [command]' for details. 48 | ``` 49 | 50 | ## Aliased Commands 51 | 52 | As long as you keep the default `envyrc` in your environment, you'll have these aliases set up: 53 | ``` 54 | alias commit='exec envy session commit' 55 | alias reload='exec envy session reload' 56 | alias switch='exec envy session switch' 57 | alias rebuild='exec envy environ rebuild' 58 | ``` 59 | Exec is necessary to exit to the session manager with status 128, which gets Envy to create a new container 60 | from the environment image. This happens quickly behind the scenes, so a session feels 61 | like a continuous experience even if happening across multiple containers. 62 | 63 | ## Envy Root 64 | 65 | When Envy is run it expects a host bind mount at /envy so it can initialize and persist its 66 | file tree. This is where most of the state in Envy is kept. Most configuration is kept here in plain 67 | files. Here is an explanation of the tree: 68 | 69 | ``` 70 | /envy 71 | /users 72 | / 73 | /environs # directory of environs for this user 74 | /sessions # directory of sessions for this user 75 | /home # home directory mounted into all sessions 76 | /root # root home mounted in all sessions (see #3) 77 | /config 78 | users # file of users allowed to login. defaults to * (any) 79 | admins # file of admin users. defaults to first logged in user 80 | /bin 81 | envy # staging of the envy binary to put into sessions 82 | ``` 83 | 84 | ## Startup Scripts 85 | 86 | Both Bash and POSIX shells are set up to source `/root/environ/envyrc` when started interactively. 87 | For Bash, this is done with a default `.bashrc`, and for POSIX by setting `ENV`. 88 | 89 | Although you can edit your `/root/environ/envyrc`, by default it will source a few other 90 | RC files. First, `~/.envyrc` if it exists, and then `/home//.envyrc_` 91 | if it exists. The latter allows you to specify an RC for the root user of all 92 | environments. One use case for this is to symlink `/root/.ssh` to your envy user's 93 | `.ssh` directory so ssh will have access to your identity keys in all environments. 94 | 95 | ## Moving Host SSH 96 | 97 | Envy is best experienced running on port 22 on a host. If you want to move your current OpenSSH 98 | to port 2222, here is a one-liner that is likely to work: 99 | ``` 100 | $ sed -i -e 's/Port 22/Port 2222/' /etc/ssh/sshd_config 101 | ``` 102 | Then restart SSH. 103 | -------------------------------------------------------------------------------- /cmd/admin.go: -------------------------------------------------------------------------------- 1 | package envy 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func init() { 12 | cmdAdmin.AddCommand(cmdAdminList) 13 | cmdAdmin.AddCommand(cmdAdminRemove) 14 | cmdAdmin.AddCommand(cmdAdminAdd) 15 | } 16 | 17 | func CheckAdminCmd() { 18 | if GetUser(os.Getenv("ENVY_USER")).Admin() { 19 | Cmd.AddCommand(cmdAdmin) 20 | } 21 | } 22 | 23 | var cmdAdmin = &cobra.Command{ 24 | Use: "admin", 25 | Run: func(cmd *cobra.Command, args []string) { 26 | cmd.Usage() 27 | }, 28 | } 29 | 30 | var cmdAdminList = &cobra.Command{ 31 | Short: "list admin users", 32 | Long: `Lists users in the admins ACL file.`, 33 | 34 | Use: "ls", 35 | Run: func(cmd *cobra.Command, args []string) { 36 | fmt.Fprintln(os.Stdout, readFile(Envy.Path("config/admins"))) 37 | }, 38 | } 39 | 40 | var cmdAdminRemove = &cobra.Command{ 41 | Short: "remove admin user", 42 | Long: `Removes user from the admins ACL file.`, 43 | 44 | Use: "rm ", 45 | Run: func(cmd *cobra.Command, args []string) { 46 | if len(args) == 0 { 47 | cmd.Usage() 48 | os.Exit(1) 49 | } 50 | var admins []string 51 | adminConfig := readFile(Envy.Path("config/admins")) 52 | for _, admin := range strings.Split(adminConfig, "\n") { 53 | if admin != args[0] { 54 | admins = append(admins, admin) 55 | } 56 | } 57 | writeFile(Envy.Path("config/admins"), strings.Join(admins, "\n")) 58 | }, 59 | } 60 | 61 | var cmdAdminAdd = &cobra.Command{ 62 | Short: "add admin user", 63 | Long: `Adds user to the admins ACL file.`, 64 | 65 | Use: "add ", 66 | Run: func(cmd *cobra.Command, args []string) { 67 | if len(args) == 0 { 68 | cmd.Usage() 69 | os.Exit(1) 70 | } 71 | if grepFile(Envy.Path("config/admins"), args[0]) { 72 | return 73 | } 74 | appendFile(Envy.Path("config/admins"), args[0]) 75 | }, 76 | } 77 | -------------------------------------------------------------------------------- /cmd/cmd.go: -------------------------------------------------------------------------------- 1 | package envy 2 | 3 | import ( 4 | "bufio" 5 | "log" 6 | "net" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | 11 | "golang.org/x/crypto/ssh" 12 | ) 13 | 14 | var EnvySocket = "/var/run/envy.sock" 15 | 16 | func ClientMode() bool { 17 | return exists(EnvySocket) 18 | } 19 | 20 | func RunClient(args []string) { 21 | log.SetFlags(0) 22 | client, err := ssh.Dial("unix", EnvySocket, &ssh.ClientConfig{HostKeyCallback: ssh.InsecureIgnoreHostKey()}) 23 | assert(err) 24 | session, err := client.NewSession() 25 | assert(err) 26 | session.Stdout = os.Stdout 27 | session.Stderr = os.Stderr 28 | session.Stdin = os.Stdin 29 | err = session.Run(strings.Join(args, " ")) 30 | session.Close() 31 | if err != nil { 32 | if exiterr, ok := err.(*ssh.ExitError); ok { 33 | os.Exit(exiterr.ExitStatus()) 34 | } else { 35 | assert(err) 36 | } 37 | } 38 | } 39 | 40 | func SetupLogging() { 41 | logSock := "/tmp/log.sock" 42 | if filepath.Base(os.Args[0]) != "serve" { 43 | log.SetFlags(0) 44 | conn, err := net.Dial("unix", logSock) 45 | assert(err) 46 | log.SetOutput(conn) 47 | } else { 48 | os.Remove(logSock) 49 | log.Println("Starting log service ...") 50 | ln, err := net.Listen("unix", logSock) 51 | assert(err) 52 | go func() { 53 | for { 54 | conn, err := ln.Accept() 55 | if err != nil { 56 | break 57 | } 58 | go func(conn net.Conn) { 59 | scanner := bufio.NewScanner(conn) 60 | for scanner.Scan() { 61 | log.Println(scanner.Text()) 62 | } 63 | assert(scanner.Err()) 64 | }(conn) 65 | } 66 | }() 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /cmd/environ.go: -------------------------------------------------------------------------------- 1 | package envy 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "os/exec" 8 | 9 | "github.com/fsouza/go-dockerclient" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | func init() { 14 | cmdEnviron.AddCommand(cmdEnvironRebuild) 15 | cmdEnviron.AddCommand(cmdEnvironList) 16 | Cmd.AddCommand(cmdEnviron) 17 | } 18 | 19 | var cmdEnviron = &cobra.Command{ 20 | Use: "environ", 21 | Run: func(cmd *cobra.Command, args []string) { 22 | cmd.Usage() 23 | }, 24 | } 25 | 26 | var cmdEnvironRebuild = &cobra.Command{ 27 | Short: "rebuild environment image", 28 | Long: `Rebuild does a Docker build with your environment Dockerfile.`, 29 | 30 | Use: "rebuild [--force]", // TODO 31 | Run: func(_ *cobra.Command, args []string) { 32 | session := GetSession(os.Getenv("ENVY_USER"), os.Getenv("ENVY_SESSION")) 33 | environ := session.Environ() 34 | log.Println(session.User.Name, "| rebuilding environ", environ.Name) 35 | cmd := exec.Command("/bin/docker", "build", "-t", environ.DockerImage(), ".") 36 | cmd.Dir = environ.Path() 37 | run(cmd) 38 | os.Exit(128) 39 | }, 40 | } 41 | 42 | var cmdEnvironList = &cobra.Command{ 43 | Short: "list environments", 44 | Long: `Lists environments for this user.`, 45 | 46 | Use: "ls", 47 | Run: func(cmd *cobra.Command, args []string) { 48 | session := GetSession(os.Getenv("ENVY_USER"), os.Getenv("ENVY_SESSION")) 49 | for _, environ := range session.User.Environs() { 50 | fmt.Println(environ) 51 | } 52 | }, 53 | } 54 | 55 | type Environ struct { 56 | User *User 57 | Name string 58 | } 59 | 60 | func (e *Environ) Path(parts ...string) string { 61 | return Envy.Path(append([]string{"users", e.User.Name, "environs", e.Name}, parts...)...) 62 | } 63 | 64 | func (e *Environ) DockerImage() string { 65 | return fmt.Sprintf("%s/%s", e.User.Name, e.Name) 66 | } 67 | 68 | func (e *Environ) DockerName() string { 69 | return fmt.Sprintf("%s.%s", e.User.Name, e.Name) 70 | } 71 | 72 | func GetEnviron(user, name string) *Environ { 73 | e := &Environ{ 74 | Name: name, 75 | User: GetUser(user), 76 | } 77 | if !exists(e.Path()) { 78 | copyTree(Envy.DataPath("environ"), e.Path()) 79 | } 80 | mkdirAll(e.Path("run")) 81 | if !dockerRunning(e.DockerName()) { 82 | dockerRemove(e.DockerName()) 83 | log.Println(user, "| starting dind for environ", e.Name) 84 | dockerRunDetached(docker.CreateContainerOptions{ 85 | Name: e.DockerName(), 86 | Config: &docker.Config{ 87 | Hostname: e.Name, 88 | Image: "progrium/dind:latest", 89 | }, 90 | HostConfig: &docker.HostConfig{ 91 | Privileged: true, 92 | RestartPolicy: docker.RestartPolicy{Name: "always"}, 93 | Binds: []string{ 94 | fmt.Sprintf("%s:/usr/bin/docker", Envy.HostPath("bin/docker")), 95 | fmt.Sprintf("%s:/var/run", Envy.HostPath(e.Path("run"))), 96 | }, 97 | }, 98 | }) 99 | } 100 | if !dockerImage(e.DockerImage()) { 101 | log.Println(user, "| building environ", e.Name) 102 | cmd := exec.Command("/bin/docker", "build", "-t", e.DockerImage(), ".") 103 | cmd.Dir = e.Path() 104 | assert(cmd.Run()) 105 | } 106 | return e 107 | } 108 | -------------------------------------------------------------------------------- /cmd/envy.go: -------------------------------------------------------------------------------- 1 | package envy 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "strings" 7 | 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | var Envy = new(EnvyRoot) 12 | 13 | var Cmd = &cobra.Command{ 14 | Use: "envy", 15 | Short: "", 16 | Run: func(cmd *cobra.Command, args []string) { 17 | cmd.Usage() 18 | }, 19 | } 20 | 21 | type EnvyRoot struct{} 22 | 23 | func (r *EnvyRoot) Path(parts ...string) string { 24 | return filepath.Join(append([]string{"/envy"}, parts...)...) 25 | } 26 | 27 | func (r *EnvyRoot) HostPath(parts ...string) string { 28 | return filepath.Join(os.Getenv("HOST_ROOT"), strings.TrimPrefix(filepath.Join(parts...), "/envy")) 29 | } 30 | 31 | func (r *EnvyRoot) DataPath(parts ...string) string { 32 | path := append([]string{"/tmp/data"}, parts...) 33 | return filepath.Join(path...) 34 | } 35 | 36 | func (r *EnvyRoot) Allow(user, environ string) bool { 37 | if !r.checkUserAcl(user) { 38 | return false 39 | } 40 | parts := strings.Split(environ, "/") 41 | if len(parts) > 1 { 42 | // TODO: shared environ acl 43 | return false 44 | } 45 | return true 46 | } 47 | 48 | func (r *EnvyRoot) checkUserAcl(user string) bool { 49 | if readFile(r.Path("config/users")) == "*" { 50 | return true 51 | } 52 | return grepFile(r.Path("config/users"), user) 53 | } 54 | 55 | func (r *EnvyRoot) Setup() { 56 | mkdirAll(r.Path("users")) 57 | mkdirAll(r.Path("config")) 58 | mkdirAll(r.Path("bin")) 59 | if !exists(r.Path("config/users")) { 60 | writeFile(r.Path("config/users"), "*") 61 | } 62 | os.RemoveAll(r.Path("bin/envy")) 63 | os.RemoveAll(r.Path("bin/docker")) 64 | copy("/bin/envy", r.Path("bin/envy")) 65 | copy("/bin/docker", r.Path("bin/docker")) 66 | } 67 | -------------------------------------------------------------------------------- /cmd/http.go: -------------------------------------------------------------------------------- 1 | package envy 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "os" 8 | "os/exec" 9 | "strings" 10 | 11 | "github.com/progrium/envy/pkg/hterm" 12 | ) 13 | 14 | func init() { 15 | http.HandleFunc("/u/", func(w http.ResponseWriter, r *http.Request) { 16 | parts := strings.Split(r.URL.Path, "/") 17 | if len(parts) < 3 { 18 | http.NotFound(w, r) 19 | return 20 | } 21 | pathUser := parts[2] 22 | var pathEnv, sshUser string 23 | if len(parts) > 3 && parts[3] != "hterm" { 24 | pathEnv = parts[3] 25 | sshUser = pathUser + "+" + pathEnv 26 | } else { 27 | sshUser = pathUser 28 | } 29 | // passthrough auth for hterm. use cookie to do this right 30 | if !strings.Contains(r.URL.Path, "hterm") { 31 | user, passwd, ok := r.BasicAuth() 32 | if !ok || user != pathUser || !githubUserAuth(user, passwd) { 33 | w.Header().Set("WWW-Authenticate", fmt.Sprintf("Basic realm=\"%s\"", pathUser)) 34 | http.Error(w, "Unauthorized", http.StatusUnauthorized) 35 | log.Println("auth[http]: not allowing", user) 36 | return 37 | } 38 | log.Println("auth[http]: allowing", user) 39 | } 40 | w.Header().Set("Hterm-Title", "Envy Term") 41 | hterm.Handle(w, r, func(args string) *hterm.Pty { 42 | cmd := exec.Command("/bin/enter", parts[2]) 43 | cmd.Env = os.Environ() 44 | cmd.Env = append(cmd.Env, fmt.Sprintf("USER=%s", sshUser)) 45 | pty, err := hterm.NewPty(cmd) 46 | if err != nil { 47 | log.Fatal(err) 48 | } 49 | return pty 50 | }) 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /cmd/session.go: -------------------------------------------------------------------------------- 1 | package envy 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "net" 8 | "os" 9 | "os/exec" 10 | "strings" 11 | "syscall" 12 | "time" 13 | 14 | "github.com/spf13/cobra" 15 | "golang.org/x/crypto/ssh" 16 | ) 17 | 18 | func init() { 19 | cmdSession.AddCommand(cmdSessionReload) 20 | cmdSession.AddCommand(cmdSessionCommit) 21 | cmdSession.AddCommand(cmdSessionSwitch) 22 | Cmd.AddCommand(cmdSession) 23 | } 24 | 25 | var cmdSession = &cobra.Command{ 26 | Use: "session", 27 | Run: func(cmd *cobra.Command, args []string) { 28 | cmd.Usage() 29 | }, 30 | } 31 | 32 | var cmdSessionReload = &cobra.Command{ 33 | Short: "reload session from environment image", 34 | Long: `Reload recreates the current session container from the environment image.`, 35 | 36 | Use: "reload", 37 | Run: func(cmd *cobra.Command, args []string) { 38 | session := GetSession(os.Getenv("ENVY_USER"), os.Getenv("ENVY_SESSION")) 39 | log.Println(session.User.Name, "| reloading session", session.Name) 40 | os.Exit(128) 41 | }, 42 | } 43 | 44 | var cmdSessionCommit = &cobra.Command{ 45 | Short: "commit session changes to environment image", 46 | Long: `Commit saves changes made in the session to the environment image.`, 47 | 48 | Use: "commit []", 49 | Run: func(cmd *cobra.Command, args []string) { 50 | session := GetSession(os.Getenv("ENVY_USER"), os.Getenv("ENVY_SESSION")) 51 | var environ *Environ 52 | if len(args) > 0 { 53 | environ = GetEnviron(os.Getenv("ENVY_USER"), args[0]) 54 | } else { 55 | environ = session.Environ() 56 | } 57 | log.Println(session.User.Name, "| committing session", session.Name, "to", environ.Name) 58 | fmt.Fprintf(os.Stdout, "Committing to %s ...\n", environ.Name) 59 | dockerCommit(session.DockerName(), environ.DockerImage()) 60 | os.Exit(128) 61 | }, 62 | } 63 | 64 | var cmdSessionSwitch = &cobra.Command{ 65 | Short: "switch session to different environment", 66 | Long: `Switch reloads session from a new environment image.`, 67 | 68 | Use: "switch ", 69 | Run: func(cmd *cobra.Command, args []string) { 70 | if len(args) == 0 { 71 | return 72 | } 73 | session := GetSession(os.Getenv("ENVY_USER"), os.Getenv("ENVY_SESSION")) 74 | session.SetEnviron(args[0]) 75 | log.Println(session.User.Name, "| switching session", session.Name, "to", args[0]) 76 | os.Exit(128) 77 | }, 78 | } 79 | 80 | type Session struct { 81 | User *User 82 | Name string 83 | } 84 | 85 | func (s *Session) Environ() *Environ { 86 | return GetEnviron(s.User.Name, readFile(s.Path("environ"))) 87 | } 88 | 89 | func (s *Session) SetEnviron(name string) { 90 | writeFile(s.Path("environ"), name) 91 | } 92 | 93 | func (s *Session) Path(parts ...string) string { 94 | return Envy.Path(append([]string{"users", s.User.Name, "sessions", s.Name}, parts...)...) 95 | } 96 | 97 | func (s *Session) DockerName() string { 98 | return s.Name 99 | } 100 | 101 | func (s *Session) Enter(environ *Environ) int { 102 | defer s.Cleanup() 103 | log.Println(s.User.Name, "| entering session", s.Name) 104 | os.Setenv("ENVY_USER", s.User.Name) 105 | os.Setenv("ENVY_SESSION", s.Name) 106 | s.SetEnviron(environ.Name) 107 | fmt.Fprintln(os.Stdout, "Entering session...") 108 | envySock := startSessionServer(s.Path("run/envy.sock")) 109 | defer envySock.Close() 110 | for { 111 | dockerRemove(s.Name) 112 | environ := s.Environ() 113 | args := []string{"run", "-it", 114 | fmt.Sprintf("--name=%s", s.Name), 115 | fmt.Sprintf("--net=container:%s", environ.DockerName()), 116 | 117 | fmt.Sprintf("--env=HOSTNAME=%s", environ.Name), 118 | fmt.Sprintf("--env=ENVY_RELOAD=%v", int32(time.Now().Unix())), 119 | fmt.Sprintf("--env=ENVY_SESSION=%s", s.Name), 120 | fmt.Sprintf("--env=ENVY_USER=%s", s.User.Name), 121 | "--env=DOCKER_HOST=unix:///var/run/docker.sock", 122 | "--env=ENV=/etc/envyrc", 123 | 124 | fmt.Sprintf("--volume=%s:/var/run/docker.sock", Envy.HostPath(environ.Path("run/docker.sock"))), 125 | fmt.Sprintf("--volume=%s:/var/run/envy.sock:ro", Envy.HostPath(s.Path("run/envy.sock"))), 126 | fmt.Sprintf("--volume=%s:/etc/envyrc:ro", Envy.HostPath(environ.Path("envyrc"))), 127 | fmt.Sprintf("--volume=%s:/root/environ", Envy.HostPath(environ.Path())), 128 | fmt.Sprintf("--volume=%s:/root", Envy.HostPath(s.User.Path("root"))), 129 | fmt.Sprintf("--volume=%s:/home/%s", Envy.HostPath(s.User.Path("home")), s.User.Name), 130 | fmt.Sprintf("--volume=%s:/sbin/envy:ro", Envy.HostPath("bin/envy")), 131 | } 132 | if s.User.Admin() { 133 | args = append(args, fmt.Sprintf("--volume=%s:/envy", Envy.HostPath())) 134 | } 135 | args = append(args, environ.DockerImage()) 136 | if dockerShellCmd(environ.DockerImage()) != nil { 137 | args = append(args, dockerShellCmd(environ.DockerImage())...) 138 | } 139 | status := run(exec.Command("/bin/docker", args...)) 140 | if status != 128 { 141 | return status 142 | } 143 | } 144 | } 145 | 146 | func (s *Session) Cleanup() { 147 | log.Println("Cleaning up") 148 | dockerRemove(s.Name) 149 | os.Remove(s.Path("run/envy.sock")) 150 | } 151 | 152 | func NewSession(user string) *Session { 153 | return GetSession(user, nextSessionName(GetUser(user))) 154 | } 155 | 156 | func GetSession(user, name string) *Session { 157 | u := GetUser(user) 158 | s := &Session{ 159 | Name: name, 160 | User: u, 161 | } 162 | mkdirAll(s.Path("run")) 163 | return s 164 | } 165 | 166 | func nextSessionName(user *User) string { 167 | n := 0 168 | // TODO: panic on max n 169 | // TODO: clean up sessions without docker running 170 | for { 171 | s := user.Session(fmt.Sprintf("%s.%v", user.Name, n)) 172 | if !exists(s.Path()) { 173 | return s.Name 174 | } 175 | n += 1 176 | } 177 | } 178 | 179 | func startSessionServer(path string) net.Listener { 180 | os.Remove(path) 181 | ln, err := net.Listen("unix", path) 182 | assert(err) 183 | go func() { 184 | for { 185 | conn, err := ln.Accept() 186 | if err != nil { 187 | break 188 | } 189 | go handleSSHConn(conn) 190 | } 191 | }() 192 | return ln 193 | } 194 | 195 | func handleSSHConn(conn net.Conn) { 196 | defer conn.Close() 197 | config := &ssh.ServerConfig{NoClientAuth: true} 198 | privateBytes, err := ioutil.ReadFile(Envy.DataPath("id_host")) 199 | assert(err) 200 | private, err := ssh.ParsePrivateKey(privateBytes) 201 | assert(err) 202 | config.AddHostKey(private) 203 | _, chans, reqs, err := ssh.NewServerConn(conn, config) 204 | if err != nil { 205 | log.Println(err) 206 | return 207 | } 208 | go ssh.DiscardRequests(reqs) 209 | for ch := range chans { 210 | if ch.ChannelType() != "session" { 211 | ch.Reject(ssh.UnknownChannelType, "unsupported channel type") 212 | continue 213 | } 214 | go handleSSHChannel(ch) 215 | } 216 | } 217 | 218 | func handleSSHChannel(newChan ssh.NewChannel) { 219 | ch, reqs, err := newChan.Accept() 220 | if err != nil { 221 | log.Println("handle channel failed:", err) 222 | return 223 | } 224 | for req := range reqs { 225 | go func(req *ssh.Request) { 226 | if req.WantReply { 227 | req.Reply(true, nil) 228 | } 229 | switch req.Type { 230 | case "exec": 231 | defer ch.Close() 232 | var payload = struct{ Value string }{} 233 | ssh.Unmarshal(req.Payload, &payload) 234 | line := strings.Trim(payload.Value, "\n") 235 | var args []string 236 | if line != "" { 237 | args = strings.Split(line, " ") 238 | } 239 | cmd := exec.Command("/bin/envy", args...) 240 | cmd.Stdout = ch 241 | cmd.Stderr = ch.Stderr() 242 | err := cmd.Run() 243 | status := struct{ Status uint32 }{0} 244 | if err != nil { 245 | if exiterr, ok := err.(*exec.ExitError); ok { 246 | if stat, ok := exiterr.Sys().(syscall.WaitStatus); ok { 247 | status = struct{ Status uint32 }{uint32(stat.ExitStatus())} 248 | } else { 249 | assert(err) 250 | } 251 | } 252 | } 253 | _, err = ch.SendRequest("exit-status", false, ssh.Marshal(&status)) 254 | assert(err) 255 | return 256 | } 257 | }(req) 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /cmd/system.go: -------------------------------------------------------------------------------- 1 | package envy 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "net/http" 8 | "os" 9 | "os/exec" 10 | "path/filepath" 11 | "strings" 12 | 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | func CheckSystemCmd() { 17 | cmd := filepath.Base(os.Args[0]) 18 | systemCmds := []string{"enter", "auth", "serve"} 19 | for i := range systemCmds { 20 | if cmd == systemCmds[i] { 21 | os.Args = append([]string{os.Args[0], cmd}, os.Args[1:]...) 22 | Cmd.AddCommand(cmdEnter) 23 | Cmd.AddCommand(cmdAuth) 24 | Cmd.AddCommand(cmdServe) 25 | return 26 | } 27 | } 28 | } 29 | 30 | var cmdEnter = &cobra.Command{ 31 | Use: "enter", 32 | Run: func(cmd *cobra.Command, args []string) { 33 | user, environ := parseUserEnviron(os.Getenv("USER")) 34 | if !Envy.Allow(user, environ) { 35 | fmt.Fprintln(os.Stderr, "User is forbidden.") 36 | os.Exit(2) 37 | } 38 | os.Exit(NewSession(user).Enter(GetEnviron(user, environ))) 39 | }, 40 | } 41 | 42 | var cmdAuth = &cobra.Command{ 43 | Use: "auth ", 44 | Run: func(cmd *cobra.Command, args []string) { 45 | if len(args) < 2 { 46 | os.Exit(2) 47 | return 48 | } 49 | if os.Getenv("ENVY_NOAUTH") != "" { 50 | log.Println("Warning: ENVY_NOAUTH is set allowing all SSH connections") 51 | return 52 | } 53 | user, _ := parseUserEnviron(args[0]) 54 | if !githubKeyAuth(user, args[1]) { 55 | log.Println("auth[ssh]: not allowing", user) 56 | os.Exit(1) 57 | } 58 | log.Println("auth[ssh]: allowing", user) 59 | }, 60 | } 61 | 62 | var cmdServe = &cobra.Command{ 63 | Use: "serve", 64 | Run: func(cmd *cobra.Command, args []string) { 65 | log.Println("Setting up Envy root ...") 66 | Envy.Setup() 67 | 68 | go func() { 69 | log.Println("Pulling progrium/dind:latest ...") 70 | exec.Command("/bin/docker", "pull", "progrium/dind:latest").Run() 71 | }() 72 | 73 | log.Println("Starting HTTP server on 80 ...") 74 | log.Fatal(http.ListenAndServe(":80", nil)) 75 | }, 76 | } 77 | 78 | func parseUserEnviron(in string) (string, string) { 79 | parts := strings.Split(in, "+") 80 | user := parts[0] 81 | var env string 82 | if len(parts) > 1 { 83 | env = parts[1] 84 | } else { 85 | env = user 86 | } 87 | return user, env 88 | } 89 | 90 | func githubUserAuth(user, passwd string) bool { 91 | client := &http.Client{} 92 | req, _ := http.NewRequest("GET", "https://api.github.com", nil) 93 | req.SetBasicAuth(user, passwd) 94 | resp, _ := client.Do(req) 95 | return resp.StatusCode == 200 96 | } 97 | 98 | func githubKeyAuth(user, key string) bool { 99 | client := &http.Client{} 100 | req, _ := http.NewRequest("GET", fmt.Sprintf("https://github.com/%s.keys", user), nil) 101 | resp, _ := client.Do(req) 102 | if resp.StatusCode != 200 { 103 | return false 104 | } 105 | data, err := ioutil.ReadAll(resp.Body) 106 | if err != nil { 107 | return false 108 | } 109 | return strings.Contains(string(data), key) 110 | } 111 | -------------------------------------------------------------------------------- /cmd/user.go: -------------------------------------------------------------------------------- 1 | package envy 2 | 3 | type User struct { 4 | Name string 5 | } 6 | 7 | func (u *User) Path(parts ...string) string { 8 | return Envy.Path(append([]string{"users", u.Name}, parts...)...) 9 | } 10 | 11 | func (u *User) Admin() bool { 12 | if !exists(Envy.Path("config/admins")) { 13 | writeFile(Envy.Path("config/admins"), u.Name) 14 | } 15 | return grepFile(Envy.Path("config/admins"), u.Name) 16 | } 17 | 18 | func (u *User) Environ(name string) *Environ { 19 | return &Environ{ 20 | Name: name, 21 | User: u, 22 | } 23 | } 24 | 25 | func (u *User) Session(name string) *Session { 26 | return &Session{ 27 | Name: name, 28 | User: u, 29 | } 30 | } 31 | 32 | func (u *User) Environs() []string { 33 | return dirs(u.Path("environs")) 34 | } 35 | 36 | func GetUser(name string) *User { 37 | u := &User{ 38 | Name: name, 39 | } 40 | mkdirAll(u.Path()) 41 | mkdirAll(u.Path("environs")) 42 | mkdirAll(u.Path("sessions")) 43 | if !exists(u.Path("home")) { 44 | mkdirAll(u.Path("home")) 45 | copy(Envy.DataPath("home", ".bashrc"), 46 | u.Path("home", ".bashrc")) 47 | } 48 | if !exists(u.Path("root")) { 49 | mkdirAll(u.Path("root")) 50 | copy(Envy.DataPath("home", ".bashrc"), 51 | u.Path("root", ".bashrc")) 52 | } 53 | return u 54 | } 55 | -------------------------------------------------------------------------------- /cmd/util.go: -------------------------------------------------------------------------------- 1 | package envy 2 | 3 | import ( 4 | "archive/tar" 5 | "bytes" 6 | "compress/gzip" 7 | "io" 8 | "io/ioutil" 9 | "os" 10 | "os/exec" 11 | "path/filepath" 12 | "strings" 13 | "syscall" 14 | 15 | "github.com/fsouza/go-dockerclient" 16 | "github.com/termie/go-shutil" 17 | ) 18 | 19 | var ( 20 | dockerEndpoint = "unix:///var/run/docker.sock" 21 | ) 22 | 23 | func run(cmd *exec.Cmd) int { 24 | cmd.Stdin = os.Stdin 25 | cmd.Stdout = os.Stdout 26 | cmd.Stderr = os.Stderr 27 | err := cmd.Run() 28 | if err != nil { 29 | if exiterr, ok := err.(*exec.ExitError); ok { 30 | if stat, ok := exiterr.Sys().(syscall.WaitStatus); ok { 31 | return int(stat.ExitStatus()) 32 | } else { 33 | assert(err) 34 | } 35 | } 36 | } 37 | return 0 38 | } 39 | 40 | func assert(err error) { 41 | if err != nil { 42 | panic(err) // TODO: replace me 43 | } 44 | } 45 | 46 | func exists(path ...string) bool { 47 | _, err := os.Stat(filepath.Join(path...)) 48 | if err == nil { 49 | return true 50 | } 51 | if os.IsNotExist(err) { 52 | return false 53 | } 54 | assert(err) 55 | return true 56 | } 57 | 58 | func dirs(path string) []string { 59 | var dirs []string 60 | dir, err := ioutil.ReadDir(path) 61 | assert(err) 62 | for _, fi := range dir { 63 | if fi.IsDir() { 64 | dirs = append(dirs, fi.Name()) 65 | } 66 | } 67 | return dirs 68 | } 69 | 70 | func readFile(path string) string { 71 | data, err := ioutil.ReadFile(path) 72 | if err != nil { 73 | return "" 74 | } 75 | return strings.Trim(string(data), "\n ") 76 | } 77 | 78 | func normalizeLine(s string) string { 79 | return strings.Trim(s, "\n") + "\n" 80 | } 81 | 82 | func writeFile(path, data string) { 83 | assert(ioutil.WriteFile(path, []byte(normalizeLine(data)), 0644)) 84 | } 85 | 86 | func appendFile(path, data string) { 87 | f, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0644) 88 | assert(err) 89 | defer f.Close() 90 | _, err = f.WriteString(normalizeLine(data)) 91 | assert(err) 92 | } 93 | 94 | func grepFile(path, line string) bool { 95 | for _, l := range strings.Split(readFile(path), "\n") { 96 | if l == line { 97 | return true 98 | } 99 | } 100 | return false 101 | } 102 | 103 | func mkdirAll(path ...string) { 104 | assert(os.MkdirAll(filepath.Join(path...), 0777)) 105 | } 106 | 107 | func copy(src, dst string) { 108 | s, err := os.Open(src) 109 | assert(err) 110 | defer s.Close() 111 | d, err := os.Create(dst) 112 | assert(err) 113 | if _, err := io.Copy(d, s); err != nil { 114 | d.Close() 115 | assert(err) 116 | } 117 | assert(d.Close()) 118 | fi, err := os.Stat(src) 119 | assert(err) 120 | assert(os.Chmod(dst, fi.Mode())) 121 | } 122 | 123 | func copyTree(src, dst string) { 124 | assert(shutil.CopyTree(src, dst, nil)) 125 | } 126 | 127 | func dockerImage(image string) bool { 128 | client, err := docker.NewClient(dockerEndpoint) 129 | assert(err) 130 | images, err := client.ListImages(docker.ListImagesOptions{}) 131 | assert(err) 132 | for _, img := range images { 133 | for _, tag := range img.RepoTags { 134 | if tag == image { 135 | return true 136 | } 137 | repo := strings.Split(tag, ":") 138 | if repo[0] == image { 139 | return true 140 | } 141 | } 142 | } 143 | return false 144 | } 145 | 146 | func dockerRunning(container string) bool { 147 | client, err := docker.NewClient(dockerEndpoint) 148 | assert(err) 149 | containers, err := client.ListContainers(docker.ListContainersOptions{ 150 | Filters: map[string][]string{ 151 | "status": []string{"running"}, 152 | }, 153 | }) 154 | assert(err) 155 | for _, cntr := range containers { 156 | if cntr.ID == container { 157 | return true 158 | } 159 | for _, name := range cntr.Names { 160 | if name[1:] == container { 161 | return true 162 | } 163 | } 164 | } 165 | return false 166 | } 167 | 168 | func dockerRemove(container string) { 169 | client, err := docker.NewClient(dockerEndpoint) 170 | assert(err) 171 | client.RemoveContainer(docker.RemoveContainerOptions{ 172 | ID: container, 173 | Force: true, 174 | }) 175 | } 176 | 177 | func dockerCommit(container, image string) { 178 | client, err := docker.NewClient(dockerEndpoint) 179 | assert(err) 180 | _, err = client.CommitContainer(docker.CommitContainerOptions{ 181 | Container: container, 182 | Repository: image, 183 | }) 184 | assert(err) 185 | } 186 | 187 | func dockerShellCmd(image string) []string { 188 | client, err := docker.NewClient(dockerEndpoint) 189 | assert(err) 190 | img, err := client.InspectImage(image) 191 | assert(err) 192 | if img.Config.Cmd != nil || img.Config.Entrypoint != nil { 193 | return nil 194 | } 195 | return []string{"/bin/sh"} 196 | } 197 | 198 | func dockerBuild(contextPath, image string, output io.Writer) { 199 | client, err := docker.NewClient(dockerEndpoint) 200 | assert(err) 201 | context := bytes.NewBuffer(nil) 202 | tarGzip(context, contextPath) 203 | assert(client.BuildImage(docker.BuildImageOptions{ 204 | Name: image, 205 | OutputStream: output, 206 | InputStream: context, 207 | })) 208 | } 209 | 210 | func tarGzip(target io.Writer, path string) { 211 | gw := gzip.NewWriter(target) 212 | defer gw.Close() 213 | tw := tar.NewWriter(gw) 214 | defer tw.Close() 215 | tarFile := func(_path string, tw *tar.Writer, fi os.FileInfo) { 216 | fr, err := os.Open(_path) 217 | assert(err) 218 | defer fr.Close() 219 | h := new(tar.Header) 220 | h.Name = _path[len(path):] 221 | h.Size = fi.Size() 222 | h.Mode = int64(fi.Mode()) 223 | h.ModTime = fi.ModTime() 224 | err = tw.WriteHeader(h) 225 | assert(err) 226 | _, err = io.Copy(tw, fr) 227 | assert(err) 228 | } 229 | tarDir(path, tw, tarFile) 230 | } 231 | 232 | func tarDir(path string, tw *tar.Writer, tarFile func(string, *tar.Writer, os.FileInfo)) { 233 | dir, err := os.Open(path) 234 | assert(err) 235 | defer dir.Close() 236 | fis, err := dir.Readdir(0) 237 | assert(err) 238 | for _, fi := range fis { 239 | curPath := path + "/" + fi.Name() 240 | if fi.IsDir() { 241 | tarDir(curPath, tw, tarFile) 242 | } else { 243 | tarFile(curPath, tw, fi) 244 | } 245 | } 246 | } 247 | 248 | func dockerRunDetached(opts docker.CreateContainerOptions) { 249 | client, err := docker.NewClient(dockerEndpoint) 250 | assert(err) 251 | cntr, err := client.CreateContainer(opts) 252 | assert(err) 253 | assert(client.StartContainer(cntr.ID, nil)) 254 | } 255 | 256 | func dockerRunInteractive(opts docker.CreateContainerOptions, stdin io.Reader, stdout, stderr io.Writer) int { 257 | client, err := docker.NewClient(dockerEndpoint) 258 | assert(err) 259 | cntr, err := client.CreateContainer(opts) 260 | assert(err) 261 | go client.AttachToContainer(docker.AttachToContainerOptions{ 262 | Container: cntr.ID, 263 | Stream: true, 264 | Stdin: true, 265 | Stdout: true, 266 | Stderr: true, 267 | RawTerminal: true, 268 | InputStream: stdin, 269 | OutputStream: stdout, 270 | ErrorStream: stderr, 271 | }) 272 | assert(client.StartContainer(cntr.ID, nil)) 273 | status, err := client.WaitContainer(cntr.ID) 274 | assert(err) 275 | return status 276 | } 277 | -------------------------------------------------------------------------------- /data/environ/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu 2 | -------------------------------------------------------------------------------- /data/environ/envyrc: -------------------------------------------------------------------------------- 1 | alias reload="exec envy session reload" 2 | alias switch="exec envy session switch" 3 | alias commit="exec envy session commit" 4 | alias rebuild="exec envy environ rebuild" 5 | 6 | unset ENV 7 | 8 | export USER="${USER:-$(id -u -n)}" 9 | 10 | if [ -f ~/.envyrc ]; then 11 | source ~/.envyrc 12 | fi 13 | 14 | if [ -f "/home/$ENVY_USER/.envyrc_$USER" ]; then 15 | source "/home/$ENVY_USER/.envyrc_$USER" 16 | fi 17 | -------------------------------------------------------------------------------- /data/home/.bashrc: -------------------------------------------------------------------------------- 1 | source /etc/envyrc 2 | 3 | if [ "$TERM" = "xterm-color" ] || \ 4 | ([ -x /usr/bin/tput ] && tput setaf 1 >&/dev/null); then 5 | PS1='\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ ' 6 | else 7 | PS1='\u@\h:\w\$ ' 8 | fi 9 | -------------------------------------------------------------------------------- /data/id_host: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIJKAIBAAKCAgEAuKy/4CBa45CCY+JVKuAHvWhgrxfRgOCETfA2Jc2WkigE3bUf 3 | xgo9bbEktT2OCaIiepZCcdAFNTE6uq05fb+ZMBx962Nw8YU1+quD8VL4yYjRVc+U 4 | lZ+8PyYsYDdExAddyga+ng6a+p+y4uyEiCXQtHLFUpYTXpf0OnnkJvFnIb7PMLAs 5 | 6rrZQVZAhkxyx/nHqFM4hggP15SW2+jaylWJvNw3v4pdPW8Ul2KSmQahXrzxJpCy 6 | foywNBTjZqVKqQ7IexnEMJq4ycUHgTrJuUB/Fttk8oIuoxXhnxB/FG9tkyzPSnbn 7 | rykqVEEklV+fg8a5kYSZKsgkNL0W3LVI5Zfj4lEvKvsNGjvVZ4cXL3RMX/H5b73S 8 | X31hDXet9E6Q8pdnWJbUL8CXp3PnaPniUxXfHsaLONzX36JI8qW1gNOgdOvc5a50 9 | QGw4+uNpplfUVEX66eVIposU0Ss2M5fNmOZK+itr8xzgJrf/AdRrG0EmPUxCf82q 10 | WmRq0rp6PUf1BM17Pev+Fdu7sLvg2UcEIHJMcNsdMJ90KnsP1wQoThahCF77nAlo 11 | FgeiuI1kLmePuqUUwIgeGtDMBC1EltE9TF9JVKQUiAfpHRueyhTxsI7JcmSxj4jW 12 | 5xk6kWXAo5F8PxSW4iNTmzLo7PQ5j4Pir5JSoV9UOo5zjuQU7TZAALw5lJcCAwEA 13 | AQKCAgBrZcNcc1SIHQVHU1vWSF0X8LixeveSrH8k2DqVN3+GVhGmYewtfs0Ems1P 14 | PZH51jmY8wOHLsOokI2n/I9/qspKqXctSjJnsuGWeuKLmIYophGfhs3RSgju7KNH 15 | /TxXiDUqBUwbnUDR2cftokDc8Kj0F/7bLX3sOBCHZVWitCcW/+F8XihxBeLM1X1G 16 | 3PSviXpsUKGBiPS1mas2DrAWlTI6DBO7p5rb3FqsQ14f8jQsZVMU63dykxzx3Of2 17 | TNAjiv5aYLywy3oFlTjtFaQ0wOZoA4UsZzr6CwKgP7a3yql9usR+eLH6MsV0JEJD 18 | QE4DE7Hdh7CPqb8skH7YMfFH3FJg0XAHGRJtvHPD1R/m05miGn3sHWUcwvsTKPdM 19 | MBf3UAokV7CZIkx+ETcJqVlS3ImyJIeoaEKS2FM12Tdt8s+kW702Lx+RrkwiZk8I 20 | ToOuXpwLi3PbdvCTJX4G/voEa80ORhX4IBjHE0x78h5yBHcK9RVTpN5t1edcHc00 21 | BSLmroaaSeEMDhhHzlyAVOP3IobEsHu4+MzBtzyxpSG1t9LeTm86YhM0gmcSjcER 22 | w5dk9jg33VxFbMq66ALnPM9Rl3su+6cr2TUn13PnCxP2pLIF2EJw5wtKtC9LCAiC 23 | OCkfKThqBYE8ho9t9zEG6UkrsdhrG8cNupP2Hznr6ZUDhQ1CKQKCAQEA4I5mbsWN 24 | JIxzpMI2kmcSEMn7ZW87D+vOVwU0mjklQDQOiiK9ky2BK3doQnsFA8ik0WhYD3Cz 25 | WcKNj0dn5wXO41Aqkmoo1VM5axs03fd+hImq5xQBgb/lPPO0x/I7/LE42JQaVG3P 26 | eha9agqGe42il3J36ud8OvjhEZZWcMD+OvFnztDtKlUKPg8AcEp6Nc10NX4QPjE5 27 | nLae4wavhepnw4Z/TI6spte4xaEZhEnzMIUCUqYS/XzdyekU4UaNVNohRspwfZ6e 28 | oRQ69ViVmdqHXfTGpORAbEQhfikjFwLa7nFWU7DEWuYwHTXCpRdEEXPfkOCtea34 29 | tP8oCoaRi5KgRQKCAQEA0oi7MNGvUG941PJt6etWhEVQVYEUDRzB86V4UKFJ5DfL 30 | MHDS/rprnc7mSG8rQ8g71qSbr7N+/SC1AIZFL9DaQSYhmZfAkQxfgF7Xez8ToSUW 31 | 1FmikOXkJTkDu2Wv/OpFD4L6TnLlqnEAOoKfDlUqBDzYjPrGOIJ8zG8K9+h+5vlp 32 | Weh3SvWzPhTPPpRUjJawdLFycXAB8xgbUmrVkqaPS7zzTJ6wWC2TgWzvLMLBlKWS 33 | f5GFxCsW0D0568g/2ELJHILK2eY3tKaNOQIIB+rZyffPvQW1YeNi+q1xtdJ32OfR 34 | BbEUK4/nDR6IYFQwXjQg9oejSiH49EdQGV/WXFcVKwKCAQEAupk6P1RD3BomQsPs 35 | Sy4BEhh1oi2S+8DsXt2Bf6J69OYNKvaBZ9rJWpBH/+5wFVvWsfiuLG5vauhDb8tb 36 | aNsntza3maFDuzkEHp+mB8kQxhwL+ydhtSr71/F/ySLefDXcUgSH+J6jaQWacpK6 37 | e9MPSCAjy/x9/BcyF8ZAoEOPPvW5WF++pI20DCu1JpqNAUZwCb9uye9nu3T5hRfa 38 | JULK5OxPvhNVHvNlpDwhkw9MWYY9juZYI3JubskTw7s32EnGmye/4HM9yAaik5v/ 39 | /LBeClJL+1t8uTrIRijy8r05pihiHvtlv09Grg8tZrh1pLcQETjSjqllYSoiYNS/ 40 | /yZhrQKCAQAJm8HnTXqR0jSSi2nmxh3RtZQgAt8WZhyX8RJXo7TKnJ1CXbPTelCV 41 | CC9MWP6Bfm70sdiFIU0HYmnAV2Bq+T4swP/BkcJxHD5zjmCJOGy96wJquJiJwmQy 42 | KrL354ErqslyFskzsVy39aZMBVAbCFn9jYVYkc1gINxvPBYlEFBSXEmpl8lx+1qt 43 | 16dJtN1S+UGeYcbWVIVSQeRlU4jhw4ZAr6Pu+EMWEyZrPrx/r3fEP/Y6qjqPpGPL 44 | JzAwiZgYV5v0GCgH39DlBsDlPCl/qwE7jXrGpq8Lg6QtyqKo6K0dkh7hAp7oCg5C 45 | dAVHWDBI7Fogxjn2lSxWgbavIceXuUW3AoIBAH7iMw27THfn6wjQSDLLE0yXNfGG 46 | CyR+ojyFAoPvGJtstZNhUNGH4bmrS0HOE/+6PK2kq/d9QubCeWVEp0Ex5MpDDyEt 47 | usob6r9db7wgY/xrO0Vk2NJbZVCJPl7PmJE4B59eSiiMe05OUWIEgPFBin9384lH 48 | /ZYThYhjWRh3Kqumm0v3o3IUpNAR8wC9L+cNHoc6mGjm6RhUaVSCLMmq4xjX5RIj 49 | 1BCIxtNX5mj2X4unEzOSIluVxk8mrRTi+Ih6rPUK/MPx1RrAPAUrqBgMy/+128ZO 50 | idbQIbiZqccdB+Hd0h/frErL4L5i9MB031YuWFRwyiRQQv6RIJERNkWUUuY= 51 | -----END RSA PRIVATE KEY----- 52 | -------------------------------------------------------------------------------- /data/id_host.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC4rL/gIFrjkIJj4lUq4Ae9aGCvF9GA4IRN8DYlzZaSKATdtR/GCj1tsSS1PY4JoiJ6lkJx0AU1MTq6rTl9v5kwHH3rY3DxhTX6q4PxUvjJiNFVz5SVn7w/JixgN0TEB13KBr6eDpr6n7Li7ISIJdC0csVSlhNel/Q6eeQm8Wchvs8wsCzqutlBVkCGTHLH+ceoUziGCA/XlJbb6NrKVYm83De/il09bxSXYpKZBqFevPEmkLJ+jLA0FONmpUqpDsh7GcQwmrjJxQeBOsm5QH8W22Tygi6jFeGfEH8Ub22TLM9KduevKSpUQSSVX5+DxrmRhJkqyCQ0vRbctUjll+PiUS8q+w0aO9VnhxcvdExf8flvvdJffWENd630TpDyl2dYltQvwJenc+do+eJTFd8exos43NffokjypbWA06B069zlrnRAbDj642mmV9RURfrp5UimixTRKzYzl82Y5kr6K2vzHOAmt/8B1GsbQSY9TEJ/zapaZGrSuno9R/UEzXs96/4V27uwu+DZRwQgckxw2x0wn3Qqew/XBChOFqEIXvucCWgWB6K4jWQuZ4+6pRTAiB4a0MwELUSW0T1MX0lUpBSIB+kdG57KFPGwjslyZLGPiNbnGTqRZcCjkXw/FJbiI1ObMujs9DmPg+KvklKhX1Q6jnOO5BTtNkAAvDmUlw== dummy@example.com 2 | -------------------------------------------------------------------------------- /envy.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/progrium/envy/cmd" 7 | ) 8 | 9 | func main() { 10 | if envy.ClientMode() { 11 | envy.RunClient(os.Args[1:]) 12 | return 13 | } 14 | 15 | envy.Envy.Setup() 16 | envy.SetupLogging() 17 | envy.CheckAdminCmd() 18 | envy.CheckSystemCmd() 19 | envy.Cmd.Execute() 20 | } 21 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/arschles/progrium-envy 2 | 3 | require ( 4 | github.com/fsouza/go-dockerclient v1.3.1 // indirect 5 | github.com/kr/pty v1.1.3 // indirect 6 | github.com/progrium/envy v0.0.0-20181018172300-dd23d719faaa 7 | github.com/spf13/cobra v0.0.3 // indirect 8 | github.com/spf13/pflag v1.0.3 // indirect 9 | github.com/termie/go-shutil v0.0.0-20140729215957-bcacb06fecae // indirect 10 | golang.org/x/crypto v0.0.0-20181015023909-0c41d7ab0a0e // indirect 11 | golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519 // indirect 12 | ) 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= 2 | github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= 3 | github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= 4 | github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= 5 | github.com/containerd/continuity v0.0.0-20180814194400-c7c5070e6f6e h1:KEBqsIJcjops96ysfjRTg3x6STnVHBxe7CZLwwnlkWA= 6 | github.com/containerd/continuity v0.0.0-20180814194400-c7c5070e6f6e/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/docker/docker v0.7.3-0.20180827131323-0c5f8d2b9b23 h1:Zl/9mUfPbYbnv895OXx9WfxPjwqSZHohuZzVcjJ5QPQ= 9 | github.com/docker/docker v0.7.3-0.20180827131323-0c5f8d2b9b23/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 10 | github.com/docker/docker v1.13.1 h1:5VBhsO6ckUxB0A8CE5LlUJdXzik9cbEbBTQ/ggeml7M= 11 | github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= 12 | github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= 13 | github.com/docker/go-units v0.3.3 h1:Xk8S3Xj5sLGlG5g67hJmYMmUgXv5N4PhkjJHHqrwnTk= 14 | github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 15 | github.com/docker/libnetwork v0.8.0-dev.2.0.20180608203834-19279f049241 h1:+ebE/hCU02srkeIg8Vp/vlUp182JapYWtXzV+bCeR2I= 16 | github.com/docker/libnetwork v0.8.0-dev.2.0.20180608203834-19279f049241/go.mod h1:93m0aTqz6z+g32wla4l4WxTrdtvBRmVzYRkYvasA5Z8= 17 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 18 | github.com/fsouza/go-dockerclient v1.3.1 h1:h0SaeiAGihssk+aZeKohbubHYKroCBlC7uuUyNhORI4= 19 | github.com/fsouza/go-dockerclient v1.3.1/go.mod h1:IN9UPc4/w7cXiARH2Yg99XxUHbAM+6rAi9hzBVbkWRU= 20 | github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo= 21 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 22 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 23 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 24 | github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= 25 | github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= 26 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 27 | github.com/kr/pty v1.1.3 h1:/Um6a/ZmD5tF7peoOJ5oN5KMQ0DrGVQSXLNwyckutPk= 28 | github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 29 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 30 | github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= 31 | github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ= 32 | github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= 33 | github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI= 34 | github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= 35 | github.com/opencontainers/runc v0.1.1 h1:GlxAyO6x8rfZYN9Tt0Kti5a/cP41iuiO2yYT0IJGY8Y= 36 | github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= 37 | github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= 38 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 39 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 40 | github.com/progrium/envy v0.0.0-20181018172300-dd23d719faaa h1:XgvAKasgNDCvW14IDNrJvG9SohqkyOcykJs2rw5pA+s= 41 | github.com/progrium/envy v0.0.0-20181018172300-dd23d719faaa/go.mod h1:3Tw8Y5ehWxChqfzi0MK2zg4nQUTulbIEmsA6JgpjxsQ= 42 | github.com/sirupsen/logrus v1.0.6 h1:hcP1GmhGigz/O7h1WVUM5KklBp1JoNS9FggWKdj/j3s= 43 | github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= 44 | github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= 45 | github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= 46 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 47 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 48 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 49 | github.com/termie/go-shutil v0.0.0-20140729215957-bcacb06fecae h1:vgGSvdW5Lqg+I1aZOlG32uyE6xHpLdKhZzcTEktz5wM= 50 | github.com/termie/go-shutil v0.0.0-20140729215957-bcacb06fecae/go.mod h1:quDq6Se6jlGwiIKia/itDZxqC5rj6/8OdFyMMAwTxCs= 51 | github.com/vishvananda/netlink v1.0.0/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk= 52 | github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI= 53 | golang.org/x/crypto v0.0.0-20180820150726-614d502a4dac/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 54 | golang.org/x/crypto v0.0.0-20181015023909-0c41d7ab0a0e h1:IzypfodbhbnViNUO/MEh0FzCUooG97cIGfdggUrUSyU= 55 | golang.org/x/crypto v0.0.0-20181015023909-0c41d7ab0a0e/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 56 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 57 | golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519 h1:x6rhz8Y9CjbgQkccRGmELH6K+LJj7tOoh3XWeC1yaQM= 58 | golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 59 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 60 | golang.org/x/sys v0.0.0-20180824143301-4910a1d54f87 h1:GqwDwfvIpC33dK9bA1fD+JiDUNsuAiQiEkpHqUKze4o= 61 | golang.org/x/sys v0.0.0-20180824143301-4910a1d54f87/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 62 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 63 | gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= 64 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 65 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 66 | gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= 67 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 68 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 69 | gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= 70 | -------------------------------------------------------------------------------- /pkg/hterm/assets/hterm.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Terminal 6 | 7 | 21 | 22 | 23 |
24 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /pkg/hterm/hterm.go: -------------------------------------------------------------------------------- 1 | //go:generate go-bindata -o assets.go -pkg hterm assets 2 | package hterm 3 | 4 | import ( 5 | "encoding/json" 6 | "io" 7 | "log" 8 | "net/http" 9 | "os" 10 | "os/exec" 11 | "strings" 12 | "syscall" 13 | "unsafe" 14 | 15 | "github.com/kr/pty" 16 | "golang.org/x/net/websocket" 17 | ) 18 | 19 | type termMsg struct { 20 | Args *string `json:"args"` 21 | Data *string `json:"data"` 22 | Width *int `json:"width"` 23 | Height *int `json:"height"` 24 | } 25 | 26 | func Handle(w http.ResponseWriter, r *http.Request, pty func(string) *Pty) { 27 | switch { 28 | case strings.HasSuffix(r.URL.Path, "/hterm.js"): 29 | log.Println("hterm handling js:", r.URL.Path) 30 | HandleAsset(w, r, "assets/hterm.js") 31 | case strings.HasSuffix(r.URL.Path, "/hterm"): 32 | log.Println("hterm handling socket:", r.URL.Path) 33 | HandleSocket(w, r, pty) 34 | default: 35 | log.Println("hterm handling index:", r.URL.Path) 36 | HandleAsset(w, r, "assets/hterm.html") 37 | } 38 | } 39 | 40 | func HandleAsset(w http.ResponseWriter, r *http.Request, asset string) { 41 | data, err := Asset(asset) 42 | if err != nil { 43 | log.Println("hterm:", err) 44 | http.NotFound(w, r) 45 | } 46 | w.Write(data) 47 | } 48 | 49 | func HandleSocket(w http.ResponseWriter, r *http.Request, ptyFunc func(string) *Pty) { 50 | websocket.Handler(func(conn *websocket.Conn) { 51 | var obj termMsg 52 | dec := json.NewDecoder(conn) 53 | err := dec.Decode(&obj) 54 | if err != nil { 55 | log.Println("hterm:", err) 56 | return 57 | } 58 | if obj.Args == nil || obj.Width == nil || obj.Height == nil { 59 | log.Println("hterm: no args") 60 | return 61 | } 62 | pty := ptyFunc(*obj.Args) 63 | pty.Size(*obj.Width, *obj.Height) 64 | go io.Copy(conn, pty) 65 | for { 66 | var obj termMsg 67 | err := dec.Decode(&obj) 68 | if err != nil { 69 | log.Println("hterm:", err) 70 | break 71 | } 72 | 73 | if obj.Width != nil && obj.Height != nil { 74 | pty.Size(*obj.Width, *obj.Height) 75 | continue 76 | } 77 | if obj.Data != nil { 78 | _, err = io.WriteString(pty, *obj.Data) 79 | if err != nil { 80 | log.Println("hterm:", err) 81 | break 82 | } 83 | } 84 | } 85 | }).ServeHTTP(w, r) 86 | } 87 | 88 | type Pty struct { 89 | *os.File 90 | } 91 | 92 | // winsize stores the Height and Width of a terminal. 93 | type winsize struct { 94 | Height uint16 95 | Width uint16 96 | } 97 | 98 | func (pty *Pty) Size(width int, height int) { 99 | ws := &winsize{Width: uint16(width), Height: uint16(height)} 100 | syscall.Syscall(syscall.SYS_IOCTL, pty.Fd(), uintptr(syscall.TIOCSWINSZ), uintptr(unsafe.Pointer(ws))) 101 | } 102 | 103 | func NewPty(cmd *exec.Cmd) (*Pty, error) { 104 | cmdPty, err := pty.Start(cmd) 105 | if err != nil { 106 | return nil, err 107 | } 108 | return &Pty{cmdPty}, nil 109 | } 110 | -------------------------------------------------------------------------------- /tests/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM progrium/dind 2 | RUN apk --update add expect bash openssh curl docker make \ 3 | && ssh-keygen -t rsa -N "" -f /root/.ssh/id_rsa \ 4 | && curl -Ls https://github.com/progrium/basht/releases/download/v0.1.0/basht_0.1.0_Linux_x86_64.tgz \ 5 | | tar -zxC /bin 6 | ENV DOCKER_OPTS -H tcp://0.0.0.0:2376 -H unix:///var/run/docker.sock 7 | ENV DOCKER_HOST tcp://localhost:2376 8 | WORKDIR /envy 9 | ADD . /envy 10 | -------------------------------------------------------------------------------- /tests/tests.bash: -------------------------------------------------------------------------------- 1 | 2 | setup() { 3 | rm -rf /envy/* 4 | docker pull progrium/dind:latest 5 | expect <<-EOF 6 | set timeout 1 7 | spawn ssh test@localhost 8 | expect { 9 | timeout { exit 1 } 10 | eof { exit 1 } 11 | "Are you sure" { 12 | send "yes\r" 13 | sleep 1 14 | exit 0 15 | } 16 | } 17 | exit 1 18 | EOF 19 | echo 20 | } 21 | setup 22 | 23 | T_session-reload() { 24 | expect <<-EOF 25 | set timeout 1 26 | spawn ssh reload@localhost 27 | expect { 28 | timeout { exit 1 } 29 | eof { exit 1 } 30 | "Building environment" { 31 | expect "root@reload" 32 | send "rm -rf /usr\r" 33 | expect "root@reload" 34 | send "reload\r" 35 | expect "root@reload" 36 | send "ls -1 usr\r" 37 | expect { 38 | "usr" { exit 0 } 39 | } 40 | } 41 | } 42 | exit 1 43 | EOF 44 | } 45 | 46 | T_environ-rebuild() { 47 | expect <<-EOF 48 | set timeout 1 49 | spawn ssh rebuild@localhost 50 | expect { 51 | timeout { exit 1 } 52 | eof { exit 1 } 53 | "Building environment" { 54 | expect "root@rebuild" 55 | send "echo FROM alpine > /env/Dockerfile\r" 56 | expect "root@rebuild" 57 | send "rebuild\r" 58 | expect { 59 | "/ #" { exit 0 } 60 | } 61 | } 62 | } 63 | exit 1 64 | EOF 65 | } 66 | --------------------------------------------------------------------------------