Skip to content

Commit 12c321a

Browse files
committed
support readonly ssh configs
1 parent 5a0efcb commit 12c321a

File tree

13 files changed

+85
-30
lines changed

13 files changed

+85
-30
lines changed

cmd/agent/workspace/up.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ func (cmd *UpCmd) Run(ctx context.Context) error {
8181

8282
tunnelClient, logger, credentialsDir, err := initWorkspace(cancelCtx, cancel, workspaceInfo, cmd.Debug, !workspaceInfo.CLIOptions.Platform.Enabled && !workspaceInfo.CLIOptions.DisableDaemon)
8383
if err != nil {
84-
err1 := clientimplementation.DeleteWorkspaceFolder(workspaceInfo.Workspace.Context, workspaceInfo.Workspace.ID, workspaceInfo.Workspace.SSHConfigPath, logger)
84+
err1 := clientimplementation.DeleteWorkspaceFolder(workspaceInfo.Workspace.Context, workspaceInfo.Workspace.ID, workspaceInfo.Workspace.SSHConfigPath, workspaceInfo.Workspace.SSHConfigIncludePath, logger)
8585
if err1 != nil {
8686
return errors.Wrap(err, err1.Error())
8787
}

cmd/build.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ func NewBuildCmd(flags *flags.GlobalFlags) *cobra.Command {
8989
cmd.DevContainerImage,
9090
cmd.DevContainerPath,
9191
sshConfigPath,
92+
"", // No include path for temporary build SSH config
9293
nil,
9394
cmd.UID,
9495
false,

cmd/pro/delete.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ func cleanupLocalWorkspaces(ctx context.Context, devPodConfig *config.Config, wo
136136
return
137137
}
138138
// delete workspace folder
139-
err = clientimplementation.DeleteWorkspaceFolder(devPodConfig.DefaultContext, client.Workspace(), client.WorkspaceConfig().SSHConfigPath, log)
139+
err = clientimplementation.DeleteWorkspaceFolder(devPodConfig.DefaultContext, client.Workspace(), client.WorkspaceConfig().SSHConfigPath, client.WorkspaceConfig().SSHConfigIncludePath, log)
140140
if err != nil {
141141
log.Errorf("Failed to remove workspace %s: %v", w.ID, err)
142142
return

cmd/up.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,8 +196,9 @@ func (cmd *UpCmd) Run(
196196
devPodHome = envDevPodHome
197197
}
198198
setupGPGAgentForwarding := cmd.GPGAgentForwarding || devPodConfig.ContextOption(config.ContextOptionGPGAgentForwarding) == "true"
199+
sshConfigIncludePath := devPodConfig.ContextOption(config.ContextOptionSSHConfigIncludePath)
199200

200-
err = configureSSH(client, cmd.SSHConfigPath, user, workdir, setupGPGAgentForwarding, devPodHome)
201+
err = configureSSH(client, cmd.SSHConfigPath, sshConfigIncludePath, user, workdir, setupGPGAgentForwarding, devPodHome)
201202
if err != nil {
202203
return err
203204
}
@@ -979,15 +980,25 @@ func startBrowserTunnel(
979980
return nil
980981
}
981982

982-
func configureSSH(client client2.BaseWorkspaceClient, sshConfigPath, user, workdir string, gpgagent bool, devPodHome string) error {
983+
func configureSSH(client client2.BaseWorkspaceClient, sshConfigPath, sshConfigIncludePath, user, workdir string, gpgagent bool, devPodHome string) error {
983984
path, err := devssh.ResolveSSHConfigPath(sshConfigPath)
984985
if err != nil {
985986
return errors.Wrap(err, "Invalid ssh config path")
986987
}
987988
sshConfigPath = path
988989

990+
// Resolve the include path if specified
991+
if sshConfigIncludePath != "" {
992+
includePath, err := devssh.ResolveSSHConfigPath(sshConfigIncludePath)
993+
if err != nil {
994+
return errors.Wrap(err, "Invalid ssh config include path")
995+
}
996+
sshConfigIncludePath = includePath
997+
}
998+
989999
err = devssh.ConfigureSSHConfig(
9901000
sshConfigPath,
1001+
sshConfigIncludePath,
9911002
client.Context(),
9921003
client.Workspace(),
9931004
user,
@@ -1399,6 +1410,7 @@ func (cmd *UpCmd) prepareClient(ctx context.Context, devPodConfig *config.Config
13991410
if cmd.SSHConfigPath == "" {
14001411
cmd.SSHConfigPath = devPodConfig.ContextOption(config.ContextOptionSSHConfigPath)
14011412
}
1413+
sshConfigIncludePath := devPodConfig.ContextOption(config.ContextOptionSSHConfigIncludePath)
14021414

14031415
client, err := workspace2.Resolve(
14041416
ctx,
@@ -1413,6 +1425,7 @@ func (cmd *UpCmd) prepareClient(ctx context.Context, devPodConfig *config.Config
14131425
cmd.DevContainerImage,
14141426
cmd.DevContainerPath,
14151427
cmd.SSHConfigPath,
1428+
sshConfigIncludePath,
14161429
source,
14171430
cmd.UID,
14181431
true,

pkg/client/clientimplementation/daemonclient/delete.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ func (c *client) Delete(ctx context.Context, opt clientpkg.DeleteOptions) error
2727
return err
2828
} else if workspace == nil {
2929
// delete the workspace folder
30-
err = clientimplementation.DeleteWorkspaceFolder(c.workspace.Context, c.workspace.ID, c.workspace.SSHConfigPath, c.log)
30+
err = clientimplementation.DeleteWorkspaceFolder(c.workspace.Context, c.workspace.ID, c.workspace.SSHConfigPath, c.workspace.SSHConfigIncludePath, c.log)
3131
if err != nil {
3232
return err
3333
}
@@ -67,7 +67,7 @@ func (c *client) Delete(ctx context.Context, opt clientpkg.DeleteOptions) error
6767
}
6868

6969
// delete the workspace folder
70-
err = clientimplementation.DeleteWorkspaceFolder(c.workspace.Context, c.workspace.ID, c.workspace.SSHConfigPath, c.log)
70+
err = clientimplementation.DeleteWorkspaceFolder(c.workspace.Context, c.workspace.ID, c.workspace.SSHConfigPath, c.workspace.SSHConfigIncludePath, c.log)
7171
if err != nil {
7272
return err
7373
}

pkg/client/clientimplementation/proxy_client.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@ func (s *proxyClient) Delete(ctx context.Context, opt client.DeleteOptions) erro
318318
s.log.Errorf("Error deleting workspace: %v", err)
319319
}
320320

321-
return DeleteWorkspaceFolder(s.workspace.Context, s.workspace.ID, s.workspace.SSHConfigPath, s.log)
321+
return DeleteWorkspaceFolder(s.workspace.Context, s.workspace.ID, s.workspace.SSHConfigPath, s.workspace.SSHConfigIncludePath, s.log)
322322
}
323323

324324
func (s *proxyClient) Stop(ctx context.Context, opt client.StopOptions) error {

pkg/client/clientimplementation/workspace_client.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -401,7 +401,7 @@ func (s *workspaceClient) Delete(ctx context.Context, opt client.DeleteOptions)
401401
}
402402
}
403403

404-
return DeleteWorkspaceFolder(s.workspace.Context, s.workspace.ID, s.workspace.SSHConfigPath, s.log)
404+
return DeleteWorkspaceFolder(s.workspace.Context, s.workspace.ID, s.workspace.SSHConfigPath, s.workspace.SSHConfigIncludePath, s.log)
405405
}
406406

407407
func (s *workspaceClient) isMachineRunning(ctx context.Context) (bool, error) {
@@ -630,14 +630,23 @@ func DeleteMachineFolder(context, machineID string) error {
630630
return nil
631631
}
632632

633-
func DeleteWorkspaceFolder(context string, workspaceID string, sshConfigPath string, log log.Logger) error {
633+
func DeleteWorkspaceFolder(context string, workspaceID string, sshConfigPath string, sshConfigIncludePath string, log log.Logger) error {
634634
path, err := ssh.ResolveSSHConfigPath(sshConfigPath)
635635
if err != nil {
636636
return err
637637
}
638638
sshConfigPath = path
639639

640-
err = ssh.RemoveFromConfig(workspaceID, sshConfigPath, log)
640+
// Resolve the include path if specified
641+
if sshConfigIncludePath != "" {
642+
includePath, err := ssh.ResolveSSHConfigPath(sshConfigIncludePath)
643+
if err != nil {
644+
return err
645+
}
646+
sshConfigIncludePath = includePath
647+
}
648+
649+
err = ssh.RemoveFromConfig(workspaceID, sshConfigPath, sshConfigIncludePath, log)
641650
if err != nil {
642651
log.Errorf("Remove workspace '%s' from ssh config: %v", workspaceID, err)
643652
}

pkg/config/context.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const (
1919
ContextOptionDotfilesScript = "DOTFILES_SCRIPT"
2020
ContextOptionSSHAgentForwarding = "SSH_AGENT_FORWARDING"
2121
ContextOptionSSHConfigPath = "SSH_CONFIG_PATH"
22+
ContextOptionSSHConfigIncludePath = "SSH_CONFIG_INCLUDE_PATH"
2223
ContextOptionAgentInjectTimeout = "AGENT_INJECT_TIMEOUT"
2324
ContextOptionRegistryCache = "REGISTRY_CACHE"
2425
ContextOptionSSHStrictHostKeyChecking = "SSH_STRICT_HOST_KEY_CHECKING"
@@ -89,6 +90,10 @@ var ContextOptions = []ContextOption{
8990
Name: ContextOptionSSHConfigPath,
9091
Description: "Specifies the path where the ssh config should be written to",
9192
},
93+
{
94+
Name: ContextOptionSSHConfigIncludePath,
95+
Description: "Specifies an alternate path where DevPod host entries should be written. Use this when your main SSH config is read-only (e.g., managed by Nix). Your main SSH config should have an Include directive pointing to this file.",
96+
},
9297
{
9398
Name: ContextOptionAgentInjectTimeout,
9499
Description: "Specifies the timeout to inject the agent",

pkg/provider/workspace.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ type Workspace struct {
7070

7171
// Path to the file where the SSH config to access the workspace is stored
7272
SSHConfigPath string `json:"sshConfigPath,omitempty"`
73+
74+
// Path to an alternate file where DevPod entries are written (for read-only SSH configs)
75+
SSHConfigIncludePath string `json:"sshConfigIncludePath,omitempty"`
7376
}
7477

7578
type ProMetadata struct {

pkg/ssh/config.go

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,27 @@ var (
2424
MarkerEndPrefix = "# DevPod End "
2525
)
2626

27-
func ConfigureSSHConfig(sshConfigPath, context, workspace, user, workdir string, gpgagent bool, devPodHome string, log log.Logger) error {
28-
return configureSSHConfigSameFile(sshConfigPath, context, workspace, user, workdir, "", gpgagent, devPodHome, log)
27+
func ConfigureSSHConfig(sshConfigPath, sshConfigIncludePath, context, workspace, user, workdir string, gpgagent bool, devPodHome string, log log.Logger) error {
28+
return configureSSHConfigSameFile(sshConfigPath, sshConfigIncludePath, context, workspace, user, workdir, "", gpgagent, devPodHome, log)
2929
}
3030

31-
func configureSSHConfigSameFile(sshConfigPath, context, workspace, user, workdir, command string, gpgagent bool, devPodHome string, log log.Logger) error {
31+
func configureSSHConfigSameFile(sshConfigPath, sshConfigIncludePath, context, workspace, user, workdir, command string, gpgagent bool, devPodHome string, log log.Logger) error {
3232
configLock.Lock()
3333
defer configLock.Unlock()
3434

35-
newFile, err := addHost(sshConfigPath, workspace+"."+"devpod", user, context, workspace, workdir, command, gpgagent, devPodHome)
35+
// If an include path is specified, write DevPod entries there instead of the main config.
36+
// This supports read-only SSH configs (e.g., managed by Nix Home Manager).
37+
targetPath := sshConfigPath
38+
if sshConfigIncludePath != "" {
39+
targetPath = sshConfigIncludePath
40+
}
41+
42+
newFile, err := addHost(targetPath, workspace+"."+"devpod", user, context, workspace, workdir, command, gpgagent, devPodHome)
3643
if err != nil {
3744
return errors.Wrap(err, "parse ssh config")
3845
}
3946

40-
return writeSSHConfig(sshConfigPath, newFile, log)
47+
return writeSSHConfig(targetPath, newFile, log)
4148
}
4249

4350
type DevPodSSHEntry struct {
@@ -159,16 +166,23 @@ func GetUser(workspaceID string, sshConfigPath string) (string, error) {
159166
return user, nil
160167
}
161168

162-
func RemoveFromConfig(workspaceID string, sshConfigPath string, log log.Logger) error {
169+
func RemoveFromConfig(workspaceID string, sshConfigPath string, sshConfigIncludePath string, log log.Logger) error {
163170
configLock.Lock()
164171
defer configLock.Unlock()
165172

166-
newFile, err := removeFromConfig(sshConfigPath, workspaceID+"."+"devpod")
173+
// If an include path is specified, remove DevPod entries from there instead of the main config.
174+
// This supports read-only SSH configs (e.g., managed by Nix Home Manager).
175+
targetPath := sshConfigPath
176+
if sshConfigIncludePath != "" {
177+
targetPath = sshConfigIncludePath
178+
}
179+
180+
newFile, err := removeFromConfig(targetPath, workspaceID+"."+"devpod")
167181
if err != nil {
168182
return errors.Wrap(err, "parse ssh config")
169183
}
170184

171-
return writeSSHConfig(sshConfigPath, newFile, log)
185+
return writeSSHConfig(targetPath, newFile, log)
172186
}
173187

174188
func writeSSHConfig(path, content string, log log.Logger) error {

0 commit comments

Comments
 (0)