Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/gitpod-cli/cmd/validate.go
2498 views
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
"bufio"
9
"bytes"
10
"context"
11
"encoding/json"
12
"fmt"
13
"io"
14
"net/url"
15
"os"
16
"os/exec"
17
"path/filepath"
18
"strings"
19
"time"
20
"unicode/utf8"
21
22
"github.com/gitpod-io/gitpod/common-go/log"
23
"github.com/gitpod-io/gitpod/gitpod-cli/pkg/supervisor"
24
"github.com/gitpod-io/gitpod/gitpod-cli/pkg/utils"
25
"github.com/sirupsen/logrus"
26
"github.com/spf13/cobra"
27
"golang.org/x/xerrors"
28
"google.golang.org/grpc/codes"
29
"google.golang.org/grpc/status"
30
31
"github.com/gitpod-io/gitpod/supervisor/api"
32
prefixed "github.com/x-cray/logrus-prefixed-formatter"
33
)
34
35
func stopDebugContainer(ctx context.Context, dockerPath string) error {
36
cmd := exec.CommandContext(ctx, dockerPath, "ps", "-q", "-f", "label=gp-rebuild")
37
containerIds, err := cmd.Output()
38
if err != nil {
39
return nil
40
}
41
42
for _, id := range strings.Split(string(containerIds), "\n") {
43
if len(id) == 0 {
44
continue
45
}
46
_ = exec.CommandContext(ctx, dockerPath, "stop", id).Run()
47
_ = exec.CommandContext(ctx, dockerPath, "rm", "-f", id).Run()
48
}
49
return nil
50
}
51
52
func runRebuild(ctx context.Context, supervisorClient *supervisor.SupervisorClient) error {
53
logLevel, err := logrus.ParseLevel(validateOpts.LogLevel)
54
if err != nil {
55
return GpError{Err: err, OutCome: utils.Outcome_UserErr, ErrorCode: utils.RebuildErrorCode_InvaligLogLevel}
56
}
57
58
// 1. validate configuration
59
wsInfo, err := supervisorClient.Info.WorkspaceInfo(ctx, &api.WorkspaceInfoRequest{})
60
if err != nil {
61
return err
62
}
63
64
checkoutLocation := validateOpts.WorkspaceFolder
65
if checkoutLocation == "" {
66
checkoutLocation = wsInfo.CheckoutLocation
67
}
68
69
gitpodConfig, err := utils.ParseGitpodConfig(checkoutLocation)
70
if err != nil {
71
fmt.Println("The .gitpod.yml file cannot be parsed: please check the file and try again")
72
fmt.Println("")
73
fmt.Println("For help check out the reference page:")
74
fmt.Println("https://www.gitpod.io/docs/references/gitpod-yml#gitpodyml")
75
return GpError{Err: err, OutCome: utils.Outcome_UserErr, ErrorCode: utils.RebuildErrorCode_MalformedGitpodYaml, Silence: true}
76
}
77
78
if gitpodConfig == nil {
79
fmt.Println("To test the image build, you need to configure your project with a .gitpod.yml file")
80
fmt.Println("")
81
fmt.Println("For a quick start, try running:\n$ gp init -i")
82
fmt.Println("")
83
fmt.Println("Alternatively, check out the following docs for getting started configuring your project")
84
fmt.Println("https://www.gitpod.io/docs/configure#configure-gitpod")
85
return GpError{Err: err, OutCome: utils.Outcome_UserErr, ErrorCode: utils.RebuildErrorCode_MissingGitpodYaml, Silence: true}
86
}
87
88
var image string
89
var dockerfilePath string
90
var dockerContext string
91
switch img := gitpodConfig.Image.(type) {
92
case nil:
93
image, err = getDefaultWorkspaceImage(ctx, wsInfo)
94
if err != nil {
95
return err
96
}
97
if image == "" {
98
image = "gitpod/workspace-full:latest"
99
}
100
fmt.Println("Using default workspace image:", image)
101
case string:
102
image = img
103
case map[interface{}]interface{}:
104
dockerfilePath = filepath.Join(checkoutLocation, img["file"].(string))
105
dockerContext = checkoutLocation
106
if context, ok := img["context"].(string); ok {
107
dockerContext = filepath.Join(checkoutLocation, context)
108
}
109
110
if _, err := os.Stat(dockerfilePath); os.IsNotExist(err) {
111
fmt.Println("Your .gitpod.yml points to a Dockerfile that doesn't exist: " + dockerfilePath)
112
return GpError{Err: err, OutCome: utils.Outcome_UserErr, Silence: true}
113
}
114
if _, err := os.Stat(dockerContext); os.IsNotExist(err) {
115
fmt.Println("Your image context doesn't exist: " + dockerContext)
116
return GpError{Err: err, OutCome: utils.Outcome_UserErr, Silence: true}
117
}
118
dockerfile, err := os.ReadFile(dockerfilePath)
119
if err != nil {
120
return err
121
}
122
if string(dockerfile) == "" {
123
fmt.Println("Your Gitpod's Dockerfile is empty")
124
fmt.Println("")
125
fmt.Println("To learn how to customize your workspace, check out the following docs:")
126
fmt.Println("https://www.gitpod.io/docs/configure/workspaces/workspace-image#use-a-custom-dockerfile")
127
fmt.Println("")
128
fmt.Println("Once you configure your Dockerfile, re-run this command to validate your changes")
129
return GpError{Err: err, OutCome: utils.Outcome_UserErr, Silence: true}
130
}
131
default:
132
fmt.Println("Check your .gitpod.yml and make sure the image property is configured correctly")
133
return GpError{Err: err, OutCome: utils.Outcome_UserErr, ErrorCode: utils.RebuildErrorCode_MalformedGitpodYaml, Silence: true}
134
}
135
136
// 2. build image
137
fmt.Println("Building the workspace image...")
138
139
tmpDir, err := os.MkdirTemp("", "gp-rebuild-*")
140
if err != nil {
141
return err
142
}
143
defer os.RemoveAll(tmpDir)
144
145
dockerPath, err := exec.LookPath("docker")
146
if err == nil {
147
// smoke test user docker cli
148
err = exec.CommandContext(ctx, dockerPath, "--version").Run()
149
}
150
if err != nil {
151
dockerPath = "/.supervisor/gitpod-docker-cli"
152
}
153
154
var dockerCmd *exec.Cmd
155
if image != "" {
156
err = exec.CommandContext(ctx, dockerPath, "image", "inspect", image).Run()
157
if err == nil {
158
fmt.Printf("%s: image found\n", image)
159
} else {
160
dockerCmd = exec.CommandContext(ctx, dockerPath, "image", "pull", image)
161
}
162
} else {
163
image = "gp-rebuild-temp-build"
164
dockerCmd = exec.CommandContext(ctx, dockerPath, "build", "-t", image, "-f", dockerfilePath, dockerContext)
165
}
166
if dockerCmd != nil {
167
dockerCmd.Stdout = os.Stdout
168
dockerCmd.Stderr = os.Stderr
169
170
imageBuildStartTime := time.Now()
171
err = dockerCmd.Run()
172
if _, ok := err.(*exec.ExitError); ok {
173
fmt.Println("Image Build Failed")
174
return GpError{Err: err, OutCome: utils.Outcome_UserErr, ErrorCode: utils.RebuildErrorCode_ImageBuildFailed, Silence: true}
175
} else if err != nil {
176
fmt.Println("Docker error")
177
return GpError{Err: err, ErrorCode: utils.RebuildErrorCode_DockerErr, Silence: true}
178
}
179
utils.TrackCommandUsageEvent.ImageBuildDuration = time.Since(imageBuildStartTime).Milliseconds()
180
}
181
182
// 3. start debug
183
fmt.Println("")
184
runLog := log.New()
185
runLog.Logger.SetLevel(logrus.TraceLevel)
186
setLoggerFormatter(runLog.Logger)
187
runLog.Logger.SetOutput(os.Stdout)
188
runLog.Info("Starting the workspace...")
189
190
if wsInfo.DebugWorkspaceType != api.DebugWorkspaceType_noDebug {
191
runLog.Error("It is not possible to restart the workspace while you are currently inside it.")
192
return GpError{Err: err, OutCome: utils.Outcome_UserErr, ErrorCode: utils.RebuildErrorCode_AlreadyInDebug, Silence: true}
193
}
194
stopDebugContainer(ctx, dockerPath)
195
196
workspaceUrl, err := url.Parse(wsInfo.WorkspaceUrl)
197
if err != nil {
198
return err
199
}
200
workspaceUrl.Host = "debug-" + workspaceUrl.Host
201
202
// TODO validate that checkout and workspace locations don't leave /workspace folder
203
workspaceLocation := gitpodConfig.WorkspaceLocation
204
if workspaceLocation != "" {
205
if !filepath.IsAbs(workspaceLocation) {
206
workspaceLocation = filepath.Join("/workspace", workspaceLocation)
207
}
208
} else {
209
workspaceLocation = checkoutLocation
210
}
211
212
// TODO what about auto derived by server, i.e. JB for prebuilds? we should move them into the workspace then
213
tasks, err := json.Marshal(gitpodConfig.Tasks)
214
if err != nil {
215
return err
216
}
217
218
workspaceType := api.DebugWorkspaceType_regular
219
contentSource := api.ContentSource_from_other
220
if validateOpts.Prebuild {
221
workspaceType = api.DebugWorkspaceType_prebuild
222
} else if validateOpts.From == "prebuild" {
223
contentSource = api.ContentSource_from_prebuild
224
} else if validateOpts.From == "snapshot" {
225
contentSource = api.ContentSource_from_backup
226
}
227
debugEnvs, err := supervisorClient.Control.CreateDebugEnv(ctx, &api.CreateDebugEnvRequest{
228
WorkspaceType: workspaceType,
229
ContentSource: contentSource,
230
WorkspaceUrl: workspaceUrl.String(),
231
CheckoutLocation: checkoutLocation,
232
WorkspaceLocation: workspaceLocation,
233
Tasks: string(tasks),
234
LogLevel: logLevel.String(),
235
})
236
if err != nil {
237
return err
238
}
239
240
serverLog := logrus.NewEntry(logrus.New())
241
serverLog.Logger.SetLevel(logLevel)
242
setLoggerFormatter(serverLog.Logger)
243
workspaceEnvs, err := getWorkspaceEnvs(ctx, &connectToServerOptions{supervisorClient, wsInfo, serverLog, envScopeRepo})
244
if err != nil {
245
return err
246
}
247
248
var envs string
249
for _, env := range debugEnvs.Envs {
250
envs += env + "\n"
251
}
252
for _, env := range validateOpts.GitpodEnvs {
253
envs += env + "\n"
254
}
255
if validateOpts.Headless {
256
envs += "GITPOD_HEADLESS=true\n"
257
}
258
for _, env := range workspaceEnvs {
259
envs += fmt.Sprintf("%s=%s\n", env.Name, env.Value)
260
}
261
262
envFile := filepath.Join(tmpDir, ".env")
263
err = os.WriteFile(envFile, []byte(envs), 0644)
264
if err != nil {
265
return err
266
}
267
268
type mnte struct {
269
IsFile bool
270
Target string
271
Source string
272
Permission os.FileMode
273
Optional bool
274
}
275
276
prepareFS := []mnte{
277
{Source: "/workspace"},
278
{Source: "/.supervisor"},
279
{Source: "/ide"},
280
{Source: "/ide-desktop", Optional: true},
281
{Source: "/ide-desktop-plugins", Optional: true},
282
{Source: "/workspace/.gitpod-debug/.docker-root", Target: "/workspace/.docker-root", Permission: 0710},
283
{Source: "/workspace/.gitpod-debug/.gitpod", Target: "/workspace/.gitpod", Permission: 0751},
284
{Source: "/workspace/.gitpod-debug/.vscode-remote", Target: "/workspace/.vscode-remote", Permission: 0751},
285
{Source: "/workspace/.gitpod-debug/.cache", Target: "/workspace/.cache", Permission: 0751},
286
{Source: "/workspace/.gitpod-debug/.config", Target: "/workspace/.config", Permission: 0751},
287
{Source: "/usr/bin/docker-up", IsFile: true},
288
{Source: "/usr/bin/runc-facade", IsFile: true},
289
{Source: "/usr/local/bin/docker-compose", IsFile: true},
290
}
291
292
dockerArgs := []string{
293
"run",
294
"--rm",
295
"--user", "root",
296
"--privileged",
297
"--label", "gp-rebuild=true",
298
"--env-file", envFile,
299
300
// ports
301
"-p", "24999:22999", // supervisor
302
"-p", "25000:23000", // Web IDE
303
"-p", "25001:23001", // SSH
304
// 23002 dekstop IDE port, but it is covered by debug workspace proxy
305
"-p", "25003:23003", // debug workspace proxy
306
}
307
308
for _, mnt := range prepareFS {
309
fd, err := os.Stat(mnt.Source)
310
if err != nil {
311
if (os.IsPermission(err) || os.IsNotExist(err)) && mnt.Optional {
312
continue
313
}
314
if !os.IsNotExist(err) {
315
return err
316
}
317
if mnt.IsFile {
318
return err
319
}
320
err = os.MkdirAll(mnt.Source, mnt.Permission)
321
if err != nil {
322
return err
323
}
324
fd, err = os.Stat(mnt.Source)
325
if err != nil {
326
return err
327
}
328
}
329
if fd.IsDir() != !mnt.IsFile {
330
return xerrors.Errorf("invalid file type for %s", mnt.Source)
331
}
332
if mnt.Target == "" {
333
mnt.Target = mnt.Source
334
} else if !mnt.IsFile {
335
// if target is not same with source and it not a file, ensure target is created by gitpod user
336
_, err = os.Stat(mnt.Target)
337
if err != nil {
338
if (os.IsPermission(err) || os.IsNotExist(err)) && mnt.Optional {
339
continue
340
}
341
if !os.IsNotExist(err) {
342
return err
343
}
344
err = os.MkdirAll(mnt.Target, mnt.Permission)
345
if err != nil {
346
return err
347
}
348
_, err = os.Stat(mnt.Target)
349
if err != nil {
350
return err
351
}
352
}
353
}
354
dockerArgs = append(dockerArgs, "-v", fmt.Sprintf("%s:%s", mnt.Source, mnt.Target))
355
}
356
357
dockerArgs = append(dockerArgs, image, "/.supervisor/supervisor", "init")
358
359
// we don't pass context on purporse to handle graceful shutdown
360
// and output all logs properly below
361
runCmd := exec.Command(
362
dockerPath,
363
dockerArgs...,
364
)
365
366
debugSupervisor, err := supervisor.New(ctx, &supervisor.SupervisorClientOption{
367
Address: "localhost:24999",
368
})
369
if err != nil {
370
return err
371
}
372
373
if validateOpts.Headless {
374
go func() {
375
tasks, ok := waitForAllTasksToOpen(ctx, debugSupervisor, runLog)
376
if !ok {
377
return
378
}
379
for _, task := range tasks {
380
go pipeTask(ctx, task, debugSupervisor, runLog)
381
}
382
}()
383
} else {
384
go func() {
385
debugSupervisor.WaitForIDEReady(ctx)
386
if ctx.Err() != nil {
387
return
388
}
389
390
ssh := "ssh 'debug-" + wsInfo.WorkspaceId + "@" + workspaceUrl.Host + ".ssh." + wsInfo.WorkspaceClusterHost + "'"
391
sep := strings.Repeat("=", len(ssh))
392
runLog.Infof(`The workspace is UP!
393
%s
394
395
Open in Browser at:
396
%s
397
398
Connect using SSH keys (https://gitpod.io/keys):
399
%s
400
401
%s`, sep, workspaceUrl, ssh, sep)
402
err := openWindow(ctx, workspaceUrl.String())
403
if err != nil && ctx.Err() == nil {
404
log.WithError(err).Error("failed to open window")
405
}
406
}()
407
}
408
409
pipeLogs := func(input io.Reader, ideLevel logrus.Level) {
410
reader := bufio.NewReader(input)
411
for {
412
line, _, err := reader.ReadLine()
413
if err != nil {
414
return
415
}
416
if len(line) == 0 {
417
continue
418
}
419
msg := make(logrus.Fields)
420
err = json.Unmarshal(line, &msg)
421
if err != nil {
422
if ideLevel > logLevel {
423
continue
424
}
425
426
runLog.WithFields(msg).Log(ideLevel, string(line))
427
} else {
428
wsLevel, err := logrus.ParseLevel(fmt.Sprintf("%v", msg["level"]))
429
if err != nil {
430
wsLevel = logrus.DebugLevel
431
}
432
if wsLevel == logrus.FatalLevel {
433
wsLevel = logrus.ErrorLevel
434
}
435
if wsLevel > logLevel {
436
continue
437
}
438
439
message := fmt.Sprintf("%v", msg["message"])
440
delete(msg, "message")
441
delete(msg, "level")
442
delete(msg, "time")
443
delete(msg, "severity")
444
delete(msg, "@type")
445
446
component := fmt.Sprintf("%v", msg["component"])
447
if wsLevel != logrus.DebugLevel && wsLevel != logrus.TraceLevel {
448
delete(msg, "file")
449
delete(msg, "func")
450
delete(msg, "serviceContext")
451
delete(msg, "component")
452
} else if component == "grpc" {
453
continue
454
}
455
456
runLog.WithFields(msg).Log(wsLevel, message)
457
}
458
}
459
}
460
461
stdout, err := runCmd.StdoutPipe()
462
if err != nil {
463
return err
464
}
465
go pipeLogs(stdout, logrus.InfoLevel)
466
467
stderr, err := runCmd.StderrPipe()
468
if err != nil {
469
return err
470
}
471
go pipeLogs(stderr, logrus.ErrorLevel)
472
473
err = runCmd.Start()
474
if err != nil {
475
fmt.Println("Failed to run a workspace")
476
return GpError{Err: err, OutCome: utils.Outcome_UserErr, ErrorCode: utils.RebuildErrorCode_DockerRunFailed, Silence: true}
477
}
478
479
stopped := make(chan struct{})
480
go func() {
481
_ = runCmd.Wait()
482
close(stopped)
483
}()
484
485
select {
486
case <-ctx.Done():
487
fmt.Println("")
488
logrus.Info("Gracefully stopping the workspace...")
489
stopDebugContainer(context.Background(), dockerPath)
490
case <-stopped:
491
}
492
493
return nil
494
}
495
496
func setLoggerFormatter(logger *logrus.Logger) {
497
logger.SetFormatter(&prefixed.TextFormatter{
498
TimestampFormat: "2006-01-02 15:04:05",
499
FullTimestamp: true,
500
ForceFormatting: true,
501
ForceColors: true,
502
})
503
}
504
505
func openWindow(ctx context.Context, workspaceUrl string) error {
506
gpPath, err := exec.LookPath("gp")
507
if err != nil {
508
return err
509
}
510
gpCmd := exec.CommandContext(ctx, gpPath, "preview", "--external", workspaceUrl)
511
gpCmd.Stdout = os.Stdout
512
gpCmd.Stderr = os.Stderr
513
return gpCmd.Run()
514
}
515
516
func waitForAllTasksToOpen(ctx context.Context, supervisor *supervisor.SupervisorClient, runLog *logrus.Entry) (tasks []*api.TaskStatus, allTasksOpened bool) {
517
for !allTasksOpened {
518
time.Sleep(1 * time.Second)
519
if ctx.Err() != nil {
520
return
521
}
522
listener, err := supervisor.Status.TasksStatus(ctx, &api.TasksStatusRequest{
523
Observe: true,
524
})
525
if err != nil {
526
continue
527
}
528
tasks, allTasksOpened = checkAllTasksOpened(ctx, listener, runLog)
529
}
530
return
531
}
532
533
func checkAllTasksOpened(ctx context.Context, listener api.StatusService_TasksStatusClient, runLog *logrus.Entry) (tasks []*api.TaskStatus, allTasksOpened bool) {
534
for !allTasksOpened {
535
resp, err := listener.Recv()
536
if err != nil {
537
return
538
}
539
tasks = resp.GetTasks()
540
allTasksOpened = areTasksOpened(tasks)
541
}
542
return
543
}
544
545
func areTasksOpened(tasks []*api.TaskStatus) bool {
546
for _, task := range tasks {
547
if task.State == api.TaskState_opening {
548
return false
549
}
550
}
551
return true
552
}
553
554
func pipeTask(ctx context.Context, task *api.TaskStatus, supervisor *supervisor.SupervisorClient, runLog *logrus.Entry) {
555
for {
556
err := listenTerminal(ctx, task, supervisor, runLog)
557
if err == nil || ctx.Err() != nil {
558
return
559
}
560
status, ok := status.FromError(err)
561
if ok && status.Code() == codes.NotFound {
562
return
563
}
564
runLog.WithError(err).Errorf("%s: failed to listen, retrying...", task.Presentation.Name)
565
time.Sleep(1 * time.Second)
566
}
567
}
568
569
// TerminalReader is an interface for anything that can receive terminal data (this is abstracted for use in testing)
570
type TerminalReader interface {
571
Recv() ([]byte, error)
572
}
573
574
type LinePrinter func(string)
575
576
// processTerminalOutput reads from a TerminalReader, processes the output, and calls the provided LinePrinter for each complete line.
577
// It handles UTF-8 decoding of characters split across chunks and control characters (\n \r \b).
578
func processTerminalOutput(reader TerminalReader, printLine LinePrinter) error {
579
var buffer, line bytes.Buffer
580
581
flushLine := func() {
582
if line.Len() > 0 {
583
printLine(line.String())
584
line.Reset()
585
}
586
}
587
588
for {
589
data, err := reader.Recv()
590
if err != nil {
591
if err == io.EOF {
592
flushLine()
593
return nil
594
}
595
return err
596
}
597
598
buffer.Write(data)
599
600
for {
601
r, size := utf8.DecodeRune(buffer.Bytes())
602
if r == utf8.RuneError && size == 0 {
603
break // incomplete character at the end
604
}
605
606
char := buffer.Next(size)
607
608
switch r {
609
case '\r':
610
flushLine()
611
case '\n':
612
flushLine()
613
case '\b':
614
if line.Len() > 0 {
615
line.Truncate(line.Len() - 1)
616
}
617
default:
618
line.Write(char)
619
}
620
}
621
}
622
}
623
624
func listenTerminal(ctx context.Context, task *api.TaskStatus, supervisor *supervisor.SupervisorClient, runLog *logrus.Entry) error {
625
listen, err := supervisor.Terminal.Listen(ctx, &api.ListenTerminalRequest{Alias: task.Terminal})
626
if err != nil {
627
return err
628
}
629
630
terminalReader := &TerminalReaderAdapter{listen}
631
printLine := func(line string) {
632
runLog.Infof("%s: %s", task.Presentation.Name, line)
633
}
634
635
return processTerminalOutput(terminalReader, printLine)
636
}
637
638
type TerminalReaderAdapter struct {
639
client api.TerminalService_ListenClient
640
}
641
642
func (t *TerminalReaderAdapter) Recv() ([]byte, error) {
643
resp, err := t.client.Recv()
644
if err != nil {
645
return nil, err
646
}
647
return resp.GetData(), nil
648
}
649
650
var validateOpts struct {
651
WorkspaceFolder string
652
LogLevel string
653
From string
654
Prebuild bool
655
Headless bool
656
657
// internal
658
GitpodEnvs []string
659
}
660
661
var validateCmd = &cobra.Command{
662
Use: "validate",
663
Short: "[experimental] Validates the workspace (useful to debug a workspace configuration)",
664
Hidden: false,
665
RunE: func(cmd *cobra.Command, args []string) error {
666
supervisorClient, err := supervisor.New(cmd.Context())
667
if err != nil {
668
return xerrors.Errorf("Could not get workspace info required to build: %w", err)
669
}
670
defer supervisorClient.Close()
671
672
return runRebuild(cmd.Context(), supervisorClient)
673
},
674
}
675
676
var rebuildCmd = &cobra.Command{
677
Hidden: true,
678
Use: "rebuild",
679
Deprecated: "please use `gp validate` instead.",
680
Short: validateCmd.Short,
681
RunE: validateCmd.RunE,
682
}
683
684
func init() {
685
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
686
defer cancel()
687
688
var workspaceFolder string
689
client, err := supervisor.New(ctx)
690
if err == nil {
691
wsInfo, err := client.Info.WorkspaceInfo(ctx, &api.WorkspaceInfoRequest{})
692
if err == nil {
693
workspaceFolder = wsInfo.CheckoutLocation
694
}
695
}
696
697
setFlags := func(cmd *cobra.Command) {
698
cmd.PersistentFlags().BoolVarP(&validateOpts.Prebuild, "prebuild", "", false, "starts as a prebuild workspace.")
699
cmd.PersistentFlags().StringVarP(&validateOpts.LogLevel, "log", "", "error", "Log level to use. Allowed values are 'error', 'warn', 'info', 'debug', 'trace'.")
700
701
// internal
702
cmd.PersistentFlags().StringArrayVarP(&validateOpts.GitpodEnvs, "gitpod-env", "", nil, "")
703
cmd.PersistentFlags().StringVarP(&validateOpts.WorkspaceFolder, "workspace-folder", "w", workspaceFolder, "Path to the workspace folder.")
704
cmd.PersistentFlags().StringVarP(&validateOpts.From, "from", "", "", "Starts from 'prebuild' or 'snapshot'.")
705
cmd.PersistentFlags().BoolVarP(&validateOpts.Headless, "headless", "", false, "Starts in headless mode.")
706
_ = cmd.PersistentFlags().MarkHidden("gitpod-env")
707
_ = cmd.PersistentFlags().MarkHidden("workspace-folder")
708
_ = cmd.PersistentFlags().MarkHidden("from")
709
_ = cmd.PersistentFlags().MarkHidden("headless")
710
}
711
712
setFlags(validateCmd)
713
setFlags(rebuildCmd)
714
715
rootCmd.AddCommand(validateCmd)
716
rootCmd.AddCommand(rebuildCmd)
717
}
718
719