Skip to content

Commit c61d095

Browse files
Andrea Falzettifelladrin
Andrea Falzetti
andcommitted
gitpod-cli: add gp rebuild cmd
Co-authored-by: Victor Nogueira <[email protected]>
1 parent fad3409 commit c61d095

File tree

3 files changed

+386
-0
lines changed

3 files changed

+386
-0
lines changed

components/gitpod-cli/cmd/rebuild.go

+235
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
// Copyright (c) 2023 Gitpod GmbH. All rights reserved.
2+
// Licensed under the GNU Affero General Public License (AGPL).
3+
// See License.AGPL.txt in the project root for license information.
4+
5+
package cmd
6+
7+
import (
8+
"context"
9+
"fmt"
10+
"os"
11+
"os/exec"
12+
"path/filepath"
13+
"strings"
14+
"time"
15+
16+
"github.com/gitpod-io/gitpod/gitpod-cli/pkg/supervisor"
17+
"github.com/gitpod-io/gitpod/gitpod-cli/pkg/utils"
18+
"github.com/gitpod-io/gitpod/supervisor/api"
19+
log "github.com/sirupsen/logrus"
20+
"github.com/spf13/cobra"
21+
)
22+
23+
func TerminateExistingContainer() error {
24+
cmd := exec.Command("docker", "ps", "-q", "-f", "label=gp-rebuild")
25+
containerIds, err := cmd.Output()
26+
if err != nil {
27+
return err
28+
}
29+
30+
for _, id := range strings.Split(string(containerIds), "\n") {
31+
if len(id) == 0 {
32+
continue
33+
}
34+
35+
cmd = exec.Command("docker", "stop", id)
36+
err := cmd.Run()
37+
if err != nil {
38+
return err
39+
}
40+
41+
cmd = exec.Command("docker", "rm", "-f", id)
42+
err = cmd.Run()
43+
if err != nil {
44+
return err
45+
}
46+
}
47+
48+
return nil
49+
}
50+
51+
var buildCmd = &cobra.Command{
52+
Use: "rebuild",
53+
Short: "Re-builds the workspace image (useful to debug a workspace custom image)",
54+
Hidden: false,
55+
Run: func(cmd *cobra.Command, args []string) {
56+
ctx := context.Background()
57+
58+
client, err := supervisor.New(ctx)
59+
if err != nil {
60+
utils.LogError(ctx, err, "Could not get workspace info required to build", client)
61+
return
62+
}
63+
defer client.Close()
64+
65+
wsInfo, err := client.Info.WorkspaceInfo(ctx, &api.WorkspaceInfoRequest{})
66+
if err != nil {
67+
utils.LogError(ctx, err, "Could not fetch the workspace info", client)
68+
return
69+
}
70+
71+
event := utils.TrackEvent(ctx, client, &utils.TrackCommandUsageParams{
72+
Command: cmd.Name(),
73+
})
74+
defer event.Send(ctx)
75+
76+
tmpDir, err := os.MkdirTemp("", "gp-rebuild-*")
77+
if err != nil {
78+
event.Set("ErrorCode", utils.SystemErrorCode_TmpDirCannotWrite)
79+
log.WithError(err).Error("Could not create temporary directory")
80+
return
81+
}
82+
defer os.RemoveAll(tmpDir)
83+
84+
gitpodConfig, err := utils.ParseGitpodConfig(wsInfo.CheckoutLocation)
85+
if err != nil {
86+
fmt.Println("The .gitpod.yml file cannot be parsed: please check the file and try again")
87+
fmt.Println("")
88+
fmt.Println("For help check out the reference page:")
89+
fmt.Println("https://www.gitpod.io/docs/references/gitpod-yml#gitpodyml")
90+
event.Set("ErrorCode", utils.RebuildErrorCode_MalformedGitpodYaml)
91+
return
92+
}
93+
94+
if gitpodConfig == nil {
95+
fmt.Println("To test the image build, you need to configure your project with a .gitpod.yml file")
96+
fmt.Println("")
97+
fmt.Println("For a quick start, try running:\n$ gp init -i")
98+
fmt.Println("")
99+
fmt.Println("Alternatively, check out the following docs for getting started configuring your project")
100+
fmt.Println("https://www.gitpod.io/docs/configure#configure-gitpod")
101+
event.Set("ErrorCode", utils.RebuildErrorCode_MissingGitpodYaml)
102+
return
103+
}
104+
105+
var baseimage string
106+
switch img := gitpodConfig.Image.(type) {
107+
case nil:
108+
baseimage = ""
109+
case string:
110+
baseimage = "FROM " + img
111+
case map[interface{}]interface{}:
112+
dockerfilePath := filepath.Join(wsInfo.CheckoutLocation, img["file"].(string))
113+
114+
if _, err := os.Stat(dockerfilePath); os.IsNotExist(err) {
115+
fmt.Println("Your .gitpod.yml points to a Dockerfile that doesn't exist: " + dockerfilePath)
116+
event.Set("ErrorCode", utils.RebuildErrorCode_DockerfileNotFound).Send(ctx)
117+
return
118+
}
119+
dockerfile, err := os.ReadFile(dockerfilePath)
120+
if err != nil {
121+
event.Set("ErrorCode", utils.RebuildErrorCode_DockerfileCannotRead)
122+
log.WithError(err).Error("Could not read the Dockerfile")
123+
return
124+
}
125+
if string(dockerfile) == "" {
126+
fmt.Println("Your Gitpod's Dockerfile is empty")
127+
fmt.Println("")
128+
fmt.Println("To learn how to customize your workspace, check out the following docs:")
129+
fmt.Println("https://www.gitpod.io/docs/configure/workspaces/workspace-image#use-a-custom-dockerfile")
130+
fmt.Println("")
131+
fmt.Println("Once you configure your Dockerfile, re-run this command to validate your changes")
132+
event.Set("ErrorCode", utils.RebuildErrorCode_DockerfileEmpty)
133+
return
134+
}
135+
baseimage = "\n" + string(dockerfile) + "\n"
136+
default:
137+
fmt.Println("Check your .gitpod.yml and make sure the image property is configured correctly")
138+
event.Set("ErrorCode", utils.RebuildErrorCode_MalformedGitpodYaml)
139+
return
140+
}
141+
142+
if baseimage == "" {
143+
fmt.Println("Your project is not using any custom Docker image.")
144+
fmt.Println("Check out the following docs, to know how to get started")
145+
fmt.Println("")
146+
fmt.Println("https://www.gitpod.io/docs/configure/workspaces/workspace-image#use-a-public-docker-image")
147+
event.Set("ErrorCode", utils.RebuildErrorCode_NoCustomImage)
148+
return
149+
}
150+
151+
err = os.WriteFile(filepath.Join(tmpDir, "Dockerfile"), []byte(baseimage), 0644)
152+
if err != nil {
153+
fmt.Println("Could not write the temporary Dockerfile")
154+
event.Set("ErrorCode", utils.RebuildErrorCode_DockerfileCannotWirte)
155+
log.WithError(err).Error(err)
156+
return
157+
}
158+
159+
dockerPath, err := exec.LookPath("docker")
160+
if err != nil {
161+
fmt.Println("Docker is not installed in your workspace")
162+
event.Set("ErrorCode", utils.RebuildErrorCode_DockerNotFound)
163+
return
164+
}
165+
166+
tag := "gp-rebuild-temp-build"
167+
168+
dockerCmd := exec.Command(dockerPath, "build", "-t", tag, "--progress=tty", ".")
169+
dockerCmd.Dir = tmpDir
170+
dockerCmd.Stdout = os.Stdout
171+
dockerCmd.Stderr = os.Stderr
172+
173+
imageBuildStartTime := time.Now()
174+
err = dockerCmd.Run()
175+
if _, ok := err.(*exec.ExitError); ok {
176+
fmt.Println("Image Build Failed")
177+
event.Set("ErrorCode", utils.RebuildErrorCode_DockerBuildFailed)
178+
log.WithError(err).Error(err)
179+
return
180+
} else if err != nil {
181+
fmt.Println("Docker error")
182+
event.Set("ErrorCode", utils.RebuildErrorCode_DockerErr)
183+
log.WithError(err).Error(err)
184+
return
185+
}
186+
iamgeBuildDurationSeconds := time.Since(imageBuildStartTime).Seconds()
187+
event.Set("ImageBuildDurationSeconds", iamgeBuildDurationSeconds)
188+
189+
err = TerminateExistingContainer()
190+
if err != nil {
191+
utils.LogError(ctx, err, "Failed to stop previous gp rebuild container", client)
192+
}
193+
194+
messages := []string{
195+
"\n\nYou are now connected to the container",
196+
"You can inspect the container and make sure the necessary tools & libraries are installed.",
197+
"When you are done, just type exit to return to your Gitpod workspace\n",
198+
}
199+
200+
welcomeMessage := strings.Join(messages, "\n")
201+
202+
dockerRunCmd := exec.Command(
203+
dockerPath,
204+
"run",
205+
"--rm",
206+
"--label", "gp-rebuild=true",
207+
"-it",
208+
tag,
209+
"bash",
210+
"-c",
211+
fmt.Sprintf("echo '%s'; bash", welcomeMessage),
212+
)
213+
214+
dockerRunCmd.Stdout = os.Stdout
215+
dockerRunCmd.Stderr = os.Stderr
216+
dockerRunCmd.Stdin = os.Stdin
217+
218+
err = dockerRunCmd.Run()
219+
if _, ok := err.(*exec.ExitError); ok {
220+
fmt.Println("Docker Run Command Failed")
221+
event.Set("ErrorCode", utils.RebuildErrorCode_DockerRunFailed)
222+
log.WithError(err).Error(err)
223+
return
224+
} else if err != nil {
225+
fmt.Println("Docker error")
226+
event.Set("ErrorCode", utils.RebuildErrorCode_DockerErr)
227+
log.WithError(err).Error(err)
228+
return
229+
}
230+
},
231+
}
232+
233+
func init() {
234+
rootCmd.AddCommand(buildCmd)
235+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright (c) 2023 Gitpod GmbH. All rights reserved.
2+
// Licensed under the GNU Affero General Public License (AGPL).
3+
// See License.AGPL.txt in the project root for license information.
4+
5+
package utils
6+
7+
import (
8+
"errors"
9+
"os"
10+
"path/filepath"
11+
12+
gitpod "github.com/gitpod-io/gitpod/gitpod-protocol"
13+
yaml "gopkg.in/yaml.v2"
14+
)
15+
16+
func ParseGitpodConfig(repoRoot string) (*gitpod.GitpodConfig, error) {
17+
if repoRoot == "" {
18+
return nil, errors.New("repoRoot is empty")
19+
}
20+
data, err := os.ReadFile(filepath.Join(repoRoot, ".gitpod.yml"))
21+
if err != nil {
22+
// .gitpod.yml not exist is ok
23+
if errors.Is(err, os.ErrNotExist) {
24+
return nil, nil
25+
}
26+
return nil, errors.New("read .gitpod.yml file failed: " + err.Error())
27+
}
28+
var config *gitpod.GitpodConfig
29+
if err = yaml.Unmarshal(data, &config); err != nil {
30+
return nil, errors.New("unmarshal .gitpod.yml file failed" + err.Error())
31+
}
32+
return config, nil
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
// Copyright (c) 2023 Gitpod GmbH. All rights reserved.
2+
// Licensed under the GNU Affero General Public License (AGPL).
3+
// See License.AGPL.txt in the project root for license information.
4+
5+
package utils
6+
7+
import (
8+
"context"
9+
"time"
10+
11+
gitpod "github.com/gitpod-io/gitpod/gitpod-cli/pkg/gitpod"
12+
"github.com/gitpod-io/gitpod/gitpod-cli/pkg/supervisor"
13+
serverapi "github.com/gitpod-io/gitpod/gitpod-protocol"
14+
"github.com/gitpod-io/gitpod/supervisor/api"
15+
log "github.com/sirupsen/logrus"
16+
)
17+
18+
const (
19+
// System
20+
SystemErrorCode_TmpDirCannotWrite = "system_tmpdir_cannot_write"
21+
22+
// Rebuild
23+
RebuildErrorCode_DockerBuildFailed = "rebuild_docker_build_failed"
24+
RebuildErrorCode_DockerErr = "rebuild_docker_err"
25+
RebuildErrorCode_DockerfileCannotRead = "rebuild_dockerfile_cannot_read"
26+
RebuildErrorCode_DockerfileCannotWirte = "rebuild_dockerfile_cannot_write"
27+
RebuildErrorCode_DockerfileEmpty = "rebuild_dockerfile_empty"
28+
RebuildErrorCode_DockerfileNotFound = "rebuild_dockerfile_not_found"
29+
RebuildErrorCode_DockerNotFound = "rebuild_docker_not_found"
30+
RebuildErrorCode_DockerRunFailed = "rebuild_docker_run_failed"
31+
RebuildErrorCode_MalformedGitpodYaml = "rebuild_malformed_gitpod_yaml"
32+
RebuildErrorCode_MissingGitpodYaml = "rebuild_missing_gitpod_yaml"
33+
RebuildErrorCode_NoCustomImage = "rebuild_no_custom_image"
34+
)
35+
36+
type TrackCommandUsageParams struct {
37+
Command string `json:"command,omitempty"`
38+
DurationMs int64 `json:"durationMs,omitempty"`
39+
ErrorCode string `json:"errorCode,omitempty"`
40+
WorkspaceId string `json:"workspaceId,omitempty"`
41+
InstanceId string `json:"instanceId,omitempty"`
42+
Timestamp int64 `json:"timestamp,omitempty"`
43+
DockerBuildDurationSeconds float64 `json:"dockerBuildDurationSeconds,omitempty"`
44+
}
45+
46+
type EventTracker struct {
47+
data *TrackCommandUsageParams
48+
startTime time.Time
49+
serverClient *serverapi.APIoverJSONRPC
50+
supervisorClient *supervisor.SupervisorClient
51+
}
52+
53+
func TrackEvent(ctx context.Context, supervisorClient *supervisor.SupervisorClient, cmdParams *TrackCommandUsageParams) *EventTracker {
54+
tracker := &EventTracker{
55+
startTime: time.Now(),
56+
supervisorClient: supervisorClient,
57+
}
58+
59+
wsInfo, err := supervisorClient.Info.WorkspaceInfo(ctx, &api.WorkspaceInfoRequest{})
60+
if err != nil {
61+
LogError(ctx, err, "Could not fetch the workspace info", supervisorClient)
62+
return nil
63+
}
64+
65+
serverClient, err := gitpod.ConnectToServer(ctx, wsInfo, []string{"function:trackEvent"})
66+
if err != nil {
67+
log.WithError(err).Fatal("error connecting to server")
68+
return nil
69+
}
70+
71+
tracker.serverClient = serverClient
72+
73+
tracker.data = &TrackCommandUsageParams{
74+
Command: cmdParams.Command,
75+
DurationMs: 0,
76+
WorkspaceId: wsInfo.WorkspaceId,
77+
InstanceId: wsInfo.InstanceId,
78+
ErrorCode: "",
79+
Timestamp: time.Now().UnixMilli(),
80+
}
81+
82+
return tracker
83+
}
84+
85+
func (t *EventTracker) Set(key string, value interface{}) *EventTracker {
86+
switch key {
87+
case "Command":
88+
t.data.Command = value.(string)
89+
case "ErrorCode":
90+
t.data.ErrorCode = value.(string)
91+
case "DurationMs":
92+
t.data.DurationMs = value.(int64)
93+
case "WorkspaceId":
94+
t.data.WorkspaceId = value.(string)
95+
case "InstanceId":
96+
t.data.InstanceId = value.(string)
97+
case "DockerBuildDurationSeconds":
98+
t.data.DockerBuildDurationSeconds = value.(float64)
99+
}
100+
return t
101+
}
102+
103+
func (t *EventTracker) Send(ctx context.Context) {
104+
defer t.serverClient.Close()
105+
106+
t.Set("DurationMs", time.Since(t.startTime).Milliseconds())
107+
108+
event := &serverapi.RemoteTrackMessage{
109+
Event: "gp_command",
110+
Properties: t.data,
111+
}
112+
113+
err := t.serverClient.TrackEvent(ctx, event)
114+
if err != nil {
115+
LogError(ctx, err, "Could not track gp command event", t.supervisorClient)
116+
return
117+
}
118+
}

0 commit comments

Comments
 (0)