Skip to content

Commit 3ebedc9

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

File tree

3 files changed

+382
-0
lines changed

3 files changed

+382
-0
lines changed

components/gitpod-cli/cmd/rebuild.go

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

0 commit comments

Comments
 (0)