diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..e749aca --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,31 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Environment:** +- Operator version: +- OpenShift/Kubernetes version: +- Helm chart version: + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..5f0a04c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,19 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: '' +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..ef33f85 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,17 @@ +## Summary +Brief description of the changes. + +## Type +- [ ] Bug fix +- [ ] New feature +- [ ] Documentation + +## Checklist +- [ ] `python -m py_compile operator/main.py` passes +- [ ] `helm lint helm-charts/tinycode/` passes +- [ ] CRD changes validated +- [ ] Sample CRs updated (if spec changed) +- [ ] README spec table updated (if fields added) + +## Test Plan +How were these changes tested? diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..986d3b1 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,3 @@ +# Dependabot is disabled — we manage dependency updates manually. +version: 2 +updates: [] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3045046 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,68 @@ +name: CI + +on: + pull_request: + branches: [main] + push: + branches: [main] + +permissions: + contents: read + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 + with: + python-version: "3.11" + + - name: Install dependencies + run: pip install kopf kubernetes pyyaml + + - name: Python syntax check + run: python -m py_compile operator/main.py + + - name: Helm lint + run: | + curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash + helm lint helm-charts/tinycode/ + + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + + - name: Validate CRD YAML + run: python -c "import yaml; yaml.safe_load(open('config/crd/tinycode.dev_tinycodeinstances.yaml'))" + + - name: Validate sample CRs + run: | + for f in config/samples/*.yaml; do + echo "Validating $f..." + python -c "import yaml; list(yaml.safe_load_all(open('$f')))" + done + + - name: Validate bundle + run: | + for f in bundle/manifests/*.yaml; do + echo "Validating $f..." + python -c "import yaml; list(yaml.safe_load_all(open('$f')))" + done + + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6 + with: + python-version: "3.11" + + - name: Install dependencies + run: pip install kopf kubernetes pyyaml pytest + + - name: Run tests + run: pytest operator/ -v diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..052e9f0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +*.egg-info/ +dist/ +build/ +.eggs/ +venv/ +.venv/ +*.egg + +# Helm +charts/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Kubernetes +kubeconfig +*.kubeconfig + +# Build +*.tar.gz diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..4a879ab --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,44 @@ +# Changelog + +## [Unreleased] + +### Fixed +- hostPath SCC selection bug (checked `enabled` instead of `path`) +- readOnly hostPath applied in Helm deployment template +- SCC runAsUser enforced as MustRunAs UID 1001 (restricted + hostpath SCCs) +- observedGeneration set in CR status updates +- Cluster-wide secret RBAC reduced to get-only (removed list/watch) +- Helm binary download checksum verification +- Spec hash skip for no-op Helm upgrades +- DynamicClient reuse (cached instead of per-call) +- CSV liveness flag matches Dockerfile ENTRYPOINT + +### Added +- 44 unit tests (pytest) covering pure functions and validation logic +- Dependabot configuration for GitHub Actions and pip + +## [0.1.0] — 2026-06-26 + +Initial public release. + +### Added +- TinycodeInstance CRD for declarative tinycode management +- kopf-based Python operator with create/update/delete handlers +- Declarative vLLM configuration with auto-probing of /v1/models +- Cross-namespace vLLM service discovery via spec.discovery.namespaces +- GitOps mode — init container clones git repo via spec.git +- Shared team workspace with ReadWriteMany PVC support +- Cluster-admin mode with kubeconfig mounting +- OpenShift SCC binding (restricted, hostpath, shell tiers) +- Helm chart-based deployment with ConfigMap config delivery +- OLM bundle for OperatorHub installation +- File-Based Catalog for private catalog deployment + +### Security +- CRD validation patterns (image registry allowlist, git URL/branch, clusterRole allowlist) +- SSRF prevention in vLLM URL probing +- Helm template value quoting +- SCC RBAC with scoped patch/update permissions +- Kubeconfig exception sanitization +- NetworkPolicy Helm template +- Secret read audit logging diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..fa61b7f --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,83 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported via [GitHub Private Vulnerability Reporting](https://github.com/bobbyjohnstx/tinycode-operator/security/advisories/new) or by emailing the maintainer directly. All reports will be handled confidentially and will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of actions. + +**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at [https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/CONTAINER.md b/CONTAINER.md new file mode 100644 index 0000000..56fa640 --- /dev/null +++ b/CONTAINER.md @@ -0,0 +1,141 @@ +# Container Contract + +This file is the authoritative source of truth for the tinycode container interface. +Both `tinycode-container` (which produces the image) and `tinycode-operator` (which deploys it) +must stay in sync with these values. + +## Image + +| Field | Value | +|-------|-------| +| Registry | `quay.io/bjohns/tinycode-container` | +| Tags | `:latest`, `:` | +| Architectures | `linux/amd64`, `linux/arm64` | +| Base | Red Hat UBI9-minimal | + +## Runtime Identity + +| Field | Value | +|-------|-------| +| User | `tinycode` | +| UID | `1001` | +| GID | `0` (OpenShift arbitrary-UID pattern: `g=u`) | + +## Port + +| Field | Value | +|-------|-------| +| Container port | `4096` (tinycode default — `server.ts:123`) | +| Override | `TINYCODE_PORT` env var or `server.port` in config | + +## Health Endpoints + +| Endpoint | Purpose | +|----------|---------| +| `GET /global/health` | Liveness + readiness (unauthenticated) | + +## Volume Mounts + +| Mount path | Purpose | PVC subPath | +|-----------|---------|-------------| +| `/home/tinycode/.local/share/tinycode` | SQLite DB, session history | `data` | +| `/home/tinycode/.config/tinycode` | Config files (config.json, tinycode.jsonc) | `config` | +| `/projects` | User workspace files | *(separate PVC)* | + +## Environment Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| **Core Configuration** | | | +| `TINYCODE_SERVER_PASSWORD` | *(none — unauthenticated)* | Server auth password | +| `TINYCODE_PORT` | `4096` | Override server port | +| `TINYCODE_SESSION_ID` | *(none)* | Attach to existing session on start | +| `TINYCODE_WORKDIR` | `/projects` | Working directory for projects (fallback: `/home/tinycode`) | +| **LLM Providers** | | | +| `TINYCODE_OLLAMA_HOST` | `http://host.containers.internal:11434` | Ollama endpoint | +| `TINYCODE_VLLM_URL` | *(none)* | vLLM endpoint (bridged to `TINYCODE_VLLM_HOST`) | +| `TINYCODE_VLLM_HOST` | *(none)* | vLLM endpoint (native tinycode env var) | +| `TINYCODE_VLLM_MODEL` | *(none)* | Default model for vLLM (written to config.json) | +| `OPENROUTER_API_KEY` | *(none)* | OpenRouter API key for cost tracking and balance display | +| **GitOps Configuration** | | | +| `TINYCODE_GIT_REPO` | *(none)* | Git repo URL to clone into `/projects` on startup | +| `TINYCODE_GIT_BRANCH` | *(default branch)* | Branch to clone | +| `TINYCODE_GIT_PULL_ON_RESTART` | `false` | Pull latest on restart if repo exists | +| `TINYCODE_GIT_CLONE_TIMEOUT` | `300` | Clone timeout in seconds | +| **Cluster Management** | | | +| `TINYCODE_CLUSTER_ADMIN` | `false` | Enable cluster-admin agent (downloads oc CLI and mounts kubeconfig) | +| `TINYCODE_OC_VERSION` | `stable` | oc CLI version to download (e.g., `4.17` for reproducibility) | +| **Auto-Detection** | | | +| `TINYCODE_AUTO_DETECT` | `true` | Auto-detect Kubernetes environment (disables LSP downloads) | +| `TINYCODE_DISABLE_LSP_DOWNLOAD` | `1` (recommended in containers) | Skip LSP binary auto-download | +| **Operator-Injected** | | | +| `TINYCODE_CONFIG_CONTENT` | *(none)* | Operator-injected config content (not directly set by users) | +| `TINYCODE_DISCOVERY_NAMESPACES` | *(none)* | Comma-separated list of namespaces for service discovery (operator-managed) | +| **Output (Set by Entrypoint)** | | | +| `TINYCODE_CLUSTER_TYPE` | *(auto-detected)* | Set to `openshift` or `kubernetes` when `TINYCODE_CLUSTER_ADMIN=true` | + +## Included Tools + +**tmux** (v3.4) is compiled from source and included in the runtime image to support the `/swarm` skill — a supervised multi-worker orchestration tool that creates split-screen sessions for distributed task solving. + +## Startup Behaviour + +The container ENTRYPOINT is `entrypoint.sh` which: +1. Sets `HOME=/home/tinycode` and `SHELL=/bin/sh` (OpenShift compatibility) +2. Validates `TINYCODE_VLLM_MODEL` format (alphanumeric, `/`, `-`, `.` only; max 255 chars; no shell injection) +3. Bridges `TINYCODE_VLLM_URL` → `TINYCODE_VLLM_HOST` +4. Auto-detects Kubernetes environment (sets `TINYCODE_DISABLE_LSP_DOWNLOAD=1` in-cluster) +5. Writes container defaults to `$XDG_CONFIG_HOME/tinycode/config.json` (lowest-priority config) + - Includes `"model"` field if `TINYCODE_VLLM_MODEL` is set +6. GitOps mode: clones `TINYCODE_GIT_REPO` into `/projects` (or pulls if already exists) +7. Initializes git repo in `/projects` if not present +8. Copies bundled agents/skills from `/opt/tinycode-defaults/` into PVC +9. Downloads `oc` CLI if `TINYCODE_CLUSTER_ADMIN=true` +10. Runs `tinycode web --hostname 0.0.0.0` (or attaches to `TINYCODE_SESSION_ID`) + +User config in `tinycode.jsonc` (PVC-persisted) is never overwritten by the entrypoint. + +### GitOps Mode Details + +When `TINYCODE_GIT_REPO` is set: +- **First run**: Clones the repo into `/projects` +- **Subsequent runs**: + - If `/projects/.git` exists with a remote URL: skips clone (preserves local changes) + - If `TINYCODE_GIT_PULL_ON_RESTART=true`: runs `git pull --ff-only` + - If `/projects/.git` exists but has no remote: deletes `.git` and clones fresh +- **Credentials**: Mount `.git-credentials` or `.netrc` at `/home/tinycode/` for private repos +- **Timeout**: Clone operation times out after `TINYCODE_GIT_CLONE_TIMEOUT` seconds (default 300) + +### In-Cluster Auto-Detection + +When `KUBERNETES_SERVICE_HOST` is detected (and `TINYCODE_AUTO_DETECT != "false"`): +- Sets `TINYCODE_DISABLE_LSP_DOWNLOAD=1` (air-gapped default) +- Logs vLLM endpoint if configured, otherwise logs "auto-discovery via Kubernetes services" +- Detection can be disabled with `TINYCODE_AUTO_DETECT=false` + +## XDG Base Directories + +| Variable | Value | +|----------|-------| +| `XDG_DATA_HOME` | `/home/tinycode/.local/share` | +| `XDG_CONFIG_HOME` | `/home/tinycode/.config` | +| `XDG_STATE_HOME` | `/home/tinycode/.local/state` | +| `XDG_CACHE_HOME` | `/home/tinycode/.cache` | + +## Config Load Order + +tinycode merges config from lowest to highest priority: + +``` +config.json (written by entrypoint — container defaults) +tinycode.json +tinycode.jsonc (PVC-persisted — user customisations survive image upgrades) +``` + +## Repositories + +| Project | Purpose | Gitea | GitHub | +|---------|---------|-------|--------| +| `tinycode` | Core server, TUI, CLI | `localhost:3000/bjohns/tinycode` | `github.com/bobbyjohnstx/tinycode` | +| `tinycode-container` | Container image | `localhost:3000/bjohns/tinycode-container` | `github.com/bobbyjohnstx/tinycode-container` | +| `tinycode-operator` | OpenShift Operator | `localhost:3000/bjohns/tinycode-operator` | `github.com/bobbyjohnstx/tinycode-operator` | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..64224f3 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,74 @@ +# Contributing to tinycode-operator + +## Development Setup + +1. Install Python 3.11+ +2. Install [Helm](https://helm.sh) +3. Clone the repository +4. Install dependencies (from operator directory): + ```bash + cd operator + pip install -r requirements.txt + ``` + +## Commands + +```bash +# Install CRDs +make install + +# Build operator image +make docker-build + +# Push operator image +make push + +# Deploy operator +helm install tinycode-operator helm-charts/tinycode-operator + +# Run operator locally (for development) +cd operator && kopf run --standalone main.py +``` + +## Pull Request Process + +1. Fork the repository +2. Create a feature branch (`git checkout -b feature/my-feature`) +3. Make your changes to: + - `operator/` — Python operator code using kopf + - `bundle/` — CRD definitions + - `helm-charts/` — Helm chart templates + - `config/` — sample configurations +4. Test operator behavior with a local cluster +5. Use conventional commit messages: `feat:`, `fix:`, `refactor:`, `docs:`, `chore:` +6. Push and open a PR against `main` + +## Operator Architecture + +The operator uses [kopf](https://kopf.readthedocs.io) to watch `TinycodeInstance` custom resources and reconcile them into Kubernetes `Deployment`, `Service`, and `ConfigMap` objects. + +## Testing Changes + +1. Build operator: `make docker-build` +2. Deploy to test cluster: `helm upgrade --install tinycode-operator helm-charts/tinycode-operator --set image.tag=` +3. Create a test CR: + ```bash + kubectl apply -f config/samples/tinycode_v1alpha1_basic.yaml + ``` +4. Verify reconciliation: + ```bash + kubectl get deployments,services,configmaps -l app.kubernetes.io/managed-by=tinycode-operator + kubectl logs -l app.kubernetes.io/name=tinycode-operator + ``` + +## CRD Changes + +When modifying CRDs in `bundle/`: +1. Update the CRD YAML +2. Run `make install` to apply to your cluster +3. Regenerate the bundle: `make bundle` +4. Test with sample resources in `config/samples/` + +## Questions? + +Open a [GitHub Issue](https://github.com/bobbyjohnstx/tinycode-operator/issues) for bugs or feature requests. diff --git a/Dockerfile b/Dockerfile index 84ab03f..f7a110a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,6 +2,7 @@ # Stage 1: build — install Python deps FROM registry.access.redhat.com/ubi9/python-311:latest AS builder +USER root WORKDIR /build COPY operator/requirements.txt . RUN pip install --no-cache-dir --target /install -r requirements.txt @@ -11,21 +12,19 @@ FROM registry.access.redhat.com/ubi9/ubi-minimal:latest # Install helm ARG HELM_VERSION=v3.17.3 -RUN microdnf install -y curl tar gzip && \ - curl -fsSL "https://get.helm.sh/helm-${HELM_VERSION}-linux-amd64.tar.gz" \ - -o /tmp/helm.tar.gz && \ - tar -xzf /tmp/helm.tar.gz -C /tmp && \ - install -m 755 /tmp/linux-amd64/helm /usr/local/bin/helm && \ - rm -rf /tmp/helm* && \ - microdnf remove -y curl tar gzip && \ +RUN microdnf install -y tar gzip shadow-utils python3.11 && \ + ARCH=$(uname -m) && \ + case "$ARCH" in x86_64) HELM_ARCH=amd64 ;; aarch64) HELM_ARCH=arm64 ;; *) HELM_ARCH=amd64 ;; esac && \ + HELM_FILE="helm-${HELM_VERSION}-linux-${HELM_ARCH}.tar.gz" && \ + curl -fsSL "https://get.helm.sh/${HELM_FILE}" -o /tmp/${HELM_FILE} && \ + curl -fsSL "https://get.helm.sh/${HELM_FILE}.sha256sum" -o /tmp/${HELM_FILE}.sha256sum && \ + cd /tmp && sha256sum -c ${HELM_FILE}.sha256sum && \ + tar -xzf /tmp/${HELM_FILE} -C /tmp && \ + install -m 755 /tmp/linux-${HELM_ARCH}/helm /usr/local/bin/helm && \ + rm -rf /tmp/helm* /tmp/linux-${HELM_ARCH} && \ + microdnf remove -y tar gzip shadow-utils && \ microdnf clean all -# Install Python 3.11 -RUN microdnf install -y python3.11 && microdnf clean all - -# Non-root user — matches the restricted SCC UID -RUN useradd -u 1000 -r -g 0 -s /sbin/nologin operator - WORKDIR /app # Copy Python dependencies from builder @@ -37,10 +36,14 @@ COPY operator/ /app/ # Copy Helm chart COPY helm-charts/ /helm-charts/ +RUN chown -R 1001:0 /app /helm-charts && chmod -R g=u /app /helm-charts + ENV PYTHONPATH=/app/deps ENV HELM_CHART_PATH=/helm-charts/tinycode ENV HOME=/tmp +ENV HELM_DRIVER=configmap -USER 1000 +# UID 1001 already exists in ubi-minimal (operator user) +USER 1001:0 -ENTRYPOINT ["python3.11", "/app/main.py"] +ENTRYPOINT ["python3.11", "-m", "kopf", "run", "--all-namespaces", "--liveness=http://0.0.0.0:8081/healthz", "/app/main.py"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..02d4b8b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 tinycode + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile index 93aae43..a50c8d8 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,12 @@ IMAGE_REGISTRY ?= quay.io IMAGE_ORG ?= tinycode IMAGE_TAG ?= latest +VERSION ?= 0.2.1 OPERATOR_IMAGE ?= $(IMAGE_REGISTRY)/$(IMAGE_ORG)/operator:$(IMAGE_TAG) +BUNDLE_IMAGE ?= $(IMAGE_REGISTRY)/$(IMAGE_ORG)/operator-bundle:v$(VERSION) +CATALOG_IMAGE ?= $(IMAGE_REGISTRY)/$(IMAGE_ORG)/operator-catalog:v$(VERSION) -.PHONY: help build push install uninstall deploy-sample lint +.PHONY: help build push install uninstall deploy-sample lint bundle-validate bundle-build bundle-push catalog-build catalog-push test-bundle help: ## Show this help @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \ @@ -26,3 +29,21 @@ deploy-sample: ## Apply the basic sample TinycodeInstance lint: ## Lint Helm chart helm lint helm-charts/tinycode + +bundle-validate: ## Validate the OLM bundle + operator-sdk bundle validate ./bundle --select-optional suite=operatorframework + +bundle-build: ## Build the OLM bundle image + podman build -f bundle.Dockerfile -t $(BUNDLE_IMAGE) . + +bundle-push: bundle-build ## Build and push the OLM bundle image + podman push $(BUNDLE_IMAGE) + +catalog-build: ## Build the catalog image (File-Based Catalog) + opm index add --bundles $(BUNDLE_IMAGE) --tag $(CATALOG_IMAGE) --container-tool podman + +catalog-push: catalog-build ## Build and push the catalog image + podman push $(CATALOG_IMAGE) + +test-bundle: ## Test the bundle locally using operator-sdk + operator-sdk run bundle $(BUNDLE_IMAGE) diff --git a/README.md b/README.md index be533c6..02c2cb3 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # tinycode-operator +[![CI](https://github.com/bobbyjohnstx/tinycode-operator/actions/workflows/ci.yml/badge.svg)](https://github.com/bobbyjohnstx/tinycode-operator/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) [![Release](https://img.shields.io/github/v/release/bobbyjohnstx/tinycode-operator)](https://github.com/bobbyjohnstx/tinycode-operator/releases) + An OpenShift Operator that installs and manages **tinycode** AI coding assistant instances on an OpenShift cluster. ## Overview @@ -17,12 +19,12 @@ The operator watches `TinycodeInstance` custom resources and reconciles them by: │ OpenShift Cluster │ │ │ │ tinycode-operator-system/ │ -│ ├── Deployment: tinycode-operator-manager (UID 1000) │ +│ ├── Deployment: tinycode-operator-manager (UID 1001) │ │ └── ServiceAccount: tinycode-operator-manager │ │ │ │ / │ │ ├── TinycodeInstance CR (user creates) │ -│ ├── Deployment: -tinycode ──→ Pod (UID 1000) │ +│ ├── Deployment: -tinycode ──→ Pod (UID 1001) │ │ ├── Service: -tinycode │ │ ├── Route: -tinycode ──→ https:// │ │ ├── PVC: -data (1Gi) │ @@ -57,20 +59,59 @@ make push IMAGE_ORG=yourorg IMAGE_TAG=v0.1.0 make install OPERATOR_IMAGE=quay.io/yourorg/operator:v0.1.0 ``` -## Creating a TinycodeInstance +## Namespace Preparation (Required) -### Basic (PVC storage) +Before 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). ```bash -# Create namespace +# 1. Create the target namespace oc new-project tinycode-dev -# Create password secret +# 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=mysecretpass \ + --from-literal=TINYCODE_SERVER_PASSWORD= \ -n tinycode-dev +``` + +Repeat 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 `edit` ClusterRole cannot create RBAC resources; `admin` adds `get/create/update/delete` on 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](docs/olm-bundle.md). + +### Cross-Namespace Discovery Setup (Optional) + +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: -# Apply the CR +```bash +# 1. Annotate the model's predictor service (tells tinycode to probe it) +oc annotate svc \ + tinycode.dev/discover=vllm \ + -n + +# 2. Add discovery.namespaces to your TinycodeInstance CR +# spec: +# discovery: +# namespaces: +# - +``` + +Without the annotation, tinycode ignores the service even if the namespace is listed — this prevents probing every service in the cluster. + +## Creating a TinycodeInstance + +### Basic (PVC storage) + +```bash +# Apply the CR (namespace must be prepared first — see above) oc apply -f config/samples/tinycode_v1alpha1_basic.yaml # Get the URL @@ -118,32 +159,72 @@ Three SCCs are installed, applied automatically based on `spec`: | SCC | When Used | hostPath | hostPID | Caps | |-----|-----------|----------|---------|------| -| `tinycode-restricted` | Default | ✗ | ✗ | ALL dropped | -| `tinycode-hostpath` | `spec.storage.hostPath` set | ✓ | ✗ | ALL dropped | -| `tinycode-shell` | `spec.shell.enabled=true` | ✗ | ✓ | SYS_PTRACE only | +| `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 1000 (non-root), with `allowPrivilegedContainer: false`. +All SCCs run as UID 1001 (non-root), GID 0, with `allowPrivilegedContainer: false`. ## TinycodeInstance Spec Reference | Field | Type | Default | Description | |-------|------|---------|-------------| -| `spec.image` | string | `quay.io/tinycode/server:latest` | Container image | +| `spec.image` | string | `quay.io/bjohns/tinycode-container:latest` | Container image | | `spec.replicas` | integer | `1` | Number of pods (1–10) | -| `spec.resources` | object | 200m/512Mi → 2/2Gi | CPU/memory limits | -| `spec.storage.dataSize` | string | `1Gi` | PVC size for DB/config | -| `spec.storage.projectsSize` | string | `10Gi` | PVC size for projects | -| `spec.storage.storageClassName` | string | cluster default | StorageClass | -| `spec.storage.hostPath.path` | string | — | Host path to mount at /projects | -| `spec.storage.hostPath.readOnly` | bool | `false` | Read-only host mount | -| `spec.hostname` | string | auto | Custom Route hostname | -| `spec.tlsTermination` | string | `edge` | Route TLS: edge/passthrough/reencrypt | -| `spec.ollama.enabled` | bool | `false` | Deploy Ollama sidecar | -| `spec.ollama.host` | string | — | External Ollama URL | -| `spec.auth.passwordSecret` | string | — | Secret with TINYCODE_SERVER_PASSWORD | -| `spec.shell.enabled` | bool | `false` | Enable host shell access (hostPID) | +| `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) | + +### CRD Security Constraints + +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 be `admin`, `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. + +### Security Features + +- **NetworkPolicy**: Recommended to restrict ingress/egress traffic to required services +- **Read-Only Root Filesystem**: Can be enabled in PodSecurityPolicy via `readOnlyRootFilesystem: true` for 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 ## Status @@ -154,12 +235,220 @@ NAME READY URL AGE my-tinycode True https://my-tinycode-tinycode-dev... 5m ``` +Status phases: `Pending` → `Deploying` → `Running` (or `Failed` / `Terminating`). + ## Uninstall ```bash make uninstall ``` +## Deploying Without the Operator + +The 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. + +### Option 1: Raw Kustomize Manifests + +The [tinycode-container](https://github.com/bobbyjohnstx/tinycode-container) repo includes Kustomize manifests for vanilla Kubernetes: + +- `k8s/base/` — Deployment, Service, PVC (works on any cluster) +- `k8s/overlays/ingress/` — Adds a standard `Ingress` instead of OpenShift `Route` + +Deploy with: + +```bash +# 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/ingress +``` + +This approach is lightweight and requires no CRD or operator installation. You manage Kustomize overlays directly for customization. + +### Option 2: Tekton + Argo CD + +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. + +### Future Enhancement + +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. + +## Multi-User Self-Service Provisioning + +**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. + +### Setup (Cluster-Admin, One-Time) + +1. **Create a ClusterRole** granting users permission to manage their own TinycodeInstance CRs: + +```yaml +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: tinycode-user-role +rules: + - apiGroups: + - tinycode.dev + resources: + - tinycodeinstances + verbs: + - create + - get + - list + - watch + - delete +``` + +Save this as `tinycode-user-role.yaml` and apply once: + +```bash +oc apply -f tinycode-user-role.yaml +``` + +### Per-User Setup (Cluster-Admin or Delegated) + +For each user who will self-provision, create a namespace and bind the ClusterRole: + +```yaml +--- +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.io +``` + +Save as `alice-tinycode-rolebinding.yaml` and apply: + +```bash +oc apply -f alice-tinycode-rolebinding.yaml +``` + +The user also needs to create Secrets (password) in their namespace. Grant that permission by adding a namespace Role: + +```yaml +--- +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.io +``` + +### User Workflow (Self-Service) + +Once the cluster-admin has set up the namespace and RoleBinding, the user can: + +```bash +# 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 - < 1. + type: string + enum: + - ReadWriteOnce + - ReadWriteMany + default: ReadWriteOnce storageClassName: description: StorageClass to use for PVCs. Defaults to cluster default. type: string @@ -188,6 +198,129 @@ spec: type: array items: type: string + clusterAdmin: + description: | + OpenShift/Kubernetes cluster management configuration. When enabled, mounts a + kubeconfig Secret and downloads the oc CLI at startup so the cluster-admin agent + can interact with the cluster. v2 will add serviceAccount mode (reserved field). + type: object + properties: + enabled: + description: Enable cluster management mode. Downloads the oc CLI and mounts the kubeconfig. + type: boolean + default: false + kubeconfigSecretName: + description: Name of a Secret in the same namespace containing a kubeconfig file. Required when enabled is true. + type: string + kubeconfigSecretKey: + description: Key within the Secret containing the kubeconfig. Defaults to "kubeconfig". + type: string + default: "kubeconfig" + ocVersion: + description: oc CLI version to download. Defaults to "stable". Use a specific version like "4.17" for reproducibility. + type: string + default: "stable" + kubeconfigNamespace: + description: | + Optional namespace where the kubeconfig Secret lives (for cross-namespace Secret mounting). + If set and differs from instance namespace, the Secret will be copied to the instance namespace. + type: string + clusterRole: + description: | + Auto-provision a ServiceAccount with this ClusterRole instead of requiring a pre-created kubeconfig. + Cannot be 'admin' or 'cluster-admin' (privilege escalation guard). + type: string + pattern: "^(view|edit|tinycode-[a-z0-9-]+)$" + vllm: + description: | + Array of vLLM endpoints to configure as tinycode providers. Each entry must + have a name and URL. The operator auto-probes models and context limits unless + you override them in the models field. + type: array + maxItems: 20 + items: + type: object + required: + - name + - url + properties: + name: + description: Provider name for this vLLM instance. Must be unique within the array. + type: string + pattern: "^[a-z0-9][a-z0-9-]*$" + minLength: 1 + maxLength: 63 + url: + description: Base URL of the vLLM instance (e.g., http://vllm-qwen.vllm:8000). + type: string + pattern: "^https?://" + models: + description: | + Optional per-model overrides. Keys are model IDs, values contain + contextLimit and/or outputLimit. Unspecified models are auto-probed. + type: object + additionalProperties: + type: object + properties: + contextLimit: + description: Override the auto-detected context limit for this model. + type: integer + minimum: 1024 + outputLimit: + description: Override the auto-detected output limit for this model. + type: integer + minimum: 256 + model: + description: | + Default model ID for the tinycode instance. Used as the model parameter in the + generated config. Example: "qwen/Qwen2.5-Coder-32B-Instruct-AWQ". + type: string + discovery: + description: Cross-namespace vLLM discovery configuration. + type: object + properties: + namespaces: + description: | + List of namespaces to search for vLLM services. Each entry may be a namespace + name or "*" for all namespaces. The operator creates a ClusterRole with service + get/list permissions and binds it to the tinycode instance. + type: array + maxItems: 50 + items: + type: string + minLength: 1 + maxLength: 63 + git: + description: | + GitOps mode — clone a repository into /projects instead of using PVC storage. + Mutually exclusive with storage.hostPath. + type: object + required: + - url + properties: + url: + description: Git repository URL (https, ssh, or git protocol). + type: string + pattern: "^(https?|ssh|git)://[a-zA-Z0-9_./@:~%+=-]+$" + branch: + description: Branch to clone. Defaults to the repository's default branch. + type: string + pattern: "^[a-zA-Z0-9_./-]+$" + maxLength: 255 + credentialsSecret: + description: | + Name of a Secret in the same namespace containing git credentials. + Expected keys: username, password (for HTTPS) or ssh-privatekey (for SSH). + type: string + pullOnRestart: + description: Pull latest changes from the repository on pod restart. Default false. + type: boolean + default: false + depth: + description: Clone depth (--depth). Default 1 (shallow clone). + type: integer + minimum: 1 + default: 1 status: description: TinycodeInstanceStatus defines the observed state. type: object diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index 253b58b..0dbb4ca 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -39,13 +39,9 @@ spec: terminationGracePeriodSeconds: 30 containers: - name: manager - image: quay.io/tinycode/operator:latest + image: quay.io/bjohns/tinycode-operator:latest imagePullPolicy: IfNotPresent - command: - - /manager - args: - - --leader-elect - - --leader-election-id=tinycode-operator + # No command override — use image ENTRYPOINT (python3.11 /app/main.py) ports: - name: metrics containerPort: 8383 @@ -62,20 +58,6 @@ spec: fieldPath: metadata.namespace - name: HELM_CHART_PATH value: /helm-charts/tinycode - livenessProbe: - httpGet: - path: /healthz - port: health - initialDelaySeconds: 15 - periodSeconds: 20 - timeoutSeconds: 5 - readinessProbe: - httpGet: - path: /readyz - port: health - initialDelaySeconds: 5 - periodSeconds: 10 - timeoutSeconds: 3 resources: limits: cpu: 500m diff --git a/config/rbac/discovery_role.yaml b/config/rbac/discovery_role.yaml new file mode 100644 index 0000000..ff8dba4 --- /dev/null +++ b/config/rbac/discovery_role.yaml @@ -0,0 +1,24 @@ +# ClusterRole and ClusterRoleBinding granting the operator permission to +# create per-instance ClusterRoles and ClusterRoleBindings for cross-namespace +# vLLM service discovery. Required when spec.discovery.namespaces is used. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: tinycode-operator-discovery-admin +rules: +- apiGroups: ["rbac.authorization.k8s.io"] + resources: ["clusterroles", "clusterrolebindings"] + verbs: ["get", "list", "create", "update", "patch", "delete"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: tinycode-operator-discovery-admin +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: tinycode-operator-discovery-admin +subjects: +- kind: ServiceAccount + name: tinycode-operator-manager + namespace: tinycode-operator-system diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index b48adc4..b27e615 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -86,8 +86,6 @@ rules: - secrets verbs: - get - - list - - watch # SCC bindings — operator binds the appropriate SCC to instance SAs - apiGroups: @@ -103,6 +101,7 @@ rules: - rbac.authorization.k8s.io resources: - rolebindings + - clusterrolebindings verbs: - get - list @@ -125,3 +124,26 @@ rules: - update - patch - delete + + # Watch CRDs (required by kopf to monitor operator's own CRD) + - apiGroups: + - apiextensions.k8s.io + resources: + - customresourcedefinitions + verbs: + - get + - list + - watch + + # Publish and watch events + - apiGroups: + - "" + resources: + - events + verbs: + - get + - list + - watch + - create + - update + - patch diff --git a/config/rbac/scc_role.yaml b/config/rbac/scc_role.yaml index 7d1ec7f..59084be 100644 --- a/config/rbac/scc_role.yaml +++ b/config/rbac/scc_role.yaml @@ -19,3 +19,8 @@ rules: - tinycode-shell verbs: - use + - get + - list + - watch + - patch + - update diff --git a/config/samples/tinycode_v1alpha1_basic.yaml b/config/samples/tinycode_v1alpha1_basic.yaml index c60cdde..3a25ffa 100644 --- a/config/samples/tinycode_v1alpha1_basic.yaml +++ b/config/samples/tinycode_v1alpha1_basic.yaml @@ -7,7 +7,7 @@ metadata: name: my-tinycode namespace: tinycode-dev spec: - image: ghcr.io/bjohns/tiny-container:latest + image: quay.io/bjohns/tinycode-container:v1.20.0 replicas: 1 resources: limits: diff --git a/config/samples/tinycode_v1alpha1_discovery.yaml b/config/samples/tinycode_v1alpha1_discovery.yaml new file mode 100644 index 0000000..fcd0003 --- /dev/null +++ b/config/samples/tinycode_v1alpha1_discovery.yaml @@ -0,0 +1,40 @@ +--- +apiVersion: tinycode.dev/v1alpha1 +kind: TinycodeInstance +metadata: + name: tinycode-discovery + namespace: tinycode +spec: + replicas: 1 + + # Cross-namespace vLLM service discovery + discovery: + namespaces: + - vllm + - ai-models + - openshift-ai + + # Declarative vLLM config for in-namespace services + vllm: + - name: local-vllm + url: http://vllm-service:8000 + + model: "qwen/Qwen2.5-Coder-32B-Instruct-AWQ" + + storage: + dataSize: 1Gi + projectsSize: 10Gi + + auth: + passwordSecret: tinycode-password + + hostname: tinycode-discovery.apps.cluster.example.com + +# NOTE: The tinycode container's local-discovery.ts must be updated to: +# 1. Read TINYCODE_DISCOVERY_NAMESPACES env var (comma-separated list or "*") +# 2. Use the Kubernetes API to list services in those namespaces +# 3. Probe http://.:/v1/models for vLLM instances +# 4. Add discovered services to the provider config at runtime +# +# This sample demonstrates the operator side of the feature. The tinycode +# core changes are tracked separately. diff --git a/config/samples/tinycode_v1alpha1_gitops.yaml b/config/samples/tinycode_v1alpha1_gitops.yaml new file mode 100644 index 0000000..823e8ec --- /dev/null +++ b/config/samples/tinycode_v1alpha1_gitops.yaml @@ -0,0 +1,29 @@ +--- +# GitOps mode — clone a public repository into /projects. +# The repository is cloned on pod start using an init container. +apiVersion: tinycode.dev/v1alpha1 +kind: TinycodeInstance +metadata: + name: my-tinycode-gitops + namespace: tinycode-dev +spec: + image: quay.io/bjohns/tinycode-container:v1.20.0 + replicas: 1 + resources: + limits: + cpu: "2" + memory: "2Gi" + requests: + cpu: "200m" + memory: "512Mi" + storage: + dataSize: "1Gi" + projectsSize: "5Gi" + git: + url: "https://github.com/example/my-project.git" + branch: "main" + depth: 1 + pullOnRestart: false + auth: + passwordSecret: tinycode-password + tlsTermination: edge diff --git a/config/samples/tinycode_v1alpha1_gitops_private.yaml b/config/samples/tinycode_v1alpha1_gitops_private.yaml new file mode 100644 index 0000000..3089cd0 --- /dev/null +++ b/config/samples/tinycode_v1alpha1_gitops_private.yaml @@ -0,0 +1,36 @@ +--- +# GitOps mode with private repository — requires a credentialsSecret. +# The Secret should contain either (username, password) for HTTPS or (ssh-privatekey) for SSH. +apiVersion: tinycode.dev/v1alpha1 +kind: TinycodeInstance +metadata: + name: my-tinycode-gitops-private + namespace: tinycode-dev +spec: + image: quay.io/bjohns/tinycode-container:v1.20.0 + replicas: 1 + resources: + limits: + cpu: "2" + memory: "2Gi" + requests: + cpu: "200m" + memory: "512Mi" + storage: + dataSize: "1Gi" + projectsSize: "5Gi" + git: + url: "https://github.com/example/my-private-project.git" + branch: "main" + depth: 1 + pullOnRestart: true + credentialsSecret: git-credentials + auth: + passwordSecret: tinycode-password + tlsTermination: edge +--- +# Example Secret for git credentials (HTTPS) +# kubectl create secret generic git-credentials \ +# --from-literal=username=myuser \ +# --from-literal=password=mytoken \ +# -n tinycode-dev diff --git a/config/samples/tinycode_v1alpha1_hostpath.yaml b/config/samples/tinycode_v1alpha1_hostpath.yaml index 2dd8d5d..83df923 100644 --- a/config/samples/tinycode_v1alpha1_hostpath.yaml +++ b/config/samples/tinycode_v1alpha1_hostpath.yaml @@ -8,13 +8,17 @@ # SECURITY: The host path at /home/developer/projects will be writable # by the tinycode container (UID 1000). Ensure the host directory # permissions allow this UID. +# +# EPHEMERAL DATA: In hostPath mode, session data (SQLite DB, config) uses +# emptyDir and is lost on pod restart. Only the /projects workspace persists +# via hostPath. apiVersion: tinycode.dev/v1alpha1 kind: TinycodeInstance metadata: name: hostpath-tinycode namespace: tinycode-dev spec: - image: ghcr.io/bjohns/tiny-container:latest + image: quay.io/bjohns/tinycode-container:v1.20.0 storage: hostPath: path: /home/developer/projects diff --git a/config/samples/tinycode_v1alpha1_shared.yaml b/config/samples/tinycode_v1alpha1_shared.yaml new file mode 100644 index 0000000..73301c3 --- /dev/null +++ b/config/samples/tinycode_v1alpha1_shared.yaml @@ -0,0 +1,26 @@ +--- +# Shared team workspace — multiple replicas with ReadWriteMany PVC. +# Session affinity is configured via Route annotations to maintain user sessions. +# The data volume uses emptyDir (SQLite DB is not shared across replicas). +apiVersion: tinycode.dev/v1alpha1 +kind: TinycodeInstance +metadata: + name: my-tinycode-shared + namespace: tinycode-dev +spec: + image: quay.io/bjohns/tinycode-container:v1.20.0 + replicas: 2 + resources: + limits: + cpu: "2" + memory: "2Gi" + requests: + cpu: "200m" + memory: "512Mi" + storage: + dataSize: "1Gi" + projectsSize: "50Gi" + projectsAccessMode: ReadWriteMany + auth: + passwordSecret: tinycode-password + tlsTermination: edge diff --git a/config/samples/tinycode_v1alpha1_shell.yaml b/config/samples/tinycode_v1alpha1_shell.yaml index 6e12988..ee9e4dd 100644 --- a/config/samples/tinycode_v1alpha1_shell.yaml +++ b/config/samples/tinycode_v1alpha1_shell.yaml @@ -19,7 +19,7 @@ metadata: name: shell-tinycode namespace: tinycode-dev spec: - image: ghcr.io/bjohns/tiny-container:latest + image: quay.io/bjohns/tinycode-container:v1.20.0 shell: enabled: true storage: diff --git a/config/samples/tinycode_v1alpha1_vllm.yaml b/config/samples/tinycode_v1alpha1_vllm.yaml new file mode 100644 index 0000000..4ef8c72 --- /dev/null +++ b/config/samples/tinycode_v1alpha1_vllm.yaml @@ -0,0 +1,38 @@ +--- +apiVersion: tinycode.dev/v1alpha1 +kind: TinycodeInstance +metadata: + name: tinycode-vllm + namespace: tinycode +spec: + replicas: 1 + + # Declarative vLLM config — operator auto-probes models and generates provider config + vllm: + - name: qwen + url: http://vllm-qwen.vllm:8000 + # Optional: override auto-detected limits for specific models + models: + qwen/Qwen2.5-Coder-32B-Instruct-AWQ: + contextLimit: 32768 + outputLimit: 4096 + + - name: deepseek + url: http://vllm-deepseek.vllm:8000 + # No models field — operator auto-probes and applies 80/20 split + + # Default model for this instance + model: "qwen/Qwen2.5-Coder-32B-Instruct-AWQ" + + # Storage + storage: + dataSize: 2Gi + projectsSize: 20Gi + + # Authentication + auth: + passwordSecret: tinycode-password + + # External URL + hostname: tinycode-vllm.apps.cluster.example.com + tlsTermination: edge diff --git a/config/scc/tinycode-hostpath-scc.yaml b/config/scc/tinycode-hostpath-scc.yaml index a961cb5..3494e56 100644 --- a/config/scc/tinycode-hostpath-scc.yaml +++ b/config/scc/tinycode-hostpath-scc.yaml @@ -29,19 +29,13 @@ runAsUser: uid: 1001 seLinuxContext: - type: MustRunAs + type: RunAsAny fsGroup: - type: MustRunAs - ranges: - - min: 1001 - max: 1001 + type: RunAsAny supplementalGroups: - type: MustRunAs - ranges: - - min: 1001 - max: 1001 + type: RunAsAny # Host path volume allowed — all others same as restricted allowHostDirVolumePlugin: true diff --git a/config/scc/tinycode-restricted-scc.yaml b/config/scc/tinycode-restricted-scc.yaml index 5d838ba..778d609 100644 --- a/config/scc/tinycode-restricted-scc.yaml +++ b/config/scc/tinycode-restricted-scc.yaml @@ -20,19 +20,13 @@ runAsUser: uid: 1001 seLinuxContext: - type: MustRunAs + type: RunAsAny fsGroup: - type: MustRunAs - ranges: - - min: 1001 - max: 1001 + type: RunAsAny supplementalGroups: - type: MustRunAs - ranges: - - min: 1001 - max: 1001 + type: RunAsAny # No host access of any kind allowHostDirVolumePlugin: false diff --git a/config/scc/tinycode-shell-scc.yaml b/config/scc/tinycode-shell-scc.yaml index a56232b..89ed535 100644 --- a/config/scc/tinycode-shell-scc.yaml +++ b/config/scc/tinycode-shell-scc.yaml @@ -29,23 +29,16 @@ labels: app.kubernetes.io/managed-by: tinycode-operator runAsUser: - type: MustRunAs - uid: 1001 + type: RunAsAny seLinuxContext: - type: MustRunAs + type: RunAsAny fsGroup: - type: MustRunAs - ranges: - - min: 1001 - max: 1001 + type: RunAsAny supplementalGroups: - type: MustRunAs - ranges: - - min: 1001 - max: 1001 + type: RunAsAny # hostPID required for nsenter-based shell execution allowHostDirVolumePlugin: false diff --git a/docs/OC_CLI_Cheatsheet.md b/docs/OC_CLI_Cheatsheet.md new file mode 100644 index 0000000..bbd5958 --- /dev/null +++ b/docs/OC_CLI_Cheatsheet.md @@ -0,0 +1,604 @@ +# OC CLI Cheatsheet +Source: Red Hat OpenShift Container Platform 4.18 + +--- + +## Authentication & Session + +```bash +oc login -u -p https://api.:6443 +oc logout +oc whoami +oc project # switch project +oc projects # list all projects +``` + +--- + +## Projects & Namespaces + +```bash +oc new-project +oc delete project +oc get projects + +# Namespace (admin-only, bypasses project template) +oc create namespace +oc delete namespace + +# Edit cluster-wide project config (template, self-provisioner) +oc edit projects.config.openshift.io cluster +``` + +--- + +## Declarative Resource Management + +```bash +# Apply / create from manifest (declarative — preferred) +oc apply -f +oc apply -f . # all files in current dir +oc apply -f . --namespace +oc apply -R -f # recursive + +# Validate without applying +oc apply -f --dry-run=server --validate=true + +# Create (imperative — errors if resource exists) +oc create -f +oc create -R -f +oc create -f https://example.com/file.yaml + +# Replace (full overwrite) +oc replace -f + +# Delete from manifest +oc delete -f +oc delete -f . --namespace + +# Diff live vs manifest +oc diff -f + +# Patch (inline JSON snippet) +oc patch deployment hello -p \ + '{"spec":{"template":{"spec":{"containers":[{"name":"hello-rhel7","resources":{"requests":{"cpu":"100m"}}}]}}}}' + +# Patch from file +oc patch deployment hello --patch-file ~/volume-mount.yaml + +# Patch with merge strategy (common for operators/subscriptions) +oc patch --type merge -p '{"spec":{"field":"value"}}' + +# Generate manifest from imperative command (--dry-run=client) +oc create deployment hello-openshift -o yaml \ + --image registry.example.com/redhattraining/hello-world-nginx:v1.0 \ + --save-config \ + --dry-run=client > ~/my-app/example-deployment.yaml + +# Interactive edit +oc edit +oc edit oauth + +# Restart pods to pick up config changes (secret/configmap updates) +oc rollout restart deployment/ +oc rollout restart deployment/ --namespace + +# Explain any field +oc explain deployment.spec.template.spec +``` + +--- + +## Kustomize + +```bash +# Preview rendered manifests (no apply) +oc kustomize overlay/production + +# Apply a kustomization directory +oc apply -k overlay/production + +# Delete resources from kustomization +oc delete -k overlay/production +``` + +--- + +## Getting & Inspecting Resources + +```bash +# Basic get +oc get pods +oc get deployments +oc get services +oc get routes +oc get configmaps +oc get secrets +oc get nodes +oc get all -n + +# Output formats +oc get -o yaml +oc get -o wide +oc get -o jsonpath='{.items[0].spec.channel}{"\n"}' +oc get -o NAME + +# Filtering +oc get -n +oc get -l app=