Skip to content

[Gitpod CLI] gp rebuild #15638

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 13, 2023
Merged
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
241 changes: 241 additions & 0 deletions components/gitpod-cli/cmd/rebuild.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
// 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 cmd

import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"time"

"github.com/gitpod-io/gitpod/gitpod-cli/pkg/supervisor"
"github.com/gitpod-io/gitpod/gitpod-cli/pkg/utils"
"github.com/gitpod-io/gitpod/supervisor/api"
"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
}

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
}
}

return nil
}

func runRebuild(ctx context.Context, supervisorClient *supervisor.SupervisorClient, event *utils.EventTracker) error {
wsInfo, err := supervisorClient.Info.WorkspaceInfo(ctx, &api.WorkspaceInfoRequest{})
if err != nil {
event.Set("ErrorCode", utils.SystemErrorCode)
return err
}

tmpDir, err := os.MkdirTemp("", "gp-rebuild-*")
if err != nil {
event.Set("ErrorCode", utils.SystemErrorCode)
return err
}
defer os.RemoveAll(tmpDir)

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
}

if gitpodConfig == nil {
fmt.Println("To test the image build, you need to configure your project with a .gitpod.yml file")
fmt.Println("")
fmt.Println("For a quick start, try running:\n$ gp init -i")
fmt.Println("")
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
}

var baseimage string
switch img := gitpodConfig.Image.(type) {
case nil:
baseimage = ""
case string:
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
}
dockerfile, err := os.ReadFile(dockerfilePath)
if err != nil {
event.Set("ErrorCode", utils.RebuildErrorCode_DockerfileCannotRead)
return err
}
if string(dockerfile) == "" {
fmt.Println("Your Gitpod's Dockerfile is empty")
fmt.Println("")
fmt.Println("To learn how to customize your workspace, check out the following docs:")
fmt.Println("https://www.gitpod.io/docs/configure/workspaces/workspace-image#use-a-custom-dockerfile")
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
Copy link
Member

@akosyakov akosyakov Jan 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@andreafalzetti @felladrin for me this branch is hanging 🤔

I think the problem is that it returns nil here. It should return some real error. We need to double check all places, where we return without having err object in advance.

Update hanging actually because of my branch, but I think we should return an error here to exit with 1 properly or another value like ok if we don't want to print an error

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i.e. something like:

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)
}
var exitCode int
if err != nil || !ok {
	exitCode = 1
}
os.Exit(exitCode)

Copy link
Contributor Author

@andreafalzetti andreafalzetti Jan 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oops, I wonder how I missed this :(

will send a follow-up PR!

Update: ok, it's not hanging then.. I will look into this anyway

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is also the default branch of the switch having the same issue, I will check them all

}
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
}

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)
Copy link
Member

@akosyakov akosyakov Jan 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@andreafalzetti @felladrin is not it bogus? 🤔

We should build in the context of /workspace because Dockerfile can make usage of COPY statements?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can use the current working direction to run docker command but then pass -f which pints to the temp dockerfile?

if err != nil {
fmt.Println("Could not write the temporary Dockerfile")
event.Set("ErrorCode", utils.RebuildErrorCode_DockerfileCannotWirte)
return err
}

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
}

tag := "gp-rebuild-temp-build"

dockerCmd := exec.Command(dockerPath, "build", "-t", tag, "--progress=tty", ".")
dockerCmd.Dir = tmpDir
dockerCmd.Stdout = os.Stdout
dockerCmd.Stderr = os.Stderr

imageBuildStartTime := time.Now()
err = dockerCmd.Run()
if _, ok := err.(*exec.ExitError); ok {
fmt.Println("Image Build Failed")
event.Set("ErrorCode", utils.RebuildErrorCode_DockerBuildFailed)
return err
} else if err != nil {
fmt.Println("Docker error")
event.Set("ErrorCode", utils.RebuildErrorCode_DockerErr)
return err
}
ImageBuildDuration := time.Since(imageBuildStartTime).Milliseconds()
event.Set("ImageBuildDuration", ImageBuildDuration)

err = TerminateExistingContainer()
if err != nil {
event.Set("ErrorCode", utils.SystemErrorCode)
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)
return err
}

return nil
}

