This commit is contained in:
Kar
2026-02-01 20:22:29 +05:30
commit 52265ed4cc
30 changed files with 2058 additions and 0 deletions

128
internal/k8s/kubectl.go Normal file
View File

@@ -0,0 +1,128 @@
package k8s
import (
"bufio"
"context"
"fmt"
"os/exec"
"strings"
"deployment-manager/internal/events"
"deployment-manager/internal/model"
)
type KubectlClient struct {
namespace string
}
func NewKubectlClient(namespace string) *KubectlClient {
if namespace == "" {
namespace = "default"
}
return &KubectlClient{namespace: namespace}
}
func (k *KubectlClient) ApplyManifest(ctx context.Context, eventChan chan<- *events.Event, repoID int64, manifest string) error {
cmd := "kubectl"
args := []string{
"apply",
"-f", "-",
"--namespace", k.namespace,
}
// Use kubectl with stdin for the manifest
kubectlCmd := exec.CommandContext(ctx, cmd, args...)
kubectlCmd.Stdin = strings.NewReader(manifest)
stdout, _ := kubectlCmd.StdoutPipe()
stderr, _ := kubectlCmd.StderrPipe()
if err := kubectlCmd.Start(); err != nil {
return fmt.Errorf("failed to start kubectl apply: %w", err)
}
// Stream output
go streamOutput(stdout, eventChan, repoID, "kubectl")
go streamOutput(stderr, eventChan, repoID, "kubectl")
return kubectlCmd.Wait()
}
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},
}
for _, args := range commands {
cmd := "kubectl"
if err := runKubectlCommand(ctx, eventChan, repoID, cmd, args...); err != nil {
// Don't fail if resources don't exist
if !strings.Contains(err.Error(), "not found") {
return fmt.Errorf("failed to delete resource with args %v: %w", args, err)
}
}
}
return nil
}
func (k *KubectlClient) ScaleDeployment(ctx context.Context, eventChan chan<- *events.Event, repoID int64, appName string, replicas int) error {
cmd := "kubectl"
args := []string{
"scale",
"deployment", appName,
"--replicas", fmt.Sprintf("%d", replicas),
"--namespace", k.namespace,
}
return runKubectlCommand(ctx, eventChan, repoID, cmd, args...)
}
func (k *KubectlClient) GetDeploymentStatus(ctx context.Context, appName string) (string, error) {
cmd := "kubectl"
args := []string{
"get", "deployment", appName,
"--namespace", k.namespace,
"-o", "jsonpath='{.status.readyReplicas}'",
}
output, err := exec.CommandContext(ctx, cmd, args...).Output()
if err != nil {
return "", fmt.Errorf("failed to get deployment status: %w", err)
}
return strings.Trim(string(output), "'"), nil
}
func runKubectlCommand(ctx context.Context, eventChan chan<- *events.Event, repoID int64, cmd string, args ...string) error {
kubectlCmd := exec.CommandContext(ctx, cmd, args...)
stdout, _ := kubectlCmd.StdoutPipe()
stderr, _ := kubectlCmd.StderrPipe()
if err := kubectlCmd.Start(); err != nil {
return fmt.Errorf("failed to start kubectl command: %w", err)
}
go streamOutput(stdout, eventChan, repoID, "kubectl")
go streamOutput(stderr, eventChan, repoID, "kubectl")
return kubectlCmd.Wait()
}
func streamOutput(r interface{ Read([]byte) (int, error) }, eventChan chan<- *events.Event, repoID int64, source string) {
scanner := bufio.NewScanner(r)
for scanner.Scan() {
event := events.NewLogEvent(repoID, scanner.Text())
event.Data["source"] = source
eventChan <- event
}
}
func GetAppName(repo *model.Repo) string {
// Generate a consistent app name based on repo
parts := strings.Split(strings.TrimSuffix(repo.RepoURL, ".git"), "/")
repoName := parts[len(parts)-1]
return fmt.Sprintf("repo-%d-%s", repo.ID, strings.ToLower(repoName))
}

132
internal/k8s/manifests.go Normal file
View File

@@ -0,0 +1,132 @@
package k8s
import (
"fmt"
"strings"
"deployment-manager/internal/model"
)
func GenerateDeploymentManifest(repo *model.Repo, imageName string) string {
appName := GetAppName(repo)
port := getAppPort(repo.Type)
return fmt.Sprintf(`apiVersion: apps/v1
kind: Deployment
metadata:
name: %s
labels:
app: %s
repo-id: "%d"
spec:
replicas: 1
selector:
matchLabels:
app: %s
template:
metadata:
labels:
app: %s
repo-id: "%d"
spec:
containers:
- name: %s
image: %s
ports:
- containerPort: %d
env:
- name: PORT
value: "%d"
resources:
requests:
memory: "64Mi"
cpu: "50m"
limits:
memory: "256Mi"
cpu: "200m"
---
apiVersion: v1
kind: Service
metadata:
name: %s
labels:
app: %s
repo-id: "%d"
spec:
selector:
app: %s
ports:
- port: 80
targetPort: %d
protocol: TCP
type: ClusterIP
`, appName, appName, repo.ID, appName, appName, repo.ID, appName, imageName, port, port, appName, appName, repo.ID, appName, port)
}
func GenerateConfigMapManifest(repo *model.Repo) string {
appName := GetAppName(repo)
return fmt.Sprintf(`apiVersion: v1
kind: ConfigMap
metadata:
name: %s
labels:
app: %s
repo-id: "%d"
data:
repo-url: "%s"
repo-type: "%s"
user-id: "%s"
`, appName, appName, repo.ID, repo.RepoURL, repo.Type, repo.UserID)
}
func GenerateIngressManifest(repo *model.Repo) string {
appName := GetAppName(repo)
host := fmt.Sprintf("repo-%d.example.com", repo.ID)
return fmt.Sprintf(`apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: %s
labels:
app: %s
repo-id: "%d"
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- host: %s
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: %s
port:
number: 80
`, appName, appName, repo.ID, host, appName)
}
func getAppPort(repoType model.RepoType) int {
switch repoType {
case model.TypeNodeJS:
return 3000
case model.TypePython:
return 8000
default:
return 8080
}
}
func GenerateFullManifest(repo *model.Repo, imageName string) string {
var manifest strings.Builder
manifest.WriteString(GenerateDeploymentManifest(repo, imageName))
manifest.WriteString("\n---\n")
manifest.WriteString(GenerateConfigMapManifest(repo))
manifest.WriteString("\n---\n")
manifest.WriteString(GenerateIngressManifest(repo))
return manifest.String()
}