From 8f6e79e90f83a5695bcfed3ff00d7367acdd81c0 Mon Sep 17 00:00:00 2001 From: Andrea Falzetti Date: Wed, 4 Jan 2023 12:45:31 +0000 Subject: [PATCH] gitpod-cli: add gp rebuild cmd Co-authored-by: Victor Nogueira --- components/gitpod-cli/cmd/rebuild.go | 241 ++++++++++++++++++ .../gitpod-cli/pkg/utils/parseGitpodConfig.go | 33 +++ components/gitpod-cli/pkg/utils/trackEvent.go | 116 +++++++++ 3 files changed, 390 insertions(+) create mode 100644 components/gitpod-cli/cmd/rebuild.go create mode 100644 components/gitpod-cli/pkg/utils/parseGitpodConfig.go create mode 100644 components/gitpod-cli/pkg/utils/trackEvent.go diff --git a/components/gitpod-cli/cmd/rebuild.go b/components/gitpod-cli/cmd/rebuild.go new file mode 100644 index 00000000000000..e6d26514cef9f4 --- /dev/null +++ b/components/gitpod-cli/cmd/rebuild.go @@ -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 + } + 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) + 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) +} diff --git a/components/gitpod-cli/pkg/utils/parseGitpodConfig.go b/components/gitpod-cli/pkg/utils/parseGitpodConfig.go new file mode 100644 index 00000000000000..c78f5656809783 --- /dev/null +++ b/components/gitpod-cli/pkg/utils/parseGitpodConfig.go @@ -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 +} diff --git a/components/gitpod-cli/pkg/utils/trackEvent.go b/components/gitpod-cli/pkg/utils/trackEvent.go new file mode 100644 index 00000000000000..38a148719aee01 --- /dev/null +++ b/components/gitpod-cli/pkg/utils/trackEvent.go @@ -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 + } +}