Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
9 changes: 9 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.git
**/node_modules
**/dist
**/cdk.out*
source/idea/ideactl/tools/parity/live
source/idea/ideactl/tools/parity/fixtures
source/idea/ideactl/docs/port
source/idea/ideactl/tools/e2e/reference
*.pem
336 changes: 336 additions & 0 deletions .github/workflows/build_push.yaml

Large diffs are not rendered by default.

61 changes: 61 additions & 0 deletions .github/workflows/ideactl_gates.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
name: ideactl gates
on:
pull_request:
paths-ignore:
- 'docs/**'
push:
branches:
- main
- 'release-*'
paths-ignore:
- 'docs/**'
workflow_dispatch:
permissions:
contents: read
jobs:
gates:
runs-on: ubuntu-large
defaults:
run:
working-directory: source/idea/ideactl
steps:
- uses: actions/checkout@v4
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: "26.7.0"
cache: npm
cache-dependency-path: source/idea/ideactl/package-lock.json
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.13'
- name: Check package version
run: |
PACKAGE_VERSION=$(node --input-type=module -e 'import packageJson from "./package.json" with { type: "json" }; process.stdout.write(packageJson.version)')
RELEASE_VERSION=$(tr -d '[:space:]' < ../../../IDEA_VERSION.txt)
if [ "$PACKAGE_VERSION" != "$RELEASE_VERSION" ]; then
echo "::error::ideactl package version ${PACKAGE_VERSION} does not match IDEA_VERSION.txt ${RELEASE_VERSION}."
exit 1
fi
- name: Install dependencies
run: npm ci
- name: Build without changing tracked files
run: |
npm run build
git diff --exit-code -- .
- name: Run credential-free gates
id: automated
# A runner has no captured cluster inputs: tools/parity/live, tools/parity/fixtures and
# docs/port are untracked by design, because they hold real cluster data and this
# repository is public. The fixture helper is fail-closed, so without this variable the
# dependent files throw at setup and the suite cannot pass here at all. Declaring the
# checkout public makes them skip instead, and the skip-policy gate records each one with
# its reason so the skips stay visible. This job therefore proves the credential-free half
# only; it is not evidence about anything that needs a captured cluster.
env:
IDEACTL_PUBLIC_CHECKOUT: '1'
run: node scripts/ci-gates.mjs all
- name: Print credential-backed gates after an earlier failure
if: always() && steps.automated.outcome == 'skipped'
run: node scripts/ci-gates.mjs human
6 changes: 2 additions & 4 deletions .github/workflows/lint_build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,12 @@ jobs:
- name: Check version consistency
run: |
SH_REV=$(grep -m1 '^IDEA_REVISION=' idea-admin.sh | sed -E 's/.*:-"([^"]+)"\}.*/\1/')
PS1_REV=$(grep -m1 '[$]IDEARevision = if' idea-admin-windows.ps1 | sed -E 's/.*else \{"([^"]+)"\}.*/\1/')
VERSION=$(tr -d '[:space:]' < IDEA_VERSION.txt)
EXPECTED="v${VERSION}"
echo "idea-admin.sh revision: ${SH_REV}"
echo "idea-admin-windows.ps1 revision: ${PS1_REV}"
echo "IDEA_VERSION.txt: ${VERSION} (expected revision ${EXPECTED})"
if [ "${SH_REV}" != "${EXPECTED}" ] || [ "${PS1_REV}" != "${EXPECTED}" ]; then
echo "::error::Version mismatch: IDEA_VERSION.txt=${VERSION} (expected revision ${EXPECTED}), idea-admin.sh default=${SH_REV}, idea-admin-windows.ps1 default=${PS1_REV}"
if [ "${SH_REV}" != "${EXPECTED}" ]; then
echo "::error::Version mismatch: IDEA_VERSION.txt=${VERSION} (expected revision ${EXPECTED}), idea-admin.sh default=${SH_REV}"
exit 1
fi
echo "Version consistency check passed (${EXPECTED})"
Expand Down
10 changes: 4 additions & 6 deletions .github/workflows/sync_docs_branch.yaml
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
name: Sync docs branch
# The GitBook space syncs with the gitbook branch, because main only accepts pull requests.
# On every merge to main, and once a day, fast-forward gitbook to main so docs edited in the
# repository reach GitBook. When gitbook is ahead because edits were merged in the GitBook
# editor, open the pull request that carries them to main. The built-in token is enough:
# a pull request it creates triggers no Actions workflows, and the test workflows skip
# docs-only pull requests anyway, while the GitBook preview comes from the GitBook app.
# The GitBook space syncs with the gitbook branch, because main accepts pull requests only.
# Fast-forward gitbook to main so repository edits reach GitBook, and when gitbook is ahead
# because edits were merged in the GitBook editor, open the pull request that carries them
# back. The built-in token is enough: a pull request it creates triggers no workflows.
on:
push:
branches:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ __pycache__/

