Skip to content

Commit 47c5f5a

Browse files
committed
feat: cloud-init visual config builder with Vanilla Framework
- Schema-driven forms for 8 cloud-init modules (users, packages, runcmd, write_files, ssh, hostname, timezone, ntp) - Real-time YAML output with Monaco Editor starting with #cloud-config - Client-side validation with Ajv against official cloud-init JSON schema - Server-side validation via FastAPI backend - 5 built-in templates: Ubuntu Server, Docker Host, Kubernetes Node, Web Server, Developer Workstation - Shareable config links via lz-string URL encoding - Full keyboard navigation and ARIA accessibility - axe-core accessibility tests in CI - Built with @canonical/react-components (Vanilla Framework) - Motivated by canonical/cloud-init#6796 and canonical/react-components#1339
0 parents  commit 47c5f5a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

83 files changed

+12318
-0
lines changed

.github/workflows/ci.yml

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
frontend:
11+
runs-on: ubuntu-latest
12+
defaults:
13+
run:
14+
working-directory: frontend
15+
16+
steps:
17+
- uses: actions/checkout@v4
18+
19+
- uses: actions/setup-node@v4
20+
with:
21+
node-version: "20"
22+
cache: "npm"
23+
cache-dependency-path: frontend/package-lock.json
24+
25+
- run: npm ci
26+
27+
- name: Typecheck
28+
run: npm run typecheck
29+
30+
- name: Lint
31+
run: npm run lint
32+
33+
- name: Unit tests
34+
run: npm run test
35+
36+
- name: Build
37+
run: npm run build
38+
39+
- name: Install Playwright
40+
run: npx playwright install --with-deps chromium
41+
42+
- name: E2E tests (including axe accessibility)
43+
run: npm run test:e2e
44+
45+
backend:
46+
runs-on: ubuntu-latest
47+
defaults:
48+
run:
49+
working-directory: backend
50+
51+
steps:
52+
- uses: actions/checkout@v4
53+
54+
- uses: actions/setup-python@v5
55+
with:
56+
python-version: "3.12"
57+
58+
- run: pip install -r requirements.txt
59+
60+
- name: Run tests
61+
run: pytest tests/ -v
62+
63+
docker:
64+
runs-on: ubuntu-latest
65+
needs: [frontend, backend]
66+
steps:
67+
- uses: actions/checkout@v4
68+
- run: docker compose build

.github/workflows/deploy.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: Deploy
2+
3+
on:
4+
workflow_dispatch:
5+
push:
6+
branches: [main]
7+
paths-ignore:
8+
- "**.md"
9+
- ".github/workflows/ci.yml"
10+
11+
jobs:
12+
deploy:
13+
runs-on: ubuntu-latest
14+
if: github.ref == 'refs/heads/main'
15+
16+
steps:
17+
- uses: actions/checkout@v4
18+
19+
- name: Deploy to server
20+
uses: appleboy/ssh-action@v1
21+
with:
22+
host: ${{ secrets.SERVER_HOST }}
23+
username: ${{ secrets.SERVER_USER }}
24+
key: ${{ secrets.SERVER_SSH_KEY }}
25+
script: |
26+
cd /opt/cloud-init-builder
27+
git pull origin main
28+
docker compose -f docker-compose.prod.yml build
29+
docker compose -f docker-compose.prod.yml up -d
30+
docker compose -f docker-compose.prod.yml ps

