From af4bbe9b15a084a96ea4907094e2958f29d384b1 Mon Sep 17 00:00:00 2001 From: Ahmad Ardiansyah Date: Thu, 13 Nov 2025 08:48:03 +0700 Subject: [PATCH] initial commit --- .gitignore | 23 + .npmrc | 1 + .prettierignore | 9 + .prettierrc | 16 + README.md | 190 +++++++++ bun.lockb | Bin 0 -> 109724 bytes eslint.config.js | 41 ++ package.json | 39 ++ src/app.css | 2 + src/app.d.ts | 13 + src/app.html | 11 + src/lib/assets/favicon.svg | 1 + src/lib/index.ts | 1 + src/lib/stores/auth.svelte.ts | 69 +++ src/lib/stores/clusters.svelte.ts | 137 ++++++ src/routes/+layout.svelte | 12 + src/routes/+page.svelte | 169 ++++++++ src/routes/admin/+layout.svelte | 193 +++++++++ src/routes/admin/+page.svelte | 273 ++++++++++++ src/routes/admin/clusters/+page.svelte | 499 ++++++++++++++++++++++ src/routes/admin/nodes/+page.svelte | 562 +++++++++++++++++++++++++ src/routes/admin/settings/+page.svelte | 360 ++++++++++++++++ src/routes/admin/ssh/+page.svelte | 470 +++++++++++++++++++++ static/robots.txt | 3 + svelte.config.js | 17 + tsconfig.json | 19 + vite.config.ts | 7 + 27 files changed, 3137 insertions(+) create mode 100644 .gitignore create mode 100644 .npmrc create mode 100644 .prettierignore create mode 100644 .prettierrc create mode 100644 README.md create mode 100644 bun.lockb create mode 100644 eslint.config.js create mode 100644 package.json create mode 100644 src/app.css create mode 100644 src/app.d.ts create mode 100644 src/app.html create mode 100644 src/lib/assets/favicon.svg create mode 100644 src/lib/index.ts create mode 100644 src/lib/stores/auth.svelte.ts create mode 100644 src/lib/stores/clusters.svelte.ts create mode 100644 src/routes/+layout.svelte create mode 100644 src/routes/+page.svelte create mode 100644 src/routes/admin/+layout.svelte create mode 100644 src/routes/admin/+page.svelte create mode 100644 src/routes/admin/clusters/+page.svelte create mode 100644 src/routes/admin/nodes/+page.svelte create mode 100644 src/routes/admin/settings/+page.svelte create mode 100644 src/routes/admin/ssh/+page.svelte create mode 100644 static/robots.txt create mode 100644 svelte.config.js create mode 100644 tsconfig.json create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3b462cb --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +node_modules + +# Output +.output +.vercel +.netlify +.wrangler +/.svelte-kit +/build + +# OS +.DS_Store +Thumbs.db + +# Env +.env +.env.* +!.env.example +!.env.test + +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..b6f27f1 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..7d74fe2 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,9 @@ +# Package Managers +package-lock.json +pnpm-lock.yaml +yarn.lock +bun.lock +bun.lockb + +# Miscellaneous +/static/ diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..8103a0b --- /dev/null +++ b/.prettierrc @@ -0,0 +1,16 @@ +{ + "useTabs": true, + "singleQuote": true, + "trailingComma": "none", + "printWidth": 100, + "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], + "overrides": [ + { + "files": "*.svelte", + "options": { + "parser": "svelte" + } + } + ], + "tailwindStylesheet": "./src/app.css" +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..edc8f0e --- /dev/null +++ b/README.md @@ -0,0 +1,190 @@ +# K3S Management + +A modern web-based management interface for K3S Kubernetes clusters built with SvelteKit 5. + +## Features + +- 🔐 **Authentication** - Secure login system +- 🖥️ **Cluster Management** - Create and manage K3S clusters +- 📦 **Node Management** - Add and monitor cluster nodes +- 🔑 **SSH Configuration** - Manage SSH connections to nodes +- ⚙️ **Settings** - Configure system preferences +- 💾 **Data Export/Import** - Backup and restore configurations + +## Tech Stack + +- **SvelteKit 5** - Frontend framework with modern runes syntax +- **TailwindCSS 4** - Utility-first CSS framework +- **TypeScript** - Type-safe development +- **K3sup** - Backend tool for K3S cluster installation (planned) + +## Getting Started + +### Prerequisites + +- Node.js 18+ or Bun +- A modern web browser + +### Installation + +1. Clone the repository +2. Install dependencies: + +```bash +bun install +# or +npm install +``` + +3. Start the development server: + +```bash +bun run dev +# or +npm run dev +``` + +4. Open your browser and navigate to `http://localhost:5173` + +### Default Login Credentials + +- **Username**: `admin` +- **Password**: `admin` + +⚠️ **Security Note**: Change these credentials in production! + +## Project Structure + +``` +k3s-management/ +├── src/ +│ ├── lib/ +│ │ └── stores/ # Svelte stores for state management +│ │ ├── auth.svelte.ts +│ │ └── clusters.svelte.ts +│ ├── routes/ +│ │ ├── +page.svelte # Login page +│ │ └── admin/ # Admin dashboard +│ │ ├── +layout.svelte +│ │ ├── +page.svelte +│ │ ├── clusters/ +│ │ ├── nodes/ +│ │ ├── ssh/ +│ │ └── settings/ +│ └── app.css # Global styles +└── package.json +``` + +## Usage + +### Creating a Cluster + +1. Navigate to the **Clusters** page +2. Click **Create Cluster** +3. Fill in cluster information: + - Cluster name and description + - K3S version + - Server node SSH details (host, port, username, password) + - Node IP address +4. Click **Create Cluster** + +### Adding Nodes + +1. Navigate to the **Nodes** page +2. Click **Add Node** +3. Select the target cluster +4. Configure node details: + - Node name and role (Server/Agent) + - SSH connection details + - Node IP address +5. Click **Add Node** + +### Managing SSH Configurations + +1. Navigate to **SSH Configurations** +2. View all SSH connections +3. Test connections to verify connectivity +4. Copy connection strings for manual SSH access + +### Settings + +- Configure K3sup version +- Enable/disable automatic backups +- Set up notifications +- Export/import data +- Clear all data + +## Development + +### Building for Production + +```bash +bun run build +# or +npm run build +``` + +### Preview Production Build + +```bash +bun run preview +# or +npm run preview +``` + +### Code Formatting + +```bash +bun run format +# or +npm run format +``` + +### Linting + +```bash +bun run lint +# or +npm run lint +``` + +## Roadmap + +- [ ] Implement actual K3sup backend integration +- [ ] Add real SSH key-based authentication +- [ ] Real-time cluster status monitoring +- [ ] Cluster logs viewer +- [ ] Multi-user support with roles +- [ ] API endpoints for external integrations +- [ ] Kubernetes resources management +- [ ] Helm charts deployment +- [ ] Cluster backup and restore +- [ ] Monitoring and alerting + +## Security Considerations + +⚠️ **Important**: This is a development/demo version. For production use: + +1. Implement proper authentication (JWT, OAuth, etc.) +2. Use SSH keys instead of passwords +3. Encrypt sensitive data +4. Use HTTPS +5. Implement rate limiting +6. Add CSRF protection +7. Validate all inputs +8. Use environment variables for configuration + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. + +## License + +MIT + +## Acknowledgments + +- Built with [SvelteKit](https://kit.svelte.dev/) +- Styled with [TailwindCSS](https://tailwindcss.com/) +- Powered by [K3sup](https://github.com/alexellis/k3sup) +- Icons from [Heroicons](https://heroicons.com/) \ No newline at end of file diff --git a/bun.lockb b/bun.lockb new file mode 100644 index 0000000000000000000000000000000000000000..d57d27763a439f9388a9c194e23c4520062f1474 GIT binary patch literal 109724 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 literal 0 HcmV?d00001 diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..2c49fa6 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,41 @@ +import prettier from 'eslint-config-prettier'; +import { fileURLToPath } from 'node:url'; +import { includeIgnoreFile } from '@eslint/compat'; +import js from '@eslint/js'; +import svelte from 'eslint-plugin-svelte'; +import { defineConfig } from 'eslint/config'; +import globals from 'globals'; +import ts from 'typescript-eslint'; +import svelteConfig from './svelte.config.js'; + +const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url)); + +export default defineConfig( + includeIgnoreFile(gitignorePath), + js.configs.recommended, + ...ts.configs.recommended, + ...svelte.configs.recommended, + prettier, + ...svelte.configs.prettier, + { + languageOptions: { + globals: { ...globals.browser, ...globals.node } + }, + rules: { + // typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects. + // see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors + 'no-undef': 'off' + } + }, + { + files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'], + languageOptions: { + parserOptions: { + projectService: true, + extraFileExtensions: ['.svelte'], + parser: ts.parser, + svelteConfig + } + } + } +); diff --git a/package.json b/package.json new file mode 100644 index 0000000..b31dcb6 --- /dev/null +++ b/package.json @@ -0,0 +1,39 @@ +{ + "name": "k3s-management", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "prepare": "svelte-kit sync || echo ''", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "format": "prettier --write .", + "lint": "prettier --check . && eslint ." + }, + "devDependencies": { + "@eslint/compat": "^1.4.0", + "@eslint/js": "^9.38.0", + "@sveltejs/adapter-auto": "^7.0.0", + "@sveltejs/kit": "^2.47.1", + "@sveltejs/vite-plugin-svelte": "^6.2.1", + "@tailwindcss/typography": "^0.5.19", + "@tailwindcss/vite": "^4.1.14", + "@types/node": "^22", + "eslint": "^9.38.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-svelte": "^3.12.4", + "globals": "^16.4.0", + "prettier": "^3.6.2", + "prettier-plugin-svelte": "^3.4.0", + "prettier-plugin-tailwindcss": "^0.7.1", + "svelte": "^5.41.0", + "svelte-check": "^4.3.3", + "tailwindcss": "^4.1.14", + "typescript": "^5.9.3", + "typescript-eslint": "^8.46.1", + "vite": "^7.1.10" + } +} diff --git a/src/app.css b/src/app.css new file mode 100644 index 0000000..1c4d2a8 --- /dev/null +++ b/src/app.css @@ -0,0 +1,2 @@ +@import 'tailwindcss'; +@plugin '@tailwindcss/typography'; diff --git a/src/app.d.ts b/src/app.d.ts new file mode 100644 index 0000000..da08e6d --- /dev/null +++ b/src/app.d.ts @@ -0,0 +1,13 @@ +// See https://svelte.dev/docs/kit/types#app.d.ts +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/src/app.html b/src/app.html new file mode 100644 index 0000000..f273cc5 --- /dev/null +++ b/src/app.html @@ -0,0 +1,11 @@ + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/src/lib/assets/favicon.svg b/src/lib/assets/favicon.svg new file mode 100644 index 0000000..cc5dc66 --- /dev/null +++ b/src/lib/assets/favicon.svg @@ -0,0 +1 @@ +svelte-logo \ No newline at end of file diff --git a/src/lib/index.ts b/src/lib/index.ts new file mode 100644 index 0000000..856f2b6 --- /dev/null +++ b/src/lib/index.ts @@ -0,0 +1 @@ +// place files you want to import through the `$lib` alias in this folder. diff --git a/src/lib/stores/auth.svelte.ts b/src/lib/stores/auth.svelte.ts new file mode 100644 index 0000000..8a6edf0 --- /dev/null +++ b/src/lib/stores/auth.svelte.ts @@ -0,0 +1,69 @@ +interface User { + username: string; + role: string; +} + +interface AuthState { + isAuthenticated: boolean; + user: User | null; +} + +class AuthStore { + private state = $state({ + isAuthenticated: false, + user: null + }); + + get isAuthenticated() { + return this.state.isAuthenticated; + } + + get user() { + return this.state.user; + } + + login(username: string, password: string): boolean { + // Temporary simple auth - akan diganti dengan backend auth + if (username === 'admin' && password === 'admin') { + this.state.isAuthenticated = true; + this.state.user = { + username: username, + role: 'admin' + }; + + // Store in localStorage + if (typeof window !== 'undefined') { + localStorage.setItem('auth', JSON.stringify(this.state.user)); + } + + return true; + } + return false; + } + + logout() { + this.state.isAuthenticated = false; + this.state.user = null; + + if (typeof window !== 'undefined') { + localStorage.removeItem('auth'); + } + } + + checkAuth() { + if (typeof window !== 'undefined') { + const stored = localStorage.getItem('auth'); + if (stored) { + try { + const user = JSON.parse(stored); + this.state.isAuthenticated = true; + this.state.user = user; + } catch { + this.logout(); + } + } + } + } +} + +export const authStore = new AuthStore(); diff --git a/src/lib/stores/clusters.svelte.ts b/src/lib/stores/clusters.svelte.ts new file mode 100644 index 0000000..482d560 --- /dev/null +++ b/src/lib/stores/clusters.svelte.ts @@ -0,0 +1,137 @@ +export interface SSHConfig { + host: string; + port: number; + username: string; + password?: string; + privateKey?: string; +} + +export interface ClusterNode { + id: string; + name: string; + role: 'server' | 'agent'; + ssh: SSHConfig; + status: 'pending' | 'installing' | 'active' | 'failed' | 'stopped'; + ip: string; + createdAt: Date; +} + +export interface K3SCluster { + id: string; + name: string; + description: string; + status: 'creating' | 'active' | 'failed' | 'stopped'; + nodes: ClusterNode[]; + createdAt: Date; + k3sVersion?: string; +} + +class ClustersStore { + private state = $state<{ + clusters: K3SCluster[]; + selectedCluster: K3SCluster | null; + loading: boolean; + }>({ + clusters: [], + selectedCluster: null, + loading: false + }); + + get clusters() { + return this.state.clusters; + } + + get selectedCluster() { + return this.state.selectedCluster; + } + + get loading() { + return this.state.loading; + } + + addCluster(cluster: K3SCluster) { + this.state.clusters.push(cluster); + this.saveToStorage(); + } + + updateCluster(id: string, updates: Partial) { + const index = this.state.clusters.findIndex((c) => c.id === id); + if (index !== -1) { + this.state.clusters[index] = { ...this.state.clusters[index], ...updates }; + this.saveToStorage(); + } + } + + deleteCluster(id: string) { + this.state.clusters = this.state.clusters.filter((c) => c.id !== id); + if (this.state.selectedCluster?.id === id) { + this.state.selectedCluster = null; + } + this.saveToStorage(); + } + + selectCluster(id: string) { + this.state.selectedCluster = this.state.clusters.find((c) => c.id === id) || null; + } + + addNodeToCluster(clusterId: string, node: ClusterNode) { + const cluster = this.state.clusters.find((c) => c.id === clusterId); + if (cluster) { + cluster.nodes.push(node); + this.saveToStorage(); + } + } + + updateNodeStatus(clusterId: string, nodeId: string, status: ClusterNode['status']) { + const cluster = this.state.clusters.find((c) => c.id === clusterId); + if (cluster) { + const node = cluster.nodes.find((n) => n.id === nodeId); + if (node) { + node.status = status; + this.saveToStorage(); + } + } + } + + removeNodeFromCluster(clusterId: string, nodeId: string) { + const cluster = this.state.clusters.find((c) => c.id === clusterId); + if (cluster) { + cluster.nodes = cluster.nodes.filter((n) => n.id !== nodeId); + this.saveToStorage(); + } + } + + setLoading(loading: boolean) { + this.state.loading = loading; + } + + loadFromStorage() { + if (typeof window !== 'undefined') { + const stored = localStorage.getItem('k3s-clusters'); + if (stored) { + try { + const data = JSON.parse(stored); + // Convert date strings back to Date objects + this.state.clusters = data.map((c: any) => ({ + ...c, + createdAt: new Date(c.createdAt), + nodes: c.nodes.map((n: any) => ({ + ...n, + createdAt: new Date(n.createdAt) + })) + })); + } catch (e) { + console.error('Failed to load clusters from storage:', e); + } + } + } + } + + private saveToStorage() { + if (typeof window !== 'undefined') { + localStorage.setItem('k3s-clusters', JSON.stringify(this.state.clusters)); + } + } +} + +export const clustersStore = new ClustersStore(); diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte new file mode 100644 index 0000000..0cf092d --- /dev/null +++ b/src/routes/+layout.svelte @@ -0,0 +1,12 @@ + + + + K3S Management + + + +{@render children()} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte new file mode 100644 index 0000000..0880bc9 --- /dev/null +++ b/src/routes/+page.svelte @@ -0,0 +1,169 @@ + + +
+
+ +
+
+
+ +
+
+ + + +
+

