-
Notifications
You must be signed in to change notification settings - Fork 1.3k
[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
[Gitpod CLI] gp rebuild
#15638
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
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) | ||
} |
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 | ||
} |
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"}) | ||
andreafalzetti marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.