# IDEA Specific
deployment/ecr/idea-administrator/*.tar.gz
deployment/ecr/*/*.tar.gz
open-source/
deployment/global-s3-assets/
deployment/regional-s3-assets/
Expand Down
151 changes: 151 additions & 0 deletions deployment/ecr/idea-control-plane/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# syntax=docker/dockerfile:1.7
# Build the control-plane image. IDEA_CONTAINER_ROLE selects the task process.
# Use the repository root as the build context:
# docker buildx build --platform linux/amd64,linux/arm64 \
# --build-context dcv-packages=/path/to/dcv-packages \
# -f deployment/ecr/idea-control-plane/Dockerfile --build-arg IDEA_VERSION=26.09.0 .
# Copy the release tarballs into this directory before the build.
# The dcv-packages context contains NICE-GPG-KEY, checksums.txt, the broker RPM,
# both gateway RPMs, and both DCV archives. checksums.txt covers each package
# file, one sha256sum entry per line.
# Each architecture-dependent step declares ARG TARGETARCH inside its stage and reads the
# value the builder supplies. Do not declare TARGETARCH before the first FROM: under the
# dockerfile:1.7 frontend a global declaration with no default shadows the builder's value,
# every stage then sees an empty string, and the build only works if a single architecture is
# passed by hand, which a two-architecture build cannot do.
# Set PBS_IMAGE_REPO and PBS_IMAGE_TAG for a registry copy, or PBS_IMAGE for a full reference.

ARG IDEA_VERSION
ARG PBS_IMAGE_REPO=idea-scheduler-pbs
ARG PBS_IMAGE_TAG=v${IDEA_VERSION}
ARG PBS_IMAGE=${PBS_IMAGE_REPO}:${PBS_IMAGE_TAG}

FROM public.ecr.aws/amazonlinux/amazonlinux:2023 AS dcv-runtime

ARG TARGETARCH

ENV IDEA_APP_DEPLOY_DIR=/opt/idea/app
ENV PYTHONUNBUFFERED=1
ENV LC_ALL="en_US.UTF-8" \
LC_CTYPE="en_US.UTF-8" \
LANG="en_US.UTF-8"
ENV PATH="/opt/pbs/bin:/opt/pbs/sbin:${PATH}"

# Install runtime packages for the module, scheduler, broker, and gateway roles.
RUN dnf install -y --setopt=install_weak_deps=False \
python3.13 python3.13-pip python3.13-devel \
gcc openldap-devel cyrus-sasl-devel \
postgresql17-server postgresql17-contrib \
expat libedit tcl tk libical hwloc-libs \
adcli krb5-workstation sssd-ad realmd \
java-11-amazon-corretto-headless \
nginx openssl nmap-ncat \
glibc-langpack-en shadow-utils procps-ng iproute hostname sudo util-linux jq tar gzip which awscli-2 curl-minimal \
&& dnf clean all

# Install the session-manager broker, connection gateway, and web viewer from the
# supplied package context. This avoids a build-time dependency on a public host.
COPY --from=dcv-packages / /tmp/dcv-packages/
RUN set -eux; \
case "${TARGETARCH:-}" in \
amd64) A=x86_64 ;; \
arm64) A=aarch64 ;; \
*) echo "TARGETARCH must be amd64 or arm64 (build with buildx); got '${TARGETARCH:-}'" >&2; exit 1 ;; \
esac; \
for F in NICE-GPG-KEY checksums.txt nice-dcv-session-manager-broker-amzn2023.noarch.rpm "nice-dcv-connection-gateway-amzn2023.${A}.rpm" "nice-dcv-amzn2023-${A}.tgz"; do \
test -f "/tmp/dcv-packages/${F}" || { echo "dcv-packages is missing ${F}" >&2; exit 1; }; \
done; \
(cd /tmp/dcv-packages && sha256sum -c checksums.txt); \
rpm --import /tmp/dcv-packages/NICE-GPG-KEY; \
rpm -K /tmp/dcv-packages/nice-dcv-session-manager-broker-amzn2023.noarch.rpm; \
dnf install -y /tmp/dcv-packages/nice-dcv-session-manager-broker-amzn2023.noarch.rpm; \
rpm -K "/tmp/dcv-packages/nice-dcv-connection-gateway-amzn2023.${A}.rpm"; \
dnf install -y "/tmp/dcv-packages/nice-dcv-connection-gateway-amzn2023.${A}.rpm"; \
cp "/tmp/dcv-packages/nice-dcv-amzn2023-${A}.tgz" /tmp/dcv.tgz; \
mkdir -p /tmp/dcv && tar -xzf /tmp/dcv.tgz -C /tmp/dcv --strip-components=1; \
rpm -K /tmp/dcv/nice-dcv-web-viewer-*.rpm; dnf install -y /tmp/dcv/nice-dcv-web-viewer-*.rpm; \
rm -rf /tmp/dcv-packages /tmp/dcv.tgz /tmp/dcv; dnf clean all; \
sed -i 's|> /dev/null|2>\&1|' /usr/share/dcv-session-manager-broker/bin/dcv-session-manager-broker.sh

