Skip to content

Commit 70ed53b

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

File tree

5 files changed

+386
-0
lines changed

5 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+
"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+
ctx = context.Background()
80+
81+
event := utils.TrackEvent(ctx, client, &utils.TrackCommandUsageParams{
82+
Command: cmd.Name(),
83+
})
84+
defer event.Send(ctx)
85+
86+
gitpodConfig, err := utils.ParseGitpodConfig(wsInfo.CheckoutLocation)
87+
if err != nil {
88+
fmt.Println("The .gitpod.yml file cannot be parsed: please check the file and try again")
89+
fmt.Println("")
90+
fmt.Println("For help check out the reference page:")
91+
fmt.Println("https://www.gitpod.io/docs/references/gitpod-yml#gitpodyml")
92+
event.Set("ErrorCode", utils.RebuildErrorCode_MalformedGitpodYaml)
93+
return
94+
}
95+
96+
if gitpodConfig == nil {
97+
fmt.Println("To test the image build, you need to configure your project with a .gitpod.yml file")
98+
fmt.Println("")
99+
fmt.Println("For a quick start, try running:\n$ gp init -i")
100+
fmt.Println("")
101+
fmt.Println("Alternatively, check out the following docs for getting started configuring your project")
102+
fmt.Println("https://www.gitpod.io/docs/configure#configure-gitpod")
103+
event.Set("ErrorCode", utils.RebuildErrorCode_MissingGitpodYaml)
104+
return
105+
}
106+
107+
var baseimage string
108+
switch img := gitpodConfig.Image.(type) {
109+
case nil:
110+
baseimage = ""
111+
case string:
112+
baseimage = "FROM " + img
113+
case map[interface{}]interface{}:
114+
dockerfilePath := filepath.Join(wsInfo.CheckoutLocation, img["file"].(string))
115+
116+
if _, err := os.Stat(dockerfilePath); os.IsNotExist(err) {
117+
fmt.Println("Your .gitpod.yml points to a Dockerfile that doesn't exist: " + dockerfilePath)
118+
event.Set("ErrorCode", utils.RebuildErrorCode_DockerfileNotFound).Send(ctx)
119+
return
120+
}
121+
dockerfile, err := os.ReadFile(dockerfilePath)
122+
if err != nil {
123+
log.Fatal("Could not read the Dockerfile")
124+
return
125+
}
126+
if string(dockerfile) == "" {
127+
fmt.Println("Your Gitpod's Dockerfile is empty")
128+
fmt.Println("")
129+
fmt.Println("To learn how to customize your workspace, check out the following docs:")
130+
fmt.Println("https://www.gitpod.io/docs/configure/workspaces/workspace-image#use-a-custom-dockerfile")
131+
fmt.Println("")
132+
fmt.Println("Once you configure your Dockerfile, re-run this command to validate your changes")
133+
event.Set("ErrorCode", utils.RebuildErrorCode_DockerfileEmpty)
134+
return
135+
}
136+
baseimage = "\n" + string(dockerfile) + "\n"
137+
default:
138+
fmt.Println("Check your .gitpod.yml and make sure the image property is configured correctly")
139+
event.Set("ErrorCode", utils.RebuildErrorCode_MalformedGitpodYaml)
140+
return
141+
}
142+
143+
if baseimage == "" {
144+
// TODO: should this fallback to workspace full?
145+
fmt.Println("Your project is not using any custom Docker image.")
146+
fmt.Println("Check out the following docs, to know how to get started")
147+
fmt.Println("")
148+
fmt.Println("https://www.gitpod.io/docs/configure/workspaces/workspace-image#use-a-public-docker-image")
149+
return
150+
}
151+
152+
err = os.WriteFile(filepath.Join(tmpDir, "Dockerfile"), []byte(baseimage), 0644)
153+
if err != nil {
154+
fmt.Println("Could not write the temporary Dockerfile")
155+
log.Fatal(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+
dockerBuildStartTime := 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.Fatal(err)
179+
return
180+
} else if err != nil {
181+
fmt.Println("Docker error")
182+
event.Set("ErrorCode", utils.RebuildErrorCode_DockerErr)
183+
log.Fatal(err)
184+
return
185+
}
186+
dockerBuildDurationSeconds := time.Since(dockerBuildStartTime).Seconds()
187+
event.Set("dockerBuildDurationSeconds", dockerBuildDurationSeconds)
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.Fatal(err)
223+
return
224+
} else if err != nil {
225+
fmt.Println("Docker error")
226+
event.Set("ErrorCode", utils.RebuildErrorCode_DockerErr)
227+
log.Fatal(err)
228+
return
229+
}
230+
},
231+
}
232+
233+
func init() {
234+
rootCmd.AddCommand(buildCmd)
235+
}

components/gitpod-cli/go.mod

+3
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,14 @@ require (
3636
github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3 // indirect
3737
github.com/inconshreveable/mousetrap v1.0.0 // indirect
3838
github.com/mattn/go-runewidth v0.0.9 // indirect
39+
github.com/segmentio/backo-go v0.0.0-20200129164019-23eae7c10bd3 // indirect
3940
github.com/spf13/pflag v1.0.5 // indirect
41+
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect
4042
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e // indirect
4143
golang.org/x/text v0.3.7 // indirect
4244
google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc // indirect
4345
google.golang.org/protobuf v1.28.1 // indirect
46+
gopkg.in/segmentio/analytics-go.v3 v3.1.0 // indirect
4447
)
4548

4649
replace github.com/gitpod-io/gitpod/gitpod-protocol => ../gitpod-protocol/go // leeway

components/gitpod-cli/go.sum

+6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
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+
}

0 commit comments

Comments
 (0)