@@ -7,6 +7,7 @@ import { Effect } from "effect"
77import { runCommandCapture , runCommandExitCode } from "@lib/shell/command-runner"
88
99import { type DockerNetworkIps , parseDockerNetworkIps , uniqueStrings } from "./controller-reachability.js"
10+ import { computeLocalControllerRevision , controllerRevisionEnvKey , parseControllerRevisionEnvOutput } from "./controller-revision.js"
1011import type { ControllerBootstrapError } from "./host-errors.js"
1112
1213export type ControllerRuntime =
@@ -18,6 +19,7 @@ export const controllerContainerName = "docker-git-api"
1819
1920const inspectNetworksTemplate = String
2021 . raw `{{range $k,$v := .NetworkSettings.Networks}}{{printf "%s=%s\n" $k $v.IPAddress}}{{end}}`
22+ const inspectEnvTemplate = String . raw `{{range .Config.Env}}{{println .}}{{end}}`
2123
2224const controllerBootstrapError = ( message : string ) : ControllerBootstrapError => ( {
2325 _tag : "ControllerBootstrapError" ,
@@ -48,6 +50,17 @@ const composeFilePath = (): Effect.Effect<string, PlatformError, FileSystem.File
4850const mapComposePathError = ( error : PlatformError ) : ControllerBootstrapError =>
4951 controllerBootstrapError ( `Failed to resolve docker-compose.yml path.\nDetails: ${ String ( error ) } ` )
5052
53+ const mapControllerRevisionError = ( error : PlatformError ) : ControllerBootstrapError =>
54+ controllerBootstrapError ( `Failed to compute docker-git controller revision.\nDetails: ${ String ( error ) } ` )
55+
56+ const renderDockerAccessDeniedMessage = ( ) : string =>
57+ [
58+ "docker-git host CLI cannot access Docker from the client process." ,
59+ "Client-side sudo fallback is disabled." ,
60+ "Keep the docker-git backend container running and reach it via DOCKER_GIT_API_URL or the default local API port, or grant this user direct Docker access (docker group/rootless Docker)." ,
61+ "Probe command: docker info"
62+ ] . join ( "\n" )
63+
5164const runExitCode = (
5265 command : string ,
5366 args : ReadonlyArray < string >
@@ -65,18 +78,16 @@ const runExitCode = (
6578
6679export const resolveDockerCommand = ( ) : Effect . Effect <
6780 ReadonlyArray < string > ,
68- never ,
81+ ControllerBootstrapError ,
6982 CommandExecutor . CommandExecutor
7083> =>
71- Effect . gen ( function * ( _ ) {
72- const dockerInfoExit = yield * _ ( runExitCode ( "docker" , [ "info" ] ) )
73- if ( dockerInfoExit === 0 ) {
74- return [ "docker" ]
75- }
76-
77- const sudoDockerInfoExit = yield * _ ( runExitCode ( "sudo" , [ "-n" , "docker" , "info" ] ) )
78- return sudoDockerInfoExit === 0 ? [ "sudo" , "docker" ] : [ "docker" ]
79- } )
84+ runExitCode ( "docker" , [ "info" ] ) . pipe (
85+ Effect . flatMap ( ( dockerInfoExit ) =>
86+ dockerInfoExit === 0
87+ ? Effect . succeed < ReadonlyArray < string > > ( [ "docker" ] )
88+ : Effect . fail ( controllerBootstrapError ( renderDockerAccessDeniedMessage ( ) ) )
89+ )
90+ )
8091
8192type DockerInvocation = {
8293 readonly command : string
@@ -104,7 +115,7 @@ const formatDockerInvocationFailure = (
104115
105116const runDockerExitCodeCommand = (
106117 args : ReadonlyArray < string >
107- ) : Effect . Effect < number , never , ControllerRuntime > =>
118+ ) : Effect . Effect < number , ControllerBootstrapError , ControllerRuntime > =>
108119 Effect . gen ( function * ( _ ) {
109120 const dockerCommand = yield * _ ( resolveDockerCommand ( ) )
110121 const invocation = buildDockerInvocation ( dockerCommand , args )
@@ -166,6 +177,37 @@ export const runCompose = (
166177 )
167178 } )
168179
180+ export const controllerExists = ( ) : Effect . Effect < boolean , ControllerBootstrapError , ControllerRuntime > =>
181+ runDockerExitCodeCommand ( [ "inspect" , controllerContainerName ] ) . pipe (
182+ Effect . map ( ( exitCode ) => exitCode === 0 )
183+ )
184+
185+ export const inspectControllerRevision = ( ) : Effect . Effect < string | null , ControllerBootstrapError , ControllerRuntime > =>
186+ controllerExists ( ) . pipe (
187+ Effect . flatMap ( ( exists ) =>
188+ exists
189+ ? runDockerCapture (
190+ [ "inspect" , "-f" , inspectEnvTemplate , controllerContainerName ] ,
191+ `Failed to inspect env for ${ controllerContainerName } `
192+ ) . pipe (
193+ Effect . map ( parseControllerRevisionEnvOutput ) ,
194+ Effect . orElseSucceed ( ( ) : string | null => null )
195+ )
196+ : Effect . succeed < string | null > ( null ) )
197+ )
198+
199+ export const prepareLocalControllerRevision = ( ) : Effect . Effect < string , ControllerBootstrapError , ControllerRuntime > =>
200+ Effect . gen ( function * ( _ ) {
201+ const composePath = yield * _ ( composeFilePath ( ) . pipe ( Effect . mapError ( mapComposePathError ) ) )
202+ const revision = yield * _ ( computeLocalControllerRevision ( composePath ) . pipe ( Effect . mapError ( mapControllerRevisionError ) ) )
203+ yield * _ (
204+ Effect . sync ( ( ) => {
205+ process . env [ controllerRevisionEnvKey ] = revision
206+ } )
207+ )
208+ return revision
209+ } )
210+
169211export const inspectContainerNetworks = (
170212 containerName : string
171213) : Effect . Effect < DockerNetworkIps , never , ControllerRuntime > =>
@@ -197,7 +239,10 @@ const connectControllerToNetworkBestEffort = (
197239 return Effect . void
198240 }
199241
200- return runDockerExitCodeCommand ( [ "network" , "connect" , trimmed , controllerContainerName ] ) . pipe ( Effect . asVoid )
242+ return runDockerExitCodeCommand ( [ "network" , "connect" , trimmed , controllerContainerName ] ) . pipe (
243+ Effect . asVoid ,
244+ Effect . orElseSucceed ( ( ) => undefined )
245+ )
201246}
202247
203248export const ensureControllerReachabilityNetworks = (
0 commit comments