Skip to content

Commit

Permalink
Add port binding support and example for container scripts
Browse files Browse the repository at this point in the history
  • Loading branch information
pelikhan committed Sep 17, 2024
1 parent 9545b8d commit e576766
Show file tree
Hide file tree
Showing 22 changed files with 288 additions and 4 deletions.
4 changes: 4 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"gptool",
"gptools",
"gptoolsjs",
"hostpath",
"limitrows",
"llmify",
"llmrequest",
Expand All @@ -41,12 +42,15 @@
"promptfoo",
"prompty",
"quoteify",
"socketserver",
"stringifying",
"sysr",
"tabletojson",
"treesitter",
"typecheck",
"unfence",
"urllib",
"urlparse",
"vsix",
"xpai"
],
Expand Down
10 changes: 10 additions & 0 deletions docs/genaisrc/genaiscript.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 19 additions & 0 deletions docs/src/content/docs/reference/scripts/container.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,25 @@ You can enable network access using `networkEnabled`.
const container = await host.container({ networkEnabled: true })
```

### Port bindings

You can bind container ports to host ports and access web servers running in the container.

For example, this configuration will map the host `8088` port to `80` on the container
and you will be able to access a local web server using `http://localhost:8088/`.

```js
const container = await host.container({
networkEnabled: true,
ports: {
containerPort: "80/tcp",
hostPort: "8088",
},

Check failure on line 72 in docs/src/content/docs/reference/scripts/container.md

View workflow job for this annotation

GitHub Actions / build

Port bindings syntax is incorrect; 'ports' should be an array of objects, not a single object.
})
```

Then

Check notice on line 76 in docs/src/content/docs/reference/scripts/container.md

View workflow job for this annotation

GitHub Actions / build

The section "Then" is incomplete and should either be expanded or removed.

## Run a command

You can run a command in the container using the `exec` method. It returns the exit code, standard output and error streams.
Expand Down
10 changes: 10 additions & 0 deletions genaisrc/genaiscript.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions packages/auto/genaiscript.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 30 additions & 4 deletions packages/cli/src/docker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,15 @@ import { randomHex } from "../../core/src/crypto"
import { errorMessage } from "../../core/src/error"
import { host } from "../../core/src/host"
import { TraceOptions } from "../../core/src/trace"
import { logError, dotGenaiscriptPath, logVerbose } from "../../core/src/util"
import {
logError,
dotGenaiscriptPath,
logVerbose,
arrayify,
} from "../../core/src/util"
import { CORE_VERSION } from "../../core/src/version"
import { isQuiet } from "./log"
import Dockerode from "dockerode"

type DockerodeType = import("dockerode")

Expand Down Expand Up @@ -127,6 +133,7 @@ export class DockerManager {
networkEnabled,
name,
} = options
const ports = arrayify(options.ports)
try {
trace?.startDetails(`📦 container start ${image}`)
await this.pullImage(image, { trace })
Expand All @@ -139,9 +146,10 @@ export class DockerManager {
)
)
await ensureDir(hostPath)
const containerPath = DOCKER_CONTAINER_VOLUME

