This commit is contained in:
Kar
2026-02-01 20:38:58 +05:30
parent 52265ed4cc
commit 5e563fb436
11 changed files with 159 additions and 51 deletions

20
.env/cluster1.yaml.sample Normal file
View File

@@ -0,0 +1,20 @@
apiVersion: v1
clusters:
- cluster:
certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURCVENDQWUyZ0F3SUJB>
server: https://192.168.99.37:6443
name: kubernetes
contexts:
- context:
cluster: kubernetes
user: kubernetes-admin
name: kubernetes-admin@kubernetes
current-context: kubernetes-admin@kubernetes
kind: Config
users:
- name: kubernetes-admin
user:
client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURLVENDQWhHZ0F3SUJBZ0l>
client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcFFJQkFBS0NBUUVBNGcyaGF>

View File

@@ -9,9 +9,9 @@ import (
"os/signal"
"sync"
"syscall"
"time"
"deployment-manager/internal/api"
"deployment-manager/internal/config"
"deployment-manager/internal/db"
"deployment-manager/internal/events"
"deployment-manager/internal/reconciler"
@@ -20,13 +20,9 @@ import (
_ "github.com/mattn/go-sqlite3"
)
const (
dbPath = "./manager.db"
maxWorkers = 2
reconcileTick = 2 * time.Second
)
func main() {
cfg := config.Load()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
@@ -40,7 +36,7 @@ func main() {
}()
// ---- DB ----
database, err := sql.Open("sqlite3", dbPath)
database, err := sql.Open("sqlite3", cfg.DBPath)
if err != nil {
log.Fatal(err)
}
@@ -61,24 +57,25 @@ func main() {
// ---- worker pool ----
var wg sync.WaitGroup
for i := 0; i < maxWorkers; i++ {
for i := 0; i < cfg.MaxWorkers; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
w := worker.NewWorker(id, database, eventChan, jobQueue)
w := worker.NewWorker(id, database, eventChan, jobQueue, cfg.Kubeconfig, cfg.Namespace)
w.Run(ctx)
}(i)
}
// ---- reconciler ----
reconciler := reconciler.NewReconciler(database, jobQueue, reconcileTick)
reconciler := reconciler.NewReconciler(database, jobQueue, cfg.ReconcileTick)
go reconciler.Run(ctx)
// ---- HTTP (API + SSE) ----
httpServer := api.NewHTTPServer(database, eventBus)
port := ":" + cfg.HTTPPort
go func() {
log.Println("HTTP server listening on :8080")
if err := httpServer.Start(":8080"); err != nil && err != http.ErrServerClosed {
log.Printf("HTTP server listening on %s", port)
if err := httpServer.Start(port); err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
}()

View File

@@ -33,7 +33,6 @@ func (s *HTTPServer) Start(addr string) error {
// API routes
mux.HandleFunc("/api/repos", s.handleRepos)
mux.HandleFunc("/api/repos/", s.handleRepo)
mux.HandleFunc("/api/repos/", s.handleRepoActions)
// SSE endpoint
@@ -94,20 +93,35 @@ func (s *HTTPServer) handleRepoActions(w http.ResponseWriter, r *http.Request) {
return
}
action := extractAction(r.URL.Path)
switch r.Method {
case http.MethodPost:
switch action {
case "stop":
s.stopRepo(w, r, repoID)
case "restart":
s.restartRepo(w, r, repoID)
// Check if this is an action endpoint
parts := strings.Split(r.URL.Path, "/")
if len(parts) >= 5 {
// This is an action endpoint (/api/repos/{id}/{action})
action := extractAction(r.URL.Path)
switch r.Method {
case http.MethodPost:
switch action {
case "stop":
s.stopRepo(w, r, repoID)
case "restart":
s.restartRepo(w, r, repoID)
default:
http.Error(w, "Invalid action", http.StatusBadRequest)
}
default:
http.Error(w, "Invalid action", http.StatusBadRequest)
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
} else {
// This is a repo detail endpoint (/api/repos/{id})
switch r.Method {
case http.MethodGet:
s.getRepo(w, r, repoID)
case http.MethodDelete:
s.deleteRepo(w, r, repoID)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}

54
internal/config/config.go Normal file
View File

@@ -0,0 +1,54 @@
package config
import (
"os"
"strconv"
"time"
)
type Config struct {
DBPath string
MaxWorkers int
ReconcileTick time.Duration
HTTPPort string
Kubeconfig string
Namespace string
}
func Load() *Config {
cfg := &Config{
DBPath: getEnv("DB_PATH", "./manager.db"),
MaxWorkers: getEnvInt("MAX_WORKERS", 2),
ReconcileTick: getEnvDuration("RECONCILE_TICK", 2*time.Second),
HTTPPort: getEnv("HTTP_PORT", "8080"),
Kubeconfig: getEnv("KUBECONFIG", ".env/cluster1.yaml"),
Namespace: getEnv("NAMESPACE", "default"),
}
return cfg
}
func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
func getEnvInt(key string, defaultValue int) int {
if value := os.Getenv(key); value != "" {
if intValue, err := strconv.Atoi(value); err == nil {
return intValue
}
}
return defaultValue
}
func getEnvDuration(key string, defaultValue time.Duration) time.Duration {
if value := os.Getenv(key); value != "" {
if intValue, err := strconv.Atoi(value); err == nil {
return time.Duration(intValue) * time.Second
}
}
return defaultValue
}

View File

@@ -51,18 +51,23 @@ func generateImageName(repo *model.Repo) string {
func GetDockerfileContent(repoType model.RepoType) string {
switch repoType {
case model.TypeNodeJS:
return `FROM node:18-alpine
return `FROM node:18-alpine as build
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
RUN npm ci
COPY . .
RUN npm run build
EXPOSE 3000
FROM nginx:alpine
CMD ["npm", "start"]`
COPY --from=build /app/build /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]`
case model.TypePython:
return `FROM python:3.11-slim

View File

@@ -4,6 +4,7 @@ import (
"bufio"
"context"
"fmt"
"os"
"os/exec"
"strings"
@@ -12,14 +13,24 @@ import (
)
type KubectlClient struct {
namespace string
namespace string
kubeconfig string
}
func NewKubectlClient(namespace string) *KubectlClient {
func NewKubectlClient(namespace, kubeconfig string) *KubectlClient {
if namespace == "" {
namespace = "default"
}
return &KubectlClient{namespace: namespace}
if kubeconfig == "" {
kubeconfig = os.Getenv("KUBECONFIG")
if kubeconfig == "" {
kubeconfig = ".env/cluster1.yaml"
}
}
return &KubectlClient{
namespace: namespace,
kubeconfig: kubeconfig,
}
}
func (k *KubectlClient) ApplyManifest(ctx context.Context, eventChan chan<- *events.Event, repoID int64, manifest string) error {
@@ -27,6 +38,7 @@ func (k *KubectlClient) ApplyManifest(ctx context.Context, eventChan chan<- *eve
args := []string{
"apply",
"-f", "-",
"--kubeconfig", k.kubeconfig,
"--namespace", k.namespace,
}
@@ -50,9 +62,9 @@ func (k *KubectlClient) ApplyManifest(ctx context.Context, eventChan chan<- *eve
func (k *KubectlClient) DeleteResources(ctx context.Context, eventChan chan<- *events.Event, repoID int64, appName string) error {
commands := [][]string{
{"delete", "deployment", appName, "--namespace", k.namespace},
{"delete", "service", appName, "--namespace", k.namespace},
{"delete", "configmap", appName, "--namespace", k.namespace},
{"delete", "deployment", appName, "--kubeconfig", k.kubeconfig, "--namespace", k.namespace},
{"delete", "service", appName, "--kubeconfig", k.kubeconfig, "--namespace", k.namespace},
{"delete", "configmap", appName, "--kubeconfig", k.kubeconfig, "--namespace", k.namespace},
}
for _, args := range commands {
@@ -74,6 +86,7 @@ func (k *KubectlClient) ScaleDeployment(ctx context.Context, eventChan chan<- *e
"scale",
"deployment", appName,
"--replicas", fmt.Sprintf("%d", replicas),
"--kubeconfig", k.kubeconfig,
"--namespace", k.namespace,
}
@@ -84,6 +97,7 @@ func (k *KubectlClient) GetDeploymentStatus(ctx context.Context, appName string)
cmd := "kubectl"
args := []string{
"get", "deployment", appName,
"--kubeconfig", k.kubeconfig,
"--namespace", k.namespace,
"-o", "jsonpath='{.status.readyReplicas}'",
}

View File

@@ -111,7 +111,7 @@ spec:
func getAppPort(repoType model.RepoType) int {
switch repoType {
case model.TypeNodeJS:
return 3000
return 80 // React apps serve on port 80 with nginx
case model.TypePython:
return 8000
default:

View File

@@ -16,10 +16,10 @@ type DeploymentManager struct {
eventChan chan<- *events.Event
}
func NewDeploymentManager(database *sql.DB, eventChan chan<- *events.Event) *DeploymentManager {
func NewDeploymentManager(database *sql.DB, eventChan chan<- *events.Event, kubeconfig, namespace string) *DeploymentManager {
return &DeploymentManager{
repoStore: db.NewRepoStore(database),
kubectl: k8s.NewKubectlClient("default"),
kubectl: k8s.NewKubectlClient(namespace, kubeconfig),
eventChan: eventChan,
}
}

View File

@@ -13,20 +13,24 @@ import (
)
type Worker struct {
id int
db *sql.DB
repoStore *db.RepoStore
eventChan chan<- *events.Event
jobChan <-chan int64
id int
db *sql.DB
repoStore *db.RepoStore
eventChan chan<- *events.Event
jobChan <-chan int64
kubeconfig string
namespace string
}
func NewWorker(id int, database *sql.DB, eventChan chan<- *events.Event, jobChan <-chan int64) *Worker {
func NewWorker(id int, database *sql.DB, eventChan chan<- *events.Event, jobChan <-chan int64, kubeconfig, namespace string) *Worker {
return &Worker{
id: id,
db: database,
repoStore: db.NewRepoStore(database),
eventChan: eventChan,
jobChan: jobChan,
id: id,
db: database,
repoStore: db.NewRepoStore(database),
eventChan: eventChan,
jobChan: jobChan,
kubeconfig: kubeconfig,
namespace: namespace,
}
}
@@ -137,7 +141,7 @@ func (w *Worker) cloneRepo(ctx context.Context, repo *model.Repo) (string, error
}
func (w *Worker) deployToK8s(ctx context.Context, repo *model.Repo, imageName string) error {
kubectl := k8s.NewKubectlClient("default")
kubectl := k8s.NewKubectlClient(w.namespace, w.kubeconfig)
// Generate manifest
manifest := k8s.GenerateFullManifest(repo, imageName)

BIN
manager

Binary file not shown.

BIN
manager.db Normal file

Binary file not shown.