Skip to content

Commit 1e24370

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

File tree

3 files changed

+390
-0
lines changed

3 files changed

+390
-0
lines changed

components/gitpod-cli/cmd/rebuild.go

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

0 commit comments

Comments
 (0)