.gitignore

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Dependencies
2+
node_modules/
3+
frontend/node_modules/
4+
5+
# Build output
6+
frontend/dist/
7+
frontend/.vite/
8+
9+
# Python
10+
__pycache__/
11+
*.py[cod]
12+
*.egg-info/
13+
.venv/
14+
venv/
15+
backend/.pytest_cache/
16+
17+
# Environment
18+
.env
19+
.env.local
20+
.env.production
21+
22+
# Editor
23+
.vscode/
24+
.idea/
25+
*.swp
26+
.DS_Store
27+
28+
# Logs
29+
*.log
30+
npm-debug.log*
31+
32+
# Test output
33+
coverage/
34+
playwright-report/
35+
test-results/
36+
37+
# Terraform
38+
infra/.terraform/
39+
infra/*.tfstate
40+
infra/*.tfstate.backup

README.md

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
# cloud-init Builder
2+
3+
A visual configuration builder for [cloud-init](https://cloud-init.io/) user-data.
4+
Build cloud-init configs through an accessible form interface instead of writing
5+
YAML by hand. Validates against the official cloud-init JSON schema in real time.
6+
7+
**Live:** https://cloudinit-builder.koushik.dev
8+
9+
![cloud-init Builder](docs/screenshot.png)
10+
11+
> Built with [@canonical/react-components](https://github.com/canonical/react-components)
12+
> (Vanilla Framework). Motivated by [PR #6796](https://github.com/canonical/cloud-init/pull/6796)
13+
> on canonical/cloud-init and [PR #1339](https://github.com/canonical/react-components/pull/1339)
14+
> on canonical/react-components.
15+
16+
![CI](https://github.com/koushik717/cloud-init-builder/workflows/CI/badge.svg)
17+
18+
## Why this exists
19+
20+
Writing cloud-init user-data YAML by hand is error-prone. The schema is large,
21+
the documentation is scattered, and a typo in an indented list means your VM
22+
boots without the config you intended.
23+
24+
This project grew out of contributing to cloud-init directly (PR #6796 adds
25+
`--timeout` to `cloud-init status --wait`) and to `@canonical/react-components`
26+
(PR #1339 fixes a keyboard accessibility bug in Modal). Having worked at the
27+
CLI and component library level, building a frontend tool on top made sense.
28+
29+
Built with Canonical's own [Vanilla Framework](https://vanillaframework.io/)
30+
via `@canonical/react-components` because this is a tool for Canonical's
31+
ecosystem and using their design system is the correct choice.
32+
33+
## Features
34+
35+
- Schema-driven forms for all major cloud-init modules
36+
- Real-time YAML output with Monaco Editor
37+
- Client-side validation against the official cloud-init JSON schema
38+
- Server-side validation via FastAPI backend
39+
- Shareable config links (state encoded and compressed in URL hash)
40+
- Five built-in templates: Ubuntu Server, Docker Host, Kubernetes Node,
41+
Web Server, Developer Workstation
42+
- Diff view for comparing two configs
43+
- LocalStorage persistence so work survives a page refresh
44+
- Full keyboard navigation and screen reader support
45+
- Zero axe-core accessibility violations in CI
46+
47+
## Tech stack
48+
49+
| Layer | Technology |
50+
|---|---|
51+
| Frontend framework | React 18 + TypeScript (strict) |
52+
| UI components | @canonical/react-components (Vanilla Framework) |
53+
| Form state | React Hook Form |
54+
| Global state | Zustand |
55+
| YAML output | Monaco Editor (lazy loaded) |
56+
| Schema validation | Ajv + ajv-formats |
57+
| URL encoding | lz-string |
58+
| Backend | Python FastAPI |
59+
| E2E testing | Playwright + axe-playwright |
60+
| Infrastructure | Docker + Terraform |
61+
| CI | GitHub Actions |
62+
63+
## Getting started
64+
65+
### Prerequisites
66+
- Node.js 20+
67+
- Python 3.12+
68+
- Docker and Docker Compose
69+
70+
### Development
71+
72+
```bash
73+
git clone https://github.com/koushik717/cloud-init-builder.git
74+
cd cloud-init-builder
75+
76+
# Start both frontend and backend
77+
docker compose up
78+
79+
# Frontend at http://localhost:5173
80+
# Backend at http://localhost:8000
81+
# API docs at http://localhost:8000/docs
82+
```
83+
84+
### Run without Docker
85+
86+
```bash
87+
# Frontend
88+
cd frontend
89+
npm install
90+
npm run dev
91+
92+
# Backend (separate terminal)
93+
cd backend
94+
pip install -r requirements.txt
95+
uvicorn main:app --reload --host 0.0.0.0 --port 8000
96+
```
97+
98+
### Run tests
99+
100+
```bash
101+
cd frontend
102+
103+
# Unit tests
104+
npm test
105+
106+
# E2E tests (includes accessibility checks)
107+
npx playwright install chromium
108+
npm run test:e2e
109+
110+
# Typecheck
111+
npm run typecheck
112+
```
113+
114+
```bash
115+
cd backend
116+
pytest tests/ -v
117+
```
118+
119+
## Project structure
120+
121+
```
122+
cloud-init-builder/
123+
├── frontend/ React + TypeScript application
124+
│ ├── src/
125+
│ │ ├── components/ UI components
126+
│ │ ├── store/ Zustand state
127+
│ │ ├── hooks/ Custom hooks
128+
│ │ ├── utils/ YAML, URL, schema utilities
129+
│ │ └── types/ TypeScript types
130+
│ └── tests/
131+
│ ├── e2e/ Playwright + axe tests
132+
│ └── unit/ Vitest unit tests
133+
├── backend/ FastAPI validation service
134+
├── infra/ Terraform infrastructure
135+
└── .github/workflows/ CI/CD pipelines
136+
```
137+
138+
## Supported modules
139+
140+
### Priority 1
141+
- **users** -- Users and Groups
142+
- **packages** -- Package installation and updates
143+
- **runcmd** -- Run shell commands
144+
- **write_files** -- Write files to disk
145+
- **ssh** -- SSH keys and configuration
146+
- **hostname** -- Hostname and FQDN
147+
- **timezone** -- System timezone
148+
- **ntp** -- NTP client configuration
149+
150+
### Priority 2 (coming soon)
151+
- **apt** -- APT sources and configuration
152+
- **snap** -- Snap packages
153+
- **disk_setup** -- Disk and filesystem setup
154+
- **bootcmd** -- Early boot commands
155+
- **final_message** -- Completion message
156+
157+
## Accessibility
158+
159+
This project targets WCAG 2.1 AA compliance throughout.
160+
161+
- Full keyboard navigation for all interactive elements
162+
- ARIA live region on YAML output panel for screen reader announcements
163+
- aria-invalid and aria-errormessage on fields with validation errors
164+
- Color contrast 4.5:1 minimum on all text
165+
- Error states indicated with both color and icon
166+
- axe-core checks run on every CI build, zero violations required to pass
167+
168+
## Contributing
169+
170+
Pull requests welcome. Please run `npm run lint` and `npm test` before
171+
submitting. PR titles should follow
172+
[conventional commits](https://www.conventionalcommits.org/).
173+
174+
## Related
175+
176+
- [cloud-init documentation](https://cloudinit.readthedocs.io/)
177+
- [cloud-init JSON schema](https://github.com/canonical/cloud-init/blob/main/cloudinit/config/schemas/schema-cloud-config-v1.json)
178+
- [Vanilla Framework](https://vanillaframework.io/)
179+
- [PR #6796 -- add --timeout to cloud-init status --wait](https://github.com/canonical/cloud-init/pull/6796)
180+
- [PR #1339 -- fix Escape key in ContextualMenu inside Modal](https://github.com/canonical/react-components/pull/1339)
181+
182+
## License
183+
184+
MIT

backend/Dockerfile

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
FROM python:3.12-slim
2+
3+
WORKDIR /app
4+
5+
COPY requirements.txt .
6+
RUN pip install --no-cache-dir -r requirements.txt
7+
8+
COPY . .
9+
10+
EXPOSE 8000
11+
12+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

backend/main.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
from fastapi import FastAPI
2+
from fastapi.middleware.cors import CORSMiddleware
3+
from models import ValidateRequest, ValidateResponse
4+
from validator import validate_config
5+
from schema import get_cached_schema
6+
7+
app = FastAPI(
8+
title="cloud-init Builder API",
9+
description="Validation API for cloud-init configurations",
10+
version="1.0.0",
11+
)
12+
13+
app.add_middleware(
14+
CORSMiddleware,
15+
allow_origins=[
16+
"http://localhost:5173",
17+
"http://localhost:3000",
18+
"https://cloudinit-builder.koushik.dev",
19+
],
20+
allow_methods=["GET", "POST"],
21+
allow_headers=["*"],
22+
)
23+
24+
25+
@app.post("/api/validate", response_model=ValidateResponse)
26+
async def validate(request: ValidateRequest) -> ValidateResponse:
27+
"""Validate a cloud-init configuration against the official JSON schema."""
28+
result = await validate_config(request.config)
29+
return result
30+
31+
32+
@app.get("/api/schema")
33+
async def get_schema() -> dict:
34+
"""Return the cached cloud-init JSON schema."""
35+
schema = await get_cached_schema()
36+
return schema
37+
38+
39+
@app.get("/api/health")
40+
async def health() -> dict:
41+
"""Health check endpoint."""
42+
return {"status": "ok"}

0 commit comments

Comments
 (0)