diff --git a/.env/cluster1.yaml.sample b/.env/cluster1.yaml.sample new file mode 100644 index 0000000..1e82760 --- /dev/null +++ b/.env/cluster1.yaml.sample @@ -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> + + diff --git a/cmd/manager/main.go b/cmd/manager/main.go index 742beff..3baa251 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -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) } }() diff --git a/internal/api/http.go b/internal/api/http.go index ded6a7a..0838592 100644 --- a/internal/api/http.go +++ b/internal/api/http.go @@ -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) } } diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..5d798c2 --- /dev/null +++ b/internal/config/config.go @@ -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 +} diff --git a/internal/executor/docker.go b/internal/executor/docker.go index b3191d9..ed67f5b 100644 --- a/internal/executor/docker.go +++ b/internal/executor/docker.go @@ -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 diff --git a/internal/k8s/kubectl.go b/internal/k8s/kubectl.go index c092269..7e28877 100644 --- a/internal/k8s/kubectl.go +++ b/internal/k8s/kubectl.go @@ -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}'", } diff --git a/internal/k8s/manifests.go b/internal/k8s/manifests.go index 425d7ee..26372c8 100644 --- a/internal/k8s/manifests.go +++ b/internal/k8s/manifests.go @@ -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: diff --git a/internal/worker/deploy.go b/internal/worker/deploy.go index 0a78b50..994e22b 100644 --- a/internal/worker/deploy.go +++ b/internal/worker/deploy.go @@ -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, } } diff --git a/internal/worker/pool.go b/internal/worker/pool.go index 7c0af03..15752cc 100644 --- a/internal/worker/pool.go +++ b/internal/worker/pool.go @@ -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) diff --git a/manager b/manager index cafe3b8..1d56ac7 100755 Binary files a/manager and b/manager differ diff --git a/manager.db b/manager.db new file mode 100644 index 0000000..e7bfd1a Binary files /dev/null and b/manager.db differ