init
This commit is contained in:
128
internal/k8s/kubectl.go
Normal file
128
internal/k8s/kubectl.go
Normal 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
132
internal/k8s/manifests.go
Normal 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()
|
||||
}
|
||||
Reference in New Issue
Block a user