K3S Management

+

Manage your Kubernetes clusters with ease

+
+ + +
+ {#if error} +
+ + + + {error} +
+ {/if} + +
+ + +
+ +
+ + +
+ + +
+ + +
+

+ Default credentials: admin / admin +

+
+
+
+ + +

Powered by K3sup & SvelteKit

+
+
+ + diff --git a/src/routes/admin/+layout.svelte b/src/routes/admin/+layout.svelte new file mode 100644 index 0000000..bdc8a4f --- /dev/null +++ b/src/routes/admin/+layout.svelte @@ -0,0 +1,193 @@ + + +
+ + + + +
+ +
+ + +
+
+

Welcome back,

+

{authStore.user?.username || 'Admin'}

+
+
+
+ + +
+ {@render children()} +
+
+
diff --git a/src/routes/admin/+page.svelte b/src/routes/admin/+page.svelte new file mode 100644 index 0000000..3af2a0c --- /dev/null +++ b/src/routes/admin/+page.svelte @@ -0,0 +1,273 @@ + + +
+ +
+

Dashboard

+

Overview of your K3S infrastructure

+
+ + +
+ +
+
+
+

Total Clusters

+

{stats.totalClusters}

+
+
+ + + +
+
+
+ + +
+
+
+

Active Clusters

+

{stats.activeClusters}

+
+
+ + + +
+
+
+ + +
+
+
+

Total Nodes

+

{stats.totalNodes}

+
+
+ + + +
+
+
+ + +
+
+
+

Active Nodes

+

{stats.activeNodes}

+
+
+ + + +
+
+
+
+ + + + + + {#if clustersStore.clusters.length > 0} +
+
+

Recent Clusters

+ + View all → + +
+
+ {#each clustersStore.clusters.slice(0, 5) as cluster} +
+
+
+ + + +
+
+

{cluster.name}

+

+ {cluster.nodes.length} nodes • {cluster.status} +

+
+
+ + Manage + +
+ {/each} +
+
+ {:else} +
+
+ + + +
+

No Clusters Yet

+

Get started by creating your first K3S cluster

+ + + + + Create Your First Cluster + +
+ {/if} +
diff --git a/src/routes/admin/clusters/+page.svelte b/src/routes/admin/clusters/+page.svelte new file mode 100644 index 0000000..9b7dee2 --- /dev/null +++ b/src/routes/admin/clusters/+page.svelte @@ -0,0 +1,499 @@ + + +
+ +
+
+

K3S Clusters

+

Manage your Kubernetes clusters

+
+ +
+ + + {#if clustersStore.clusters.length > 0} +
+ {#each clustersStore.clusters as cluster} +
+
+
+
+

{cluster.name}

+ + {cluster.status} + +
+

{cluster.description}

+
+ +
+ +
+
+

Nodes

+

{cluster.nodes.length}

+
+
+

K3S Version

+

{cluster.k3sVersion || 'N/A'}

+
+
+

Active Nodes

+

+ {cluster.nodes.filter((n) => n.status === 'active').length} +

+
+
+

Created

+

+ {cluster.createdAt.toLocaleDateString()} +

+
+
+ + +
+

Nodes

+ {#each cluster.nodes as node} +
+
+
+ + + +
+
+

{node.name}

+

{node.ip} • {node.role}

+
+
+ + {node.status} + +
+ {/each} +
+ + +
+ {/each} +
+ {:else} +
+
+ + + +
+

No Clusters Yet

+

+ Create your first K3S cluster to get started with container orchestration +

+ +
+ {/if} +
+ + +{#if showCreateModal} +
e.target === e.currentTarget && closeCreateModal()} + > +
e.stopPropagation()} + > +
+
+

Create New Cluster

+ +
+
+ +
+
+ +
+

Cluster Information

+
+
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ + +
+

Server Node (Master)

+
+
+
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+ + +

+ Note: In production, consider using SSH keys instead of passwords +

+
+
+
+
+ +
+ + +
+
+
+
+{/if} + + +{#if showDeleteModal && clusterToDelete} +
e.target === e.currentTarget && closeDeleteModal()} + > +
e.stopPropagation()} + > +
+
+ + + +
+

Delete Cluster

+

+ Are you sure you want to delete cluster "{clusterToDelete.name}"? This action cannot be undone. +

+
+ + +
+
+
+
+{/if} diff --git a/src/routes/admin/nodes/+page.svelte b/src/routes/admin/nodes/+page.svelte new file mode 100644 index 0000000..9705083 --- /dev/null +++ b/src/routes/admin/nodes/+page.svelte @@ -0,0 +1,562 @@ + + +
+ +
+
+

Nodes

+

Manage nodes across all clusters

+
+ +
+ + +
+
+
+
+

Total Nodes

+

{allNodes.length}

+
+
+ + + +
+
+
+ +
+
+
+

Active Nodes

+

+ {allNodes.filter((n) => n.status === 'active').length} +

+
+
+ + + +
+
+
+ +
+
+
+

Server Nodes

+

+ {allNodes.filter((n) => n.role === 'server').length} +

+
+
+ + + +
+
+
+ +
+
+
+

Agent Nodes

+

+ {allNodes.filter((n) => n.role === 'agent').length} +

+
+
+ + + +
+
+
+
+ + + {#if allNodes.length > 0} +
+
+ + + + + + + + + + + + + + {#each allNodes as node} + + + + + + + + + + {/each} + +
+ Node Name + + Cluster + + IP Address + + Role + + Status + + Created + + Actions +
+
+
+ + + +
+
+

{node.name}

+

{node.ssh.username}@{node.ssh.host}

+
+
+
+
+

{node.clusterName}

+

{node.clusterStatus}

+
+
+

{node.ip}

+
+ + {node.role} + + + + {node.status} + + +

{node.createdAt.toLocaleDateString()}

+
+ +
+
+
+ {:else} +
+
+ + + +
+

No Nodes Yet

+

+ {#if clustersStore.clusters.length === 0} + Create a cluster first before adding nodes + {:else} + Add nodes to your clusters to expand your infrastructure + {/if} +

+ {#if clustersStore.clusters.length > 0} + + {:else} + + Create Cluster + + {/if} +
+ {/if} +
+ + +{#if showAddNodeModal} +
e.target === e.currentTarget && closeAddNodeModal()} + > +
e.stopPropagation()} + > +
+
+

Add New Node

+ +
+
+ +
+
+ +
+ + +
+ + +
+
+ + +
+ +
+ + +
+
+ + +
+

SSH Configuration

+
+
+
+ + +
+
+ + +
+ +
+ + +
+
+ +
+ + +
+
+
+
+ +
+ + +
+
+
+
+{/if} diff --git a/src/routes/admin/settings/+page.svelte b/src/routes/admin/settings/+page.svelte new file mode 100644 index 0000000..9e7b5f1 --- /dev/null +++ b/src/routes/admin/settings/+page.svelte @@ -0,0 +1,360 @@ + + +
+ +
+

Settings

+

Configure your K3S management system

+
+ + + {#if showSaveMessage} +
+ + + + Settings saved successfully! +
+ {/if} + +
+ +
+

K3sup Configuration

+
+
+ + +

+ Version of k3sup to use for cluster installation +

+
+ +
+

About K3sup

+

+ k3sup is a light-weight utility to get from zero to KUBE with k3s on any local or + remote VM. All you need is ssh access and the k3sup binary to get kubectl access + immediately. +

+ + Learn more + + + + +
+
+
+ + +
+

Backup Settings

+
+
+
+

Automatic Backup

+

Automatically backup cluster configurations

+
+ +
+ +
+ + +
+
+
+ + +
+

Notifications

+
+
+
+

Enable Notifications

+

Receive alerts about cluster events

+
+ +
+ +
+ + +
+
+
+ + +
+

Appearance

+
+ + +

Currently only dark mode is supported

+
+
+ + +
+ +
+
+ + +
+

Data Management

+
+
+ + + +
+ + +
+
+ + +
+

System Information

+
+
+ Application Version + v0.0.1 +
+
+ Total Clusters + {clustersStore.clusters.length} +
+
+ Total Nodes + + {clustersStore.clusters.reduce((acc, c) => acc + c.nodes.length, 0)} + +
+
+ Current User + {authStore.user?.username || 'N/A'} +
+
+
+
diff --git a/src/routes/admin/ssh/+page.svelte b/src/routes/admin/ssh/+page.svelte new file mode 100644 index 0000000..18c74ab --- /dev/null +++ b/src/routes/admin/ssh/+page.svelte @@ -0,0 +1,470 @@ + + +
+ +
+

SSH Configurations

+

Manage SSH connections to your nodes

+
+ + +
+ + + +
+

SSH Security Notice

+

+ For production environments, it's recommended to use SSH key-based authentication instead of + passwords. Store your private keys securely and never commit them to version control. +

+
+
+ + +
+
+
+
+

Total Connections

+

{allSSHConfigs.length}

+
+
+ + + +
+
+
+ +
+
+
+

Active Connections

+

+ {allSSHConfigs.filter((c) => c.status === 'active').length} +

+
+
+ + + +
+
+
+ +
+
+
+

Unique Hosts

+

+ {new Set(allSSHConfigs.map((c) => c.ssh.host)).size} +

+
+
+ + + +
+
+
+
+ + + {#if allSSHConfigs.length > 0} +
+
+ + + + + + + + + + + + + {#each allSSHConfigs as config} + + + + + + + + + {/each} + +
+ Node + + Cluster + + SSH Connection + + IP Address + + Status + + Actions +
+
+
+ + + +
+
+

{config.nodeName}

+
+
+
+

{config.clusterName}

+
+
+ + {config.ssh.username}@{config.ssh.host}:{config.ssh.port} + + +
+
+ {config.ip} + +
+
+ + {config.status} + +
+
+ +
+
+
+ {:else} +
+
+ + + +
+

No SSH Configurations

+

+ Create a cluster and add nodes to see SSH configurations here +

+ + Go to Clusters + +
+ {/if} + + +
+

SSH Commands Reference

+
+
+

Connect to a node via SSH:

+ + ssh username@host -p port + +
+
+

Connect using SSH key:

+ + ssh -i /path/to/private_key username@host -p port + +
+
+

Generate SSH key pair:

+ + ssh-keygen -t ed25519 -C "your_email@example.com" + +
+
+

Copy SSH key to remote host:

+ + ssh-copy-id username@host + +
+
+
+
+ + +{#if showTestModal} +
e.target === e.currentTarget && closeTestModal()} + > +
e.stopPropagation()} + > +
+
+

Test SSH Connection

+ +
+
+ +
+ {#if testingConfig} +
+

Testing connection to:

+ + {testingConfig.username}@{testingConfig.host} + +
+ {/if} + + {#if testResult === null} +
+ + + + +

Testing connection...

+
+ {:else if testResult.success} +
+
+ + + +
+

Connection Successful

+

{testResult.message}

+
+
+
+ {:else} +
+
+ + + +
+

Connection Failed

+

{testResult.message}

+
+
+
+ {/if} + + {#if testResult !== null} + + {/if} +
+
+
+{/if} diff --git a/static/robots.txt b/static/robots.txt new file mode 100644 index 0000000..b6dd667 --- /dev/null +++ b/static/robots.txt @@ -0,0 +1,3 @@ +# allow crawling everything by default +User-agent: * +Disallow: diff --git a/svelte.config.js b/svelte.config.js new file mode 100644 index 0000000..a0133e6 --- /dev/null +++ b/svelte.config.js @@ -0,0 +1,17 @@ +import adapter from '@sveltejs/adapter-auto'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + // Consult https://svelte.dev/docs/kit/integrations + // for more information about preprocessors + preprocess: vitePreprocess(), + kit: { + // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. + // If your environment is not supported, or you settled on a specific environment, switch out the adapter. + // See https://svelte.dev/docs/kit/adapters for more information about adapters. + adapter: adapter() + } +}; + +export default config; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..a5567ee --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } + // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias + // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files + // + // To make changes to top-level options such as include and exclude, we recommend extending + // the generated config; see https://svelte.dev/docs/kit/configuration#typescript +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..2d35c4f --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,7 @@ +import tailwindcss from '@tailwindcss/vite'; +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [tailwindcss(), sveltekit()] +});