forked from loft-sh/devpod
-
Notifications
You must be signed in to change notification settings - Fork 22
Expand file tree
/
Copy pathup.go
More file actions
1868 lines (1667 loc) · 50.5 KB
/
up.go
File metadata and controls
1868 lines (1667 loc) · 50.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package cmd
import (
"bytes"
"context"
"fmt"
"io"
"net"
"os"
"os/exec"
"os/signal"
"path/filepath"
"slices"
"strconv"
"strings"
"syscall"
"al.essio.dev/pkg/shellescape"
"github.com/blang/semver/v4"
"github.com/sirupsen/logrus"
"github.com/skevetter/devpod/cmd/flags"
"github.com/skevetter/devpod/pkg/agent"
"github.com/skevetter/devpod/pkg/agent/tunnelserver"
client2 "github.com/skevetter/devpod/pkg/client"
"github.com/skevetter/devpod/pkg/client/clientimplementation"
"github.com/skevetter/devpod/pkg/command"
"github.com/skevetter/devpod/pkg/config"
config2 "github.com/skevetter/devpod/pkg/devcontainer/config"
"github.com/skevetter/devpod/pkg/devcontainer/sshtunnel"
"github.com/skevetter/devpod/pkg/ide"
"github.com/skevetter/devpod/pkg/ide/fleet"
"github.com/skevetter/devpod/pkg/ide/jetbrains"
"github.com/skevetter/devpod/pkg/ide/jupyter"
"github.com/skevetter/devpod/pkg/ide/openvscode"
"github.com/skevetter/devpod/pkg/ide/rstudio"
"github.com/skevetter/devpod/pkg/ide/vscode"
"github.com/skevetter/devpod/pkg/ide/zed"
open2 "github.com/skevetter/devpod/pkg/open"
options2 "github.com/skevetter/devpod/pkg/options"
"github.com/skevetter/devpod/pkg/platform"
"github.com/skevetter/devpod/pkg/port"
provider2 "github.com/skevetter/devpod/pkg/provider"
devssh "github.com/skevetter/devpod/pkg/ssh"
"github.com/skevetter/devpod/pkg/telemetry"
"github.com/skevetter/devpod/pkg/tunnel"
"github.com/skevetter/devpod/pkg/util"
"github.com/skevetter/devpod/pkg/version"
workspace2 "github.com/skevetter/devpod/pkg/workspace"
"github.com/skevetter/log"
"github.com/skratchdot/open-golang/open"
"github.com/spf13/cobra"
"golang.org/x/crypto/ssh"
)
// UpCmd holds the up cmd flags.
type UpCmd struct {
provider2.CLIOptions
*flags.GlobalFlags
Machine string
ProviderOptions []string
ConfigureSSH bool
GPGAgentForwarding bool
OpenIDE bool
Reconfigure bool
SSHConfigPath string
DotfilesSource string
DotfilesScript string
DotfilesScriptEnv []string // Key=Value to pass to install script
DotfilesScriptEnvFile []string // Paths to files containing Key=Value pairs to pass to install script
}
// NewUpCmd creates a new up command.
func NewUpCmd(f *flags.GlobalFlags) *cobra.Command {
cmd := &UpCmd{GlobalFlags: f}
upCmd := &cobra.Command{
Use: "up [flags] [workspace-path|workspace-name]",
Short: "Starts a new workspace",
RunE: cmd.execute,
}
cmd.registerFlags(upCmd)
return upCmd
}
func (cmd *UpCmd) execute(cobraCmd *cobra.Command, args []string) error {
if err := cmd.validate(); err != nil {
return err
}
devPodConfig, err := config.LoadConfig(cmd.Context, cmd.Provider)
if err != nil {
return err
}
if devPodConfig.ContextOption(config.ContextOptionSSHStrictHostKeyChecking) == "true" {
cmd.StrictHostKeyChecking = true
}
ctx, cancel := WithSignals(cobraCmd.Context())
defer cancel()
client, logger, err := cmd.prepareClient(ctx, devPodConfig, args)
if err != nil {
return fmt.Errorf("prepare workspace client: %w", err)
}
if cmd.ExtraDevContainerPath != "" && client.Provider() != "docker" {
return fmt.Errorf("extra devcontainer file is only supported with local provider")
}
telemetry.CollectorCLI.SetClient(client)
return cmd.Run(ctx, devPodConfig, client, args, logger)
}
func (cmd *UpCmd) validate() error {
if err := validatePodmanFlags(cmd); err != nil {
return err
}
if cmd.ExtraDevContainerPath != "" {
absPath, err := filepath.Abs(cmd.ExtraDevContainerPath)
if err != nil {
return err
}
cmd.ExtraDevContainerPath = absPath
}
return nil
}
func (cmd *UpCmd) registerFlags(upCmd *cobra.Command) {
cmd.registerSSHFlags(upCmd)
cmd.registerDotfilesFlags(upCmd)
cmd.registerDevContainerFlags(upCmd)
cmd.registerIDEFlags(upCmd)
cmd.registerGitFlags(upCmd)
cmd.registerPodmanFlags(upCmd)
cmd.registerWorkspaceFlags(upCmd)
cmd.registerTestingFlags(upCmd)
}
func (cmd *UpCmd) registerSSHFlags(upCmd *cobra.Command) {
upCmd.Flags().
BoolVar(&cmd.ConfigureSSH, "configure-ssh", true,
"If true will configure the ssh config to include the DevPod workspace")
upCmd.Flags().
BoolVar(&cmd.GPGAgentForwarding, "gpg-agent-forwarding", false,
"If true forward the local gpg-agent to the DevPod workspace")
upCmd.Flags().
StringVar(&cmd.SSHConfigPath, "ssh-config", "",
"The path to the ssh config to modify, if empty will use ~/.ssh/config")
}
func (cmd *UpCmd) registerDotfilesFlags(upCmd *cobra.Command) {
upCmd.Flags().
StringVar(&cmd.DotfilesSource, "dotfiles", "", "The path or url to the dotfiles to use in the container")
upCmd.Flags().
StringVar(&cmd.DotfilesScript, "dotfiles-script", "",
"The path in dotfiles directory to use to install the dotfiles, if empty will try to guess")
upCmd.Flags().
StringSliceVar(&cmd.DotfilesScriptEnv, "dotfiles-script-env", []string{},
"Extra environment variables to put into the dotfiles install script. E.g. MY_ENV_VAR=MY_VALUE")
upCmd.Flags().
StringSliceVar(&cmd.DotfilesScriptEnvFile, "dotfiles-script-env-file", []string{},
"The path to files containing environment variables to set for the dotfiles install script")
}
func (cmd *UpCmd) registerDevContainerFlags(upCmd *cobra.Command) {
upCmd.Flags().
StringVar(&cmd.DevContainerImage, "devcontainer-image", "",
"The container image to use, this will override the devcontainer.json value in the project")
upCmd.Flags().
StringVar(&cmd.DevContainerPath, "devcontainer-path", "", "The path to the devcontainer.json relative to the project")
upCmd.Flags().
StringVar(&cmd.DevContainerID, "devcontainer-id", "",
"The ID of the devcontainer to use when multiple exist "+
"(e.g., folder name in .devcontainer/FOLDER/devcontainer.json)")
upCmd.Flags().
StringVar(&cmd.ExtraDevContainerPath, "extra-devcontainer-path", "",
"The path to an additional devcontainer.json file to override original devcontainer.json")
upCmd.Flags().
StringVar(&cmd.FallbackImage, "fallback-image", "",
"The fallback image to use if no devcontainer configuration has been detected")
upCmd.Flags().
StringVar(&cmd.AdditionalFeatures, "additional-features", "",
`Additional features to apply to the dev container (JSON as per "features" section in devcontainer.json)`)
}
func (cmd *UpCmd) registerIDEFlags(upCmd *cobra.Command) {
upCmd.Flags().
StringVar(&cmd.IDE, "ide", "", "The IDE to open the workspace in. If empty will use vscode locally or in browser")
upCmd.Flags().
StringArrayVar(&cmd.IDEOptions, "ide-option", []string{}, "IDE option in the form KEY=VALUE")
upCmd.Flags().
BoolVar(&cmd.OpenIDE, "open-ide", true,
"If this is false and an IDE is configured, DevPod will only install the IDE server backend, but not open it")
}
func (cmd *UpCmd) registerGitFlags(upCmd *cobra.Command) {
upCmd.Flags().
Var(&cmd.GitCloneStrategy, "git-clone-strategy",
"The git clone strategy DevPod uses to checkout git based workspaces. "+
"Can be full (default), blobless, treeless or shallow")
upCmd.Flags().
BoolVar(&cmd.GitCloneRecursiveSubmodules, "git-clone-recursive-submodules", false,
"If true will clone git submodule repositories recursively")
upCmd.Flags().
StringVar(&cmd.GitSSHSigningKey, "git-ssh-signing-key", "",
"The ssh key to use when signing git commits. Used to explicitly setup DevPod's ssh signature "+
"forwarding with given key. Should be same format as value of `git config user.signingkey`")
}
func (cmd *UpCmd) registerPodmanFlags(upCmd *cobra.Command) {
upCmd.Flags().
StringVar(&cmd.Userns, "userns", "",
"User namespace to use for the container (Podman only; e.g. \"keep-id\", \"host\", or \"auto\")")
upCmd.Flags().
StringSliceVar(&cmd.UidMap, "uidmap", []string{},
"UID mapping for Podman user namespace "+
"(Podman only; format: container_id:host_id:amount, e.g. \"0:1000:1\")")
upCmd.Flags().
StringSliceVar(&cmd.GidMap, "gidmap", []string{},
"GID mapping for Podman user namespace "+
"(Podman only; format: container_id:host_id:amount, e.g. \"0:1000:1\")")
}
func (cmd *UpCmd) registerWorkspaceFlags(upCmd *cobra.Command) {
upCmd.Flags().StringVar(&cmd.ID, "id", "", "The id to use for the workspace")
upCmd.Flags().
StringVar(&cmd.Machine, "machine", "",
"The machine to use for this workspace. The machine needs to exist beforehand or the "+
"command will fail. If the workspace already exists, this option has no effect")
upCmd.Flags().
StringVar(&cmd.Source, "source", "", "Optional source for the workspace. E.g. git:https://github.com/my-org/my-repo")
upCmd.Flags().
StringArrayVar(&cmd.ProviderOptions, "provider-option", []string{}, "Provider option in the form KEY=VALUE")
upCmd.Flags().
BoolVar(&cmd.Reconfigure, "reconfigure", false,
"Reconfigure the options for this workspace. Only supported in DevPod Pro right now.")
upCmd.Flags().
BoolVar(&cmd.Recreate, "recreate", false, "If true will remove any existing containers and recreate them")
upCmd.Flags().
BoolVar(&cmd.Reset, "reset", false,
"If true will remove any existing containers including sources, and recreate them")
upCmd.Flags().
StringSliceVar(&cmd.PrebuildRepositories, "prebuild-repository", []string{},
"Docker repository that hosts devpod prebuilds for this workspace")
upCmd.Flags().
StringArrayVar(&cmd.WorkspaceEnv, "workspace-env", []string{},
"Extra env variables to put into the workspace. E.g. MY_ENV_VAR=MY_VALUE")
upCmd.Flags().
StringSliceVar(&cmd.WorkspaceEnvFile, "workspace-env-file", []string{},
"The path to files containing a list of extra env variables to put into the workspace. "+
"E.g. MY_ENV_VAR=MY_VALUE")
upCmd.Flags().
StringArrayVar(&cmd.InitEnv, "init-env", []string{},
"Extra env variables to inject during the initialization of the workspace. E.g. MY_ENV_VAR=MY_VALUE")
upCmd.Flags().
BoolVar(&cmd.DisableDaemon, "disable-daemon", false,
"If enabled, will not install a daemon into the target machine to track activity")
}
func (cmd *UpCmd) registerTestingFlags(upCmd *cobra.Command) {
upCmd.Flags().StringVar(&cmd.DaemonInterval, "daemon-interval", "", "TESTING ONLY")
_ = upCmd.Flags().MarkHidden("daemon-interval")
upCmd.Flags().BoolVar(&cmd.ForceDockerless, "force-dockerless", false, "TESTING ONLY")
_ = upCmd.Flags().MarkHidden("force-dockerless")
}
// Run runs the command logic.
func (cmd *UpCmd) Run(
ctx context.Context,
devPodConfig *config.Config,
client client2.BaseWorkspaceClient,
args []string,
log log.Logger,
) error {
cmd.prepareWorkspace(client, log)
wctx, err := cmd.executeDevPodUp(ctx, devPodConfig, client, log)
if err != nil {
return err
}
if wctx == nil {
return nil // Platform mode
}
if err := cmd.configureWorkspace(devPodConfig, client, wctx, log); err != nil {
return err
}
return cmd.openIDE(ctx, devPodConfig, client, wctx, log)
}
// workspaceContext holds the result of workspace preparation.
type workspaceContext struct {
result *config2.Result
user string
workdir string
}
// prepareWorkspace handles initial setup and validation.
func (cmd *UpCmd) prepareWorkspace(client client2.BaseWorkspaceClient, log log.Logger) {
if cmd.Reset {
cmd.Recreate = true
}
targetIDE := client.WorkspaceConfig().IDE.Name
if cmd.IDE != "" {
targetIDE = cmd.IDE
}
if !cmd.Platform.Enabled && ide.ReusesAuthSock(targetIDE) {
cmd.SSHAuthSockID = util.RandStringBytes(10)
log.Debug("Reusing SSH_AUTH_SOCK", cmd.SSHAuthSockID)
} else if cmd.Platform.Enabled && ide.ReusesAuthSock(targetIDE) {
log.Debug(
"Reusing SSH_AUTH_SOCK is not supported with platform mode, consider launching the IDE from the platform UI",
)
}
}
// executeDevPodUp runs the agent and returns workspace context.
func (cmd *UpCmd) executeDevPodUp(
ctx context.Context,
devPodConfig *config.Config,
client client2.BaseWorkspaceClient,
log log.Logger,
) (*workspaceContext, error) {
result, err := cmd.devPodUp(ctx, devPodConfig, client, log)
if err != nil {
return nil, err
}
if result == nil {
return nil, fmt.Errorf("did not receive a result back from agent")
}
if cmd.Platform.Enabled {
return nil, nil
}
user := config2.GetRemoteUser(result)
workdir := ""
if result.MergedConfig != nil && result.MergedConfig.WorkspaceFolder != "" {
workdir = result.MergedConfig.WorkspaceFolder
}
if client.WorkspaceConfig().Source.GitSubPath != "" {
result.SubstitutionContext.ContainerWorkspaceFolder = filepath.Join(
result.SubstitutionContext.ContainerWorkspaceFolder,
client.WorkspaceConfig().Source.GitSubPath,
)
workdir = result.SubstitutionContext.ContainerWorkspaceFolder
}
return &workspaceContext{result: result, user: user, workdir: workdir}, nil
}
// configureWorkspace sets up SSH, Git, and dotfiles.
func (cmd *UpCmd) configureWorkspace(
devPodConfig *config.Config,
client client2.BaseWorkspaceClient,
wctx *workspaceContext,
log log.Logger,
) error {
if cmd.ConfigureSSH {
devPodHome := ""
if envDevPodHome, ok := os.LookupEnv("DEVPOD_HOME"); ok {
devPodHome = envDevPodHome
}
setupGPGAgentForwarding := cmd.GPGAgentForwarding ||
devPodConfig.ContextOption(config.ContextOptionGPGAgentForwarding) == "true"
sshConfigIncludePath := devPodConfig.ContextOption(config.ContextOptionSSHConfigIncludePath)
if err := configureSSH(client, configureSSHParams{
sshConfigPath: cmd.SSHConfigPath,
sshConfigIncludePath: sshConfigIncludePath,
user: wctx.user,
workdir: wctx.workdir,
gpgagent: setupGPGAgentForwarding,
devPodHome: devPodHome,
}); err != nil {
return err
}
log.Info("SSH configuration completed in workspace")
}
if err := setupDotfiles(
cmd.DotfilesSource,
cmd.DotfilesScript,
cmd.DotfilesScriptEnvFile,
cmd.DotfilesScriptEnv,
client,
devPodConfig,
log,
); err != nil {
return err
}
// Run after dotfiles so the signing config isn't overwritten by a
// dotfiles installer that replaces .gitconfig.
if cmd.GitSSHSigningKey != "" {
if err := setupGitSSHSignature(cmd.GitSSHSigningKey, client); err != nil {
return err
}
}
return nil
}
// openIDE opens the configured IDE.
func (cmd *UpCmd) openIDE(
ctx context.Context,
devPodConfig *config.Config,
client client2.BaseWorkspaceClient,
wctx *workspaceContext,
log log.Logger,
) error {
if !cmd.OpenIDE {
return nil
}
ideConfig := client.WorkspaceConfig().IDE
opener := newIDEOpener(cmd, devPodConfig, client, wctx, log)
return opener.open(ctx, ideConfig.Name, ideConfig.Options)
}
// ideOpener handles opening different IDE types.
type ideOpener struct {
cmd *UpCmd
devPodConfig *config.Config
client client2.BaseWorkspaceClient
wctx *workspaceContext
log log.Logger
}
func newIDEOpener(
cmd *UpCmd,
devPodConfig *config.Config,
client client2.BaseWorkspaceClient,
wctx *workspaceContext,
log log.Logger,
) *ideOpener {
return &ideOpener{
cmd: cmd,
devPodConfig: devPodConfig,
client: client,
wctx: wctx,
log: log,
}
}
func (o *ideOpener) open(
ctx context.Context,
ideName string,
ideOptions map[string]config.OptionValue,
) error {
folder := o.wctx.result.SubstitutionContext.ContainerWorkspaceFolder
workspace := o.client.Workspace()
user := o.wctx.user
switch ideName {
case string(config.IDEVSCode), string(config.IDEVSCodeInsiders), string(config.IDECursor),
string(config.IDECodium), string(config.IDEPositron), string(config.IDEWindsurf), string(config.IDEAntigravity):
return o.openVSCodeFlavor(ctx, ideName, folder, ideOptions)
case string(config.IDERustRover), string(config.IDEGoland), string(config.IDEPyCharm),
string(config.IDEPhpStorm), string(config.IDEIntellij), string(config.IDECLion),
string(config.IDERider), string(config.IDERubyMine), string(config.IDEWebStorm), string(config.IDEDataSpell):
return o.openJetBrains(ideName, folder, workspace, user, ideOptions)
case string(config.IDEOpenVSCode):
return startVSCodeInBrowser(
o.cmd.GPGAgentForwarding,
ctx,
o.devPodConfig,
o.client,
folder,
user,
ideOptions,
o.cmd.SSHAuthSockID,
o.log,
)
case string(config.IDEFleet):
return startFleet(ctx, o.client, o.log)
case string(config.IDEZed):
return zed.Open(ctx, ideOptions, user, folder, workspace, o.log)
case string(config.IDEJupyterNotebook):
return startJupyterNotebookInBrowser(
o.cmd.GPGAgentForwarding,
ctx,
o.devPodConfig,
o.client,
user,
ideOptions,
o.cmd.SSHAuthSockID,
o.log,
)
case string(config.IDERStudio):
return startRStudioInBrowser(
o.cmd.GPGAgentForwarding,
ctx,
o.devPodConfig,
o.client,
user,
ideOptions,
o.cmd.SSHAuthSockID,
o.log,
)
default:
return nil
}
}
func (o *ideOpener) openVSCodeFlavor(
ctx context.Context,
ideName, folder string,
ideOptions map[string]config.OptionValue,
) error {
flavorMap := map[string]vscode.Flavor{
string(config.IDEVSCode): vscode.FlavorStable,
string(config.IDEVSCodeInsiders): vscode.FlavorInsiders,
string(config.IDECursor): vscode.FlavorCursor,
string(config.IDECodium): vscode.FlavorCodium,
string(config.IDEPositron): vscode.FlavorPositron,
string(config.IDEWindsurf): vscode.FlavorWindsurf,
string(config.IDEAntigravity): vscode.FlavorAntigravity,
}
params := vscode.OpenParams{
Workspace: o.client.Workspace(),
Folder: folder,
NewWindow: vscode.Options.GetValue(ideOptions, vscode.OpenNewWindow) == "true",
Flavor: flavorMap[ideName],
Log: o.log,
}
return vscode.Open(ctx, params)
}
func (o *ideOpener) openJetBrains(
ideName, folder, workspace, user string,
ideOptions map[string]config.OptionValue,
) error {
type jetbrainsFactory func() interface{ OpenGateway(string, string) error }
jetbrainsMap := map[string]jetbrainsFactory{
string(config.IDERustRover): func() interface{ OpenGateway(string, string) error } {
return jetbrains.NewRustRoverServer(user, ideOptions, o.log)
},
string(config.IDEGoland): func() interface{ OpenGateway(string, string) error } {
return jetbrains.NewGolandServer(user, ideOptions, o.log)
},
string(config.IDEPyCharm): func() interface{ OpenGateway(string, string) error } {
return jetbrains.NewPyCharmServer(user, ideOptions, o.log)
},
string(config.IDEPhpStorm): func() interface{ OpenGateway(string, string) error } {
return jetbrains.NewPhpStorm(user, ideOptions, o.log)
},
string(config.IDEIntellij): func() interface{ OpenGateway(string, string) error } {
return jetbrains.NewIntellij(user, ideOptions, o.log)
},
string(config.IDECLion): func() interface{ OpenGateway(string, string) error } {
return jetbrains.NewCLionServer(user, ideOptions, o.log)
},
string(config.IDERider): func() interface{ OpenGateway(string, string) error } {
return jetbrains.NewRiderServer(user, ideOptions, o.log)
},
string(config.IDERubyMine): func() interface{ OpenGateway(string, string) error } {
return jetbrains.NewRubyMineServer(user, ideOptions, o.log)
},
string(config.IDEWebStorm): func() interface{ OpenGateway(string, string) error } {
return jetbrains.NewWebStormServer(user, ideOptions, o.log)
},
string(config.IDEDataSpell): func() interface{ OpenGateway(string, string) error } {
return jetbrains.NewDataSpellServer(user, ideOptions, o.log)
},
}
if factory, ok := jetbrainsMap[ideName]; ok {
return factory().OpenGateway(folder, workspace)
}
return fmt.Errorf("unknown JetBrains IDE: %s", ideName)
}
func (cmd *UpCmd) devPodUp(
ctx context.Context,
devPodConfig *config.Config,
client client2.BaseWorkspaceClient,
log log.Logger,
) (*config2.Result, error) {
var err error
// only lock if we are not in platform mode
if !cmd.Platform.Enabled {
err := client.Lock(ctx)
if err != nil {
return nil, err
}
defer client.Unlock()
}
// get result
var result *config2.Result
switch client := client.(type) {
case client2.WorkspaceClient:
result, err = cmd.devPodUpMachine(ctx, devPodConfig, client, log)
if err != nil {
return nil, err
}
case client2.ProxyClient:
result, err = cmd.devPodUpProxy(ctx, client, log)
if err != nil {
return nil, err
}
case client2.DaemonClient:
result, err = cmd.devPodUpDaemon(ctx, client)
if err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unsupported client type: %T", client)
}
// save result to file
err = provider2.SaveWorkspaceResult(client.WorkspaceConfig(), result)
if err != nil {
return nil, fmt.Errorf("save workspace result: %w", err)
}
return result, nil
}
func (cmd *UpCmd) devPodUpProxy(
ctx context.Context,
client client2.ProxyClient,
log log.Logger,
) (*config2.Result, error) {
// create pipes
stdoutReader, stdoutWriter, err := os.Pipe()
if err != nil {
return nil, err
}
stdinReader, stdinWriter, err := os.Pipe()
if err != nil {
return nil, err
}
defer func() { _ = stdoutWriter.Close() }()
defer func() { _ = stdinWriter.Close() }()
// start machine on stdio
cancelCtx, cancel := context.WithCancel(ctx)
defer cancel()
// create up command
errChan := make(chan error, 1)
go func() {
defer log.Debug("done executing up command")
defer cancel()
// build devpod up options
workspace := client.WorkspaceConfig()
baseOptions := cmd.CLIOptions
baseOptions.ID = workspace.ID
baseOptions.DevContainerPath = workspace.DevContainerPath
baseOptions.DevContainerImage = workspace.DevContainerImage
baseOptions.IDE = workspace.IDE.Name
baseOptions.IDEOptions = nil
baseOptions.Source = workspace.Source.String()
for optionName, optionValue := range workspace.IDE.Options {
baseOptions.IDEOptions = append(
baseOptions.IDEOptions,
optionName+"="+optionValue.Value,
)
}
// run devpod up elsewhere
err = client.Up(ctx, client2.UpOptions{
CLIOptions: baseOptions,
Debug: cmd.Debug,
Stdin: stdinReader,
Stdout: stdoutWriter,
})
if err != nil {
errChan <- fmt.Errorf("executing up proxy command: %w", err)
} else {
errChan <- nil
}
}()
// create container etc.
result, err := tunnelserver.RunUpServer(
cancelCtx,
stdoutReader,
stdinWriter,
true,
true,
client.WorkspaceConfig(),
log,
)
if err != nil {
return nil, fmt.Errorf("run tunnel machine: %w", err)
}
// wait until command finished
return result, <-errChan
}
func (cmd *UpCmd) devPodUpDaemon(
ctx context.Context,
client client2.DaemonClient,
) (*config2.Result, error) {
// build devpod up options
workspace := client.WorkspaceConfig()
baseOptions := cmd.CLIOptions
baseOptions.ID = workspace.ID
baseOptions.DevContainerPath = workspace.DevContainerPath
baseOptions.DevContainerImage = workspace.DevContainerImage
baseOptions.IDE = workspace.IDE.Name
baseOptions.IDEOptions = nil
baseOptions.Source = workspace.Source.String()
for optionName, optionValue := range workspace.IDE.Options {
baseOptions.IDEOptions = append(
baseOptions.IDEOptions,
optionName+"="+optionValue.Value,
)
}
// run devpod up elsewhere
return client.Up(ctx, client2.UpOptions{
CLIOptions: baseOptions,
Debug: cmd.Debug,
})
}
func (cmd *UpCmd) devPodUpMachine(
ctx context.Context,
devPodConfig *config.Config,
client client2.WorkspaceClient,
log log.Logger,
) (*config2.Result, error) {
err := clientimplementation.StartWait(ctx, client, true, log)
if err != nil {
return nil, err
}
// compress info
workspaceInfo, wInfo, err := client.AgentInfo(cmd.CLIOptions)
if err != nil {
return nil, err
}
// create container etc.
log.Info("creating devcontainer")
defer log.Debug("done creating devcontainer")
// if we run on a platform, we need to pass the platform options
if cmd.Platform.Enabled {
return clientimplementation.BuildAgentClient(
ctx,
clientimplementation.BuildAgentClientOptions{
WorkspaceClient: client,
CLIOptions: cmd.CLIOptions,
AgentCommand: "up",
Log: log,
TunnelOptions: []tunnelserver.Option{
tunnelserver.WithPlatformOptions(&cmd.Platform),
},
},
)
}
// ssh tunnel command
sshTunnelCmd := fmt.Sprintf("'%s' helper ssh-server --stdio", client.AgentPath())
if log.GetLevel() == logrus.DebugLevel {
sshTunnelCmd += " --debug"
}
// create agent command
agentCommand := fmt.Sprintf(
"'%s' agent workspace up --workspace-info '%s'",
client.AgentPath(),
workspaceInfo,
)
if log.GetLevel() == logrus.DebugLevel {
agentCommand += " --debug"
}
agentInjectFunc := func(
cancelCtx context.Context, sshCmd string, sshTunnelStdinReader, sshTunnelStdoutWriter *os.File,
writer io.WriteCloser,
) error {
return agent.InjectAgent(&agent.InjectOptions{
Ctx: cancelCtx,
Exec: func(ctx context.Context, command string, stdin io.Reader, stdout io.Writer, stderr io.Writer) error {
return client.Command(ctx, client2.CommandOptions{
Command: command,
Stdin: stdin,
Stdout: stdout,
Stderr: stderr,
})
},
IsLocal: client.AgentLocal(),
RemoteAgentPath: client.AgentPath(),
DownloadURL: client.AgentURL(),
Command: sshCmd,
Stdin: sshTunnelStdinReader,
Stdout: sshTunnelStdoutWriter,
Stderr: writer,
Log: log.ErrorStreamOnly(),
Timeout: wInfo.InjectTimeout,
})
}
return sshtunnel.ExecuteCommand(ctx, sshtunnel.ExecuteCommandOptions{
Client: client,
AddPrivateKeys: devPodConfig.ContextOption(config.ContextOptionSSHAddPrivateKeys) == "true",
AgentInject: agentInjectFunc,
SSHCommand: sshTunnelCmd,
Command: agentCommand,
Log: log,
TunnelServerFunc: func(ctx context.Context, stdin io.WriteCloser, stdout io.Reader) (*config2.Result, error) {
return tunnelserver.RunUpServer(
ctx,
stdout,
stdin,
client.AgentInjectGitCredentials(cmd.CLIOptions),
client.AgentInjectDockerCredentials(cmd.CLIOptions),
client.WorkspaceConfig(),
log,
)
},
})
}
func startJupyterNotebookInBrowser(
forwardGpg bool,
ctx context.Context,
devPodConfig *config.Config,
client client2.BaseWorkspaceClient,
user string,
ideOptions map[string]config.OptionValue,
authSockID string,
logger log.Logger,
) error {
if forwardGpg {
err := performGpgForwarding(client, logger)
if err != nil {
return err
}
}
// determine port
jupyterAddress, jupyterPort, err := parseAddressAndPort(
jupyter.Options.GetValue(ideOptions, jupyter.BindAddressOption),
jupyter.DefaultServerPort,
)
if err != nil {
return err
}
// wait until reachable then open browser
targetURL := fmt.Sprintf("http://localhost:%d/lab", jupyterPort)
if jupyter.Options.GetValue(ideOptions, jupyter.OpenOption) == "true" {
go func() {
err = open2.Open(ctx, targetURL, logger)
if err != nil {
logger.WithFields(logrus.Fields{"error": err}).
Error("error opening jupyter notebook")
}
logger.Info(
"started jupyter notebook in browser mode. Please keep this terminal open as long as you use Jupyter Notebook",
)
}()
}
// start in browser
logger.Infof("Starting jupyter notebook in browser mode at %s", targetURL)
extraPorts := []string{fmt.Sprintf("%s:%d", jupyterAddress, jupyter.DefaultServerPort)}
return startBrowserTunnel(
ctx,
devPodConfig,
client,
user,
targetURL,
false,
extraPorts,
authSockID,
logger,
)
}
func startRStudioInBrowser(
forwardGpg bool,
ctx context.Context,
devPodConfig *config.Config,
client client2.BaseWorkspaceClient,
user string,
ideOptions map[string]config.OptionValue,
authSockID string,
logger log.Logger,
) error {
if forwardGpg {
err := performGpgForwarding(client, logger)
if err != nil {
return err
}
}
// determine port
addr, port, err := parseAddressAndPort(
rstudio.Options.GetValue(ideOptions, rstudio.BindAddressOption),
rstudio.DefaultServerPort,
)
if err != nil {
return err
}
// wait until reachable then open browser
targetURL := fmt.Sprintf("http://localhost:%d", port)
if rstudio.Options.GetValue(ideOptions, rstudio.OpenOption) == "true" {
go func() {
err = open2.Open(ctx, targetURL, logger)
if err != nil {
logger.Errorf("error opening rstudio: %v", err)
}
logger.Infof(
"started RStudio Server in browser mode. Please keep this terminal open as long as you use it",
)
}()
}
// start in browser
logger.Infof("Starting RStudio server in browser mode at %s", targetURL)
extraPorts := []string{fmt.Sprintf("%s:%d", addr, rstudio.DefaultServerPort)}
return startBrowserTunnel(
ctx,
devPodConfig,
client,
user,
targetURL,
false,
extraPorts,
authSockID,
logger,
)
}
func startFleet(ctx context.Context, client client2.BaseWorkspaceClient, logger log.Logger) error {
// create ssh command
stdout := &bytes.Buffer{}
cmd, err := createSSHCommand(
ctx,
client,
logger,
[]string{"--command", "cat " + fleet.FleetURLFileName},
)
if err != nil {
return err
}
cmd.Stdout = stdout
err = cmd.Run()
if err != nil {
return command.WrapCommandError(stdout.Bytes(), err)
}
url := strings.TrimSpace(stdout.String())
if len(url) == 0 {
return fmt.Errorf("seems like fleet is not running within the container")
}
logger.Warnf(
"Fleet is exposed at a publicly reachable URL, please make sure to not disclose this URL " +
"to anyone as they will be able to reach your workspace from that",
)
logger.Infof("Starting Fleet at %s ...", url)
err = open.Run(url)
if err != nil {
return err
}
return nil
}
func startVSCodeInBrowser(
forwardGpg bool,
ctx context.Context,
devPodConfig *config.Config,
client client2.BaseWorkspaceClient,
workspaceFolder, user string,
ideOptions map[string]config.OptionValue,
authSockID string,