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"
|
"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)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|||||||
@@ -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
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 {
|
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
|
||||||
|
|||||||
@@ -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}'",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.db
Normal file
BIN
manager.db
Normal file
Binary file not shown.
Reference in New Issue
Block a user