feat(deployment): package as Docker image & Helm Chart (#98)

* feat(deployment): package as Docker image & Helm Chart

* remove erroneous permission request

* fix corepack issue

* fix prettier

* deployment testing of helm chart with ctlptl, tilt & kind

* fix linting

* remove need for license to be mounted

* ensure consistency in naming

* incorporate copilot comments

* fix CI pipeline

* fix

* incorporate copilot review comments

* include MIXPANEL environment variable

* remove single quotes from NODE_ENV ARG

---------

Co-authored-by: Oğuzhan Koral <45078678+oguzhankoral@users.noreply.github.com>
This commit is contained in:
Iain Sproat
2026-04-10 09:42:14 +01:00
committed by GitHub
parent 8e2f507286
commit c37235381f
29 changed files with 1045 additions and 37 deletions
+41
View File
@@ -0,0 +1,41 @@
# Irrelevant source files
deployment/
# Build output and other temporary files
.husky/_/
.netlify/
.nuxt/
dist/
node_modules/
# Version control
.git/
.gitignore
# GitHub / CI metadata
.github/
# Environment files
.env
*.env
# Logs
*.log
# IDE / editor settings
.vscode/
.idea/
.zed/
*.iml
# OS / editor junk
.DS_Store
*.swp
*.swo
# AI
.claude/
.cursor/
# testing
tests/
+48
View File
@@ -0,0 +1,48 @@
name: Build Docker Container
on:
workflow_call:
inputs:
PUBLISH:
required: false
type: boolean
default: false
IMAGE_VERSION_TAG:
required: true
type: string
permissions: {} # purposefully empty by default at workflow level, explicitly overridden for specific jobs below
concurrency:
group: ${{ github.workflow }}-build-${{ github.ref }}
cancel-in-progress: true
jobs:
docker-build:
runs-on: blacksmith-4vcpu-ubuntu-2404
name: Build Docker image
permissions:
contents: read
packages: write # to be able to push images to ghcr.io. As permissions is static, it has to be granted even if PUBLISH is false
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
fetch-depth: 0
persist-credentials: false
- name: Login to Helm Chart & Container Image Registry
if: ${{ inputs.PUBLISH == true }}
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ github.token }}
- name: Setup Docker Builder
uses: useblacksmith/setup-docker-builder@affa10db466676f3dfb3e54caeb228ee0691510f
- name: Build and push
uses: useblacksmith/build-push-action@30c71162f16ea2c27c3e21523255d209b8b538c1
with:
push: ${{ inputs.PUBLISH }}
tags: ghcr.io/specklesystems/speckle-dui:${{ inputs.IMAGE_VERSION_TAG }}
file: ./deployment/docker/Dockerfile
network: host # to be able to connect to Tailscale and pull private base image during build
allow: network.host # to be able to connect to Tailscale and pull private base image during build
+63
View File
@@ -0,0 +1,63 @@
name: Get Version
on:
workflow_call:
outputs:
IMAGE_VERSION_TAG:
description: 'The image version tag under which the Helm chart and docker image should be published'
value: ${{ jobs.get-version.outputs.VERSION }}
permissions: {} # purposefully empty by default at workflow level, explicitly overridden for specific jobs below
concurrency:
group: ${{ github.workflow }}-get-version-${{ github.ref }}
cancel-in-progress: true
jobs:
get-version:
outputs:
VERSION: ${{ steps.get-version.outputs.VERSION }}
name: Get Version
permissions:
contents: read
runs-on: blacksmith-4vcpu-ubuntu-2404
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
sparse-checkout: ''
fetch-depth: 1
fetch-tags: 1
persist-credentials: true # zizmor: ignore[artipacked] need to fetch tags in the next step and this ensures that git is configured & authenticated
- run: git fetch origin 'refs/tags/*:refs/tags/*'
- name: Get version tag
id: get-version
run: |
VERSION=""
if [[ "${GITHUB_REF_NAME}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
VERSION="${GITHUB_REF_NAME}"
echo "VERSION=${VERSION}" >> ${GITHUB_OUTPUT}
echo "${VERSION} is a valid semver, we shall use it. Exiting"
exit 0
fi
LAST_RELEASE="$(git describe --always --tags $(git rev-list --tags --max-count=1) | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1 || true)" # get the last release tag. FIXME: Fails if a commit is tagged with more than one tag: https://stackoverflow.com/questions/8089002/git-describe-with-two-tags-on-the-same-commit/56039163#56039163
LAST_RELEASE="${LAST_RELEASE:-0.0.0}"
NEXT_RELEASE="$(echo "${LAST_RELEASE}" | awk -F. -v OFS=. '{$NF += 1 ; print}')"
if [[ "${GITHUB_REF_NAME}" == "main" ]]; then
VERSION="${NEXT_RELEASE}-alpha.${GITHUB_RUN_NUMBER}"
echo "VERSION=${VERSION}" >> ${GITHUB_OUTPUT}
echo "${VERSION} will be an alpha version. Exiting"
exit 0
fi
BRANCH_NAME_TRUNCATED="$(echo "${GITHUB_REF_NAME}" | cut -c -28 | sed 's/[^a-zA-Z0-9.-]/-/g')" # docker has a 128 character tag limit, so ensuring the branch name will be short enough
PADDED_RUN_NUMBER="$(printf "%06d" "${GITHUB_RUN_NUMBER}")"
COMMIT_SHA1_TRUNCATED="$(echo "${GITHUB_SHA}" | cut -c -7)"
VERSION="${NEXT_RELEASE}-branch.${BRANCH_NAME_TRUNCATED}.${PADDED_RUN_NUMBER}-${COMMIT_SHA1_TRUNCATED}"
echo "VERSION=${VERSION}" >> ${GITHUB_OUTPUT}
echo "${VERSION} will be a branch build version. Exiting"
exit 0
+35
View File
@@ -0,0 +1,35 @@
name: Lint
on:
workflow_call: {}
permissions: {} # purposefully empty by default at workflow level, explicitly overridden for specific jobs below
concurrency:
group: ${{ github.workflow }}-lint-${{ github.ref }}
cancel-in-progress: true
jobs:
lint:
runs-on: blacksmith-4vcpu-ubuntu-2404
permissions:
contents: read
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
fetch-depth: 1
persist-credentials: false
- name: Enable Corepack
run: corepack enable
- name: Setup Node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f
with:
node-version: '22.14.0'
cache: 'yarn'
- name: Install Dependencies
run: yarn install --immutable
- name: Run Linter
run: yarn lint
+29 -34
View File
@@ -1,44 +1,39 @@
name: Linting
name: Pull Request
on:
pull_request:
branches:
- main
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true # other running workflows get cancelled on the same branch
permissions: {} # purposefully empty by default at workflow level, explicitly overridden for specific jobs below
jobs:
lint-and-build:
runs-on: ubuntu-latest
get-version:
uses: ./.github/workflows/get-version.yml
with: {}
secrets: {}
permissions:
contents: read
steps:
- name: Checkout Code
uses: actions/checkout@v4
lint:
uses: ./.github/workflows/lint.yml
with: {}
secrets: {}
permissions:
contents: read
- name: Setup Node.js
uses: actions/setup-node@v4
build:
needs:
- get-version
uses: ./.github/workflows/build.yml
with:
node-version: '22.14.0'
- name: Enable Corepack and Install Correct Yarn Version
run: |
corepack enable
corepack prepare yarn@$(jq -r .packageManager package.json | cut -d'@' -f2) --activate
yarn --version
- name: Cache node_modules
uses: actions/cache@v4
with:
path: |
**/node_modules
.yarn/cache
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install Dependencies
run: yarn install --immutable
- name: Run Linter
run: yarn lint
- name: Run generate
run: yarn generate
PUBLISH: false
IMAGE_VERSION_TAG: ${{ needs.get-version.outputs.IMAGE_VERSION_TAG }}
secrets: {}
permissions:
contents: read
packages: write # to be able to push images to ghcr.io, even if PUBLISH is false, as permissions is static at workflow level
+41
View File
@@ -0,0 +1,41 @@
name: Release
on:
push:
branches:
- main
tags:
- '[0-9]+.[0-9]+.[0-9]+'
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true # other running workflows get cancelled on the same branch
permissions: {} # purposefully empty by default at workflow level, explicitly overridden for specific jobs below
jobs:
get-version:
uses: ./.github/workflows/get-version.yml
with: {}
secrets: {}
permissions:
contents: read
lint:
uses: ./.github/workflows/lint.yml
with: {}
secrets: {}
permissions:
contents: read
build:
uses: ./.github/workflows/build.yml
needs:
- get-version
- lint
with:
PUBLISH: true
IMAGE_VERSION_TAG: ${{ needs.get-version.outputs.IMAGE_VERSION_TAG }}
secrets: {}
permissions:
contents: read
packages: write # to be able to push images to ghcr.io
+4
View File
@@ -33,3 +33,7 @@ venv
storybook-static
.tshy
.tshy-build
# Helm
deployment/helm
tests/deployment
+1
View File
@@ -0,0 +1 @@
dist/
+19
View File
@@ -0,0 +1,19 @@
FROM node:22-bookworm@sha256:7e791fc54bd02fc89fd4fb39eb37e5bea753c75679c8022478d81679367d995a AS build-stage
WORKDIR /app
RUN corepack enable
COPY package.json .
COPY yarn.lock .
COPY .yarnrc.yml .
RUN yarn install --immutable || (cat /tmp/xfs-*/build.log && exit 1)
COPY . .
# NODE_ENV must be set after the dependencies are installed because @nuxt/kit is a devDependency and is required to build the application
ARG NODE_ENV=production
ENV NODE_ENV=${NODE_ENV}
ENV NUXT_PUBLIC_MIXPANEL_TOKEN_ID=acd87c5a50b56df91a795e999812a3a4
ENV NUXT_PUBLIC_MIXPANEL_API_HOST=https://analytics.speckle.systems
RUN yarn generate
FROM joseluisq/static-web-server:2.40@sha256:63528bfba5d86b00572e23b4e44ed0f7a791f931df650125156d0c24f7a8f877 AS production-stage
WORKDIR /app
COPY --from=build-stage /app/dist /app/dist
CMD ["--config-file", "/app/configuration.toml"]
+23
View File
@@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/
+24
View File
@@ -0,0 +1,24 @@
apiVersion: v2
name: speckle-dui-chart
description: A Helm chart for deploying the Speckle DUI3 application
# A chart can be either an 'application' or a 'library' chart.
#
# Application charts are a collection of templates that can be packaged into versioned archives
# to be deployed.
#
# Library charts provide useful utilities or functions for the chart developer. They're included as
# a dependency of application charts to inject those utilities and functions into the rendering
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.1.0
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "0.1.0"
@@ -0,0 +1,62 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "speckle-dui.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "speckle-dui.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "speckle-dui.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "speckle-dui.labels" -}}
helm.sh/chart: {{ include "speckle-dui.chart" . }}
{{ include "speckle-dui.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "speckle-dui.selectorLabels" -}}
app.kubernetes.io/name: {{ include "speckle-dui.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "speckle-dui.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "speckle-dui.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}
@@ -0,0 +1,122 @@
kind: ConfigMap
apiVersion: v1
metadata:
name: {{ include "speckle-dui.fullname" . }}-configuration
labels:
{{- include "speckle-dui.labels" . | nindent 4 }}
data:
configuration.toml: |
[general]
#### Address & Root dir
host = "::"
port = 80
root = "/app/dist"
#### Logging
log-level = "info"
#### Cache Control headers
cache-control-headers = true
#### Auto Compression
compression = true
compression-level = "default"
#### Error pages
# Note: If a relative path is used then it will be resolved under the root directory.
page404 = "./404.html"
page50x = "./50x.html"
#### HTTP/2 + TLS
# Note: We expect TLS termination to be handled by a reverse proxy (e.g. Nginx, Traefik, Cloudflare, etc.)
http2 = false
http2-tls-cert = ""
http2-tls-key = ""
## we are terminating https upstream; redirect is at edge proxy (ingress/gateway)
https-redirect = false
https-redirect-host = "localhost"
https-redirect-from-port = 80
https-redirect-from-hosts = "localhost"
#### CORS & Security headers
## security-headers must be disabled for iframe compatibility as they include x-frame-options: deny as default
# security-headers = false
## cors-allows-origins is unset as iframe embedding does not require CORS, we are not fetching from another origin via XHR/fetch, and wildcard increases attack surface.
# cors-allow-origins = ""
#### Directory listing
directory-listing = false
#### Directory listing sorting code
directory-listing-order = 1
#### Directory listing content format
directory-listing-format = "html"
#### Directory listing download format
directory-listing-download = []
#### File descriptor binding
# fd = ""
#### Worker threads
threads-multiplier = 1
#### Grace period after a graceful shutdown
grace-period = 0
#### Page fallback for 404s
# page-fallback = ""
#### Log request Remote Address if available
log-remote-address = true
#### Log real IP from X-Forwarded-For header if available
log-forwarded-for = true
#### IPs to accept the X-Forwarded-For header from. Empty means all
trusted-proxies = {{ .Values.security.trustedProxies | toJson }}
#### Redirect to trailing slash in the requested directory uri
redirect-trailing-slash = true
#### Check for existing pre-compressed files
compression-static = true
#### Health-check endpoint (GET or HEAD `/health`)
health = true
#### Markdown content negotiation
accept-markdown = false
#### Maintenance Mode
maintenance-mode = false
# maintenance-mode-status = 503
# maintenance-mode-file = "./maintenance.html"
[advanced]
#### HTTP Headers customization
[[advanced.headers]]
source = "/*.html"
[advanced.headers.headers]
# Cache-Control = "public, max-age=36000"
Content-Security-Policy = """\
frame-ancestors {{ if .Values.security.frameAncestors }}{{ .Values.security.frameAncestors | join " " }}{{ else }}'self'{{ end }}; \
default-src 'self'; \
frame-src {{ if .Values.security.frameSource }}{{ .Values.security.frameSource | join " " }}{{ else }}'self'{{ end }}; \
script-src {{ if .Values.security.frameSource }}{{ .Values.security.frameSource | join " " }}{{ else }}'self'{{ end }} 'unsafe-inline'; \
style-src {{ if .Values.security.frameSource }}{{ .Values.security.frameSource | join " " }}{{ else }}'self'{{ end }} 'unsafe-inline'; \
img-src {{ if .Values.security.frameSource }}{{ .Values.security.frameSource | join " " }}{{ else }}'self'{{ end }} data: blob:; \
connect-src {{ if .Values.security.frameAncestors }}{{ .Values.security.frameAncestors | join " " }}{{ else }}'self'{{ end }}; \
object-src 'none'; \
base-uri 'self'; \
form-action {{ if .Values.security.frameAncestors }}{{ .Values.security.frameAncestors | join " " }}{{ else }}'self'{{ end }};\
"""
# Strict-Transport-Security = "max-age=63072000; includeSubDomains; preload"
X-Content-Type-Options = "nosniff"
Referrer-Policy = "strict-origin-when-cross-origin"
Permissions-Policy = "geolocation=(), microphone=(), camera=()"
## Purposefully do not set X-Frame-Options as this is intended to be an iframe
@@ -0,0 +1,84 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "speckle-dui.fullname" . }}
labels:
{{- include "speckle-dui.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "speckle-dui.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "speckle-dui.labels" . | nindent 8 }}
{{- with .Values.podLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "speckle-dui.serviceAccountName" . }}
automountServiceAccountToken: {{ .Values.serviceAccount.automount }}
{{- with .Values.podSecurityContext }}
securityContext:
{{- toYaml . | nindent 8 }}
{{- end }}
containers:
- name: {{ .Chart.Name }}
{{- with .Values.securityContext }}
securityContext:
{{- toYaml . | nindent 12 }}
{{- end }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: 80 # Needs to match port defined in deployment/docker/configuration.toml
protocol: TCP
{{- with .Values.livenessProbe }}
livenessProbe:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.readinessProbe }}
readinessProbe:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.resources }}
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
volumeMounts:
- name: configuration
mountPath: /app/configuration.toml
subPath: configuration.toml
readOnly: true
{{- with .Values.volumeMounts }}
{{- toYaml . | nindent 12 }}
{{- end }}
volumes:
- name: configuration
configMap:
name: {{ include "speckle-dui.fullname" . }}-configuration
{{- with .Values.volumes }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
@@ -0,0 +1,38 @@
{{- if .Values.httpRoute.enabled -}}
{{- $fullName := include "speckle-dui.fullname" . -}}
{{- $svcPort := .Values.service.port -}}
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: {{ $fullName }}
labels:
{{- include "speckle-dui.labels" . | nindent 4 }}
{{- with .Values.httpRoute.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
parentRefs:
{{- with .Values.httpRoute.parentRefs }}
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.httpRoute.hostnames }}
hostnames:
{{- toYaml . | nindent 4 }}
{{- end }}
rules:
{{- range .Values.httpRoute.rules }}
- backendRefs:
- name: {{ $fullName }}
port: {{ $svcPort }}
weight: 1
{{- with .filters }}
filters:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .matches }}
matches:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- end }}
{{- end }}
@@ -0,0 +1,43 @@
{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "speckle-dui.fullname" . }}
labels:
{{- include "speckle-dui.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- with .Values.ingress.className }}
ingressClassName: {{ . }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
{{- with .pathType }}
pathType: {{ . }}
{{- end }}
backend:
service:
name: {{ include "speckle-dui.fullname" $ }}
port:
number: {{ $.Values.service.port }}
{{- end }}
{{- end }}
{{- end }}
@@ -0,0 +1,13 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "speckle-dui.fullname" . }}
labels: {{- include "speckle-dui.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector: {{- include "speckle-dui.selectorLabels" . | nindent 4 }}
@@ -0,0 +1,13 @@
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "speckle-dui.serviceAccountName" . }}
labels:
{{- include "speckle-dui.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
automountServiceAccountToken: {{ .Values.serviceAccount.automount }}
{{- end }}
+150
View File
@@ -0,0 +1,150 @@
# Default values for speckle-dui3.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
# This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/
replicaCount: 1
# This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/
image:
repository: ghcr.io/specklesystems/speckle-dui
# This sets the pull policy for images.
pullPolicy: IfNotPresent
# Overrides the image tag whose default is the chart appVersion.
tag: ""
# This is for the secrets for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
imagePullSecrets: []
# This is to override the chart name.
nameOverride: ""
fullnameOverride: ""
# This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/
serviceAccount:
# Specifies whether a service account should be created.
create: true
# Automatically mount a ServiceAccount's API credentials?
automount: false
# Annotations to add to the service account.
annotations: {}
# The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template.
name: ""
# This is for setting Kubernetes Annotations to a Pod.
# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/
podAnnotations: {}
# This is for setting Kubernetes Labels to a Pod.
# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
podLabels: {}
podSecurityContext: {}
# fsGroup: 2000
securityContext: {}
# capabilities:
# drop:
# - ALL
# readOnlyRootFilesystem: true
# runAsNonRoot: true
# runAsUser: 1000
# This is for setting up a service more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/
service:
# This sets the service type more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types
type: ClusterIP
# This sets the ports more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#field-spec-ports
port: 80
security:
## The IP addresses of trusted proxies, such as loadbalancers or WAFs, that may be forwarding traffic to the dashboards. This is important for correctly handling the X-Forwarded-For header and ensuring accurate client IP logging and security measures. Empty means all proxies are trusted, which may not be secure in production environments. We recommend setting this to the specific IP addresses of your trusted proxies.
trustedProxies: []
## A list of urls to be added as frame-ancestors of the Content-Security-Policy header. Empty means 'self', allowing embedding only from the same origin as the dashboards. We recommend setting this to the specific hostnames of your parent applications that will be embedding the dashboards in iframes.
frameAncestors: []
## A list of urls to be added as frame-src (and script-src, style-src, img-src) of the Content-Security-Policy header. Empty means 'self', allowing embedding of dashboards resources only from the same origin. We recommend setting this to the specific hostnames of Speckle DUI3.
frameSource: []
# This block is for setting up the ingress for more information can be found here: https://kubernetes.io/docs/concepts/services-networking/ingress/
ingress:
enabled: false
className: ""
annotations: {}
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true"
hosts:
# As dashboards expect to serve all paths under the root, we recommend using a dedicated hostname for the service, e.g. dashboards.example.com, and not sharing it with other services.
- host: chart-example.local
paths:
# Please retain this path, the dashboards expect to serve all paths under the root.
- path: /
pathType: ImplementationSpecific
tls: []
# - secretName: chart-example-tls
# hosts:
# - chart-example.local
# -- Expose the service via gateway-api HTTPRoute
# Requires Gateway API resources and suitable controller installed within the cluster
# (see: https://gateway-api.sigs.k8s.io/guides/)
httpRoute:
# HTTPRoute enabled.
enabled: false
# HTTPRoute annotations.
annotations: {}
# Which Gateways this Route is attached to.
parentRefs:
- name: gateway
sectionName: http
# namespace: default
# Hostnames matching HTTP header.
hostnames:
# As dashboards expect to serve all paths under the root, we recommend using a dedicated hostname for the service, e.g. dashboards.example.com, and not sharing it with other services.
- chart-example.local
# List of rules and filters applied.
rules:
- matches:
# Please retain this path, the dashboards expect to serve all paths under the root.
- path:
type: PathPrefix
value: /
resources: {}
# We usually recommend not to specify default resources and to leave this as a conscious
# choice for the user. This also increases chances charts run on environments with little
# resources, such as Minikube. If you do want to specify resources, uncomment the following
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
# limits:
# cpu: 100m
# memory: 128Mi
# requests:
# cpu: 100m
# memory: 128Mi
# This is to setup the liveness and readiness probes more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/
livenessProbe:
httpGet:
path: /health
port: http
readinessProbe:
httpGet:
path: /health
port: http
# Additional volumes on the output Deployment definition.
volumes: []
# - name: foo
# secret:
# secretName: mysecret
# optional: false
# Additional volumeMounts on the output Deployment definition.
volumeMounts: []
# - name: foo
# mountPath: "/etc/foo"
# readOnly: true
nodeSelector: {}
tolerations: []
affinity: {}
+9 -1
View File
@@ -10,6 +10,13 @@
"build": "nuxt build",
"dev:nuxt": "nuxt dev",
"dev": "concurrently \"nuxt dev\" \"yarn gqlgen:watch\"",
"dev:kind:up": "ctlptl apply --filename ./tests/deployment/helm/cluster-config.yaml",
"dev:kind:down": "ctlptl delete -f ./tests/deployment/helm/cluster-config.yaml",
"dev:kind:helm:up": "yarn run dev:kind:up && tilt up --file ./tests/deployment/helm/Tiltfile --context kind-speckle-dui",
"dev:kind:helm:down": "tilt down --file ./tests/deployment/helm/Tiltfile --context kind-speckle-dui",
"dev:kind:helm:ci": "tilt ci --file ./tests/deployment/helm/Tiltfile --context kind-speckle-dui --timeout 10m",
"docker:build": "docker build -f ./deployment/docker/Dockerfile -t ghcr.io/specklesystems/speckle-dui:local .",
"docker:run": "docker run --rm -p 8083:80 -v ./deployment/docker/configuration.toml:/app/configuration.toml:ro ghcr.io/specklesystems/speckle-dui:local",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare",
@@ -20,7 +27,8 @@
"lint": "yarn lint:js && yarn lint:tsc && yarn lint:prettier && yarn lint:css",
"lint:ci": "yarn lint:tsc && yarn lint:css",
"gqlgen": "graphql-codegen",
"gqlgen:watch": "graphql-codegen --watch"
"gqlgen:watch": "graphql-codegen --watch",
"prettier:fix": "prettier --config .prettierrc --ignore-path .prettierignore --write ."
},
"dependencies": {
"@apollo/client": "^3.7.14",
+65
View File
@@ -0,0 +1,65 @@
print('🚀 Deploying Speckle DUI into a Kind Cluster via Tilt...')
# we limit tilt to run only on the kind cluster
allow_k8s_contexts(['kind-speckle-dui'])
if k8s_context() != 'kind-speckle-dui':
fail('Failing early as tilt should only ever connect to kind-speckle-dui.')
# Install extensions
load('ext://helm_resource', 'helm_resource', 'helm_repo')
load('ext://k8s_yaml_glob', 'k8s_yaml_glob')
docker_build('ghcr.io/specklesystems/speckle-dui',
context='../../..',
dockerfile='../../../deployment/docker/Dockerfile',
ignore = ['**/.nuxt', '**/node_modules', '**/dist', '**/build', '**/.git', '**/.claude', '**/.cursor', '**/deployment/**/*', '**/tests/**/*']
)
# Create namespaces
k8s_yaml_glob('./manifests/*.namespace.yaml')
k8s_yaml('./manifests/coredns.configmap.yaml')
k8s_resource(new_name='coredns',
objects=['coredns:configmap:kube-system'],
resource_deps=[],
labels=['coredns'])
# Update CoreDNS to allow for local resolution of services internally (i.e. speckle.internal will be routed to nginx)
local_resource('coredns-up',
cmd='./scripts/coredns-up.sh',
resource_deps=['coredns'],
deps=['./manifests/coredns.configmap.yaml', './scripts/coredns-up.sh'],
labels=['coredns'])
helm_repo('ingress-nginx-repo',
'https://kubernetes.github.io/ingress-nginx')
#nginx should be deployed as the last dependency as it opens ports to services
#it expects these services to exist, which are created by the helm charts above
helm_resource('ingress-nginx',
release_name='ingress-nginx',
namespace='ingress-nginx',
chart='ingress-nginx-repo/ingress-nginx',
flags=['--version=4.8.0',
'--values=./values/nginx.values.yaml',
'--kube-context=kind-speckle-dui'],
deps=['./values/nginx.values.yaml'],
resource_deps=['ingress-nginx-repo', 'coredns'],
labels=['speckle-dependencies'])
helm_resource('speckle-dui',
release_name='speckle-dui',
namespace='speckle-dui',
chart='./../../../deployment/helm/speckle-dui',
flags=['--values=./values/speckle-dui.values.yaml',
'--kube-context=kind-speckle-dui'],
image_deps=[
'ghcr.io/specklesystems/speckle-dui'
],
image_keys=[
('image.repository', 'image.tag')
],
deps=['./../../../deployment/helm/speckle-dui',
'./values/speckle-dui.values.yaml'],
resource_deps=['ingress-nginx', 'coredns'],
labels=['speckle-dui'])
+29
View File
@@ -0,0 +1,29 @@
apiVersion: ctlptl.dev/v1alpha1
kind: Registry
name: ctlptl-registry
port: 5000
---
apiVersion: ctlptl.dev/v1alpha1
kind: Cluster
product: kind
registry: ctlptl-registry
name: kind-speckle-dui
kindV1Alpha4Cluster:
nodes:
- role: control-plane
kubeadmConfigPatches:
- |
kind: InitConfiguration
nodeRegistration:
kubeletExtraArgs:
node-labels: "ingress-ready=true"
extraMounts: []
extraPortMappings:
- containerPort: 80
hostPort: 80 # Docker requires privileged ports binding permissions https://docs.docker.com/desktop/mac/permission-requirements/#binding-privileged-ports
protocol: TCP
listenAddress: '127.0.0.1' #DO NOT REMOVE - this is required to prevent access from the local network or the world!!!
- containerPort: 443
hostPort: 443 # Docker requires privileged ports binding permissions https://docs.docker.com/desktop/mac/permission-requirements/#binding-privileged-ports
protocol: TCP
listenAddress: '127.0.0.1' #DO NOT REMOVE - this is required to prevent access from the local network or the world!!!
@@ -0,0 +1,24 @@
{ pkgs ? import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/63dacb46bf939521bdc93981b4cbb7ecb58427a0.tar.gz") {} }:
let
corepack = pkgs.stdenv.mkDerivation {
name = "corepack";
buildInputs = [ pkgs.nodejs_22 ];
phases = [ "installPhase" ];
installPhase = ''
mkdir -p $out/bin
corepack enable --install-directory=$out/bin
'';
};
in pkgs.mkShell {
buildInputs = [
pkgs.docker
pkgs.kind
pkgs.kubectl
pkgs.nodejs_22
pkgs.ctlptl
pkgs.kubernetes-helm
pkgs.tilt
corepack
];
}
@@ -0,0 +1,28 @@
apiVersion: v1
data:
Corefile: |
.:53 {
errors
health {
lameduck 5s
}
ready
rewrite name speckle.internal ingress-nginx-controller.ingress-nginx.svc.cluster.local.
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
ttl 30
}
prometheus :9153
forward . /etc/resolv.conf {
max_concurrent 1000
}
cache 30
loop
reload
loadbalance
}
kind: ConfigMap
metadata:
name: coredns
namespace: kube-system
@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: 'ingress-nginx'
@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: 'speckle-dui'
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail
kubectl --context="kind-speckle-dui" --namespace="kube-system" rollout restart deployment/coredns
kubectl --context="kind-speckle-dui" --namespace="kube-system" rollout status deployment "coredns" --timeout=90s
@@ -0,0 +1,7 @@
controller:
# We must set the kind cluster listen address for every port to '127.0.0.1' when hostNetwork is true
hostNetwork: true
admissionWebhooks:
enabled: false
# progressDeadlineSeconds: 600 #HACK helm chart was complaining that this was less than minReadySeconds https://github.com/kubernetes/ingress-nginx/blob/c72441585e1ab1a32df86e760613d36fa804315d/charts/ingress-nginx/templates/controller-deployment.yaml#L26
tcp: {}
@@ -0,0 +1,16 @@
ingress:
enabled: true
className: "nginx"
annotations: {}
hosts:
- host: speckle.internal
paths:
# Please retain this path, the dashboards expect to serve all paths under the root.
- path: /
pathType: ImplementationSpecific
security:
trustedProxies: []
frameAncestors:
- "speckle.internal"
frameSource:
- "speckle.internal"