|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bufio"
|
|
|
|
_ "embed"
|
|
|
|
"errors"
|
|
|
|
"flag"
|
|
|
|
"fmt"
|
|
|
|
"log"
|
|
|
|
"os"
|
|
|
|
"os/exec"
|
|
|
|
"os/user"
|
|
|
|
"runtime"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
"syscall"
|
|
|
|
)
|
|
|
|
|
|
|
|
const usage = `workspace - Use a containerized workspace
|
|
|
|
|
|
|
|
COMMANDS
|
|
|
|
|
|
|
|
dump-init-files: Dump emacs/bash init files to home directory, overwriting existing files.
|
|
|
|
List of files: ~/.bash_aliases, ~/.emacs, ~/.custom.el, ~/.gitconfig, ~/.gitignore
|
|
|
|
|
|
|
|
toggle-nightlight: Toggle Gnome display nightlight feature.
|
|
|
|
`
|
|
|
|
|
|
|
|
//go:embed emacs/init.el
|
|
|
|
var initLisp string
|
|
|
|
|
|
|
|
//go:embed emacs/custom.el
|
|
|
|
var customLisp string
|
|
|
|
|
|
|
|
//go:embed emacs/epaper-theme.el
|
|
|
|
var elispEpaperTheme string
|
|
|
|
|
|
|
|
//go:embed bash/bash_aliases
|
|
|
|
var bashAliases string
|
|
|
|
|
|
|
|
//go:embed git/config
|
|
|
|
var gitConfig string
|
|
|
|
|
|
|
|
//go:embed git/ignore
|
|
|
|
var gitIgnore string
|
|
|
|
|
|
|
|
var ErrAddUserFailExit = errors.New("useradd command returned no-zero exit code")
|
|
|
|
var ErrAddGroupFailExit = errors.New("groupadd command returned no-zero exit code")
|
|
|
|
|
|
|
|
func AddUser(username string, uid string) error {
|
|
|
|
commandArgs := []string{
|
|
|
|
"--uid", uid,
|
|
|
|
"--user-group",
|
|
|
|
username,
|
|
|
|
}
|
|
|
|
|
|
|
|
if os.Getenv("WORKSPACE_OS") == "darwin" {
|
|
|
|
commandArgs = append(commandArgs, "--create-home")
|
|
|
|
} else {
|
|
|
|
commandArgs = append(commandArgs, "--no-create-home")
|
|
|
|
}
|
|
|
|
|
|
|
|
cmd := exec.Command("useradd", commandArgs...)
|
|
|
|
|
|
|
|
output, err := cmd.CombinedOutput()
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf(
|
|
|
|
"Error adding user: %w. Process error: %w. Process output: %s",
|
|
|
|
ErrAddUserFailExit,
|
|
|
|
err,
|
|
|
|
output,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func AddGroup(name string, gid string) error {
|
|
|
|
cmd := exec.Command(
|
|
|
|
"groupadd",
|
|
|
|
"--gid", gid,
|
|
|
|
name,
|
|
|
|
)
|
|
|
|
|
|
|
|
output, err := cmd.CombinedOutput()
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf(
|
|
|
|
"Error adding group: %w. Process error: %w. Process output: %s",
|
|
|
|
ErrAddGroupFailExit,
|
|
|
|
err,
|
|
|
|
output,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
const osDarwin = "darwin"
|
|
|
|
const osLinux = "linux"
|
|
|
|
|
|
|
|
func EntryPoint(command []string) error {
|
|
|
|
hostOS := os.Getenv("WORKSPACE_OS")
|
|
|
|
if hostOS != osLinux {
|
|
|
|
log.Println(fmt.Sprintf("Note: Workspce OS is \"%s\"", hostOS))
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(command) == 0 {
|
|
|
|
command = []string{"/bin/bash", "-i"}
|
|
|
|
}
|
|
|
|
|
|
|
|
uidString := os.Getenv("WORKSPACE_USER")
|
|
|
|
uid, err := strconv.Atoi(uidString)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf(
|
|
|
|
"Failed to parse UID from WORKSPACE_USER env var with contents \"%s\". Error: %w",
|
|
|
|
os.Getenv("WORKSPACE_USER"),
|
|
|
|
err,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
dockerGidString := os.Getenv("WORKSPACE_DOCKER_GID")
|
|
|
|
dockerGid, err := strconv.Atoi(dockerGidString)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf(
|
|
|
|
"Failed to parse GID from WORKSPACE_DOCKER_GID env var with contents \"%s\". Error: %w",
|
|
|
|
os.Getenv("WORKSPACE_DOCKER_GID"),
|
|
|
|
err,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
if uid != 0 {
|
|
|
|
err = AddUser(os.Getenv("WORKSPACE_USERNAME"), uidString)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
err = AddGroup("docker", dockerGidString)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
err = syscall.Setgroups([]int{dockerGid})
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
err = syscall.Setgid(int(uid))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
err = syscall.Setuid(int(uid))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
path, err := exec.LookPath(command[0])
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
SetEnvVars()
|
|
|
|
|
|
|
|
return syscall.Exec(path, command, os.Environ())
|
|
|
|
}
|
|
|
|
|
|
|
|
func Run(detach bool, mounts []string, command []string) error {
|
|
|
|
workDir, err := os.Getwd()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
curUser, err := user.Current()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
home := os.Getenv("HOME")
|
|
|
|
hostOS := runtime.GOOS
|
|
|
|
|
|
|
|
dockerBin, err := exec.LookPath("docker")
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
dockerCommand := []string{
|
|
|
|
dockerBin, "run", "--network=host",
|
|
|
|
"--workdir=" + workDir,
|
|
|
|
"--rm",
|
|
|
|
"-e", "WORKSPACE_OS=" + hostOS,
|
|
|
|
}
|
|
|
|
|
|
|
|
if hostOS != "darwin" {
|
|
|
|
dockerGroup, err := user.LookupGroup("docker")
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
dockerCommand = append(
|
|
|
|
dockerCommand,
|
|
|
|
"-e", "WORKSPACE_DOCKER_GID="+dockerGroup.Gid,
|
|
|
|
"-v", "/var/run/docker.sock:/var/run/docker.sock",
|
|
|
|
"-v", "/etc/hosts:/etc/hosts:ro",
|
|
|
|
"-v", "/etc/resolv.conf:/etc/resolv.conf:ro",
|
|
|
|
"-v", home+":"+home,
|
|
|
|
"-e", "SSH_AGENT_LAUNCHER="+os.Getenv("SSH_AGENT_LAUNCHER"),
|
|
|
|
"-e", "SSH_AUTH_SOCK="+os.Getenv("SSH_AUTH_SOCK"),
|
|
|
|
"-e", "PULSE_SERVER=unix:/run/user/"+curUser.Uid+"/pulse/native",
|
|
|
|
"-e", "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/"+curUser.Uid+"/bus",
|
|
|
|
"-v", "/tmp/.X11-unix:/tmp/.X11-unix",
|
|
|
|
"-v", "/run/user/"+curUser.Uid+":/run/user/"+curUser.Uid,
|
|
|
|
"-v", "/dev/snd",
|
|
|
|
"-e", "DISPLAY="+os.Getenv("DISPLAY"),
|
|
|
|
"-e", "WORKSPACE_USER="+curUser.Uid,
|
|
|
|
"-e", "WORKSPACE_USERNAME="+curUser.Username,
|
|
|
|
"-e", "HOME="+home,
|
|
|
|
"-h", os.Getenv("HOSTNAME"),
|
|
|
|
"-e", "TERM="+os.Getenv("TERM"),
|
|
|
|
)
|
|
|
|
|
|
|
|
fileInfo, err := os.Lstat(home)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf(
|
|
|
|
"Failed to determine whether home directory is a symbolic link: %w",
|
|
|
|
err,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
if fileInfo.Mode()&os.ModeSymlink == os.ModeSymlink {
|
|
|
|
destination, err := os.Readlink(home)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf(
|
|
|
|
"Failed to determine whether home directory is a symbolic link: %w",
|
|
|
|
err,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
dockerCommand = append(dockerCommand, "-v", destination+":"+destination)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
dockerCommand = append(
|
|
|
|
dockerCommand,
|
|
|
|
"-e", "WORKSPACE_DOCKER_GID=1001",
|
|
|
|
"-v", workDir+":"+workDir,
|
|
|
|
"-e", "WORKSPACE_USER=1000",
|
|
|
|
"-e", "WORKSPACE_USERNAME="+curUser.Username+"-workspace",
|
|
|
|
"-e", "HOME=/home/"+curUser.Username+"-workspace",
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
if detach {
|
|
|
|
dockerCommand = append(dockerCommand, "--detach")
|
|
|
|
}
|
|
|
|
|
|
|
|
if fileInfo, _ := os.Stdout.Stat(); (fileInfo.Mode() & os.ModeCharDevice) != 0 {
|
|
|
|
dockerCommand = append(dockerCommand, "-ti")
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, mount := range mounts {
|
|
|
|
dockerCommand = append(dockerCommand, "-v", mount+":"+mount)
|
|
|
|
}
|
|
|
|
|
|
|
|
dockerCommand = append(dockerCommand, "hugotty/workspace:latest")
|
|
|
|
dockerCommand = append(dockerCommand, command...)
|
|
|
|
|
|
|
|
return syscall.Exec(dockerCommand[0], dockerCommand, os.Environ())
|
|
|
|
}
|
|
|
|
|
|
|
|
func SetEnvVars() {
|
|
|
|
home := os.Getenv("HOME")
|
|
|
|
os.Setenv("PATH", home+"/bin:"+os.Getenv("PATH"))
|
|
|
|
os.Setenv("PATH", home+"/dotfiles/bin:"+os.Getenv("PATH"))
|
|
|
|
|
|
|
|
// PHP
|
|
|
|
os.Setenv("PATH", home+"/.config/composer/vendor/bin:"+os.Getenv("PATH"))
|
|
|
|
|
|
|
|
// Node
|
|
|
|
os.Setenv("PATH", home+"/.npm_packages/bin:"+os.Getenv("PATH"))
|
|
|
|
|
|
|
|
// Perl
|
|
|
|
os.Setenv("PATH", home+"/perl5/perlbrew/bin:"+os.Getenv("PATH"))
|
|
|
|
|
|
|
|
// Rust
|
|
|
|
os.Setenv("PATH", home+"/.cargo/bin:"+os.Getenv("PATH"))
|
|
|
|
|
|
|
|
// Locally installed python packages
|
|
|
|
os.Setenv("PATH", home+"/.local/bin:"+os.Getenv("PATH"))
|
|
|
|
|
|
|
|
// Home for virtualenvs
|
|
|
|
os.Setenv("WORKON_HOME", home+"/.local/share/virtualenvs")
|
|
|
|
|
|
|
|
// GO Stuff
|
|
|
|
os.Setenv("GOPATH", home+"/go")
|
|
|
|
os.Setenv("PATH", home+"/go/bin:"+os.Getenv("PATH"))
|
|
|
|
os.Setenv("PATH", "/usr/local/go/bin:"+os.Getenv("PATH"))
|
|
|
|
os.Setenv("PATH", "/usr/local/gopkg/bin:"+os.Getenv("PATH"))
|
|
|
|
os.Setenv("GOPRIVATE", "git.snorba.art")
|
|
|
|
}
|
|
|
|
|
|
|
|
func DumpStringToFile(data string, path string) error {
|
|
|
|
file, err := os.Create(path)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
writer := bufio.NewWriter(file)
|
|
|
|
_, err = writer.WriteString(data)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return writer.Flush()
|
|
|
|
}
|
|
|
|
|
|
|
|
func ToggleNightLight() error {
|
|
|
|
getState := exec.Command(
|
|
|
|
"gsettings",
|
|
|
|
"get",
|
|
|
|
"org.gnome.settings-daemon.plugins.color",
|
|
|
|
"night-light-enabled",
|
|
|
|
)
|
|
|
|
state, err := getState.CombinedOutput()
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("%w: %s", err, state)
|
|
|
|
}
|
|
|
|
|
|
|
|
stateStr := strings.TrimSpace(string(state))
|
|
|
|
|
|
|
|
desiredState := "true"
|
|
|
|
if stateStr == desiredState {
|
|
|
|
desiredState = "false"
|
|
|
|
}
|
|
|
|
|
|
|
|
out, err := exec.Command(
|
|
|
|
"gsettings",
|
|
|
|
"set",
|
|
|
|
"org.gnome.settings-daemon.plugins.color",
|
|
|
|
"night-light-enabled",
|
|
|
|
desiredState,
|
|
|
|
).CombinedOutput()
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("%w: %s", err, out)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func DumpInitFiles() error {
|
|
|
|
home := os.Getenv("HOME")
|
|
|
|
elispThemeDir := home + "/.workspace/elisp-themes"
|
|
|
|
|
|
|
|
if _, err := os.Stat(elispThemeDir); os.IsNotExist(err) {
|
|
|
|
err = os.MkdirAll(elispThemeDir, 0755)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf(
|
|
|
|
"Failed to create elisp theme dir in %v: %w",
|
|
|
|
elispThemeDir,
|
|
|
|
err,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
files := map[string]string{
|
|
|
|
home + "/.emacs": initLisp,
|
|
|
|
home + "/.custom.el": customLisp,
|
|
|
|
home + "/.bash_aliases": bashAliases,
|
|
|
|
elispThemeDir + "/epaper-theme.el": elispEpaperTheme,
|
|
|
|
home + "/.gitconfig": gitConfig,
|
|
|
|
home + "/.gitignore": gitIgnore,
|
|
|
|
}
|
|
|
|
|
|
|
|
for file, contents := range files {
|
|
|
|
fmt.Fprintln(os.Stderr, "Dumping to "+file)
|
|
|
|
err := DumpStringToFile(contents, file)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
var cmd string
|
|
|
|
if len(os.Args) > 1 {
|
|
|
|
cmd = os.Args[1]
|
|
|
|
}
|
|
|
|
|
|
|
|
var detach bool
|
|
|
|
run := flag.NewFlagSet("run", flag.ExitOnError)
|
|
|
|
run.BoolVar(
|
|
|
|
&detach,
|
|
|
|
"detach",
|
|
|
|
false,
|
|
|
|
"Whether or not to detach from the container after running the command",
|
|
|
|
)
|
|
|
|
|
|
|
|
var mounts arrayFlag
|
|
|
|
run.Var(&mounts, "mount", "Directory to mount inside the workspace container")
|
|
|
|
|
|
|
|
switch cmd {
|
|
|
|
case "entrypoint":
|
|
|
|
err := EntryPoint(os.Args[2:])
|
|
|
|
if err != nil {
|
|
|
|
fmt.Println("error executing entrypoint command", err)
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
|
|
|
|
case "run":
|
|
|
|
err := run.Parse(os.Args[2:])
|
|
|
|
if err != nil {
|
|
|
|
fmt.Println(err)
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
|
|
|
|
err = Run(detach, mounts, run.Args())
|
|
|
|
if err != nil {
|
|
|
|
fmt.Println("error running command in container", err)
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
case "dump-init-files":
|
|
|
|
err := DumpInitFiles()
|
|
|
|
if err != nil {
|
|
|
|
fmt.Fprintln(os.Stderr, err)
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
case "toggle-nightlight":
|
|
|
|
err := ToggleNightLight()
|
|
|
|
if err != nil {
|
|
|
|
fmt.Fprintln(os.Stderr, err)
|
|
|
|
os.Exit(1)
|
|
|
|
}
|
|
|
|
default:
|
|
|
|
fmt.Println(usage)
|
|
|
|
fmt.Println("run COMMAND: Start workspace container and run COMMAND within it")
|
|
|
|
run.Usage()
|
|
|
|
}
|
|
|
|
}
|