Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
209 changes: 112 additions & 97 deletions components/gitpod-cli/cmd/rebuild.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import (
"fmt"
"os"
"os/exec"
"os/signal"
"path/filepath"
"strings"
"syscall"
"time"

"github.com/gitpod-io/gitpod/gitpod-cli/pkg/supervisor"
Expand All @@ -19,56 +20,59 @@ import (
"github.com/spf13/cobra"
)

func TerminateExistingContainer() error {
cmd := exec.Command("docker", "ps", "-q", "-f", "label=gp-rebuild")
containerIds, err := cmd.Output()
if err != nil {
return err
func runRebuild(ctx context.Context, supervisorClient *supervisor.SupervisorClient, event *utils.EventTracker) (bool, error) {
rebuildCtx := &rebuildContext{
supervisorClient: supervisorClient,
imageTag: "gp-rebuild",
}

for _, id := range strings.Split(string(containerIds), "\n") {
if len(id) == 0 {
continue
}

cmd = exec.Command("docker", "stop", id)
err := cmd.Run()
if err != nil {
return err
}

cmd = exec.Command("docker", "rm", "-f", id)
err = cmd.Run()
if err != nil {
return err
}
valid, err := validate(ctx, event, rebuildCtx)
if !valid || err != nil {
return valid, err
}

return nil
}

func runRebuild(ctx context.Context, supervisorClient *supervisor.SupervisorClient, event *utils.EventTracker) error {
wsInfo, err := supervisorClient.Info.WorkspaceInfo(ctx, &api.WorkspaceInfoRequest{})
buildDir, err := os.MkdirTemp("", "gp-rebuild-*")
if err != nil {
event.Set("ErrorCode", utils.SystemErrorCode)
return err
return false, err
}
defer os.RemoveAll(buildDir)
rebuildCtx.buildDir = buildDir

tmpDir, err := os.MkdirTemp("", "gp-rebuild-*")
err = buildImage(ctx, event, rebuildCtx)
if err != nil {
return false, err
}
err = debug(ctx, event, rebuildCtx)
if err != nil {
return false, err
}
return true, nil
}

type rebuildContext struct {
supervisorClient *supervisor.SupervisorClient
baseImage string
buildDir string
dockerPath string
imageTag string
}

func validate(ctx context.Context, event *utils.EventTracker, rebuildCtx *rebuildContext) (bool, error) {
wsInfo, err := rebuildCtx.supervisorClient.Info.WorkspaceInfo(ctx, &api.WorkspaceInfoRequest{})
if err != nil {
event.Set("ErrorCode", utils.SystemErrorCode)
return err
return false, err
}
defer os.RemoveAll(tmpDir)

// TODO make it flexible, look for first .gitopd.yml traversing parents?
gitpodConfig, err := utils.ParseGitpodConfig(wsInfo.CheckoutLocation)
if err != nil {
fmt.Println("The .gitpod.yml file cannot be parsed: please check the file and try again")
fmt.Println("")
fmt.Println("For help check out the reference page:")
fmt.Println("https://www.gitpod.io/docs/references/gitpod-yml#gitpodyml")
event.Set("ErrorCode", utils.RebuildErrorCode_MalformedGitpodYaml)
return err
return false, err
}

if gitpodConfig == nil {
Expand All @@ -79,27 +83,27 @@ func runRebuild(ctx context.Context, supervisorClient *supervisor.SupervisorClie
fmt.Println("Alternatively, check out the following docs for getting started configuring your project")
fmt.Println("https://www.gitpod.io/docs/configure#configure-gitpod")
event.Set("ErrorCode", utils.RebuildErrorCode_MissingGitpodYaml)
return err
return false, err
}

var baseimage string
var baseImage string
switch img := gitpodConfig.Image.(type) {
case nil:
baseimage = ""
baseImage = ""
case string:
baseimage = "FROM " + img
baseImage = "FROM " + img
case map[interface{}]interface{}:
dockerfilePath := filepath.Join(wsInfo.CheckoutLocation, img["file"].(string))

if _, err := os.Stat(dockerfilePath); os.IsNotExist(err) {
fmt.Println("Your .gitpod.yml points to a Dockerfile that doesn't exist: " + dockerfilePath)
event.Set("ErrorCode", utils.RebuildErrorCode_DockerfileNotFound).Send(ctx)
return err
return false, err
}
dockerfile, err := os.ReadFile(dockerfilePath)
if err != nil {
event.Set("ErrorCode", utils.RebuildErrorCode_DockerfileCannotRead)
return err
return false, err
}
if string(dockerfile) == "" {
fmt.Println("Your Gitpod's Dockerfile is empty")
Expand All @@ -109,46 +113,50 @@ func runRebuild(ctx context.Context, supervisorClient *supervisor.SupervisorClie
fmt.Println("")
fmt.Println("Once you configure your Dockerfile, re-run this command to validate your changes")
event.Set("ErrorCode", utils.RebuildErrorCode_DockerfileEmpty)
return err
return false, err
}
baseimage = "\n" + string(dockerfile) + "\n"
baseImage = "\n" + string(dockerfile) + "\n"
default:
fmt.Println("Check your .gitpod.yml and make sure the image property is configured correctly")
event.Set("ErrorCode", utils.RebuildErrorCode_MalformedGitpodYaml)
return err
return false, err
}

if baseimage == "" {
if baseImage == "" {
fmt.Println("Your project is not using any custom Docker image.")
fmt.Println("Check out the following docs, to know how to get started")
fmt.Println("")
fmt.Println("https://www.gitpod.io/docs/configure/workspaces/workspace-image#use-a-public-docker-image")
event.Set("ErrorCode", utils.RebuildErrorCode_NoCustomImage)
return err
}

err = os.WriteFile(filepath.Join(tmpDir, "Dockerfile"), []byte(baseimage), 0644)
if err != nil {
fmt.Println("Could not write the temporary Dockerfile")
event.Set("ErrorCode", utils.RebuildErrorCode_DockerfileCannotWirte)
return err
return false, err
}
rebuildCtx.baseImage = baseImage
return true, nil
}

func buildImage(ctx context.Context, event *utils.EventTracker, rebuildCtx *rebuildContext) error {
dockerPath, err := exec.LookPath("docker")
if err != nil {
fmt.Println("Docker is not installed in your workspace")
event.Set("ErrorCode", utils.RebuildErrorCode_DockerNotFound)
return err
}
rebuildCtx.dockerPath = dockerPath

tag := "gp-rebuild-temp-build"
dockerFile := filepath.Join(rebuildCtx.buildDir, "Dockerfile")
err = os.WriteFile(dockerFile, []byte(rebuildCtx.baseImage), 0644)
if err != nil {
fmt.Println("Could not write the temporary Dockerfile")
event.Set("ErrorCode", utils.RebuildErrorCode_DockerfileCannotWrite)
return err
}

dockerCmd := exec.Command(dockerPath, "build", "-t", tag, "--progress=tty", ".")
dockerCmd.Dir = tmpDir
imageBuildStartTime := time.Now()
fmt.Println("building the workspace image...")
dockerCmd := exec.CommandContext(ctx, dockerPath, "build", "-t", rebuildCtx.imageTag, ".", "-f", dockerFile)
dockerCmd.Stdout = os.Stdout
dockerCmd.Stderr = os.Stderr

imageBuildStartTime := time.Now()
dockerCmd.Env = append(os.Environ(), "DOCKER_BUILDKIT=1")
err = dockerCmd.Run()
if _, ok := err.(*exec.ExitError); ok {
fmt.Println("Image Build Failed")
Expand All @@ -161,57 +169,56 @@ func runRebuild(ctx context.Context, supervisorClient *supervisor.SupervisorClie
}
ImageBuildDuration := time.Since(imageBuildStartTime).Milliseconds()
event.Set("ImageBuildDuration", ImageBuildDuration)
return err
}

err = TerminateExistingContainer()
func debug(ctx context.Context, event *utils.EventTracker, rebuildCtx *rebuildContext) (err error) {
// TODO: stopDebug to release the root FS

// TODO skip if image did not change, it is very slow, how to tune?
exportStartTime := time.Now()
exportPhase := "exporting the workspace root filesystem"
fmt.Println(exportPhase + ", may take sometime...")
exportCmd := exec.CommandContext(ctx, "sudo", "/.supervisor/supervisor", "export-rootfs", rebuildCtx.buildDir, rebuildCtx.dockerPath, rebuildCtx.imageTag)
exportCmd.Stdout = os.Stdout
exportCmd.Stderr = os.Stderr
err = exportCmd.Run()
event.Data.ExportRootFileSystemDuration = time.Since(exportStartTime).Milliseconds()
fmt.Printf(exportPhase+" finished in %s \n",
(time.Duration(event.Data.ExportRootFileSystemDuration) * time.Millisecond).Round(time.Second).String(),
)
if err != nil {
event.Set("ErrorCode", utils.SystemErrorCode)
event.Data.ErrorCode = utils.RebuildErrorCode_ExportRootFSFailed
return err
}

messages := []string{
"\n\nYou are now connected to the container",
"You can inspect the container and make sure the necessary tools & libraries are installed.",
"When you are done, just type exit to return to your Gitpod workspace\n",
}

welcomeMessage := strings.Join(messages, "\n")

dockerRunCmd := exec.Command(
dockerPath,
"run",
"--rm",
"--label", "gp-rebuild=true",
"-it",
tag,
"bash",
"-c",
fmt.Sprintf("echo '%s'; bash", welcomeMessage),
)

dockerRunCmd.Stdout = os.Stdout
dockerRunCmd.Stderr = os.Stderr
dockerRunCmd.Stdin = os.Stdin

err = dockerRunCmd.Run()
if _, ok := err.(*exec.ExitError); ok {
fmt.Println("Docker Run Command Failed")
event.Set("ErrorCode", utils.RebuildErrorCode_DockerRunFailed)
return err
} else if err != nil {
fmt.Println("Docker error")
event.Set("ErrorCode", utils.RebuildErrorCode_DockerErr)
fmt.Println("starting a debug workspace...")
debugCmd := exec.CommandContext(ctx, "/.supervisor/supervisor", "inner-loop")
debugCmd.Stdout = os.Stdout
debugCmd.Stderr = os.Stderr
err = debugCmd.Run()
if err != nil {
event.Data.ErrorCode = utils.RebuildErrorCode_DebugFailed
return err
}

return nil
}

var buildCmd = &cobra.Command{
Use: "rebuild",
Short: "Re-builds the workspace image (useful to debug a workspace custom image)",
Short: "Re-builds the workspace (useful to debug a workspace configuration)",
Hidden: false,
Run: func(cmd *cobra.Command, args []string) {
ctx := context.Background()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP)
go func() {
<-sigChan
cancel()
}()

supervisorClient, err := supervisor.New(ctx)
if err != nil {
utils.LogError(ctx, err, "Could not get workspace info required to build", supervisorClient)
Expand All @@ -223,16 +230,24 @@ var buildCmd = &cobra.Command{
Command: cmd.Name(),
})

err = runRebuild(ctx, supervisorClient, event)
if err != nil && event.Data.ErrorCode == "" {
event.Set("ErrorCode", utils.SystemErrorCode)
ok, err := runRebuild(ctx, supervisorClient, event)
if event.Data.ErrorCode == "" {
if err != nil {
event.Set("ErrorCode", utils.SystemErrorCode)
} else if !ok {
event.Set("ErrorCode", utils.UserErrorCode)
}
}
event.Send(ctx)

if err != nil {
utils.LogError(ctx, err, "Failed to rebuild", supervisorClient)
os.Exit(1)
}
var exitCode int
if err != nil || !ok {
exitCode = 1
}
os.Exit(exitCode)
},
}

Expand Down
6 changes: 6 additions & 0 deletions components/gitpod-cli/cmd/stop-workspace.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ package cmd

import (
"context"
"syscall"
"time"

gitpod "github.com/gitpod-io/gitpod/gitpod-cli/pkg/gitpod"
"github.com/gitpod-io/gitpod/gitpod-cli/pkg/utils"
"github.com/spf13/cobra"
)

Expand All @@ -20,6 +22,10 @@ var stopWorkspaceCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if utils.IsRunningInDebugWorkspace() {
_ = syscall.Kill(1, syscall.SIGTERM)
return
}
wsInfo, err := gitpod.GetWSInfo(ctx)
if err != nil {
fail(err.Error())
Expand Down
11 changes: 11 additions & 0 deletions components/gitpod-cli/pkg/utils/debug.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright (c) 2023 Gitpod GmbH. All rights reserved.
// Licensed under the GNU Affero General Public License (AGPL).
// See License.AGPL.txt in the project root for license information.

package utils

import "os"

func IsRunningInDebugWorkspace() bool {
return os.Getenv("SUPERVISOR_DEBUG_WORKSPACE") == "true"
}
Loading