diff --git a/.github/workflows/publish-foundry.yml b/.github/workflows/publish-foundry.yml index 4499e97..43754df 100644 --- a/.github/workflows/publish-foundry.yml +++ b/.github/workflows/publish-foundry.yml @@ -112,14 +112,43 @@ jobs: if: steps.meta.outputs.publish == 'true' env: FOUNDRY_ARTIFACT_REPOSITORY_RID: ${{ secrets.FOUNDRY_ARTIFACT_REPOSITORY_RID }} - FOUNDRY_TOKEN: ${{ secrets.FOUNDRY_TOKEN }} FOUNDRY_REGISTRY_HOST: ${{ secrets.FOUNDRY_REGISTRY_HOST }} + # Preferred: OAuth2 Client Credentials (short-lived access token minted at runtime) + FOUNDRY_URL: ${{ secrets.FOUNDRY_URL }} + FOUNDRY_OAUTH_CLIENT_ID: ${{ secrets.FOUNDRY_OAUTH_CLIENT_ID }} + FOUNDRY_OAUTH_CLIENT_SECRET: ${{ secrets.FOUNDRY_OAUTH_CLIENT_SECRET }} + # Optional: explicitly request a scope in the token request. + # Some enrollments/app-scope configurations may require this. + FOUNDRY_OAUTH_SCOPE: ${{ secrets.FOUNDRY_OAUTH_SCOPE }} + # Fallback: legacy static token (e.g., user-generated / UI token) + FOUNDRY_TOKEN: ${{ secrets.FOUNDRY_TOKEN }} run: | set -euo pipefail : "${FOUNDRY_ARTIFACT_REPOSITORY_RID:?missing FOUNDRY_ARTIFACT_REPOSITORY_RID secret}" - : "${FOUNDRY_TOKEN:?missing FOUNDRY_TOKEN secret}" : "${FOUNDRY_REGISTRY_HOST:?missing FOUNDRY_REGISTRY_HOST secret}" - echo "${FOUNDRY_TOKEN}" | docker login -u "${FOUNDRY_ARTIFACT_REPOSITORY_RID}" --password-stdin "${FOUNDRY_REGISTRY_HOST}" + + if [[ -n "${FOUNDRY_URL:-}" && -n "${FOUNDRY_OAUTH_CLIENT_ID:-}" && -n "${FOUNDRY_OAUTH_CLIENT_SECRET:-}" ]]; then + foundry_url="${FOUNDRY_URL%/}" + scope_arg=() + if [[ -n "${FOUNDRY_OAUTH_SCOPE:-}" ]]; then + scope_arg=(--data-urlencode "scope=${FOUNDRY_OAUTH_SCOPE}") + fi + token_json="$( + curl -fsS -X POST "${foundry_url}/multipass/api/oauth2/token" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + --data-urlencode "grant_type=client_credentials" \ + --data-urlencode "client_id=${FOUNDRY_OAUTH_CLIENT_ID}" \ + --data-urlencode "client_secret=${FOUNDRY_OAUTH_CLIENT_SECRET}" \ + "${scope_arg[@]}" + )" + + oauth_token="$(python -c 'import json,sys; print(json.loads(sys.stdin.read())["access_token"])' <<<"${token_json}")" + echo "::add-mask::${oauth_token}" + echo "${oauth_token}" | docker login -u "${FOUNDRY_ARTIFACT_REPOSITORY_RID}" --password-stdin "${FOUNDRY_REGISTRY_HOST}" + else + : "${FOUNDRY_TOKEN:?missing FOUNDRY_TOKEN secret (or set FOUNDRY_URL + FOUNDRY_OAUTH_CLIENT_ID + FOUNDRY_OAUTH_CLIENT_SECRET)}" + echo "${FOUNDRY_TOKEN}" | docker login -u "${FOUNDRY_ARTIFACT_REPOSITORY_RID}" --password-stdin "${FOUNDRY_REGISTRY_HOST}" + fi - name: Build (and maybe push) env: @@ -209,4 +238,3 @@ jobs: echo "- Publish: no (build + validate only)" fi } >> "${GITHUB_STEP_SUMMARY}" - diff --git a/README.md b/README.md index d00ad38..f814a3f 100644 --- a/README.md +++ b/README.md @@ -364,6 +364,14 @@ commands. ## 5. Foundry OpenAPI Compute Module Deployment +This repo is configured to support a Foundry-friendly workflow: ship a Docker image that embeds an OpenAPI contract (as the `server.openapi` image label), then import FastAPI routes as Foundry functions via **Detect from OpenAPI specification**. + +![Foundry function call from imported OpenAPI](assets/palantir-compute-modules-function-call.png) + +More context + screenshots: + +- `docs/foundry-auto-deploy.md` + Generate and validate the Foundry-constrained OpenAPI artifact: ```bash diff --git a/assets/palantir-artifact-repository.png b/assets/palantir-artifact-repository.png new file mode 100644 index 0000000..dd46213 Binary files /dev/null and b/assets/palantir-artifact-repository.png differ diff --git a/assets/palantir-compute-module-openapi-interop.png b/assets/palantir-compute-module-openapi-interop.png new file mode 100644 index 0000000..cb5d226 Binary files /dev/null and b/assets/palantir-compute-module-openapi-interop.png differ diff --git a/assets/palantir-compute-modules-function-call.png b/assets/palantir-compute-modules-function-call.png new file mode 100644 index 0000000..90c5e64 Binary files /dev/null and b/assets/palantir-compute-modules-function-call.png differ diff --git a/docs/deploy-ci.md b/docs/deploy-ci.md index 92f20c0..580dc88 100644 --- a/docs/deploy-ci.md +++ b/docs/deploy-ci.md @@ -68,6 +68,8 @@ Mapping: - Image label `server.openapi` is required and must exactly match `openapi.foundry.json`. - Foundry registry should primarily use version tags (e.g. `0.1.2`) rather than `main-...` tags. +For a short explanation of the "OpenAPI -> functions" import path (plus screenshots), see `docs/foundry-auto-deploy.md`. + ## Railway Runtime Notes - Runtime continues to honor `PORT` with default `8080`. diff --git a/docs/foundry-auto-deploy.md b/docs/foundry-auto-deploy.md new file mode 100644 index 0000000..4941fb5 --- /dev/null +++ b/docs/foundry-auto-deploy.md @@ -0,0 +1,70 @@ +# Foundry Auto-Deploy (OpenAPI -> Functions) + +This repo is set up so a tagged release can publish a Docker image to Foundry and make its FastAPI routes importable as Foundry **functions**. + +The key idea: publish a container image that includes a Foundry-compatible OpenAPI contract (as the `server.openapi` image label). In Foundry, you can then run **Detect from OpenAPI specification** to auto-register the functions. + +## What You Get + +- A deterministic OpenAPI surface area (`openapi.foundry.json`) for Foundry import. +- A container image with the OpenAPI contract embedded in metadata. +- UI-driven function registration from the contract (no handwritten wrappers). + +## How It Works (High Level) + +1. Generate a Foundry-constrained OpenAPI spec. +2. Build a linux/amd64 image that embeds the spec as `server.openapi`. +3. Push the image to a Foundry Artifact Repository. +4. In Foundry Compute Modules, link the image tag and run **Detect from OpenAPI specification**. + +## Screenshots + +Compute module function-call flow (functions module + query panel): + +![Foundry function call from imported OpenAPI](../assets/palantir-compute-modules-function-call.png) + +OpenAPI schema view inside Foundry (this is the contract Foundry imports): + +![Foundry OpenAPI schema view](../assets/palantir-compute-module-openapi-interop.png) + +Artifact repository tags (images pushed by CI/CD show up here): + +![Foundry artifact repository tags](../assets/palantir-artifact-repository.png) + +Add any additional Foundry UI screenshots to `assets/` and reference them from this doc and `README.md`. + +## Commands (Local) + +Generate and validate the Foundry-constrained OpenAPI artifact: + +```bash +uv run python scripts/deploy/foundry_openapi.py --generate --spec-path openapi.foundry.json +uv run python scripts/deploy/foundry_openapi.py --spec-path openapi.foundry.json +``` + +Build an image that includes the OpenAPI as metadata: + +```bash +export OPENAPI_JSON="$(uv run python -c 'import json; print(json.dumps(json.load(open("openapi.foundry.json", encoding="utf-8")), separators=(",", ":")))')" + +docker buildx build \ + --platform linux/amd64 \ + --build-arg SERVER_OPENAPI="${OPENAPI_JSON}" \ + --tag "//:" \ + --load \ + . +``` + +## CI/CD (Recommended) + +- `/.github/workflows/publish-foundry.yml` publishes to Foundry on tag builds (`v*`). +- `/.github/workflows/release-version.yml` can auto-create a `vX.Y.Z` tag after CI passes on `main`. + +Foundry workflow docs live in: + +- `docs/deploy-ci.md` +- `docs/foundry-openapi-runbook.md` + +## Auth Notes + +For CI, prefer a dedicated non-admin Foundry user that has **Edit** permission on the target Artifact Repository. Generate a long-lived token as that user and store it as the GitHub secret `FOUNDRY_TOKEN`. diff --git a/docs/foundry-openapi-runbook.md b/docs/foundry-openapi-runbook.md index 3012ba8..41bf10e 100644 --- a/docs/foundry-openapi-runbook.md +++ b/docs/foundry-openapi-runbook.md @@ -2,6 +2,8 @@ This runbook captures the full M1-M7 flow for the FastAPI compute module import path. +For the conceptual overview and screenshots of the import UX, see `docs/foundry-auto-deploy.md`. + ## Locked Targets - `FOUNDRY_URL=https://23dimethyl.usw-3.palantirfoundry.com`