From efcfd03e3b8cba7aa6b1f433133fa05b0b6f8722 Mon Sep 17 00:00:00 2001 From: Mohit Nagaraj Date: Tue, 14 Apr 2026 20:08:30 +0530 Subject: [PATCH] fix: embed compose files, fix module path, MongoDB refs, lint and security MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Embed all docker-compose files into binary via go:embed so orchcli init writes them to docker/ — fixes broken init+start flow - Fix Go module path: github.com/kubeorchestra/cli → github.com/kubeorch/cli to match GitHub org; fixes version injection via ldflags in releases - Replace all PostgreSQL references with MongoDB (waitForMongoDB, exec service map, next-steps output) - Auto-generate core/config.yaml (with random secrets) and ui/.env.local during orchcli init based on dev mode - Fix Docker image names: ghcr.io/kubeorchestra/* → ghcr.io/kubeorch/* - Fix exec.go to use actual running container name (resolves -dev/-hybrid suffix issue in dev and hybrid modes) - Fix API_URL_INTERNAL consistency across all compose files - Fix lint: replace magic numbers with named constants (dirPerm, configFilePerm, composeFilePerm, secretKeyBytes) - Fix gosec G306: add #nosec annotations for compose/env files (0644 is intentional — these files contain no secrets) - Fix Windows CI: add shell: bash to steps using bash syntax Fixes #45 Signed-off-by: Mohit Nagaraj --- .github/workflows/test.yml | 2 + cmd/docker/docker-compose.dev.yml | 33 ++++++++++ cmd/docker/docker-compose.hybrid-core.yml | 76 ++++++++++++++++++++++ cmd/docker/docker-compose.hybrid-ui.yml | 54 ++++++++++++++++ cmd/docker/docker-compose.prod.yml | 68 ++++++++++++++++++++ cmd/embedded.go | 12 ++++ cmd/exec.go | 51 ++++++++------- cmd/init.go | 40 ++++++++++-- cmd/start.go | 27 ++++---- cmd/templates/config.yaml | 26 ++++++++ cmd/templates/env.local | 4 ++ cmd/utils.go | 77 +++++++++++++++++++++++ docker/docker-compose.hybrid-core.yml | 4 +- docker/docker-compose.hybrid-ui.yml | 2 +- docker/docker-compose.prod.yml | 4 +- go.mod | 2 +- main.go | 2 +- tests/integration/cli_integration_test.go | 2 +- tests/unit/cmd_test.go | 64 ++++++++++--------- tests/unit/concurrent_test.go | 2 +- tests/unit/config_concurrent_test.go | 2 +- tests/unit/config_test.go | 2 +- tests/unit/utils_test.go | 2 +- 23 files changed, 477 insertions(+), 81 deletions(-) create mode 100644 cmd/docker/docker-compose.dev.yml create mode 100644 cmd/docker/docker-compose.hybrid-core.yml create mode 100644 cmd/docker/docker-compose.hybrid-ui.yml create mode 100644 cmd/docker/docker-compose.prod.yml create mode 100644 cmd/embedded.go create mode 100644 cmd/templates/config.yaml create mode 100644 cmd/templates/env.local diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 320dbdc..b38d289 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -56,6 +56,7 @@ jobs: go mod verify - name: Run tests with coverage + shell: bash run: | go test -v -race -coverprofile=coverage.out -covermode=atomic ./... go tool cover -func=coverage.out @@ -73,6 +74,7 @@ jobs: - name: Generate coverage badge if: matrix.go-version == '1.22' + shell: bash run: | COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//') echo "COVERAGE=$COVERAGE" >> $GITHUB_ENV diff --git a/cmd/docker/docker-compose.dev.yml b/cmd/docker/docker-compose.dev.yml new file mode 100644 index 0000000..17257b5 --- /dev/null +++ b/cmd/docker/docker-compose.dev.yml @@ -0,0 +1,33 @@ +version: '3.8' + +# Development mode - both UI and Core run on host +# Only MongoDB runs in Docker + +services: + mongodb: + image: mongo:8.0 + container_name: kubeorchestra-mongodb-dev + restart: unless-stopped + environment: + MONGO_INITDB_DATABASE: kubeorchestra + ports: + - "27017:27017" + volumes: + - mongodb_data:/data/db + - mongodb_config:/data/configdb + healthcheck: + test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + +# UI runs on host: cd ui && npm install && npm run dev (port 3001) +# Core runs on host: cd core && air (port 3000) +# Both connect to MongoDB at localhost:27017 + +volumes: + mongodb_data: + name: kubeorchestra_mongodb_data_dev + mongodb_config: + name: kubeorchestra_mongodb_config_dev \ No newline at end of file diff --git a/cmd/docker/docker-compose.hybrid-core.yml b/cmd/docker/docker-compose.hybrid-core.yml new file mode 100644 index 0000000..8c6986b --- /dev/null +++ b/cmd/docker/docker-compose.hybrid-core.yml @@ -0,0 +1,76 @@ +version: '3.8' + +# Hybrid Core mode - Core repo cloned, UI from Docker image +# All services run in Docker for simplicity + +networks: + kubeorchestra-net: + driver: bridge + name: kubeorchestra-network-hybrid + +services: + mongodb: + image: mongo:8.0 + container_name: kubeorchestra-mongodb-hybrid + restart: unless-stopped + networks: + - kubeorchestra-net + environment: + MONGO_INITDB_DATABASE: kubeorchestra + ports: + - "27017:27017" + volumes: + - mongodb_data:/data/db + - mongodb_config:/data/configdb + healthcheck: + test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + + # Core runs in container with mounted code for hot reload + # This way backend devs don't need Go installed locally + core: + image: cosmtrek/air:latest + container_name: kubeorchestra-core-hybrid + restart: unless-stopped + networks: + - kubeorchestra-net + volumes: + - ../core:/app + - go-modules:/go/pkg/mod # Cache Go modules + working_dir: /app + ports: + - "3000:3000" + environment: + MONGO_URI: mongodb://mongodb:27017/kubeorchestra + MONGO_DB_NAME: kubeorchestra + depends_on: + mongodb: + condition: service_healthy + command: air + + ui: + image: ghcr.io/kubeorch/ui:latest + container_name: kubeorchestra-ui-hybrid + restart: unless-stopped + networks: + - kubeorchestra-net + ports: + - "3001:3001" + environment: + # Both use internal Docker network names + NEXT_PUBLIC_API_URL: http://localhost:3000 + API_URL_INTERNAL: http://core:3000 + PORT: 3001 + depends_on: + - core + +volumes: + mongodb_data: + name: kubeorchestra_mongodb_data_hybrid + mongodb_config: + name: kubeorchestra_mongodb_config_hybrid + go-modules: + name: kubeorchestra_go_modules \ No newline at end of file diff --git a/cmd/docker/docker-compose.hybrid-ui.yml b/cmd/docker/docker-compose.hybrid-ui.yml new file mode 100644 index 0000000..3c75969 --- /dev/null +++ b/cmd/docker/docker-compose.hybrid-ui.yml @@ -0,0 +1,54 @@ +version: '3.8' + +# Hybrid UI mode - UI runs on host, Core from Docker image +# MongoDB and Core run in Docker + +networks: + kubeorchestra-net: + driver: bridge + name: kubeorchestra-network-hybrid + +services: + mongodb: + image: mongo:8.0 + container_name: kubeorchestra-mongodb-hybrid + restart: unless-stopped + networks: + - kubeorchestra-net + environment: + MONGO_INITDB_DATABASE: kubeorchestra + ports: + - "27017:27017" + volumes: + - mongodb_data:/data/db + - mongodb_config:/data/configdb + healthcheck: + test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + + core: + image: ghcr.io/kubeorch/core:latest + container_name: kubeorchestra-core-hybrid + restart: unless-stopped + networks: + - kubeorchestra-net + ports: + - "3000:3000" + environment: + MONGO_URI: mongodb://mongodb:27017/kubeorchestra + MONGO_DB_NAME: kubeorchestra + depends_on: + mongodb: + condition: service_healthy + +# UI runs on host: cd ui && npm install && npm run dev (port 3001) +# UI connects to Core at localhost:3000 + +volumes: + mongodb_data: + name: kubeorchestra_mongodb_data_hybrid + mongodb_config: + name: kubeorchestra_mongodb_config_hybrid \ No newline at end of file diff --git a/cmd/docker/docker-compose.prod.yml b/cmd/docker/docker-compose.prod.yml new file mode 100644 index 0000000..a396f5e --- /dev/null +++ b/cmd/docker/docker-compose.prod.yml @@ -0,0 +1,68 @@ +version: '3.8' + +networks: + kubeorchestra-net: + driver: bridge + name: kubeorchestra-network + +services: + mongodb: + image: mongo:8.0 + container_name: kubeorchestra-mongodb + restart: unless-stopped + networks: + - kubeorchestra-net + environment: + MONGO_INITDB_DATABASE: kubeorchestra + ports: + - "27017:27017" + expose: + - "27017" + volumes: + - mongodb_data:/data/db + - mongodb_config:/data/configdb + healthcheck: + test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + + core: + image: ghcr.io/kubeorch/core:latest + container_name: kubeorchestra-core + restart: unless-stopped + networks: + - kubeorchestra-net + ports: + - "3000:3000" + expose: + - "3000" + environment: + MONGO_URI: mongodb://mongodb:27017/kubeorchestra + MONGO_DB_NAME: kubeorchestra + depends_on: + mongodb: + condition: service_healthy + + ui: + image: ghcr.io/kubeorch/ui:latest + container_name: kubeorchestra-ui + restart: unless-stopped + networks: + - kubeorchestra-net + ports: + - "3001:3001" + expose: + - "3001" + environment: + NEXT_PUBLIC_API_URL: http://localhost:3000 + API_URL_INTERNAL: http://core:3000 + depends_on: + - core + +volumes: + mongodb_data: + name: kubeorchestra_mongodb_data + mongodb_config: + name: kubeorchestra_mongodb_config \ No newline at end of file diff --git a/cmd/embedded.go b/cmd/embedded.go new file mode 100644 index 0000000..09372d6 --- /dev/null +++ b/cmd/embedded.go @@ -0,0 +1,12 @@ +package cmd + +import "embed" + +//go:embed docker/* +var embeddedComposeFiles embed.FS + +//go:embed templates/config.yaml +var configTemplate string + +//go:embed templates/env.local +var envLocalTemplate string diff --git a/cmd/exec.go b/cmd/exec.go index a8ea77e..79f0b3a 100644 --- a/cmd/exec.go +++ b/cmd/exec.go @@ -13,19 +13,19 @@ var execCmd = &cobra.Command{ Use: "exec [command]", Short: "Execute a command in a running service container", Long: `Execute a command in a running service container. - -Services: postgres, core, ui + +Services: mongodb, core, ui Examples: - # Open PostgreSQL shell - orchcli exec postgres psql -U kubeorchestra -d kubeorchestra - + # Open MongoDB shell + orchcli exec mongodb mongosh kubeorchestra + # Run migrations in Core orchcli exec core go run . migrate - + # Open bash shell in UI container orchcli exec ui bash - + # Check Node version in UI container orchcli exec ui node --version`, Args: cobra.MinimumNArgs(1), @@ -43,26 +43,33 @@ func runExec(cmd *cobra.Command, args []string) error { service := args[0] validServices := map[string]string{ - "postgres": "kubeorchestra-postgres", - "core": "kubeorchestra-core", - "ui": "kubeorchestra-ui", + "mongodb": "kubeorchestra-mongodb", + "core": "kubeorchestra-core", + "ui": "kubeorchestra-ui", } containerName, valid := validServices[service] if !valid { - return fmt.Errorf("invalid service: %s. Valid services: postgres, core, ui", service) + return fmt.Errorf("invalid service: %s. Valid services: mongodb, core, ui", service) } - // Check if container is running + // Find the actual running container — name may have a -dev or -hybrid suffix + // depending on which orchcli init mode was used. // #nosec G204 -- containerName is validated from a fixed allowlist above checkCmd := exec.Command("docker", "ps", "--filter", fmt.Sprintf("name=%s", containerName), "--format", "{{.Names}}") output, err := checkCmd.Output() - if err != nil || strings.TrimSpace(string(output)) == "" { + // Take the first line only — docker ps may return multiple matches. + trimmed := strings.TrimSpace(string(output)) + if idx := strings.IndexByte(trimmed, '\n'); idx != -1 { + trimmed = trimmed[:idx] + } + actualName := trimmed + if err != nil || actualName == "" { return fmt.Errorf("service %s is not running. Start it with: orchcli start", service) } - // Build docker exec command - dockerArgs := []string{"exec", "-it", containerName} + // Build docker exec command using the actual running container name + dockerArgs := []string{"exec", "-it", actualName} if len(args) > 1 { // Specific command provided @@ -70,8 +77,8 @@ func runExec(cmd *cobra.Command, args []string) error { } else { // Default shells for each service switch service { - case "postgres": - dockerArgs = append(dockerArgs, "psql", "-U", "kubeorchestra", "-d", "kubeorchestra") + case "mongodb": + dockerArgs = append(dockerArgs, "mongosh", "kubeorchestra") case "core": dockerArgs = append(dockerArgs, "sh") case "ui": @@ -80,10 +87,10 @@ func runExec(cmd *cobra.Command, args []string) error { } // #nosec G204 -- dockerArgs are constructed from validated inputs - execCmd := exec.Command("docker", dockerArgs...) - execCmd.Stdout = os.Stdout - execCmd.Stderr = os.Stderr - execCmd.Stdin = os.Stdin + execCommand := exec.Command("docker", dockerArgs...) + execCommand.Stdout = os.Stdout + execCommand.Stderr = os.Stderr + execCommand.Stdin = os.Stdin - return execCmd.Run() + return execCommand.Run() } diff --git a/cmd/init.go b/cmd/init.go index 5236a5d..73027d6 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -86,14 +86,18 @@ func setupProduction() error { return fmt.Errorf("failed to get current directory: %w", err) } - const dirMode = 0750 dirs := []string{"docker", "scripts"} for _, dir := range dirs { - if err := os.MkdirAll(dir, dirMode); err != nil { + if err := os.MkdirAll(dir, dirPerm); err != nil { return fmt.Errorf("failed to create directory %s: %w", dir, err) } } + // Write embedded docker-compose files + if err := writeEmbeddedComposeFiles(filepath.Join(cwd, "docker")); err != nil { + return fmt.Errorf("failed to write docker-compose files: %w", err) + } + // Save project configuration if err := setProjectConfig(cwd, "", ""); err != nil { fmt.Printf("⚠️ warning: failed to save project configuration: %v\n", err) @@ -101,9 +105,9 @@ func setupProduction() error { fmt.Println("\n✅ Production environment ready!") fmt.Printf("📁 Project initialized at: %s\n", cwd) - fmt.Println("\n📝 Image tags that will be used:") - fmt.Println(" - ghcr.io/kubeorch/ui:latest") + fmt.Println("\n📝 Docker images that will be used:") fmt.Println(" - ghcr.io/kubeorch/core:latest") + fmt.Println(" - ghcr.io/kubeorch/ui:latest") fmt.Println("\n You can specify versions with: orchcli start --version=v1.2.3") fmt.Println(" Run 'orchcli start' to start services with latest images") return nil @@ -179,6 +183,15 @@ func setupDevelopment(cloneUI, cloneCore bool) error { } } + // Write embedded docker-compose files + dockerDir := filepath.Join(cwd, "docker") + if err := os.MkdirAll(dockerDir, dirPerm); err != nil { + return fmt.Errorf("failed to create docker directory: %w", err) + } + if err := writeEmbeddedComposeFiles(dockerDir); err != nil { + return fmt.Errorf("failed to write docker-compose files: %w", err) + } + // Setup upstreams for forks (sequential as they're quick) if cloneUI && uiIsFork { fmt.Println("🔗 Setting up upstream for UI fork...") @@ -237,6 +250,25 @@ func setupDevelopment(cloneUI, cloneCore bool) error { } } + // Generate config files with sensible defaults + if cloneCore { + configPath := filepath.Join(corePath, "config.yaml") + if err := writeConfigYAML(configPath); err != nil { + fmt.Printf("⚠️ warning: failed to generate config.yaml: %v\n", err) + } else { + fmt.Println("✅ Generated core/config.yaml with default values") + } + } + + if cloneUI { + envPath := filepath.Join(uiPath, ".env.local") + if err := writeEnvLocal(envPath); err != nil { + fmt.Printf("⚠️ warning: failed to generate .env.local: %v\n", err) + } else { + fmt.Println("✅ Generated ui/.env.local with default API URL") + } + } + // Save project configuration if err := setProjectConfig(cwd, uiPath, corePath); err != nil { fmt.Printf("⚠️ warning: failed to save project configuration: %v\n", err) diff --git a/cmd/start.go b/cmd/start.go index 28ba752..b97ed6c 100644 --- a/cmd/start.go +++ b/cmd/start.go @@ -92,12 +92,12 @@ func runStart(cmd *cobra.Command, args []string) error { fmt.Println("✅ docker services started in background") fmt.Println() - fmt.Println("⏳ waiting for postgres to be ready...") - if err := waitForPostgres(); err != nil { + fmt.Println("⏳ waiting for mongodb to be ready...") + if err := waitForMongoDB(); err != nil { fmt.Printf("⚠️ warning: %v\n", err) fmt.Println(" services may take a moment to be fully ready") } else { - fmt.Println("✅ postgres is ready") + fmt.Println("✅ mongodb is ready") } fmt.Println() @@ -111,14 +111,14 @@ func runStart(cmd *cobra.Command, args []string) error { fmt.Println() fmt.Println(" core will run on http://localhost:3000") fmt.Println(" ui will run on http://localhost:3001") - fmt.Println(" postgresql is at localhost:5432") + fmt.Println(" mongodb is at localhost:27017") case uiLocal: fmt.Println("📝 next steps for ui development:") fmt.Printf(" start ui: cd %s && npm run dev\n", projectConfig.UIPath) fmt.Println() fmt.Println(" ui will run on http://localhost:3001") fmt.Println(" core api is at http://localhost:3000 (docker)") - fmt.Println(" postgresql is at localhost:5432 (docker)") + fmt.Println(" mongodb is at localhost:27017 (docker)") case coreLocal: fmt.Println("📝 backend development mode:") fmt.Println(" ✅ core is running in docker with your code mounted") @@ -126,14 +126,14 @@ func runStart(cmd *cobra.Command, args []string) error { fmt.Println() fmt.Println(" core api: http://localhost:3000 (docker with mounted code)") fmt.Println(" ui: http://localhost:3001 (docker)") - fmt.Println(" postgresql: localhost:5432 (docker)") + fmt.Println(" mongodb: localhost:27017 (docker)") fmt.Println() fmt.Println(" note: no go installation required!") default: fmt.Println("📊 all services running in docker:") fmt.Println(" ui: http://localhost:3001") fmt.Println(" api: http://localhost:3000") - fmt.Println(" postgresql: localhost:5432") + fmt.Println(" mongodb: localhost:27017") } fmt.Println() @@ -145,17 +145,18 @@ func runStart(cmd *cobra.Command, args []string) error { return nil } -func waitForPostgres() error { +func waitForMongoDB() error { maxRetries := 30 containerNames := []string{ - "kubeorchestra-postgres", - "kubeorchestra-postgres-dev", - "kubeorchestra-postgres-hybrid", + "kubeorchestra-mongodb", + "kubeorchestra-mongodb-dev", + "kubeorchestra-mongodb-hybrid", } for i := 0; i < maxRetries; i++ { for _, name := range containerNames { - cmd := exec.Command("docker", "exec", name, "pg_isready", "-U", "kubeorchestra", "-d", "kubeorchestra") + // #nosec G204 -- name is from a hardcoded list of known container names + cmd := exec.Command("docker", "exec", name, "mongosh", "--eval", "db.adminCommand('ping')") if err := cmd.Run(); err == nil { return nil } @@ -164,5 +165,5 @@ func waitForPostgres() error { _ = exec.Command("sleep", "1").Run() } - return fmt.Errorf("postgres did not become ready in 30 seconds") + return fmt.Errorf("mongodb did not become ready in 30 seconds") } diff --git a/cmd/templates/config.yaml b/cmd/templates/config.yaml new file mode 100644 index 0000000..d4b675f --- /dev/null +++ b/cmd/templates/config.yaml @@ -0,0 +1,26 @@ +# KubeOrch Core Configuration +# Generated by orchcli init — edit as needed + +PORT: 3000 +GIN_MODE: debug +LOG_LEVEL: info + +# Database — points to the MongoDB started by orchcli +MONGO_URI: "mongodb://localhost:27017/kubeorchestra" + +# Security — auto-generated secrets for local development +JWT_SECRET: "{{JWT_SECRET}}" +ENCRYPTION_KEY: "{{ENCRYPTION_KEY}}" + +# Token settings +TOKEN_REFRESH_MAX_AGE_DAYS: 7 + +# URLs +BASE_URL: "http://localhost:3000" +FRONTEND_URL: "http://localhost:3001" + +# Cluster log retention +CLUSTER_LOG_TTL_HOURS: 24 + +# Invite system +REGENERATE_INVITE_AFTER_SIGNUP: true diff --git a/cmd/templates/env.local b/cmd/templates/env.local new file mode 100644 index 0000000..094d598 --- /dev/null +++ b/cmd/templates/env.local @@ -0,0 +1,4 @@ +# KubeOrch UI Environment +# Generated by orchcli init — edit as needed + +NEXT_PUBLIC_API_URL=http://localhost:3000/v1/api diff --git a/cmd/utils.go b/cmd/utils.go index c8fcbc9..fe7d095 100644 --- a/cmd/utils.go +++ b/cmd/utils.go @@ -1,11 +1,88 @@ package cmd import ( + "crypto/rand" + "encoding/hex" "fmt" "os" "os/exec" + "path/filepath" + "strings" ) +const ( + // dirPerm is the permission for directories created by orchcli (rwxr-x---). + dirPerm os.FileMode = 0750 + // configFilePerm restricts config.yaml to owner-only since it contains secrets. + configFilePerm os.FileMode = 0600 + // composeFilePerm allows group/world read for docker-compose files (no secrets). + composeFilePerm os.FileMode = 0644 + // secretKeyBytes is the number of random bytes used for JWT/encryption keys. + secretKeyBytes = 32 +) + +// writeEmbeddedComposeFiles extracts all docker-compose files from the +// embedded filesystem and writes them into the target directory. +func writeEmbeddedComposeFiles(targetDir string) error { + entries, err := embeddedComposeFiles.ReadDir("docker") + if err != nil { + return fmt.Errorf("failed to read embedded compose files: %w", err) + } + + for _, entry := range entries { + if entry.IsDir() { + continue + } + data, err := embeddedComposeFiles.ReadFile("docker/" + entry.Name()) + if err != nil { + return fmt.Errorf("failed to read embedded file %s: %w", entry.Name(), err) + } + dest := filepath.Join(targetDir, entry.Name()) + // #nosec G306 -- compose files contain no secrets; world-readable is intentional + if err := os.WriteFile(dest, data, composeFilePerm); err != nil { + return fmt.Errorf("failed to write %s: %w", dest, err) + } + } + return nil +} + +// generateRandomHex returns a cryptographically random hex string of the given byte length. +func generateRandomHex(n int) string { + b := make([]byte, n) + _, _ = rand.Read(b) + return hex.EncodeToString(b) +} + +// writeConfigYAML generates a config.yaml with random secrets at the given path. +// Skips if the file already exists. +func writeConfigYAML(targetPath string) error { + if _, err := os.Stat(targetPath); err == nil { + return nil // already exists, don't overwrite + } + + content := strings.ReplaceAll(configTemplate, "{{JWT_SECRET}}", generateRandomHex(secretKeyBytes)) + content = strings.ReplaceAll(content, "{{ENCRYPTION_KEY}}", generateRandomHex(secretKeyBytes)) + + if err := os.WriteFile(targetPath, []byte(content), configFilePerm); err != nil { + return fmt.Errorf("failed to write config.yaml: %w", err) + } + return nil +} + +// writeEnvLocal generates a .env.local file at the given path. +// Skips if the file already exists. +func writeEnvLocal(targetPath string) error { + if _, err := os.Stat(targetPath); err == nil { + return nil // already exists, don't overwrite + } + + // #nosec G306 -- .env.local contains only the API URL, no secrets; world-readable is intentional + if err := os.WriteFile(targetPath, []byte(envLocalTemplate), composeFilePerm); err != nil { + return fmt.Errorf("failed to write .env.local: %w", err) + } + return nil +} + func dirExists(path string) bool { info, err := os.Stat(path) if os.IsNotExist(err) { diff --git a/docker/docker-compose.hybrid-core.yml b/docker/docker-compose.hybrid-core.yml index 0e312fe..8c6986b 100644 --- a/docker/docker-compose.hybrid-core.yml +++ b/docker/docker-compose.hybrid-core.yml @@ -52,7 +52,7 @@ services: command: air ui: - image: ghcr.io/kubeorchestra/ui:latest + image: ghcr.io/kubeorch/ui:latest container_name: kubeorchestra-ui-hybrid restart: unless-stopped networks: @@ -62,7 +62,7 @@ services: environment: # Both use internal Docker network names NEXT_PUBLIC_API_URL: http://localhost:3000 - API_URL: http://core:3000 + API_URL_INTERNAL: http://core:3000 PORT: 3001 depends_on: - core diff --git a/docker/docker-compose.hybrid-ui.yml b/docker/docker-compose.hybrid-ui.yml index 9a2b1f2..3c75969 100644 --- a/docker/docker-compose.hybrid-ui.yml +++ b/docker/docker-compose.hybrid-ui.yml @@ -30,7 +30,7 @@ services: start_period: 10s core: - image: ghcr.io/kubeorchestra/core:latest + image: ghcr.io/kubeorch/core:latest container_name: kubeorchestra-core-hybrid restart: unless-stopped networks: diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml index 18bb49f..a396f5e 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -29,7 +29,7 @@ services: start_period: 10s core: - image: ghcr.io/kubeorchestra/core:latest + image: ghcr.io/kubeorch/core:latest container_name: kubeorchestra-core restart: unless-stopped networks: @@ -46,7 +46,7 @@ services: condition: service_healthy ui: - image: ghcr.io/kubeorchestra/ui:latest + image: ghcr.io/kubeorch/ui:latest container_name: kubeorchestra-ui restart: unless-stopped networks: diff --git a/go.mod b/go.mod index c7ba2a9..420c32a 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/kubeorchestra/cli +module github.com/kubeorch/cli go 1.22.2 diff --git a/main.go b/main.go index c8ad7a9..c1af975 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,7 @@ package main import ( - "github.com/kubeorchestra/cli/cmd" + "github.com/kubeorch/cli/cmd" ) func main() { diff --git a/tests/integration/cli_integration_test.go b/tests/integration/cli_integration_test.go index e5f6e26..46dc61d 100644 --- a/tests/integration/cli_integration_test.go +++ b/tests/integration/cli_integration_test.go @@ -11,7 +11,7 @@ import ( "testing" "time" - "github.com/kubeorchestra/cli/tests/helpers" + "github.com/kubeorch/cli/tests/helpers" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" ) diff --git a/tests/unit/cmd_test.go b/tests/unit/cmd_test.go index 34ecf7f..580da46 100644 --- a/tests/unit/cmd_test.go +++ b/tests/unit/cmd_test.go @@ -7,8 +7,8 @@ import ( "strings" "testing" - "github.com/kubeorchestra/cli/cmd" - "github.com/kubeorchestra/cli/tests/helpers" + "github.com/kubeorch/cli/cmd" + "github.com/kubeorch/cli/tests/helpers" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" ) @@ -28,6 +28,12 @@ func (s *CommandTestSuite) SetupTest() { s.tempDir = s.T().TempDir() os.Chdir(s.tempDir) + // Clear global config between tests to prevent state leaks + // (e.g. CurrentProject fallback from a previous test). + if configPath, err := cmd.GetConfigPath(); err == nil { + os.Remove(configPath) + } + cmd.ResetCommands() } @@ -58,31 +64,27 @@ func (s *CommandTestSuite) TestHelpCommand() { } func (s *CommandTestSuite) TestInitProductionMode() { + // Mock binaries go in a bin/ subdir so they don't conflict with the + // docker/ directory that orchcli init creates in the project CWD. + binDir := filepath.Join(s.tempDir, "bin") + os.MkdirAll(binDir, 0755) if runtime.GOOS == "windows" { - // Create mock docker.bat and docker-compose.bat - helpers.CreateMockCommand(s.T(), s.tempDir, + helpers.CreateMockCommand(s.T(), binDir, "docker", `echo Docker version 24.0.0`) - helpers.CreateMockCommand(s.T(), s.tempDir, + helpers.CreateMockCommand(s.T(), binDir, "docker-compose", `echo Docker Compose version v2.20.0`) } else { - mockScript := `#!/bin/sh -echo "Docker Compose version v2.20.0" -` os.WriteFile( - filepath.Join(s.tempDir, "docker-compose"), - []byte(mockScript), 0755, + filepath.Join(binDir, "docker-compose"), + []byte("#!/bin/sh\necho 'Docker Compose version v2.20.0'\n"), 0755, ) - // Also mock docker itself - dockerMock := `#!/bin/sh -echo "Docker version 24.0.0" -` os.WriteFile( - filepath.Join(s.tempDir, "docker"), - []byte(dockerMock), 0755, + filepath.Join(binDir, "docker"), + []byte("#!/bin/sh\necho 'Docker version 24.0.0'\n"), 0755, ) } - helpers.MockPATH(s.tempDir) + helpers.MockPATH(binDir) rootCmd := cmd.GetRootCommand() output, err := cmd.ExecuteCommandC(rootCmd, "init", "--skip-deps") @@ -234,27 +236,29 @@ func (s *CommandTestSuite) setupProductionProject() { assert.NoError(s.T(), err) os.MkdirAll(filepath.Join(s.tempDir, "docker"), 0755) - composeContent := "version: '3.8'\nservices:\n core:\n" + - " image: kubeorch/core:latest\n" + composeContent := "services:\n core:\n" + + " image: ghcr.io/kubeorch/core:latest\n" os.WriteFile( filepath.Join(s.tempDir, "docker/docker-compose.prod.yml"), []byte(composeContent), 0644, ) + // Mock binaries in bin/ subdir to avoid conflicting with docker/ project dir. + binDir := filepath.Join(s.tempDir, "bin") + os.MkdirAll(binDir, 0755) + if runtime.GOOS == "windows" { - helpers.CreateMockCommand(s.T(), s.tempDir, "docker", + helpers.CreateMockCommand(s.T(), binDir, "docker", "echo mock-docker-output") - helpers.CreateMockCommand(s.T(), s.tempDir, "docker-compose", + helpers.CreateMockCommand(s.T(), binDir, "docker-compose", "echo mock-compose-output") } else { - dockerMock := "#!/bin/sh\necho mock-docker-output\n" - os.WriteFile(filepath.Join(s.tempDir, "docker"), - []byte(dockerMock), 0755) - composeMock := "#!/bin/sh\necho mock-compose-output\n" - os.WriteFile(filepath.Join(s.tempDir, "docker-compose"), - []byte(composeMock), 0755) + os.WriteFile(filepath.Join(binDir, "docker"), + []byte("#!/bin/sh\necho mock-docker-output\n"), 0755) + os.WriteFile(filepath.Join(binDir, "docker-compose"), + []byte("#!/bin/sh\necho mock-compose-output\n"), 0755) } - helpers.MockPATH(s.tempDir) + helpers.MockPATH(binDir) } func (s *CommandTestSuite) TestStopCommand() { @@ -321,8 +325,8 @@ func (s *CommandTestSuite) TestExecWithValidService() { s.setupProductionProject() rootCmd := cmd.GetRootCommand() - // postgres is a valid service name per the allowlist in exec.go - _, err := cmd.ExecuteCommandC(rootCmd, "exec", "postgres", "ls") + // mongodb is a valid service name per the allowlist in exec.go + _, err := cmd.ExecuteCommandC(rootCmd, "exec", "mongodb", "ls") // May error because container isn't running, but should not // error with "invalid service" diff --git a/tests/unit/concurrent_test.go b/tests/unit/concurrent_test.go index 2f8a564..9d093bb 100644 --- a/tests/unit/concurrent_test.go +++ b/tests/unit/concurrent_test.go @@ -6,7 +6,7 @@ import ( "testing" "time" - "github.com/kubeorchestra/cli/cmd" + "github.com/kubeorch/cli/cmd" ) func TestRunConcurrent(t *testing.T) { diff --git a/tests/unit/config_concurrent_test.go b/tests/unit/config_concurrent_test.go index 6ac69c2..8ed6f32 100644 --- a/tests/unit/config_concurrent_test.go +++ b/tests/unit/config_concurrent_test.go @@ -7,7 +7,7 @@ import ( "sync" "testing" - "github.com/kubeorchestra/cli/cmd" + "github.com/kubeorch/cli/cmd" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/tests/unit/config_test.go b/tests/unit/config_test.go index 27e0528..212fc6f 100644 --- a/tests/unit/config_test.go +++ b/tests/unit/config_test.go @@ -6,7 +6,7 @@ import ( "path/filepath" "testing" - "github.com/kubeorchestra/cli/cmd" + "github.com/kubeorch/cli/cmd" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/tests/unit/utils_test.go b/tests/unit/utils_test.go index d4ce9a3..0c10883 100644 --- a/tests/unit/utils_test.go +++ b/tests/unit/utils_test.go @@ -5,7 +5,7 @@ import ( "path/filepath" "testing" - "github.com/kubeorchestra/cli/tests/helpers" + "github.com/kubeorch/cli/tests/helpers" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" )