var buildCmd = &cobra.Command{
Use: "rebuild",
Short: "Re-builds the workspace image (useful to debug a workspace custom image)",
Hidden: false,
Run: func(cmd *cobra.Command, args []string) {
ctx := context.Background()
supervisorClient, err := supervisor.New(ctx)
if err != nil {
utils.LogError(ctx, err, "Could not get workspace info required to build", supervisorClient)
return
}
defer supervisorClient.Close()

event := utils.TrackEvent(ctx, supervisorClient, &utils.TrackCommandUsageParams{
Command: cmd.Name(),
})

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

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

func init() {
rootCmd.AddCommand(buildCmd)
}
33 changes: 33 additions & 0 deletions components/gitpod-cli/pkg/utils/parseGitpodConfig.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// 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 (
"errors"
"os"
"path/filepath"

gitpod "github.com/gitpod-io/gitpod/gitpod-protocol"
yaml "gopkg.in/yaml.v2"
)

func ParseGitpodConfig(repoRoot string) (*gitpod.GitpodConfig, error) {
if repoRoot == "" {
return nil, errors.New("repoRoot is empty")
}
data, err := os.ReadFile(filepath.Join(repoRoot, ".gitpod.yml"))
if err != nil {
// .gitpod.yml not exist is ok
if errors.Is(err, os.ErrNotExist) {
return nil, nil
}
return nil, errors.New("read .gitpod.yml file failed: " + err.Error())
}
var config *gitpod.GitpodConfig
if err = yaml.Unmarshal(data, &config); err != nil {
return nil, errors.New("unmarshal .gitpod.yml file failed" + err.Error())
}
return config, nil
}
116 changes: 116 additions & 0 deletions components/gitpod-cli/pkg/utils/trackEvent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// 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 (
"context"
"time"

gitpod "github.com/gitpod-io/gitpod/gitpod-cli/pkg/gitpod"
"github.com/gitpod-io/gitpod/gitpod-cli/pkg/supervisor"
serverapi "github.com/gitpod-io/gitpod/gitpod-protocol"
"github.com/gitpod-io/gitpod/supervisor/api"
log "github.com/sirupsen/logrus"
)

const (
// System
SystemErrorCode = "system_error"

// Rebuild
RebuildErrorCode_DockerBuildFailed = "rebuild_docker_build_failed"
RebuildErrorCode_DockerErr = "rebuild_docker_err"
RebuildErrorCode_DockerfileCannotRead = "rebuild_dockerfile_cannot_read"
RebuildErrorCode_DockerfileCannotWirte = "rebuild_dockerfile_cannot_write"
RebuildErrorCode_DockerfileEmpty = "rebuild_dockerfile_empty"
RebuildErrorCode_DockerfileNotFound = "rebuild_dockerfile_not_found"
RebuildErrorCode_DockerNotFound = "rebuild_docker_not_found"
RebuildErrorCode_DockerRunFailed = "rebuild_docker_run_failed"
RebuildErrorCode_MalformedGitpodYaml = "rebuild_malformed_gitpod_yaml"
RebuildErrorCode_MissingGitpodYaml = "rebuild_missing_gitpod_yaml"
RebuildErrorCode_NoCustomImage = "rebuild_no_custom_image"
)

type TrackCommandUsageParams struct {
Command string `json:"command,omitempty"`
Duration int64 `json:"duration,omitempty"`
ErrorCode string `json:"errorCode,omitempty"`
WorkspaceId string `json:"workspaceId,omitempty"`
InstanceId string `json:"instanceId,omitempty"`
Timestamp int64 `json:"timestamp,omitempty"`
ImageBuildDuration int64 `json:"imageBuildDuration,omitempty"`
}

type EventTracker struct {
Data *TrackCommandUsageParams
startTime time.Time
serverClient *serverapi.APIoverJSONRPC
supervisorClient *supervisor.SupervisorClient
}

func TrackEvent(ctx context.Context, supervisorClient *supervisor.SupervisorClient, cmdParams *TrackCommandUsageParams) *EventTracker {
tracker := &EventTracker{
startTime: time.Now(),
supervisorClient: supervisorClient,
}

wsInfo, err := supervisorClient.Info.WorkspaceInfo(ctx, &api.WorkspaceInfoRequest{})
if err != nil {
LogError(ctx, err, "Could not fetch the workspace info", supervisorClient)
return nil
}

serverClient, err := gitpod.ConnectToServer(ctx, wsInfo, []string{"function:trackEvent"})
if err != nil {
log.WithError(err).Fatal("error connecting to server")
return nil
}

tracker.serverClient = serverClient

tracker.Data = &TrackCommandUsageParams{
Command: cmdParams.Command,
Duration: 0,
WorkspaceId: wsInfo.WorkspaceId,
InstanceId: wsInfo.InstanceId,
ErrorCode: "",
Timestamp: time.Now().UnixMilli(),
}

return tracker
}

func (t *EventTracker) Set(key string, value interface{}) *EventTracker {
switch key {
case "Command":
t.Data.Command = value.(string)
case "ErrorCode":
t.Data.ErrorCode = value.(string)
case "Duration":
t.Data.Duration = value.(int64)
case "WorkspaceId":
t.Data.WorkspaceId = value.(string)
case "InstanceId":
t.Data.InstanceId = value.(string)
case "ImageBuildDuration":
t.Data.ImageBuildDuration = value.(int64)
}
return t
}

func (t *EventTracker) Send(ctx context.Context) {
t.Set("Duration", time.Since(t.startTime).Milliseconds())

event := &serverapi.RemoteTrackMessage{
Event: "gp_command",
Properties: t.Data,
}

err := t.serverClient.TrackEvent(ctx, event)
if err != nil {
LogError(ctx, err, "Could not track gp command event", t.supervisorClient)
return
}
}