const container = await this._docker.createContainer({
const containerPath = DOCKER_CONTAINER_VOLUME
logVerbose(`container: create ${image} ${name ?? ""}`)
const containerOptions: Dockerode.ContainerCreateOptions = {
name,
Image: image,
AttachStdin: false,
Expand All @@ -162,10 +170,28 @@ export class DockerManager {
? key
: `${key}=${value}`
),
ExposedPorts: ports.reduce(
(acc, { containerPort }) => {
acc[containerPort] = {}
return acc
},
<Record<string, any>>{}

Check failure on line 178 in packages/cli/src/docker.ts

View workflow job for this annotation

GitHub Actions / build

There is no error handling for the `reduce` function. If `ports` is `undefined` or `null`, this will throw a TypeError. Consider adding a check before using `reduce`. 😊
),
HostConfig: {
Binds: [`${hostPath}:${containerPath}`],
PortBindings: ports?.reduce(
(acc, { containerPort, hostPort }) => {
acc[containerPort] = [
{ HostPort: String(hostPort) },
]
return acc
},
<Record<string, { HostPort: string }[]>>{}
),

Check failure on line 190 in packages/cli/src/docker.ts

View workflow job for this annotation

GitHub Actions / build

Similar to the previous point, there is no error handling for the `reduce` function here. If `ports` is `undefined` or `null`, this will throw a TypeError. Consider adding a check before using `reduce`. 😊
},
})
}
const container =
await this._docker.createContainer(containerOptions)
trace?.itemValue(`id`, container.id)
trace?.itemValue(`host path`, hostPath)
trace?.itemValue(`container path`, containerPath)
Expand Down
10 changes: 10 additions & 0 deletions packages/core/src/genaisrc/genaiscript.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions packages/core/src/types/prompt_template.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2212,6 +2212,11 @@ interface ShellHost {
confirm(message: string, options?: ShellConfirmOptions): Promise<boolean>
}

interface ContainerPortBinding {
containerPort: string
hostPort: string | number
}

Check failure on line 2219 in packages/core/src/types/prompt_template.d.ts

View workflow job for this annotation

GitHub Actions / build

The `ContainerPortBinding` interface is missing a type definition for `hostPort`. It is currently defined as `string | number`, but it would be better to specify the exact type for better type safety. For example, if `hostPort` is always a number, then it should be defined as `number`. If it can be either a string or a number, consider creating a type alias for it. 😊
interface ContainerOptions {
/**
* Container image names.
Expand Down Expand Up @@ -2239,6 +2244,11 @@ interface ContainerOptions {
* Disable automatic purge of container and volume directory
*/
disablePurge?: boolean

/**
* List of exposed TCP ports
*/
ports?: ElementOrArray<ContainerPortBinding>
}

interface PromptHost extends ShellHost {
Expand Down
10 changes: 10 additions & 0 deletions packages/sample/genaisrc/blog/genaiscript.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

55 changes: 55 additions & 0 deletions packages/sample/genaisrc/container-web.genai.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
script({
model: "gpt-3.5-turbo",
})

const disablePurge = env.vars.purge === "no"

// start web server
const hostPort = 8089
const webContainer = await host.container({
disablePurge,
networkEnabled: true,
ports: { containerPort: "80/tcp", hostPort },
})
await webContainer.writeText(
"main.py",
`
import http.server
import socketserver
import urllib.parse
PORT = 80
class CustomHandler(http.server.SimpleHTTPRequestHandler):
def do_GET(self):
parsed_path = urllib.parse.urlparse(self.path)
if parsed_path.path == '/echo':
query = urllib.parse.parse_qs(parsed_path.query)
message = query.get('message', [''])[0]
self.send_response(200)
self.send_header('Content-type', 'text/plain')
self.end_headers()
self.wfile.write(message.encode())
else:
super().do_GET()
with socketserver.TCPServer(("", PORT), CustomHandler) as httpd:
print(f"Serving at port {PORT}")
httpd.serve_forever()
`
)
webContainer.exec("python", ["main.py"]) // don't await
await sleep(1000) // wait for server to start
const msg = Math.random().toString()
const url = `http://localhost:${hostPort}/echo?message=` + msg
console.log(`fetching ${url} with msg ${msg}`)
const res = await fetch(url)
console.log(res.status)
const text = await res.text()
console.log(text)
if (text !== msg) throw new Error(`expected ${msg}, got ${text} instead`)

function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms))
}

10 changes: 10 additions & 0 deletions packages/sample/genaisrc/genaiscript.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions packages/sample/genaisrc/node/genaiscript.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions packages/sample/genaisrc/python/genaiscript.d.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit e576766

Please sign in to comment.