Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 143 additions & 3 deletions cmd/devenv/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,27 @@ package main
import (
"fmt"
"os"
"path/filepath"
"time"

"github.com/spf13/cobra"
"github.com/walkerlab/devenv-engine/internal/config"
"github.com/walkerlab/devenv-engine/internal/templates"
)

// DeveloperJob represents work to be done for one developer
type DeveloperJob struct {
Name string
}

// ProcessingResult represents the outcome of processing one developer
type ProcessingResult struct {
Developer string
Success bool
Error error
Duration time.Duration
}

var (
// Command-specific flags for generate
outputDir string
Expand Down Expand Up @@ -45,7 +60,7 @@ Examples:
if verbose {
fmt.Printf("Output directory: %s\n", outputDir)
}
// TODO: implement all developers logic
generateAllDevelopersWithProgress()
} else {
developerName := args[0]
generateSingleDeveloper(developerName)
Expand All @@ -62,6 +77,129 @@ func init() {

}

func generateAllDevelopersWithProgress() {
// Step 1: Discover all developers
developers, err := findAllDevelopers(configDir)
if err != nil {
fmt.Fprintf(os.Stderr, "Error discovering developers: %v\n", err)
os.Exit(1)
}

if len(developers) == 0 {
fmt.Printf("No deveolopers found in %s\n", configDir)
return
}

fmt.Printf("Found %d developers to process.\n", len(developers))

// Step 2: Set up channels for worker communication
const numWorkers = 4
jobs := make(chan DeveloperJob, len(developers))
results := make(chan ProcessingResult, len(developers))

// Step 3: Start worker goroutines
for i := 0; i < numWorkers; i++ {
go developerWorker(jobs, results)
}

// Step 4: Send all jobs to workers
for _, dev := range developers {
jobs <- DeveloperJob{Name: dev}
}
close(jobs)

// Step 5: Collect results
var successCount, failureCount int
var failures []ProcessingResult

for i := 0; i < len(developers); i++ {
result := <-results
if result.Success {
successCount++
fmt.Printf("[%d/%d] ✅ %s (%.1fs)\n",
i+1, len(developers), result.Developer, result.Duration.Seconds())
} else {
failureCount++
failures = append(failures, result)
fmt.Printf("[%d/%d] ❌ %s (%.1fs): %v\n",
i+1, len(developers), result.Developer, result.Duration.Seconds(), result.Error)
}
}

// Step 6: Print final summary
fmt.Printf("\n🎉 Batch processing complete!\n")
fmt.Printf("✅ Successful: %d\n", successCount)
if failureCount > 0 {
fmt.Printf("❌ Failed: %d\n", failureCount)
}

if failureCount > 0 {
fmt.Printf("\nFailures:\n")
for _, failure := range failures {
fmt.Printf(" - %s: %v\n", failure.Developer, failure.Error)
}
os.Exit(1) // Exit with error if any failures
}
}

func developerWorker(jobs <-chan DeveloperJob, results chan<- ProcessingResult) {
for job := range jobs {
startTime := time.Now()
err := processSingleDeveloperForBatchWithError(job.Name)

results <- ProcessingResult{
Developer: job.Name,
Success: err == nil,
Error: err,
Duration: time.Since(startTime),
}
}
}

// processSingleDeveloperForBatchWithError processes a single developer for batch mode
func processSingleDeveloperForBatchWithError(developerName string) error {
if verbose {
fmt.Printf("Processing developer: %s\n", developerName)
}

cfg, err := config.LoadDeveloperConfigWithGlobalDefaults(configDir, developerName)
if err != nil {
return fmt.Errorf("failed to load config: %w", err)
}

// Create user-specific output directory
userOutputDir := filepath.Join(outputDir, developerName)

if !dryRun {
if err := generateManifests(cfg, userOutputDir); err != nil {
return fmt.Errorf("failed to generate manifests: %w", err)
}
}

return nil
}

func findAllDevelopers(configDir string) ([]string, error) {
var developers []string

entries, err := os.ReadDir(configDir)
if err != nil {
return nil, fmt.Errorf("failed to read config directory: %w", err)
}

for _, entry := range entries {
if entry.IsDir() {
// Check to make sure devenv-config.yaml exists in this directory
configPath := filepath.Join(configDir, entry.Name(), "devenv-config.yaml")
if _, err := os.Stat(configPath); err == nil {
developers = append(developers, entry.Name())
}
}
}

return developers, nil
}

// generateSingleDeveloper handles generation for a single developer
func generateSingleDeveloper(developerName string) {
fmt.Printf("Generating manifests for developer: %s\n", developerName)
Expand All @@ -72,6 +210,8 @@ func generateSingleDeveloper(developerName string) {
fmt.Printf("Dry run mode: %t\n", dryRun)
}

userOutputDir := filepath.Join(outputDir, developerName)

cfg, err := config.LoadDeveloperConfigWithGlobalDefaults(configDir, developerName)
if err != nil {
fmt.Fprintf(os.Stderr, "Error loading config for developer %s: %v\n", developerName, err)
Expand All @@ -85,12 +225,12 @@ func generateSingleDeveloper(developerName string) {
}

if !dryRun {
if err := generateManifests(cfg, outputDir); err != nil {
if err := generateManifests(cfg, userOutputDir); err != nil {
fmt.Fprintf(os.Stderr, "Error generating manifests: %v\n", err)
os.Exit(1)
}
} else {
fmt.Printf("🔍 Dry run - would generate manifests to: %s\n", outputDir)
fmt.Printf("🔍 Dry run - would generate manifests to: %s\n", userOutputDir)
}
}

Expand Down
2 changes: 1 addition & 1 deletion internal/config/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ type BaseConfig struct {
InstallHomebrew bool `yaml:"installHomebrew,omitempty"`
ClearLocalPackages bool `yaml:"clearLocalPackages,omitempty"`
ClearVSCodeCache bool `yaml:"clearVSCodeCache,omitempty"`
PythonBinPath string `yaml:"pythonBinPath,omitempty" validate:"omitempty,min=1"`
PythonBinPath string `yaml:"pythonBinPath,omitempty" validate:"omitempty,min=1,filepath"`
}

// DevEnvConfig represents the complete configuration for a developer environment.
Expand Down
10 changes: 0 additions & 10 deletions internal/templates/files/env-setup.tmpl

This file was deleted.

6 changes: 5 additions & 1 deletion internal/templates/files/startup-scripts.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,8 @@ data:

# Static requirements file
requirements.txt: |
{{getStaticScript "requirements.txt" | indent 4}}
{{getStaticScript "requirements.txt" | indent 4}}

# User setup script
setup.sh: |
{{getTemplatedScript "user-setup.sh" . | indent 4}}
7 changes: 0 additions & 7 deletions internal/templates/files/statefulset.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,6 @@ spec:
- name: startup-scripts
mountPath: /scripts
readOnly: true
- name: env-setup
mountPath: /env-setup
readOnly: true
- name: ssh-keys
mountPath: /home/{{.Name}}/.ssh
readOnly: true
Expand All @@ -119,10 +116,6 @@ spec:
configMap:
name: startup-scripts-{{.Name}}
defaultMode: 0755
- name: env-setup
configMap:
name: env-setup-{{.Name}}
defaultMode: 0755
- name: ssh-keys
secret:
secretName: ssh-keys-{{.Name}}
Expand Down
2 changes: 1 addition & 1 deletion internal/templates/renderer.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ func (r *Renderer) RenderTemplate(templateName string, config *config.DevEnvConf
}

func (r *Renderer) RenderAll(config *config.DevEnvConfig) error {
templatesToRender := []string{"statefulset", "service", "env-vars", "secret", "startup-scripts", "env-setup"}
templatesToRender := []string{"statefulset", "service", "env-vars", "secret", "startup-scripts"}
for _, templateName := range templatesToRender {
if err := r.RenderTemplate(templateName, config); err != nil {
return fmt.Errorf("failed to render template %s: %w", templateName, err)
Expand Down
8 changes: 3 additions & 5 deletions internal/templates/scripts/templated/startup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -149,15 +149,15 @@ echo "Section 6: Package installation complete"

# === USER ENVIRONMENT SETUP ===
# Set up environment for the user
if [ -f /env-setup/setup.sh ]; then
if [ -f /scripts/setup.sh ]; then
echo "Running user environment setup script"
sudo -u ${DEV_USERNAME} \
GIT_USER_NAME="{{.Git.Name}}" \
GIT_USER_EMAIL="{{.Git.Email}}" \
ENV_BASH_SCRIPT=${ENV_BASH_SCRIPT} \
ENV_INIT_SCRIPT=${ENV_INIT_SCRIPT} \
PYTHON_BIN_PATH=${PYTHON_BIN_PATH} \
bash /env-setup/setup.sh
bash /scripts/setup.sh
fi

if [ -f "${ENV_INIT_SCRIPT}" ]; then
Expand All @@ -175,9 +175,7 @@ echo "Clearing VSCode server cache"
rm -rf /home/${DEV_USERNAME}/.vscode-server/
{{- end}}

# Add default VSCode remote machine config, setting default python path
mkdir -p /home/${DEV_USERNAME}/.vscode-server/data/Machine
echo "{\"python.defaultInterpreterPath\": \"${PYTHON_PATH}\"}" > /home/${DEV_USERNAME}/.vscode-server/data/Machine/settings.json

# Make sure .vscode-server directory is owned by ${DEV_USERNAME}
chown -R ${DEV_USERNAME}:${DEV_USERNAME} /home/${DEV_USERNAME}/.vscode-server

Expand Down
Loading