Skip to content

Commit 2efa961

Browse files
authored
Merge pull request #2 from skulidropek/pr-refs-pull-150-head
fix: support direct container IP for SSH access when running inside D…
2 parents b4e3f82 + 93e6a22 commit 2efa961

5 files changed

Lines changed: 153 additions & 67 deletions

File tree

packages/lib/src/usecases/actions/create-project.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@ import { renderError } from "../errors.js"
2323
import { applyGithubForkConfig } from "../github-fork.js"
2424
import { defaultProjectsRoot } from "../menu-helpers.js"
2525
import { findSshPrivateKey } from "../path-helpers.js"
26-
import { buildSshCommand } from "../projects-core.js"
26+
import {
27+
buildSshCommand,
28+
getContainerIpIfInsideContainer
29+
} from "../projects-core.js"
2730
import { resolveTemplateResourceLimits } from "../resource-limits.js"
2831
import { autoSyncState } from "../state-repo.js"
2932
import { ensureTerminalCursorVisible } from "../terminal-cursor.js"
@@ -97,8 +100,11 @@ const isInteractiveTty = (): boolean => process.stdin.isTTY && process.stdout.is
97100
const buildSshArgs = (
98101
config: CreateCommand["config"],
99102
sshKeyPath: string | null,
100-
remoteCommand?: string
103+
remoteCommand?: string,
104+
ipAddress?: string
101105
): ReadonlyArray<string> => {
106+
const host = ipAddress ?? "localhost"
107+
const port = ipAddress ? 22 : config.sshPort
102108
const args: Array<string> = []
103109
if (sshKeyPath !== null) {
104110
args.push("-i", sshKeyPath)
@@ -113,8 +119,8 @@ const buildSshArgs = (
113119
"-o",
114120
"UserKnownHostsFile=/dev/null",
115121
"-p",
116-
String(config.sshPort),
117-
`${config.sshUser}@localhost`
122+
String(port),
123+
`${config.sshUser}@${host}`
118124
)
119125
if (remoteCommand !== undefined) {
120126
args.push(remoteCommand)
@@ -140,8 +146,14 @@ const openSshBestEffort = (
140146
const fs = yield* _(FileSystem.FileSystem)
141147
const path = yield* _(Path.Path)
142148

149+
const ipAddress = yield* _(
150+
getContainerIpIfInsideContainer(fs, process.cwd(), template.containerName).pipe(
151+
Effect.catchAll(() => Effect.succeed(undefined))
152+
)
153+
)
154+
143155
const sshKey = yield* _(findSshPrivateKey(fs, path, process.cwd()))
144-
const sshCommand = buildSshCommand(template, sshKey)
156+
const sshCommand = buildSshCommand(template, sshKey, ipAddress)
145157

146158
const remoteCommandLabel = remoteCommand === undefined ? "" : ` (${remoteCommand})`
147159

@@ -152,7 +164,7 @@ const openSshBestEffort = (
152164
{
153165
cwd: process.cwd(),
154166
command: "ssh",
155-
args: buildSshArgs(template, sshKey, remoteCommand)
167+
args: buildSshArgs(template, sshKey, remoteCommand, ipAddress)
156168
},
157169
[0, 130],
158170
(exitCode) => new CommandFailedError({ command: "ssh", exitCode })

packages/lib/src/usecases/actions/docker-up.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
runDockerComposeUpRecreate,
1313
runDockerExecExitCode,
1414
runDockerInspectContainerBridgeIp,
15+
runDockerInspectContainerIp,
1516
runDockerNetworkConnectBridge
1617
} from "../../shell/docker.js"
1718
import type { DockerCommandError } from "../../shell/errors.js"
@@ -31,14 +32,29 @@ const agentFailPath = "/run/docker-git/agent.failed"
3132
const logSshAccess = (
3233
baseDir: string,
3334
config: CreateCommand["config"]
34-
): Effect.Effect<void, PlatformError, FileSystem.FileSystem | Path.Path> =>
35+
): Effect.Effect<void, PlatformError, FileSystem.FileSystem | Path.Path | CommandExecutor.CommandExecutor> =>
3536
Effect.gen(function*(_) {
3637
const fs = yield* _(FileSystem.FileSystem)
3738
const path = yield* _(Path.Path)
39+
40+
const isInsideContainer = yield* _(fs.exists("/.dockerenv"))
41+
let ipAddress: string | undefined = undefined
42+
43+
if (isInsideContainer) {
44+
const containerIp = yield* _(
45+
runDockerInspectContainerIp(baseDir, config.containerName).pipe(
46+
Effect.catchAll(() => Effect.succeed(""))
47+
)
48+
)
49+
if (containerIp.length > 0) {
50+
ipAddress = containerIp
51+
}
52+
}
53+
3854
const resolvedAuthorizedKeys = resolveAuthorizedKeysPath(path, baseDir, config.authorizedKeysPath)
3955
const authExists = yield* _(fs.exists(resolvedAuthorizedKeys))
4056
const sshKey = yield* _(findSshPrivateKey(fs, path, process.cwd()))
41-
const sshCommand = buildSshCommand(config, sshKey)
57+
const sshCommand = buildSshCommand(config, sshKey, ipAddress)
4258

4359
yield* _(Effect.log(`SSH access: ${sshCommand}`))
4460
if (!authExists) {

packages/lib/src/usecases/projects-core.ts

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
import type * as CommandExecutor from "@effect/platform/CommandExecutor"
12
import type { PlatformError } from "@effect/platform/Error"
23
import * as FileSystem from "@effect/platform/FileSystem"
34
import * as Path from "@effect/platform/Path"
45
import { Effect, pipe } from "effect"
56

67
import type { ProjectConfig, TemplateConfig } from "../core/domain.js"
78
import { deriveRepoPathParts } from "../core/domain.js"
9+
import { runDockerInspectContainerIp } from "../shell/docker.js"
810
import { readProjectConfig } from "../shell/config.js"
911
import type { ConfigDecodeError, ConfigNotFoundError } from "../shell/errors.js"
1012
import { resolveBaseDir } from "../shell/paths.js"
@@ -20,16 +22,21 @@ export type ProjectLoadError = PlatformError | ConfigNotFoundError | ConfigDecod
2022

2123
export const buildSshCommand = (
2224
config: TemplateConfig,
23-
sshKey: string | null
24-
): string =>
25-
sshKey === null
26-
? `ssh ${sshOptions} -p ${config.sshPort} ${config.sshUser}@localhost`
27-
: `ssh -i ${sshKey} ${sshOptions} -p ${config.sshPort} ${config.sshUser}@localhost`
25+
sshKey: string | null,
26+
ipAddress?: string
27+
): string => {
28+
const host = ipAddress ?? "localhost"
29+
const port = ipAddress ? 22 : config.sshPort
30+
return sshKey === null
31+
? `ssh ${sshOptions} -p ${port} ${config.sshUser}@${host}`
32+
: `ssh -i ${sshKey} ${sshOptions} -p ${port} ${config.sshUser}@${host}`
33+
}
2834

2935
export type ProjectSummary = {
3036
readonly projectDir: string
3137
readonly config: ProjectConfig
3238
readonly sshCommand: string
39+
readonly ipAddress?: string | undefined
3340
readonly authorizedKeysPath: string
3441
readonly authorizedKeysExists: boolean
3542
}
@@ -45,6 +52,7 @@ export type ProjectItem = {
4552
readonly sshPort: number
4653
readonly targetDir: string
4754
readonly sshCommand: string
55+
readonly ipAddress?: string | undefined
4856
readonly sshKeyPath: string | null
4957
readonly authorizedKeysPath: string
5058
readonly authorizedKeysExists: boolean
@@ -73,6 +81,24 @@ type ProjectBase = {
7381
readonly config: ProjectConfig
7482
}
7583

84+
export const getContainerIpIfInsideContainer = (
85+
fs: FileSystem.FileSystem,
86+
projectDir: string,
87+
containerName: string
88+
): Effect.Effect<string | undefined, PlatformError, CommandExecutor.CommandExecutor> =>
89+
Effect.gen(function*(_) {
90+
const isInsideContainer = yield* _(fs.exists("/.dockerenv"))
91+
if (!isInsideContainer) {
92+
return undefined
93+
}
94+
return yield* _(
95+
runDockerInspectContainerIp(projectDir, containerName).pipe(
96+
Effect.map((ip) => (ip.length > 0 ? ip : undefined)),
97+
Effect.catchAll(() => Effect.succeed(undefined))
98+
)
99+
)
100+
})
101+
76102
const loadProjectBase = (
77103
configPath: string
78104
): Effect.Effect<ProjectBase, ProjectLoadError, FileSystem.FileSystem | Path.Path> =>
@@ -91,21 +117,25 @@ const findProjectConfigPaths = (
91117
export const loadProjectSummary = (
92118
configPath: string,
93119
sshKey: string | null
94-
): Effect.Effect<ProjectSummary, ProjectLoadError, FileSystem.FileSystem | Path.Path> =>
120+
): Effect.Effect<ProjectSummary, ProjectLoadError, FileSystem.FileSystem | Path.Path | CommandExecutor.CommandExecutor> =>
95121
Effect.gen(function*(_) {
96122
const { config, fs, path, projectDir } = yield* _(loadProjectBase(configPath))
123+
124+
const ipAddress = yield* _(getContainerIpIfInsideContainer(fs, projectDir, config.template.containerName))
125+
97126
const resolvedAuthorizedKeys = resolveAuthorizedKeysPath(
98127
path,
99128
projectDir,
100129
config.template.authorizedKeysPath
101130
)
102131
const authExists = yield* _(fs.exists(resolvedAuthorizedKeys))
103-
const sshCommand = buildSshCommand(config.template, sshKey)
132+
const sshCommand = buildSshCommand(config.template, sshKey, ipAddress)
104133

105134
return {
106135
projectDir,
107136
config,
108137
sshCommand,
138+
ipAddress,
109139
authorizedKeysPath: resolvedAuthorizedKeys,
110140
authorizedKeysExists: authExists
111141
}
@@ -139,13 +169,16 @@ const formatDisplayName = (repoUrl: string): string => {
139169
export const loadProjectItem = (
140170
configPath: string,
141171
sshKey: string | null
142-
): Effect.Effect<ProjectItem, ProjectLoadError, FileSystem.FileSystem | Path.Path> =>
172+
): Effect.Effect<ProjectItem, ProjectLoadError, FileSystem.FileSystem | Path.Path | CommandExecutor.CommandExecutor> =>
143173
Effect.gen(function*(_) {
144174
const { config, fs, path, projectDir } = yield* _(loadProjectBase(configPath))
145175
const template = config.template
176+
177+
const ipAddress = yield* _(getContainerIpIfInsideContainer(fs, projectDir, template.containerName))
178+
146179
const resolvedAuthorizedKeys = resolveAuthorizedKeysPath(path, projectDir, template.authorizedKeysPath)
147180
const authExists = yield* _(fs.exists(resolvedAuthorizedKeys))
148-
const sshCommand = buildSshCommand(template, sshKey)
181+
const sshCommand = buildSshCommand(template, sshKey, ipAddress)
149182
const displayName = formatDisplayName(template.repoUrl)
150183

151184
return {
@@ -159,6 +192,7 @@ export const loadProjectItem = (
159192
sshPort: template.sshPort,
160193
targetDir: template.targetDir,
161194
sshCommand,
195+
ipAddress,
162196
sshKeyPath: sshKey,
163197
authorizedKeysPath: resolvedAuthorizedKeys,
164198
authorizedKeysExists: authExists,

packages/lib/src/usecases/projects-list.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import {
2929
export const listProjects: Effect.Effect<
3030
void,
3131
PlatformError,
32-
FileSystem.FileSystem | Path.Path
32+
FileSystem.FileSystem | Path.Path | CommandExecutor.CommandExecutor
3333
> = pipe(
3434
withProjectIndexAndSsh((index, sshKey) =>
3535
Effect.gen(function*(_) {
@@ -78,9 +78,9 @@ const emptyItems = (): ReadonlyArray<ProjectItem> => []
7878
const collectProjectValues = <A, B, E>(
7979
configPaths: ReadonlyArray<string>,
8080
sshKey: string | null,
81-
load: (configPath: string, sshKey: string | null) => Effect.Effect<A, E, FileSystem.FileSystem | Path.Path>,
81+
load: (configPath: string, sshKey: string | null) => Effect.Effect<A, E, FileSystem.FileSystem | Path.Path | CommandExecutor.CommandExecutor>,
8282
toValue: (value: A) => B
83-
): Effect.Effect<ReadonlyArray<B>, never, FileSystem.FileSystem | Path.Path> =>
83+
): Effect.Effect<ReadonlyArray<B>, never, FileSystem.FileSystem | Path.Path | CommandExecutor.CommandExecutor> =>
8484
Effect.gen(function*(_) {
8585
const available: Array<B> = []
8686

@@ -102,10 +102,10 @@ const collectProjectValues = <A, B, E>(
102102
})
103103

104104
const listProjectValues = <A, B, E>(
105-
load: (configPath: string, sshKey: string | null) => Effect.Effect<A, E, FileSystem.FileSystem | Path.Path>,
105+
load: (configPath: string, sshKey: string | null) => Effect.Effect<A, E, FileSystem.FileSystem | Path.Path | CommandExecutor.CommandExecutor>,
106106
toValue: (value: A) => B,
107107
empty: () => ReadonlyArray<B>
108-
): Effect.Effect<ReadonlyArray<B>, PlatformError, FileSystem.FileSystem | Path.Path> =>
108+
): Effect.Effect<ReadonlyArray<B>, PlatformError, FileSystem.FileSystem | Path.Path | CommandExecutor.CommandExecutor> =>
109109
pipe(
110110
withProjectIndexAndSsh((index, sshKey) => collectProjectValues(index.configPaths, sshKey, load, toValue)),
111111
Effect.map((values) => values ?? empty())
@@ -114,7 +114,7 @@ const listProjectValues = <A, B, E>(
114114
export const listProjectSummaries: Effect.Effect<
115115
ReadonlyArray<string>,
116116
PlatformError,
117-
FileSystem.FileSystem | Path.Path
117+
FileSystem.FileSystem | Path.Path | CommandExecutor.CommandExecutor
118118
> = listProjectValues(loadProjectSummary, renderProjectSummary, emptySummaries)
119119

120120
// CHANGE: load docker-git projects for TUI selection
@@ -130,7 +130,7 @@ export const listProjectSummaries: Effect.Effect<
130130
export const listProjectItems: Effect.Effect<
131131
ReadonlyArray<ProjectItem>,
132132
PlatformError,
133-
FileSystem.FileSystem | Path.Path
133+
FileSystem.FileSystem | Path.Path | CommandExecutor.CommandExecutor
134134
> = listProjectValues(loadProjectItem, (value) => value, emptyItems)
135135

136136
// CHANGE: list only running docker-git projects (for "Stop container" UI)

0 commit comments

Comments
 (0)