FROM ${PBS_IMAGE} AS pbs

FROM dcv-runtime

ARG IDEA_VERSION

COPY --from=pbs /opt/pbs /opt/pbs

# Install all module packages from the release bundle. The selected role runs at startup.
# Keep the release bundle in /root/.idea/downloads for package commands.
ADD deployment/ecr/idea-control-plane/all-${IDEA_VERSION}.tar.gz /root/.idea/downloads/
RUN set -eux; \
for MODULE in idea-cluster-manager idea-virtual-desktop-controller idea-scheduler; do \
APP_NAME="${MODULE#idea-}"; \
PKG="/tmp/idea/${MODULE}"; \
mkdir -p "${PKG}"; \
tar -xf "/root/.idea/downloads/${MODULE}-${IDEA_VERSION}.tar.gz" -C "${PKG}"; \
python3.13 -m pip install --no-cache-dir -r "${PKG}/requirements.txt"; \
python3.13 -m pip install --no-cache-dir "${PKG}"/*-lib.tar.gz; \
mkdir -p "${IDEA_APP_DEPLOY_DIR}/${APP_NAME}"; \
cp -r "${PKG}/resources" "${IDEA_APP_DEPLOY_DIR}/${APP_NAME}/"; \
if [ -d "${PKG}/webapp" ]; then cp -r "${PKG}/webapp" "${IDEA_APP_DEPLOY_DIR}/${APP_NAME}/"; fi; \
done; \
rm -rf /root/.cache/pip; \
mkdir -p "${IDEA_APP_DEPLOY_DIR}/logs"

# Overlay the gateway web resources.
ADD deployment/ecr/idea-control-plane/idea-dcv-connection-gateway-${IDEA_VERSION}.tar.gz /tmp/idea-gateway/
RUN cp -a /tmp/idea-gateway/static_resources/. /usr/share/dcv/www/ && rm -rf /tmp/idea-gateway /tmp/idea

# Build ideactl and include its pinned CDK CLI.
# The build uses the repository path expected by its resource-copy script.
ARG NODE_VERSION=22.23.2
RUN set -eux; \
case "${TARGETARCH:-}" in \
amd64) N=x64 ;; \
arm64) N=arm64 ;; \
*) echo "TARGETARCH must be amd64 or arm64 (build with buildx); got '${TARGETARCH:-}'" >&2; exit 1 ;; \
esac; \
mkdir -p /usr/local/node; \
curl -fsSL -o /tmp/node.tar.gz "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${N}.tar.gz"; \
tar -xzf /tmp/node.tar.gz -C /usr/local/node --strip-components=1 --no-same-owner; \
rm -f /tmp/node.tar.gz

ENV PATH="/usr/local/node/bin:/opt/idea/ideactl/node_modules/.bin:${PATH}"
ENV CDK_DISABLE_CLI_TELEMETRY=true

COPY IDEA_VERSION.txt /idea-build/IDEA_VERSION.txt
COPY source/idea/idea-administrator/resources /idea-build/source/idea/idea-administrator/resources
# scripts/copy-resources.mjs stages this tree into dist/resources/bootstrap.
COPY source/idea/idea-bootstrap /idea-build/source/idea/idea-bootstrap
COPY source/idea/ideactl/package.json source/idea/ideactl/package-lock.json source/idea/ideactl/tsconfig.json source/idea/ideactl/cdk.json /idea-build/source/idea/ideactl/
COPY source/idea/ideactl/src /idea-build/source/idea/ideactl/src
COPY source/idea/ideactl/scripts /idea-build/source/idea/ideactl/scripts
# The container module's config templates live here until resource ownership moves into the
# package. copy-resources.mjs overlays them onto dist/resources/config.
COPY source/idea/ideactl/resources-ecs /idea-build/source/idea/ideactl/resources-ecs
RUN set -eux; \
cd /idea-build/source/idea/ideactl; \
npm ci; \
npm run build; \
scripts/build-lambda-zips.sh; \
npm prune --omit=dev; \
npm cache clean --force; \
find node_modules -type f -name '*.d.ts' -delete; \
mkdir -p /opt/idea; \
mv /idea-build/source/idea/ideactl /opt/idea/ideactl; \
rm -rf /idea-build; \
cd /opt/idea/ideactl; \
node /opt/idea/ideactl/dist/src/cli/main.js about

COPY deployment/ecr/idea-control-plane/nginx.conf /etc/nginx/conf.d/default.conf
COPY deployment/ecr/idea-control-plane/entrypoint.sh /opt/idea/entrypoint.sh
COPY deployment/ecr/idea-control-plane/sync_users.py /opt/idea/sync_users.py
COPY deployment/ecr/idea-control-plane/roles /opt/idea/roles
RUN chmod +x /opt/idea/entrypoint.sh /opt/idea/roles/*.sh

# module 8443; broker 8444/8445/8446 client/agent/gateway and 47100/47500 between brokers;
# gateway 8443 web and QUIC and 8989 health.
EXPOSE 8443 8444 8445 8446 8989 47100 47500

ENTRYPOINT ["/opt/idea/entrypoint.sh"]
76 changes: 76 additions & 0 deletions deployment/ecr/idea-control-plane/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#!/bin/bash
#
# Run the process selected by IDEA_CONTAINER_ROLE.
set -euo pipefail

# A role passed as the first argument overrides IDEA_CONTAINER_ROLE.
case "${1:-}" in
ideactl|cluster-manager|vdc|scheduler|dcv-broker|dcv-gateway) ROLE="$1"; shift ;;
*) ROLE="${IDEA_CONTAINER_ROLE:?IDEA_CONTAINER_ROLE is required: ideactl|cluster-manager|vdc|scheduler|dcv-broker|dcv-gateway}" ;;
esac

# Module packages share an ideaserver command, so run each module's main directly.
run_module() {
exec python3.13 -c "import sys; sys.argv=['ideaserver']; from ${1}.app.app_main import main; sys.exit(main())"
}

setting() {
aws dynamodb get-item --region "${AWS_DEFAULT_REGION}" \
--table-name "${IDEA_CLUSTER_NAME}.cluster-settings" \
--key "{\"key\":{\"S\":\"$1\"}}" --query 'Item.value.S' --output text
}

# The three module roles bind TLS against one certificate pair under the cluster home on the
# applications file system. This substrate owns creating it: no host bootstrap runs, so on a fresh
# install the pair does not exist and the module raises a file-not-found before it binds its port.
# The roles start together on the same shared volume, so the pair is published under a link that
# elects one writer rather than by two roles each writing half of it.
ensure_app_certs() {
local home certs key crt zone tmp
home="$(setting cluster.home_dir)"
case "${home}" in ''|None) echo "[entrypoint] cluster.home_dir is not set" >&2; return 1 ;; esac
certs="${home}/certs"
key="${certs}/idea.key"
crt="${certs}/idea.crt"
if [[ -s "${key}" && -s "${crt}" ]]; then
return 0
fi
zone="$(setting cluster.route53.private_hosted_zone_name)"
case "${zone}" in ''|None) echo "[entrypoint] cluster.route53.private_hosted_zone_name is not set" >&2; return 1 ;; esac
install -d -m 700 "${certs}"
tmp="$(mktemp -d "${certs}/.new.XXXXXX")"
openssl req -new -newkey rsa:4096 -days 3650 -nodes -x509 \
-subj "/C=US/ST=California/L=Sunnyvale/CN=*.${zone}" \
-keyout "${tmp}/idea.key" -out "${tmp}/idea.crt" 2>/dev/null
chmod 600 "${tmp}/idea.key"
# A hard link fails when the name is taken, so the first role to get there publishes the pair it
# generated and the rest wait for that one.
if ln "${tmp}/idea.key" "${key}" 2>/dev/null; then
mv -f "${tmp}/idea.crt" "${crt}"
echo "[entrypoint] generated the application certificate pair in ${certs}"
else
for _ in $(seq 1 60); do
[[ -s "${crt}" ]] && break
sleep 1
done
fi
rm -rf "${tmp}"
if [[ ! -s "${key}" || ! -s "${crt}" ]]; then
echo "[entrypoint] no application certificate pair in ${certs}" >&2
return 1
fi
}

case "${ROLE}" in
cluster-manager|vdc|scheduler) ensure_app_certs ;;
esac

case "${ROLE}" in
ideactl) exec node /opt/idea/ideactl/dist/src/cli/main.js "$@" ;;
cluster-manager) run_module ideaclustermanager ;;
vdc) run_module ideavirtualdesktopcontroller ;;
scheduler) exec /opt/idea/roles/scheduler.sh ;;
dcv-broker) exec /opt/idea/roles/broker.sh ;;
dcv-gateway) exec /opt/idea/roles/gateway.sh ;;
*) echo "[entrypoint] unknown IDEA_CONTAINER_ROLE=${ROLE}" >&2; exit 1 ;;
esac
5 changes: 5 additions & 0 deletions deployment/ecr/idea-control-plane/nginx.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
server {
listen 80;
listen [::]:80;
root /usr/share/dcv/www;
}
Loading
Loading