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

View File

@@ -33,7 +33,6 @@ func (s *HTTPServer) Start(addr string) error {
// API routes // API routes
mux.HandleFunc("/api/repos", s.handleRepos) mux.HandleFunc("/api/repos", s.handleRepos)
mux.HandleFunc("/api/repos/", s.handleRepo)
mux.HandleFunc("/api/repos/", s.handleRepoActions) mux.HandleFunc("/api/repos/", s.handleRepoActions)
// SSE endpoint // SSE endpoint
@@ -94,6 +93,10 @@ func (s *HTTPServer) handleRepoActions(w http.ResponseWriter, r *http.Request) {
return return
} }
// 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) action := extractAction(r.URL.Path)
switch r.Method { switch r.Method {
@@ -109,6 +112,17 @@ func (s *HTTPServer) handleRepoActions(w http.ResponseWriter, r *http.Request) {
default: default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) 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)
}
}
} }
func (s *HTTPServer) getRepos(w http.ResponseWriter, r *http.Request) { func (s *HTTPServer) getRepos(w http.ResponseWriter, r *http.Request) {

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 { func GetDockerfileContent(repoType model.RepoType) string {
switch repoType { switch repoType {
case model.TypeNodeJS: case model.TypeNodeJS:
return `FROM node:18-alpine return `FROM node:18-alpine as build
WORKDIR /app WORKDIR /app
COPY package*.json ./ COPY package*.json ./
RUN npm ci --only=production RUN npm ci
COPY . . 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: case model.TypePython:
return `FROM python:3.11-slim return `FROM python:3.11-slim

View File

@@ -4,6 +4,7 @@ import (
"bufio" "bufio"
"context" "context"
"fmt" "fmt"
"os"
"os/exec" "os/exec"
"strings" "strings"
@@ -13,13 +14,23 @@ import (
type KubectlClient struct { type KubectlClient struct {
namespace string namespace string
kubeconfig string
} }
func NewKubectlClient(namespace string) *KubectlClient { func NewKubectlClient(namespace, kubeconfig string) *KubectlClient {
if namespace == "" { if namespace == "" {
namespace = "default" 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 { 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{ args := []string{
"apply", "apply",
"-f", "-", "-f", "-",
"--kubeconfig", k.kubeconfig,
"--namespace", k.namespace, "--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 { func (k *KubectlClient) DeleteResources(ctx context.Context, eventChan chan<- *events.Event, repoID int64, appName string) error {
commands := [][]string{ commands := [][]string{
{"delete", "deployment", appName, "--namespace", k.namespace}, {"delete", "deployment", appName, "--kubeconfig", k.kubeconfig, "--namespace", k.namespace},
{"delete", "service", appName, "--namespace", k.namespace}, {"delete", "service", appName, "--kubeconfig", k.kubeconfig, "--namespace", k.namespace},
{"delete", "configmap", appName, "--namespace", k.namespace}, {"delete", "configmap", appName, "--kubeconfig", k.kubeconfig, "--namespace", k.namespace},
} }
for _, args := range commands { for _, args := range commands {
@@ -74,6 +86,7 @@ func (k *KubectlClient) ScaleDeployment(ctx context.Context, eventChan chan<- *e
"scale", "scale",
"deployment", appName, "deployment", appName,
"--replicas", fmt.Sprintf("%d", replicas), "--replicas", fmt.Sprintf("%d", replicas),
"--kubeconfig", k.kubeconfig,
"--namespace", k.namespace, "--namespace", k.namespace,
} }
@@ -84,6 +97,7 @@ func (k *KubectlClient) GetDeploymentStatus(ctx context.Context, appName string)
cmd := "kubectl" cmd := "kubectl"
args := []string{ args := []string{
"get", "deployment", appName, "get", "deployment", appName,
"--kubeconfig", k.kubeconfig,
"--namespace", k.namespace, "--namespace", k.namespace,
"-o", "jsonpath='{.status.readyReplicas}'", "-o", "jsonpath='{.status.readyReplicas}'",
} }

View File

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

View File

@@ -16,10 +16,10 @@ type DeploymentManager struct {
eventChan chan<- *events.Event 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{ return &DeploymentManager{
repoStore: db.NewRepoStore(database), repoStore: db.NewRepoStore(database),
kubectl: k8s.NewKubectlClient("default"), kubectl: k8s.NewKubectlClient(namespace, kubeconfig),
eventChan: eventChan, eventChan: eventChan,
} }
} }

View File

@@ -18,15 +18,19 @@ type Worker struct {
repoStore *db.RepoStore repoStore *db.RepoStore
eventChan chan<- *events.Event eventChan chan<- *events.Event
jobChan <-chan int64 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{ return &Worker{
id: id, id: id,
db: database, db: database,
repoStore: db.NewRepoStore(database), repoStore: db.NewRepoStore(database),
eventChan: eventChan, eventChan: eventChan,
jobChan: jobChan, 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 { 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 // Generate manifest
manifest := k8s.GenerateFullManifest(repo, imageName) manifest := k8s.GenerateFullManifest(repo, imageName)

BIN
manager

Binary file not shown.

BIN
manager.db Normal file

Binary file not shown.