Files
deployment-manager/internal/k8s/kubectl.go
2026-02-01 20:22:29 +05:30

129 lines
3.6 KiB
Go

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))
}