s1
This commit is contained in:
20
.env/cluster1.yaml.sample
Normal file
20
.env/cluster1.yaml.sample
Normal 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>
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -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
54
internal/config/config.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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}'",
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.db
Normal file
BIN
manager.db
Normal file
Binary file not shown.
Reference in New Issue
Block a user