Skip to content

Commit 755b956

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

File tree

3 files changed

+378
-0
lines changed

3 files changed

+378
-0
lines changed

components/gitpod-cli/cmd/rebuild.go

+231
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
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, cancel := context.WithTimeout(context.Background(), 5*time.Second)
57+
defer cancel()
58+
59+
client, err := supervisor.New(ctx)
60+
if err != nil {
61+
utils.LogError(ctx, err, "Could not get workspace info required to build", client)
62+
return
63+
}
64+
defer client.Close()
65+
66+
tmpDir, err := os.MkdirTemp("", "gp-rebuild-*")
67+
if err != nil {
68+
log.Fatal("Could not create temporary directory")
69+
return
70+
}
71+
defer os.RemoveAll(tmpDir)
72+
73+
wsInfo, err := client.Info.WorkspaceInfo(ctx, &api.WorkspaceInfoRequest{})
74+
if err != nil {
75+
utils.LogError(ctx, err, "Could not fetch the workspace info", client)
76+
return
77+
}
78+
79+
event := utils.TrackEvent(ctx, client, &utils.TrackCommandUsageParams{
80+
Command: cmd.Name(),
81+
})
82+
83+
ctx = context.Background()
84+
85+
gitpodConfig, err := utils.ParseGitpodConfig(wsInfo.CheckoutLocation)
86+
if err != nil {
87+
fmt.Println("The .gitpod.yml file cannot be parsed: please check the file and try again")
88+
fmt.Println("")
89+
fmt.Println("For help check out the reference page:")
90+
fmt.Println("https://www.gitpod.io/docs/references/gitpod-yml#gitpodyml")
91+
event.Set("ErrorCode", utils.RebuildErrorCode_MalformedGitpodYaml).Send(ctx)
92+
return
93+
}
94+
95+
if gitpodConfig == nil {
96+
fmt.Println("To test the image build, you need to configure your project with a .gitpod.yml file")
97+
fmt.Println("")
98+
fmt.Println("For a quick start, try running:\n$ gp init -i")
99+
fmt.Println("")
100+
fmt.Println("Alternatively, check out the following docs for getting started configuring your project")
101+
fmt.Println("https://www.gitpod.io/docs/configure#configure-gitpod")
102+
event.Set("ErrorCode", utils.RebuildErrorCode_MissingGitpodYaml).Send(ctx)
103+
return
104+
}
105+
106+
var baseimage string
107+
switch img := gitpodConfig.Image.(type) {
108+
case nil:
109+
baseimage = ""
110+
case string:
111+
baseimage = "FROM " + img
112+
case map[interface{}]interface{}:
113+
dockerfilePath := filepath.Join(wsInfo.CheckoutLocation, img["file"].(string))
114+
115+
if _, err := os.Stat(dockerfilePath); os.IsNotExist(err) {
116+
fmt.Println("Your .gitpod.yml points to a Dockerfile that doesn't exist: " + dockerfilePath)
117+
event.Set("ErrorCode", utils.RebuildErrorCode_DockerfileNotFound).Send(ctx)
118+
return
119+
}
120+
dockerfile, err := os.ReadFile(dockerfilePath)
121+
if err != nil {
122+
log.Fatal("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).Send(ctx)
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).Send(ctx)
139+
return
140+
}
141+
142+
if baseimage == "" {
143+
// TODO: should this fallback to workspace full?
144+
fmt.Println("Your project is not using any custom Docker image.")
145+
fmt.Println("Check out the following docs, to know how to get started")
146+
fmt.Println("")
147+
fmt.Println("https://www.gitpod.io/docs/configure/workspaces/workspace-image#use-a-public-docker-image")
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+
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).Send(ctx)
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+
err = dockerCmd.Run()
173+
if _, ok := err.(*exec.ExitError); ok {
174+
fmt.Println("Image Build Failed")
175+
event.Set("ErrorCode", utils.RebuildErrorCode_DockerBuildFailed).Send(ctx)
176+
log.Fatal(err)
177+
return
178+
} else if err != nil {
179+
fmt.Println("Docker error")
180+
event.Set("ErrorCode", utils.RebuildErrorCode_DockerErr).Send(ctx)
181+
log.Fatal(err)
182+
return
183+
}
184+
185+
err = TerminateExistingContainer()
186+
if err != nil {
187+
utils.LogError(ctx, err, "Failed to stop previous gp rebuild container", client)
188+
}
189+
190+
messages := []string{
191+
"\n\nYou are now connected to the container",
192+
"You can inspect the container and make sure the necessary tools & libraries are installed.",
193+
"When you are done, just type exit to return to your Gitpod workspace\n",
194+
}
195+
196+
welcomeMessage := strings.Join(messages, "\n")
197+
198+
dockerRunCmd := exec.Command(
199+
dockerPath,
200+
"run",
201+
"--rm",
202+
"--label", "gp-rebuild=true",
203+
"-it",
204+
tag,
205+
"bash",
206+
"-c",
207+
fmt.Sprintf("echo '%s'; bash", welcomeMessage),
208+
)
209+
210+
dockerRunCmd.Stdout = os.Stdout
211+
dockerRunCmd.Stderr = os.Stderr
212+
dockerRunCmd.Stdin = os.Stdin
213+
214+
err = dockerRunCmd.Run()
215+
if _, ok := err.(*exec.ExitError); ok {
216+
fmt.Println("Docker Run Command Failed")
217+
event.Set("ErrorCode", utils.RebuildErrorCode_DockerRunFailed).Send(ctx)
218+
log.Fatal(err)
219+
return
220+
} else if err != nil {
221+
fmt.Println("Docker error")
222+
event.Set("ErrorCode", utils.RebuildErrorCode_DockerErr).Send(ctx)
223+
log.Fatal(err)
224+
return
225+
}
226+
},
227+
}
228+
229+
func init() {
230+
rootCmd.AddCommand(buildCmd)
231+
}
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,114 @@
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_MalformedGitpodYaml = "rebuild_malformed_gitpod_yaml"
21+
RebuildErrorCode_MissingGitpodYaml = "rebuild_missing_gitpod_yaml"
22+
RebuildErrorCode_DockerfileNotFound = "rebuild_dockerfile_not_found"
23+
RebuildErrorCode_DockerfileEmpty = "rebuild_dockerfile_empty"
24+
RebuildErrorCode_DockerNotFound = "rebuild_docker_not_found"
25+
RebuildErrorCode_DockerBuildFailed = "rebuild_docker_build_failed"
26+
RebuildErrorCode_DockerRunFailed = "rebuild_docker_run_failed"
27+
RebuildErrorCode_DockerErr = "rebuild_docker_err"
28+
)
29+
30+
type TrackCommandUsageParams struct {
31+
Command string `json:"command,omitempty"`
32+
DurationMs int64 `json:"durationMs,omitempty"`
33+
Success bool `json:"success,omitempty"`
34+
ErrorCode string `json:"errorCode,omitempty"`
35+
WorkspaceId string `json:"workspaceId,omitempty"`
36+
WorkspaceInstanceId string `json:"workspaceInstanceId,omitempty"`
37+
Timestamp int64 `json:"timestamp,omitempty"`
38+
}
39+
40+
type EventTracker struct {
41+
data *TrackCommandUsageParams
42+
startTime time.Time
43+
serverClient *serverapi.APIoverJSONRPC
44+
supervisorClient *supervisor.SupervisorClient
45+
}
46+
47+
func TrackEvent(ctx context.Context, supervisorClient *supervisor.SupervisorClient, cmdParams *TrackCommandUsageParams) *EventTracker {
48+
tracker := &EventTracker{
49+
startTime: time.Now(),
50+
supervisorClient: supervisorClient,
51+
}
52+
53+
wsInfo, err := supervisorClient.Info.WorkspaceInfo(ctx, &api.WorkspaceInfoRequest{})
54+
if err != nil {
55+
LogError(ctx, err, "Could not fetch the workspace info", supervisorClient)
56+
return nil
57+
}
58+
59+
serverClient, err := gitpod.ConnectToServer(ctx, wsInfo, []string{"function:trackEvent"})
60+
if err != nil {
61+
log.WithError(err).Fatal("error connecting to server")
62+
return nil
63+
}
64+
65+
tracker.serverClient = serverClient
66+
tracker.data = &TrackCommandUsageParams{
67+
Command: cmdParams.Command,
68+
DurationMs: 0,
69+
WorkspaceId: wsInfo.WorkspaceId,
70+
WorkspaceInstanceId: wsInfo.InstanceId,
71+
ErrorCode: "",
72+
Timestamp: time.Now().UnixMilli(),
73+
}
74+
75+
return tracker
76+
}
77+
78+
func (t *EventTracker) Set(key string, value interface{}) *EventTracker {
79+
switch key {
80+
case "Command":
81+
t.data.Command = value.(string)
82+
case "Success":
83+
t.data.Success = value.(bool)
84+
case "ErrorCode":
85+
t.data.ErrorCode = value.(string)
86+
t.data.Success = false
87+
case "DurationMs":
88+
t.data.DurationMs = value.(int64)
89+
case "WorkspaceId":
90+
t.data.WorkspaceId = value.(string)
91+
case "WorkspaceInstanceId":
92+
t.data.WorkspaceInstanceId = value.(string)
93+
default:
94+
log.Warnf("Field not allowed: '%s'", key)
95+
}
96+
return t
97+
}
98+
99+
func (t *EventTracker) Send(ctx context.Context) {
100+
defer t.serverClient.Close()
101+
102+
t.Set("DurationMs", time.Since(t.startTime).Milliseconds())
103+
104+
event := &serverapi.RemoteTrackMessage{
105+
Event: "gp_command",
106+
Properties: t.data,
107+
}
108+
109+
err := t.serverClient.TrackEvent(ctx, event)
110+
if err != nil {
111+
LogError(ctx, err, "Could not track gp command event", t.supervisorClient)
112+
return
113+
}
114+
}

0 commit comments

Comments
 (0)