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