Path: blob/main/components/local-app/cmd/workspace-up.go
2497 views
// Copyright (c) 2023 Gitpod GmbH. All rights reserved.1// Licensed under the GNU Affero General Public License (AGPL).2// See License.AGPL.txt in the project root for license information.34package cmd56import (7"bytes"8"context"9"errors"10"fmt"11"log/slog"12"os"13"os/exec"14"path/filepath"15"strings"16"time"1718"github.com/bufbuild/connect-go"19v1 "github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1"20"github.com/gitpod-io/local-app/pkg/config"21"github.com/gitpod-io/local-app/pkg/helper"22"github.com/gitpod-io/local-app/pkg/prettyprint"23"github.com/go-git/go-git/v5"24gitcfg "github.com/go-git/go-git/v5/config"25"github.com/gookit/color"26"github.com/melbahja/goph"27"github.com/spf13/cobra"28"golang.org/x/crypto/ssh"29)3031// workspaceUpCmd creates a new workspace32var workspaceUpCmd = &cobra.Command{33Use: "up [path/to/git/working-copy]",34Hidden: true,35Short: "Creates a new workspace, pushes the Git working copy and adds it as remote",36Args: cobra.MaximumNArgs(1),37RunE: func(cmd *cobra.Command, args []string) error {38cmd.SilenceUsage = true3940workingDir := "."41if len(args) != 0 {42workingDir = args[0]43}4445cfg := config.FromContext(cmd.Context())46gpctx, err := cfg.GetActiveContext()47if err != nil {48return err49}50gitpod, err := getGitpodClient(cmd.Context())51if err != nil {52return err53}5455if workspaceCreateOpts.WorkspaceClass != "" {56resp, err := gitpod.Workspaces.ListWorkspaceClasses(cmd.Context(), connect.NewRequest(&v1.ListWorkspaceClassesRequest{}))57if err != nil {58return prettyprint.MarkExceptional(prettyprint.AddResolution(fmt.Errorf("cannot list workspace classes: %w", err),59"don't pass an explicit workspace class, i.e. omit the --class flag",60))61}62var (63classes []string64found bool65)66for _, cls := range resp.Msg.GetResult() {67classes = append(classes, cls.Id)68if cls.Id == workspaceCreateOpts.WorkspaceClass {69found = true70}71}72if !found {73return prettyprint.AddResolution(fmt.Errorf("workspace class %s not found", workspaceCreateOpts.WorkspaceClass),74fmt.Sprintf("use one of the available workspace classes: %s", strings.Join(classes, ", ")),75)76}77}7879if workspaceCreateOpts.Editor != "" {80resp, err := gitpod.Editors.ListEditorOptions(cmd.Context(), connect.NewRequest(&v1.ListEditorOptionsRequest{}))81if err != nil {82return prettyprint.MarkExceptional(prettyprint.AddResolution(fmt.Errorf("cannot list editor options: %w", err),83"don't pass an explicit editor, i.e. omit the --editor flag",84))85}86var (87editors []string88found bool89)90for _, editor := range resp.Msg.GetResult() {91editors = append(editors, editor.Id)92if editor.Id == workspaceCreateOpts.Editor {93found = true94}95}96if !found {97return prettyprint.AddResolution(fmt.Errorf("editor %s not found", workspaceCreateOpts.Editor),98fmt.Sprintf("use one of the available editor options: %s", strings.Join(editors, ", ")),99)100}101}102103var (104orgId = gpctx.OrganizationID105ctx = cmd.Context()106)107108defer func() {109// If the error doesn't have a resolution, assume it's a system error and add an apology110if err != nil && !errors.Is(err, &prettyprint.ErrResolution{}) {111err = prettyprint.MarkExceptional(err)112}113}()114115currentDir, err := filepath.Abs(workingDir)116if err != nil {117return err118}119for {120// Check if current directory contains .git folder121_, err := os.Stat(filepath.Join(currentDir, ".git"))122if err == nil {123break124}125if !os.IsNotExist(err) {126return err127}128129// Move to the parent directory130parentDir := filepath.Dir(currentDir)131if parentDir == currentDir {132// No more parent directories133return prettyprint.AddResolution(fmt.Errorf("no Git repository found"),134fmt.Sprintf("make sure %s is a valid Git repository", workingDir),135"run `git clone` to clone an existing repository",136"open a remote repository using `{gitpod} workspace create <repo-url>`",137)138}139currentDir = parentDir140}141142slog.Debug("found Git working copy", "dir", currentDir)143repo, err := git.PlainOpen(currentDir)144if err != nil {145return prettyprint.MarkExceptional(fmt.Errorf("cannot open Git working copy at %s: %w", currentDir, err))146}147_ = repo.DeleteRemote("gitpod")148head, err := repo.Head()149if err != nil {150return prettyprint.MarkExceptional(fmt.Errorf("cannot get HEAD: %w", err))151}152branch := head.Name().Short()153154newWorkspace, err := gitpod.Workspaces.CreateAndStartWorkspace(ctx, connect.NewRequest(155&v1.CreateAndStartWorkspaceRequest{156Source: &v1.CreateAndStartWorkspaceRequest_ContextUrl{ContextUrl: "GITPODCLI_CONTENT_INIT=push/https://github.com/gitpod-io/empty"},157OrganizationId: orgId,158StartSpec: &v1.StartWorkspaceSpec{159IdeSettings: &v1.IDESettings{160DefaultIde: workspaceCreateOpts.Editor,161UseLatestVersion: false,162},163WorkspaceClass: workspaceCreateOpts.WorkspaceClass,164},165},166))167if err != nil {168return err169}170workspaceID := newWorkspace.Msg.WorkspaceId171if len(workspaceID) == 0 {172return prettyprint.MarkExceptional(prettyprint.AddResolution(fmt.Errorf("workspace was not created"),173"try to create the workspace again",174))175}176ws, err := helper.ObserveWorkspaceUntilStarted(ctx, gitpod, workspaceID)177if err != nil {178return err179}180slog.Debug("workspace started", "workspaceID", workspaceID)181182token, err := gitpod.Workspaces.GetOwnerToken(ctx, connect.NewRequest(&v1.GetOwnerTokenRequest{WorkspaceId: workspaceID}))183if err != nil {184return err185}186var (187ownerToken = token.Msg.Token188host = strings.TrimPrefix(strings.ReplaceAll(ws.Instance.Status.Url, workspaceID, workspaceID+".ssh"), "https://")189)190sess, err := goph.NewConn(&goph.Config{191User: fmt.Sprintf("%s#%s", workspaceID, ownerToken),192Addr: host,193Callback: ssh.InsecureIgnoreHostKey(),194Timeout: 10 * time.Second,195Port: 22,196})197if err != nil {198return prettyprint.AddResolution(fmt.Errorf("cannot connect to workspace: %w", err),199"make sure you can connect to SSH servers on port 22",200)201}202defer sess.Close()203204slog.Debug("initializing remote workspace Git repository")205err = runSSHCommand(ctx, sess, "rm", "-r", "/workspace/empty/.git")206if err != nil {207return err208}209err = runSSHCommand(ctx, sess, "git", "init", "/workspace/remote")210if err != nil {211return err212}213214slog.Debug("pushing to workspace")215sshRemote := fmt.Sprintf("%s#%s@%s:/workspace/remote", workspaceID, ownerToken, helper.WorkspaceSSHHost(&v1.Workspace{WorkspaceId: workspaceID, Status: ws}))216_, err = repo.CreateRemote(&gitcfg.RemoteConfig{217Name: "gitpod",218URLs: []string{sshRemote},219})220if err != nil {221return fmt.Errorf("cannot create remote: %w", err)222}223224// Pushing using Go git is tricky because of the SSH host verification. Shelling out to git is easier.225slog.Info("pushing to local working copy to remote workspace")226pushcmd := exec.Command("git", "push", "--progress", "gitpod")227pushcmd.Stdout = os.Stdout228pushcmd.Stderr = os.Stderr229pushcmd.Dir = currentDir230pushcmd.Env = append(os.Environ(), "GIT_SSH_COMMAND=ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null")231err = pushcmd.Run()232if err != nil {233return fmt.Errorf("cannot push to remote: %w", err)234}235236slog.Debug("checking out branch in workspace")237err = runSSHCommand(ctx, sess, "sh -c 'cd /workspace/empty && git clone /workspace/remote .'")238if err != nil {239return err240}241err = runSSHCommand(ctx, sess, "sh -c 'cd /workspace/empty && git checkout "+branch+"'")242if err != nil {243return err244}245err = runSSHCommand(ctx, sess, "sh -c 'cd /workspace/empty && git config receive.denyCurrentBranch ignore'")246if err != nil {247return err248}249250doneBanner := fmt.Sprintf("\n\n%s\n\nDon't forget to pull your changes to your local working copy before stopping the workspace.\nUse `cd %s && git pull gitpod %s`\n\n", color.New(color.FgGreen, color.Bold).Sprintf("Workspace ready!"), currentDir, branch)251slog.Info(doneBanner)252253switch {254case workspaceCreateOpts.StartOpts.OpenSSH:255err = helper.SSHConnectToWorkspace(ctx, gitpod, workspaceID, false)256if err != nil && err.Error() == "exit status 255" {257err = nil258} else if err != nil {259return err260}261case workspaceCreateOpts.StartOpts.OpenEditor:262return helper.OpenWorkspaceInPreferredEditor(ctx, gitpod, workspaceID)263default:264slog.Info("Access your workspace at", "url", ws.Instance.Status.Url)265}266return nil267},268}269270func runSSHCommand(ctx context.Context, sess *goph.Client, name string, args ...string) error {271cmd, err := sess.Command(name, args...)272if err != nil {273return err274}275out := bytes.NewBuffer(nil)276cmd.Stdout = out277cmd.Stderr = out278slog.Debug("running remote command", "cmd", name, "args", args)279280err = cmd.Run()281if err != nil {282return fmt.Errorf("%w: %s", err, out.String())283}284return nil285}286287func init() {288workspaceCmd.AddCommand(workspaceUpCmd)289addWorkspaceStartOptions(workspaceUpCmd, &workspaceCreateOpts.StartOpts)290291workspaceUpCmd.Flags().StringVar(&workspaceCreateOpts.WorkspaceClass, "class", "", "the workspace class")292workspaceUpCmd.Flags().StringVar(&workspaceCreateOpts.Editor, "editor", "code", "the editor to use")293294_ = workspaceUpCmd.RegisterFlagCompletionFunc("class", classCompletionFunc)295_ = workspaceUpCmd.RegisterFlagCompletionFunc("editor", editorCompletionFunc)296}297298299