From 9bf49b956cb14beb7653ba162bed867fc36166a7 Mon Sep 17 00:00:00 2001 From: dvirlabs Date: Wed, 25 Mar 2026 17:06:10 +0200 Subject: [PATCH] Add ttyd app --- argocd-apps/ttyd.yaml | 25 +++++ charts/ttyd/Chart.yaml | 16 ++++ charts/ttyd/templates/_helpers.tpl | 62 ++++++++++++ charts/ttyd/templates/clusterrole.yaml | 94 +++++++++++++++++++ charts/ttyd/templates/clusterrolebinding.yaml | 18 ++++ charts/ttyd/templates/deployment.yaml | 72 ++++++++++++++ charts/ttyd/templates/ingress.yaml | 35 +++++++ charts/ttyd/templates/service.yaml | 15 +++ charts/ttyd/templates/serviceaccount.yaml | 8 ++ charts/ttyd/values.yaml | 54 +++++++++++ manifests/ttyd/values.yaml | 48 ++++++++++ 11 files changed, 447 insertions(+) create mode 100644 argocd-apps/ttyd.yaml create mode 100644 charts/ttyd/Chart.yaml create mode 100644 charts/ttyd/templates/_helpers.tpl create mode 100644 charts/ttyd/templates/clusterrole.yaml create mode 100644 charts/ttyd/templates/clusterrolebinding.yaml create mode 100644 charts/ttyd/templates/deployment.yaml create mode 100644 charts/ttyd/templates/ingress.yaml create mode 100644 charts/ttyd/templates/service.yaml create mode 100644 charts/ttyd/templates/serviceaccount.yaml create mode 100644 charts/ttyd/values.yaml create mode 100644 manifests/ttyd/values.yaml diff --git a/argocd-apps/ttyd.yaml b/argocd-apps/ttyd.yaml new file mode 100644 index 0000000..9d15836 --- /dev/null +++ b/argocd-apps/ttyd.yaml @@ -0,0 +1,25 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: ttyd + namespace: argocd + labels: + env: infra +spec: + project: infra + source: + repoURL: 'ssh://git@gitea-ssh.dev-tools.svc.cluster.local:2222/dvirlabs/infra.git' + targetRevision: HEAD + path: charts/ttyd + helm: + valueFiles: + - ../../manifests/ttyd/values.yaml + destination: + server: https://kubernetes.default.svc + namespace: infra + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true diff --git a/charts/ttyd/Chart.yaml b/charts/ttyd/Chart.yaml new file mode 100644 index 0000000..5215edd --- /dev/null +++ b/charts/ttyd/Chart.yaml @@ -0,0 +1,16 @@ +apiVersion: v2 +name: ttyd +description: Browser-based terminal via ttyd, managed by ArgoCD +type: application +version: 0.1.0 +appVersion: "latest" +keywords: + - terminal + - ttyd + - kubectl + - web-terminal +maintainers: + - name: dvirlabs +home: https://github.com/tsl0922/ttyd +sources: + - https://github.com/tsl0922/ttyd diff --git a/charts/ttyd/templates/_helpers.tpl b/charts/ttyd/templates/_helpers.tpl new file mode 100644 index 0000000..0b2493d --- /dev/null +++ b/charts/ttyd/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "ttyd.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 "ttyd.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 "ttyd.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "ttyd.labels" -}} +helm.sh/chart: {{ include "ttyd.chart" . }} +{{ include "ttyd.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "ttyd.selectorLabels" -}} +app.kubernetes.io/name: {{ include "ttyd.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "ttyd.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "ttyd.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/charts/ttyd/templates/clusterrole.yaml b/charts/ttyd/templates/clusterrole.yaml new file mode 100644 index 0000000..b85579d --- /dev/null +++ b/charts/ttyd/templates/clusterrole.yaml @@ -0,0 +1,94 @@ +{{- if .Values.serviceAccount.create -}} +# WARNING: This ClusterRole grants broad read + exec access across the cluster. +# It is intentionally permissive for lab/troubleshooting use. +# Review and restrict these permissions before using in a production environment. +# +# Future auth integration note: +# When oauth2-proxy is added in front of ttyd, consider scoping this role +# further to match the actual user's identity or group permissions. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "ttyd.fullname" . }} + labels: + {{- include "ttyd.labels" . | nindent 4 }} +rules: + # Core workload resources — read + basic management for kubectl troubleshooting + - apiGroups: [""] + resources: + - pods + - pods/log + - services + - endpoints + - configmaps + - secrets # WARNING: includes secret read access; tighten in production + - events + - namespaces + - nodes + - persistentvolumeclaims + - persistentvolumes + - replicationcontrollers + - serviceaccounts + verbs: ["get", "list", "watch"] + + # Pod exec and log streaming (needed for `kubectl exec` and `kubectl logs -f`) + - apiGroups: [""] + resources: + - pods/exec + - pods/attach + - pods/portforward + verbs: ["create"] + + # Pod and service management (basic ops for lab use) + - apiGroups: [""] + resources: + - pods + - services + - configmaps + verbs: ["delete", "patch", "update"] + + # Apps resources + - apiGroups: ["apps"] + resources: + - deployments + - replicasets + - statefulsets + - daemonsets + verbs: ["get", "list", "watch", "patch", "delete"] + + # Batch resources + - apiGroups: ["batch"] + resources: + - jobs + - cronjobs + verbs: ["get", "list", "watch", "delete"] + + # Networking resources + - apiGroups: ["networking.k8s.io"] + resources: + - ingresses + - ingressclasses + verbs: ["get", "list", "watch"] + + # RBAC resources (read-only, for inspection purposes) + - apiGroups: ["rbac.authorization.k8s.io"] + resources: + - roles + - rolebindings + - clusterroles + - clusterrolebindings + verbs: ["get", "list", "watch"] + + # Storage classes (read-only) + - apiGroups: ["storage.k8s.io"] + resources: + - storageclasses + verbs: ["get", "list", "watch"] + + # Metrics (optional, useful for `kubectl top`) + - apiGroups: ["metrics.k8s.io"] + resources: + - pods + - nodes + verbs: ["get", "list", "watch"] +{{- end }} diff --git a/charts/ttyd/templates/clusterrolebinding.yaml b/charts/ttyd/templates/clusterrolebinding.yaml new file mode 100644 index 0000000..8fad5e5 --- /dev/null +++ b/charts/ttyd/templates/clusterrolebinding.yaml @@ -0,0 +1,18 @@ +{{- if .Values.serviceAccount.create -}} +# Binds the ttyd ClusterRole to its dedicated ServiceAccount. +# WARNING: This grants cluster-wide permissions. See clusterrole.yaml for details. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "ttyd.fullname" . }} + labels: + {{- include "ttyd.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "ttyd.fullname" . }} +subjects: + - kind: ServiceAccount + name: {{ include "ttyd.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +{{- end }} diff --git a/charts/ttyd/templates/deployment.yaml b/charts/ttyd/templates/deployment.yaml new file mode 100644 index 0000000..d4773a3 --- /dev/null +++ b/charts/ttyd/templates/deployment.yaml @@ -0,0 +1,72 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "ttyd.fullname" . }} + labels: + {{- include "ttyd.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + {{- include "ttyd.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "ttyd.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "ttyd.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + # ttyd args: bind port, then pass the shell command to execute in the browser terminal. + # To use kubectl, switch image.repository to a custom image that bundles ttyd + kubectl. + args: + - "--port={{ .Values.ttyd.port }}" + - {{ .Values.ttyd.command | quote }} + ports: + - name: http + containerPort: {{ .Values.ttyd.port }} + protocol: TCP + livenessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 10 + periodSeconds: 20 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/charts/ttyd/templates/ingress.yaml b/charts/ttyd/templates/ingress.yaml new file mode 100644 index 0000000..fb6e814 --- /dev/null +++ b/charts/ttyd/templates/ingress.yaml @@ -0,0 +1,35 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "ttyd.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "ttyd.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.ingress.className }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls.enabled }} + tls: + - hosts: + - {{ .Values.ingress.host | quote }} + secretName: {{ .Values.ingress.tls.secretName }} + {{- end }} + rules: + - host: {{ .Values.ingress.host | quote }} + http: + paths: + - path: {{ .Values.ingress.path }} + pathType: {{ .Values.ingress.pathType }} + backend: + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} +{{- end }} diff --git a/charts/ttyd/templates/service.yaml b/charts/ttyd/templates/service.yaml new file mode 100644 index 0000000..1b6013f --- /dev/null +++ b/charts/ttyd/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "ttyd.fullname" . }} + labels: + {{- include "ttyd.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "ttyd.selectorLabels" . | nindent 4 }} diff --git a/charts/ttyd/templates/serviceaccount.yaml b/charts/ttyd/templates/serviceaccount.yaml new file mode 100644 index 0000000..49096c0 --- /dev/null +++ b/charts/ttyd/templates/serviceaccount.yaml @@ -0,0 +1,8 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "ttyd.serviceAccountName" . }} + labels: + {{- include "ttyd.labels" . | nindent 4 }} +{{- end }} diff --git a/charts/ttyd/values.yaml b/charts/ttyd/values.yaml new file mode 100644 index 0000000..6407630 --- /dev/null +++ b/charts/ttyd/values.yaml @@ -0,0 +1,54 @@ +replicaCount: 1 + +image: + repository: tsl0922/ttyd + tag: latest + pullPolicy: IfNotPresent + +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + create: true + name: "" + +service: + type: ClusterIP + port: 7681 + +ttyd: + port: 7681 + # Shell command passed to ttyd. Switch to a custom image with kubectl for full functionality. + command: "/bin/sh" + +ingress: + enabled: true + className: traefik + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: websecure + host: kctl.dvirlabs.com + path: / + pathType: Prefix + tls: + enabled: true + secretName: tls-ttyd-ingress + +resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 200m + memory: 128Mi + +podAnnotations: {} + +podSecurityContext: {} + +securityContext: {} + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/manifests/ttyd/values.yaml b/manifests/ttyd/values.yaml new file mode 100644 index 0000000..2bea44b --- /dev/null +++ b/manifests/ttyd/values.yaml @@ -0,0 +1,48 @@ +# ttyd environment-specific values +# Overrides charts/ttyd/values.yaml defaults + +replicaCount: 1 + +image: + # Switch to a custom image that bundles ttyd + kubectl for full kubectl support. + # Example: repository: registry.dvirlabs.com/ttyd-kubectl + repository: tsl0922/ttyd + tag: latest + pullPolicy: IfNotPresent + +serviceAccount: + create: true + name: "" + +service: + port: 7681 + +ttyd: + port: 7681 + # Shell to launch in the browser terminal. + # Change to /bin/bash if using a custom image that includes bash + kubectl. + command: "/bin/sh" + +ingress: + enabled: true + className: traefik + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: websecure + host: kctl.dvirlabs.com + path: / + pathType: Prefix + tls: + enabled: true + secretName: tls-ttyd-ingress + +resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 200m + memory: 128Mi + +nodeSelector: {} +tolerations: [] +affinity: {}