An OpenShift Operator that installs and manages tinycode AI coding assistant instances on an OpenShift cluster.
The operator watches TinycodeInstance custom resources and reconciles them by:
- Selecting the appropriate SecurityContextConstraint based on spec
- Running
helm upgrade --installwith values derived from the CR - Creating an OpenShift Route for external access
- Updating CR status with the URL and ready condition
┌─────────────────────────────────────────────────────────┐
│ OpenShift Cluster │
│ │
│ tinycode-operator-system/ │
│ ├── Deployment: tinycode-operator-manager (UID 1001) │
│ └── ServiceAccount: tinycode-operator-manager │
│ │
│ <user-namespace>/ │
│ ├── TinycodeInstance CR (user creates) │
│ ├── Deployment: <name>-tinycode ──→ Pod (UID 1001) │
│ ├── Service: <name>-tinycode │
│ ├── Route: <name>-tinycode ──→ https://<host> │
│ ├── PVC: <name>-data (1Gi) │
│ └── PVC: <name>-projects (10Gi default) │
│ │
│ Cluster-scoped: │
│ ├── CRD: tinycodeinstances.tinycode.dev │
│ ├── SCC: tinycode-restricted (default) │
│ ├── SCC: tinycode-hostpath (optional) │
│ └── SCC: tinycode-shell (optional, requires review) │
└─────────────────────────────────────────────────────────┘
- OpenShift 4.12+ (OCP, ROSA, ARO)
- cluster-admin access for installation
ocCLI andhelm3.x in PATH- Operator image built and pushed to a registry accessible to the cluster
# 1. Clone the operator repo
git clone https://github.com/bobbyjohnstx/tinycode-operator
cd tinycode-operator
# 2. Build and push the operator image
make push IMAGE_ORG=yourorg IMAGE_TAG=v0.1.0
# 3. Install on the cluster (cluster-admin required)
make install OPERATOR_IMAGE=quay.io/yourorg/operator:v0.1.0Before creating a TinycodeInstance, a cluster-admin must prepare the target namespace. The operator uses Helm to create Deployments, Services, Routes, PVCs, Roles, and RoleBindings in the target namespace — this requires admin-level access (not just edit, which cannot create RBAC resources).
# 1. Create the target namespace
oc new-project tinycode-dev
# 2. Grant the operator admin in this namespace (cluster-admin required)
oc create rolebinding tinycode-operator-admin \
--clusterrole=admin \
--serviceaccount=tinycode-operator-system:tinycode-operator-manager \
-n tinycode-dev
# 3. Create a password secret for the tinycode web UI
oc create secret generic tinycode-password \
--from-literal=TINYCODE_SERVER_PASSWORD=<your-password> \
-n tinycode-devRepeat for each namespace where tinycode instances will be deployed. The admin ClusterRole is namespace-scoped — it does not grant cluster-wide privileges.
Why admin and not edit? The Helm chart creates Roles and RoleBindings for Kubernetes service discovery (finding vLLM endpoints). The
editClusterRole cannot create RBAC resources;adminaddsget/create/update/deleteon Roles and RoleBindings.
Environments where cluster-admin is unavailable: If your organization restricts cluster-admin access, request that an administrator run steps 1-3 above for your namespace. The operator itself (installed separately by cluster-admin) handles everything else. Alternatively, install via OLM where the subscription handles RBAC automatically — see docs/olm-bundle.md.
If your vLLM models run in a different namespace than tinycode (common on RHOAI), the operator needs to create ClusterRoles for cross-namespace service listing. The hack/install.sh script handles this automatically via config/rbac/discovery_role.yaml.
To enable discovery for a model service:
# 1. Annotate the model's predictor service (tells tinycode to probe it)
oc annotate svc <predictor-service-name> \
tinycode.dev/discover=vllm \
-n <model-namespace>
# 2. Add discovery.namespaces to your TinycodeInstance CR
# spec:
# discovery:
# namespaces:
# - <model-namespace>Without the annotation, tinycode ignores the service even if the namespace is listed — this prevents probing every service in the cluster.
# Apply the CR (namespace must be prepared first — see above)
oc apply -f config/samples/tinycode_v1alpha1_basic.yaml
# Get the URL
oc get tinycodeinstance my-tinycode -n tinycode-dev -o jsonpath='{.status.url}'Deploy Ollama separately (or use an existing instance), then set spec.ollama.host:
spec:
ollama:
host: "http://ollama.ollama-system.svc.cluster.local:11434"spec:
storage:
hostPath:
path: /home/developer/projects
readOnly: falseSecurity: Requires cluster-admin to pre-approve the
tinycode-hostpathSCC binding. The operator will bind it automatically if you have permission.
spec:
shell:
enabled: trueSecurity: Requires cluster-admin review. Grants
hostPIDwhich allows the tinycode shell tool to run commands on the host viansenter. Only use in dedicated namespaces with trusted users.
Three SCCs are installed, applied automatically based on spec:
| SCC | When Used | hostPath | hostPID | Caps |
|---|---|---|---|---|
tinycode-restricted |
Default | no | no | ALL dropped |
tinycode-hostpath |
spec.storage.hostPath set |
yes | no | ALL dropped |
tinycode-shell |
spec.shell.enabled=true |
no | yes | SYS_PTRACE only |
All SCCs run as UID 1001 (non-root), GID 0, with allowPrivilegedContainer: false.
| Field | Type | Default | Description |
|---|---|---|---|
spec.image |
string | quay.io/bjohns/tinycode-container:latest |
Container image |
spec.replicas |
integer | 1 |
Number of pods (1–10) |
spec.resources.limits.cpu |
string | 2 |
CPU limit |
spec.resources.limits.memory |
string | 2Gi |
Memory limit |
spec.resources.requests.cpu |
string | 200m |
CPU request |
spec.resources.requests.memory |
string | 512Mi |
Memory request |
spec.storage.dataSize |
string | 1Gi |
PVC size for SQLite DB and config |
spec.storage.projectsSize |
string | 10Gi |
PVC size for project workspace |
spec.storage.projectsAccessMode |
string | ReadWriteOnce |
Access mode for projects PVC (ReadWriteOnce or ReadWriteMany for multi-replica shared workspaces) |
spec.storage.storageClassName |
string | cluster default | StorageClass for PVCs |
spec.storage.hostPath.path |
string | — | Absolute path on host node to mount at /projects (mutually exclusive with spec.git.url) |
spec.storage.hostPath.readOnly |
bool | false |
Mount host path as read-only |
spec.hostname |
string | auto | Custom hostname for the tinycode Route |
spec.tlsTermination |
string | edge |
Route TLS mode: edge, passthrough, or reencrypt |
spec.ollama.enabled |
bool | false |
Deploy an Ollama sidecar |
spec.ollama.host |
string | — | External Ollama host URL (when enabled is false) |
spec.ollama.models |
array | — | Ollama model names to pre-pull on startup |
spec.auth.passwordSecret |
string | — | Secret name containing TINYCODE_SERVER_PASSWORD |
spec.shell.enabled |
bool | false |
Enable host shell access (grants hostPID for nsenter-based commands) |
spec.shell.allowedCommands |
array | — | Restrict shell commands (future admission webhook enforcement) |
spec.nodeSelector |
object | — | Node selection constraints |
spec.tolerations |
array | — | Pod tolerations |
spec.model |
string | — | Default model ID (e.g., qwen/Qwen2.5-Coder-32B-Instruct-AWQ). Written to generated config. |
spec.clusterAdmin.enabled |
bool | false |
Enable cluster-admin mode (mounts kubeconfig, downloads oc CLI) |
spec.clusterAdmin.kubeconfigSecretName |
string | — | Secret name containing kubeconfig (required when enabled=true) |
spec.clusterAdmin.kubeconfigSecretKey |
string | kubeconfig |
Key within the Secret containing the kubeconfig file |
spec.clusterAdmin.ocVersion |
string | stable |
oc CLI version (e.g., 4.17 for reproducibility) |
spec.clusterAdmin.kubeconfigNamespace |
string | — | Namespace where kubeconfig Secret resides (for cross-namespace mounting) |
spec.clusterAdmin.clusterRole |
string | — | Auto-provision ServiceAccount with this ClusterRole (cannot be admin or cluster-admin) |
spec.vllm |
array | — | Array of vLLM endpoints to configure as tinycode providers |
spec.vllm[].name |
string | — | Provider name (must be unique, lowercase alphanumeric + dashes) |
spec.vllm[].url |
string | — | Base URL of vLLM instance (e.g., http://vllm-qwen.vllm:8000) |
spec.vllm[].models |
object | — | Per-model overrides with contextLimit and outputLimit (auto-probed if omitted) |
spec.discovery.namespaces |
array | — | Namespaces to search for vLLM services (enables cross-namespace discovery) |
spec.git.url |
string | — | Git repository URL to clone into /projects (validated against URL scheme; mutually exclusive with spec.storage.hostPath.path) |
spec.git.branch |
string | — | Branch to clone (validated to prevent injection); defaults to repository's default branch |
spec.git.credentialsSecret |
string | — | Secret name with git credentials (keys: username/password for HTTPS, ssh-privatekey for SSH) |
spec.git.pullOnRestart |
bool | false |
Pull latest changes from repository on pod restart |
spec.git.depth |
integer | 1 |
Clone depth (shallow clone by default) |
The TinycodeInstance CRD enforces validation patterns for security-sensitive fields:
- Image Registry Restriction (
spec.image): Must be from explicitly allowed registries (defaults to quay.io, ghcr.io). Prevents arbitrary image injection. - Git URL Validation (
spec.git.url): Validated to ensure proper URL format. Allowed schemes:http://,https://,ssh://,git://. HTTPS recommended for credential safety. Prevents command injection via shell metacharacter blocking. - Git Branch Validation (
spec.git.branch): Alphanumeric,/,-,., and_only; prevents shell injection during clone operations. - ClusterRole Allowlist (
spec.clusterAdmin.clusterRole): Cannot beadmin,cluster-admin, or any ClusterRole that would escalate privileges. Prevents privilege escalation in cluster-admin mode. - SSRF Prevention (
spec.vllm[].url): URL validation prevents internal service discovery attacks. Private IP ranges and localhost are allowed by default but can be restricted via NetworkPolicy.
- NetworkPolicy: Recommended to restrict ingress/egress traffic to required services
- Read-Only Root Filesystem: Can be enabled in PodSecurityPolicy via
readOnlyRootFilesystem: truefor additional hardening - Security Context: All pods run as UID 1001 (non-root) with dropped Linux capabilities (ALL dropped by default)
- Audit Logging: Operator actions are logged to cluster audit logs for compliance tracking
oc get tinycodeinstances -n tinycode-dev
NAME READY URL AGE
my-tinycode True https://my-tinycode-tinycode-dev... 5mStatus phases: Pending → Deploying → Running (or Failed / Terminating).
make uninstallThe tinycode-operator is OpenShift-specific: it creates OpenShift Route objects (not standard Ingress) and manages SecurityContextConstraints (SCCs), which are OpenShift-only resources. However, the underlying container image (tinycode-container) is portable and runs on any Kubernetes cluster.
The tinycode-container repo includes Kustomize manifests for vanilla Kubernetes:
k8s/base/— Deployment, Service, PVC (works on any cluster)k8s/overlays/ingress/— Adds a standardIngressinstead of OpenShiftRoute
Deploy with:
# Clone the container repo
git clone https://github.com/bobbyjohnstx/tinycode-container
cd tinycode-container
# Deploy base resources + standard Ingress
kubectl apply -k k8s/overlays/ingressThis approach is lightweight and requires no CRD or operator installation. You manage Kustomize overlays directly for customization.
For a fully Kubernetes-native CI/CD pipeline that replaces both the operator and GitHub Actions:
- Tekton — Replaces GitHub Actions. Builds the container image from the ContainerFile and pushes to your registry, triggered by git pushes.
- Argo CD — Replaces the operator's deployment and reconciliation. Watches the Kustomize manifests (or a Helm chart) in the tinycode-container repo and auto-syncs changes to the cluster.
Together, Tekton + Argo CD cover everything the operator + GitHub Actions do today. The tradeoff: you lose the TinycodeInstance CRD abstraction and instead manage Kustomize overlays or Helm values directly — arguably simpler for single-instance deployments.
Making the operator itself Kubernetes-portable (auto-detecting OpenShift vs vanilla Kubernetes and falling back to Ingress when SCCs are unavailable) is a potential enhancement for future releases.
Primary deployment model: One TinycodeInstance CR per user, managed centrally by a cluster-admin in the "Creating a TinycodeInstance" section above.
Alternative for self-service teams: Users can provision their own TinycodeInstance CRs if your team prefers a self-service model. No code changes to the operator are required — it already watches all namespaces and scopes all resources (Deployments, Services, Routes, PVCs, ServiceAccounts, SCC bindings) by CR name + namespace. Two users in different namespaces get completely isolated resources with no collisions.
- Create a ClusterRole granting users permission to manage their own TinycodeInstance CRs:
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: tinycode-user-role
rules:
- apiGroups:
- tinycode.dev
resources:
- tinycodeinstances
verbs:
- create
- get
- list
- watch
- deleteSave this as tinycode-user-role.yaml and apply once:
oc apply -f tinycode-user-role.yamlFor each user who will self-provision, create a namespace and bind the ClusterRole:
---
apiVersion: v1
kind: Namespace
metadata:
name: tinycode-alice
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: alice-tinycode-user
namespace: tinycode-alice
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: tinycode-user-role
subjects:
- kind: User
name: alice@example.com
apiGroup: rbac.authorization.k8s.ioSave as alice-tinycode-rolebinding.yaml and apply:
oc apply -f alice-tinycode-rolebinding.yamlThe user also needs to create Secrets (password) in their namespace. Grant that permission by adding a namespace Role:
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: tinycode-secret-creator
namespace: tinycode-alice
rules:
- apiGroups:
- ""
resources:
- secrets
verbs:
- create
- get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: alice-secret-creator
namespace: tinycode-alice
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: tinycode-secret-creator
subjects:
- kind: User
name: alice@example.com
apiGroup: rbac.authorization.k8s.ioOnce the cluster-admin has set up the namespace and RoleBinding, the user can:
# 1. Create a password secret in their namespace
oc create secret generic tinycode-password \
--from-literal=TINYCODE_SERVER_PASSWORD=mypassword \
-n tinycode-alice
# 2. Create their TinycodeInstance CR
oc apply -f - <<EOF
---
apiVersion: tinycode.dev/v1alpha1
kind: TinycodeInstance
metadata:
name: alice-dev
namespace: tinycode-alice
spec:
image: quay.io/bjohns/tinycode-container:latest
replicas: 1
resources:
limits:
cpu: "2"
memory: "2Gi"
requests:
cpu: "200m"
memory: "512Mi"
storage:
dataSize: "1Gi"
projectsSize: "10Gi"
auth:
passwordSecret: tinycode-password
tlsTermination: edge
EOF
# 3. Wait for the instance to be ready
oc get tinycodeinstance alice-dev -n tinycode-alice -w
# 4. Get the URL
oc get tinycodeinstance alice-dev -n tinycode-alice -o jsonpath='{.status.url}'Each user gets their own pod, PVCs (data and projects), and OpenShift Route. This is instance-per-user isolation, not shared multi-tenancy. Resource consumption scales linearly with users:
- Default limits: 2 CPU, 2Gi memory per instance
- Default requests: 200m CPU, 512Mi memory per instance
- Storage: 1Gi (config/DB) + 10Gi (projects) per instance
For 10 users with default settings, the cluster needs capacity for at least 2 CPU cores and 5Gi memory across all tinycode instances, plus infrastructure and other workloads.
A user can delete their instance:
oc delete tinycodeinstance alice-dev -n tinycode-aliceCluster-admin can revoke access by removing the RoleBinding:
oc delete rolebinding alice-tinycode-user -n tinycode-alicetinycode-operator/
├── Dockerfile # Operator container image
├── Makefile # Build/install targets
├── README.md
├── CONTAINER.md # Container interface contract
├── operator/
│ ├── main.py # Operator controller (kopf)
│ └── requirements.txt
├── helm-charts/
│ └── tinycode/ # Helm chart deployed per CR
│ ├── Chart.yaml
│ ├── values.yaml
│ └── templates/
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── route.yaml
│ ├── serviceaccount.yaml
│ └── pvc.yaml
├── config/
│ ├── crd/ # CRD definition
│ ├── rbac/ # Operator RBAC
│ ├── scc/ # SecurityContextConstraints
│ ├── manager/ # Operator Deployment
│ └── samples/ # Example CRs
├── bundle/
│ └── manifests/ # OLM ClusterServiceVersion
└── hack/
├── install.sh # Cluster install script
├── uninstall.sh # Cluster uninstall script
└── build-push.sh # Image build/push script
| Project | Description | Repository |
|---|---|---|
| tinycode | Core AI coding assistant — server, TUI, web UI | github.com/bobbyjohnstx/tinycode |
| tinycode-container | Container image packaging tinycode + oh-my-tiny | github.com/bobbyjohnstx/tinycode-container |
MIT — see LICENSE.