Skip to content

Commit

Permalink
Add dotfile repo support
Browse files Browse the repository at this point in the history
  • Loading branch information
csweichel committed Jan 3, 2022
1 parent 81372bc commit f4d5b56
Show file tree
Hide file tree
Showing 5 changed files with 180 additions and 1 deletion.
18 changes: 18 additions & 0 deletions components/dashboard/src/settings/Preferences.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,13 @@ export default function Preferences() {
const browserIdeOptions = ideOptions && orderedIdeOptions(ideOptions, "browser");
const desktopIdeOptions = ideOptions && orderedIdeOptions(ideOptions, "desktop");

const [dotfileRepo, setDotfileRepo] = useState<string>(user?.additionalData?.dotfileRepo || "");
const actuallySetDotfileRepo = async (value: string) => {
const additionalData = user?.additionalData || {};
additionalData.dotfileRepo = value;
await getGitpodService().server.updateLoggedInUser({ additionalData });
};

return <div>
<PageWithSubMenu subMenu={settingsMenu} title='Preferences' subtitle='Configure user preferences.'>
{ideOptions && browserIdeOptions && <>
Expand Down Expand Up @@ -153,6 +160,17 @@ export default function Preferences() {
</div>
</SelectableCard>
</div>

<h3 className="mt-12">Dotfiles <PillLabel type="warn" className="font-semibold mt-2 py-0.5 px-2 self-center">Beta</PillLabel></h3>
<p className="text-base text-gray-500 dark:text-gray-400">Customize workspaces using dotfiles.</p>
<div className="mt-4 max-w-md">
<h4>Repository URL</h4>
<input type="text" value={dotfileRepo} className="w-full" placeholder="e.g. https://github.com/username/dotfiles" onChange={(e) => setDotfileRepo(e.target.value)} />
<p className="text-base text-gray-500 dark:text-gray-400">Add a repository URL that includes dotfiles. Gitpod will clone and install your dotfiles for every new workspace.</p>
<div className="mt-4 max-w-md">
<button onClick={() => actuallySetDotfileRepo(dotfileRepo)}>Save Changes</button>
</div>
</div>
</PageWithSubMenu>
</div>;
}
Expand Down
3 changes: 3 additions & 0 deletions components/gitpod-protocol/src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ export interface AdditionalUserData {
oauthClientsApproved?: { [key: string]: string }
// to remember GH Orgs the user installed/updated the GH App for
knownGitHubOrgs?: string[];

// Git clone URL pointing to the user's dotfile repo
dotfileRepo?: string;
}

export interface EmailNotificationSettings {
Expand Down
6 changes: 6 additions & 0 deletions components/server/src/workspace/workspace-starter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,12 @@ export class WorkspaceStarter {
vsxRegistryUrl.setValue(this.config.vsxRegistryUrl);
envvars.push(vsxRegistryUrl);

// supervisor ensures dotfiles are only used if the workspace is a regular workspace
const dotfileEnv = new EnvironmentVariable();
dotfileEnv.setName("SUPERVISOR_DOTFILE_REPO");
dotfileEnv.setValue(user.additionalData?.dotfileRepo || "");
envvars.push(dotfileEnv);

const createGitpodTokenPromise = (async () => {
const scopes = this.createDefaultGitpodAPITokenScopes(workspace, instance);
const token = crypto.randomBytes(30).toString('hex');
Expand Down
4 changes: 4 additions & 0 deletions components/supervisor/pkg/supervisor/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,10 @@ type WorkspaceConfig struct {

// WorkspaceClusterHost is a host under which this workspace is served, e.g. ws-eu11.gitpod.io
WorkspaceClusterHost string `env:"GITPOD_WORKSPACE_CLUSTER_HOST"`

// DotfileRepo is a user-configurable repository which contains their dotfiles to customise
// the in-workspace epxerience.
DotfileRepo string `env:"SUPERVISOR_DOTFILE_REPO"`
}

// WorkspaceGitpodToken is a list of tokens that should be added to supervisor's token service.
Expand Down
150 changes: 149 additions & 1 deletion components/supervisor/pkg/supervisor/supervisor.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"errors"
"fmt"
"io"
"io/fs"
"io/ioutil"
"net"
"net/http"
Expand All @@ -35,6 +36,7 @@ import (
"github.com/soheilhy/cmux"
"golang.org/x/crypto/ssh"
"golang.org/x/sys/unix"
"golang.org/x/xerrors"
"google.golang.org/grpc"
"google.golang.org/protobuf/proto"

Expand Down Expand Up @@ -278,6 +280,10 @@ func Run(options ...RunOption) {
// - in headless task we can not use reaper, because it breaks headlessTaskFailed report
if !cfg.isHeadless() {
go reaper(terminatingReaper)

// We need to checkout dotfiles first, because they may be changing the path which affects the IDE.
// TODO(cw): provide better feedback if the IDE start fails because of the dotfiles (provide any feedback at all).
installDotfiles(ctx, termMuxSrv, cfg)
}

var ideWG sync.WaitGroup
Expand Down Expand Up @@ -364,6 +370,148 @@ func Run(options ...RunOption) {
wg.Wait()
}

func installDotfiles(ctx context.Context, term *terminal.MuxTerminalService, cfg *Config) {
repo := cfg.DotfileRepo
if repo == "" {
return
}

const dotfilePath = "/home/gitpod/.dotfiles"
if _, err := os.Stat(dotfilePath); err == nil {
// dotfile path exists already - nothing to do here
return
}

prep := func(cfg *Config, out io.Writer, name string, args ...string) *exec.Cmd {
cmd := exec.Command(name, args...)
cmd.Dir = "/home/gitpod"
cmd.Env = buildChildProcEnv(cfg, nil)
cmd.SysProcAttr = &syscall.SysProcAttr{
// All supervisor children run as gitpod user. The environment variables we produce are also
// gitpod user specific.
Credential: &syscall.Credential{
Uid: gitpodUID,
Gid: gitpodGID,
},
}
cmd.Stdout = out
cmd.Stderr = out
return cmd
}

err := func() (err error) {
out, err := os.OpenFile("/home/gitpod/.dotfiles.log", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer out.Close()

defer func() {
if err != nil {
out.WriteString(fmt.Sprintf("# dotfile init failed: %s\n", err.Error()))
}
}()

done := make(chan error, 1)
go func() {
done <- prep(cfg, out, "git", "clone", "--depth=1", repo, "/home/gitpod/.dotfiles").Run()
close(done)
}()
select {
case err := <-done:
if err != nil {
return err
}
case <-time.After(120 * time.Second):
return xerrors.Errorf("dotfiles repo clone did not finish within two minutes")
}

// at this point we have the dotfile repo cloned, let's try and install it
var candidates = []string{
"install.sh",
"install",
"bootstrap.sh",
"bootstrap",
"script/bootstrap",
"setup.sh",
"setup",
"script/setup",
}
for _, c := range candidates {
fn := filepath.Join(dotfilePath, c)
stat, err := os.Stat(fn)
if err != nil {
_, _ = out.WriteString(fmt.Sprintf("# installation script candidate %s is not available\n", fn))
continue
}
if stat.IsDir() {
_, _ = out.WriteString(fmt.Sprintf("# installation script candidate %s is a directory\n", fn))
continue
}
if stat.Mode()&0111 == 0 {
_, _ = out.WriteString(fmt.Sprintf("# installation script candidate %s is not executable\n", fn))
continue
}

_, _ = out.WriteString(fmt.Sprintf("# executing installation script candidate %s\n", fn))

// looks like we've found a candidate, let's run it
cmd := prep(cfg, out, "/bin/sh", "-c", "exec "+fn)
err = cmd.Start()
if err != nil {
return err
}
done := make(chan error, 1)
go func() {
done <- cmd.Wait()
close(done)
}()

select {
case err = <-done:
return err
case <-time.After(120 * time.Second):
cmd.Process.Kill()
return xerrors.Errorf("installation process %s tool longer than 120 seconds", fn)
}
}

// no installation script candidate was found, let's try and symlink this stuff
err = filepath.Walk(dotfilePath, func(path string, info fs.FileInfo, err error) error {
if strings.Contains(path, "/.git") {
// don't symlink the .git directory or any of its content
return nil
}

homeFN := filepath.Join("/home/gitpod", strings.TrimPrefix(path, dotfilePath))
if _, err := os.Stat(homeFN); err == nil {
// homeFN exists already - do nothing
return nil
}

if info.IsDir() {
err = os.MkdirAll(homeFN, info.Mode().Perm())
if err != nil {
return err
}
return nil
}

// write some feedback to the terminal
out.WriteString(fmt.Sprintf("# echo linking %s -> %s\n", path, homeFN))

return os.Symlink(path, homeFN)
})

return nil
}()
if err != nil {
// installing the dotfiles failed for some reason - we must tell the user
// TODO(cw): tell the user
log.WithError(err).Warn("installing dotfiles failed")
}
}

func createGitpodService(cfg *Config, tknsrv api.TokenServiceServer) *gitpod.APIoverJSONRPC {
endpoint, host, err := cfg.GitpodAPIEndpoint()
if err != nil {
Expand Down Expand Up @@ -1223,7 +1371,7 @@ func socketActivationForDocker(ctx context.Context, wg *sync.WaitGroup, term *te
cmd.ExtraFiles = []*os.File{socketFD}
alias, err := term.Start(cmd, terminal.TermOptions{
Annotations: map[string]string{
"supervisor": "true",
"gitpod.supervisor": "true",
},
LogToStdout: true,
})
Expand Down

0 comments on commit f4d5b56

Please sign in to comment.