diff --git a/.github/workflows/wren-launcher-ci.yaml b/.github/workflows/wren-launcher-ci.yaml new file mode 100644 index 0000000000..8baf1ce995 --- /dev/null +++ b/.github/workflows/wren-launcher-ci.yaml @@ -0,0 +1,83 @@ +name: Wren Launcher CI + +on: + pull_request: + types: [synchronize, labeled] + paths: + - 'wren-launcher/**' + - '.github/workflows/wren-launcher-ci.yaml' + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.event.number || github.sha }} + cancel-in-progress: true + +defaults: + run: + working-directory: wren-launcher + +jobs: + golangci: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: '1.24' + cache-dependency-path: wren-launcher/go.sum + - name: golangci-lint + uses: golangci/golangci-lint-action@v8 + with: + version: v2.3.1 + working-directory: wren-launcher + + fmt-and-test: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.24' + cache-dependency-path: wren-launcher/go.sum + - name: Install goimports + run: go install golang.org/x/tools/cmd/goimports@latest + - name: Download dependencies + run: go mod download + - name: Run format check + run: | + make fmt + # Check if there are any formatting changes + if [ -n "$(git diff --name-only)" ]; then + echo "Code is not formatted properly. Please run 'make fmt' and commit the changes." + git diff + exit 1 + fi + - name: Run go vet + run: make vet + - name: Run tests + run: go test ./commands/dbt + + security-scan: + runs-on: ubuntu-latest + needs: fmt-and-test + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.24' + cache-dependency-path: wren-launcher/go.sum + - name: Run Gosec Security Scanner + run: | + go install github.com/securego/gosec/v2/cmd/gosec@latest + gosec ./... + - name: Run Go mod audit + run: | + go mod verify + go list -json -deps ./... | jq -r '.Module | select(.Version) | "\(.Path) \(.Version)"' | sort -u diff --git a/wren-launcher/.golangci.yml b/wren-launcher/.golangci.yml new file mode 100644 index 0000000000..ddf058dff8 --- /dev/null +++ b/wren-launcher/.golangci.yml @@ -0,0 +1,25 @@ +# golangci-lint configuration for WrenAI wren-launcher +version: "2" + +run: + timeout: 5m + +linters: + enable: + - errcheck + - govet + - ineffassign + - staticcheck + - unused + - misspell + - unconvert + - gosec + - dupl + - goconst + - gocyclo + - bodyclose + - whitespace + +issues: + max-issues-per-linter: 0 + max-same-issues: 0 diff --git a/wren-launcher/Makefile b/wren-launcher/Makefile index 8f9749c26c..52e20dae7f 100644 --- a/wren-launcher/Makefile +++ b/wren-launcher/Makefile @@ -1,5 +1,6 @@ BINARY_NAME=wren-launcher +# Build targets build: env GOARCH=amd64 GOOS=darwin CGO_ENABLED=1 go build -o dist/${BINARY_NAME}-darwin main.go env GOARCH=arm64 GOOS=darwin CGO_ENABLED=1 go build -o dist/${BINARY_NAME}-darwin-arm64 main.go @@ -18,3 +19,50 @@ clean: rm -rf dist rebuild: clean build + +# Code quality targets +.PHONY: fmt +fmt: + go fmt ./... + +.PHONY: imports +imports: + $(shell go env GOPATH)/bin/goimports -w . + +.PHONY: vet +vet: + go vet ./... + +.PHONY: lint +lint: + golangci-lint run + +.PHONY: lint-fix +lint-fix: + golangci-lint run --fix + +.PHONY: check +check: fmt vet lint + +.PHONY: fix +fix: fmt imports lint-fix + +.PHONY: test +test: + go test ./... + +.PHONY: help +help: + @echo "Available targets:" + @echo " build - Build binaries for all platforms" + @echo " clean - Clean build artifacts" + @echo " rebuild - Clean and build" + @echo " fmt - Format Go code" + @echo " imports - Fix imports" + @echo " vet - Run go vet" + @echo " lint - Run golangci-lint" + @echo " lint-fix - Run golangci-lint with auto-fix" + @echo " check - Run fmt, vet, and lint" + @echo " fix - Run fmt, imports, and lint-fix" + @echo " test - Run tests" + @echo " help - Show this help" diff --git a/wren-launcher/README.md b/wren-launcher/README.md index b812b20605..265c73509e 100644 --- a/wren-launcher/README.md +++ b/wren-launcher/README.md @@ -6,6 +6,36 @@ go build main.go env GOOS=windows GOARCH=amd64 go build main.go ``` +## Code Quality +```bash +make check # Run all checks (fmt, vet, lint) +make test # Run tests +make fmt # Format code +make vet # Run go vet +make lint # Run golangci-lint +``` + +## Continuous Integration + +This project uses GitHub Actions for CI/CD. The workflow runs automatically on: + +- **Push to main branch**: Runs all checks and tests +- **Pull Request with label `launcher`**: Runs all checks and tests when PR is labeled +- **Manual trigger**: Can be triggered manually via GitHub Actions UI + +### CI Jobs: + +1. **Lint and Test**: + - Code formatting check + - Go vet analysis + - golangci-lint checks + - Unit tests + - All quality checks + +2. **Security Scan**: + - Gosec security analysis + - Go module verification + ## How to update dependencies ```bash diff --git a/wren-launcher/commands/dbt/converter.go b/wren-launcher/commands/dbt/converter.go index 9153f85296..350ab39928 100644 --- a/wren-launcher/commands/dbt/converter.go +++ b/wren-launcher/commands/dbt/converter.go @@ -37,7 +37,7 @@ func ConvertDbtProjectCore(opts ConvertOptions) (*ConvertResult, error) { } // Create output directory if it doesn't exist - if err := os.MkdirAll(opts.OutputDir, 0755); err != nil { + if err := os.MkdirAll(opts.OutputDir, 0750); err != nil { return nil, fmt.Errorf("failed to create output directory: %w", err) } @@ -169,7 +169,7 @@ func ConvertDbtProjectCore(opts ConvertOptions) (*ConvertResult, error) { return nil, fmt.Errorf("failed to marshal data source JSON: %w", err) } - if err := os.WriteFile(dataSourcePath, dataSourceJSON, 0644); err != nil { + if err := os.WriteFile(dataSourcePath, dataSourceJSON, 0600); err != nil { return nil, fmt.Errorf("failed to write data source file: %w", err) } @@ -198,7 +198,7 @@ func ConvertDbtProjectCore(opts ConvertOptions) (*ConvertResult, error) { return nil, fmt.Errorf("failed to marshal MDL JSON: %w", err) } - if err := os.WriteFile(mdlPath, mdlJSON, 0644); err != nil { + if err := os.WriteFile(mdlPath, mdlJSON, 0600); err != nil { return nil, fmt.Errorf("failed to write MDL file: %w", err) } @@ -244,7 +244,7 @@ func handleLocalhostForContainer(host string) string { // ConvertDbtCatalogToWrenMDL converts dbt catalog.json to Wren MDL format func ConvertDbtCatalogToWrenMDL(catalogPath string, data_source DataSource, manifestPath string) (*WrenMDLManifest, error) { // Read and parse the catalog.json file - data, err := os.ReadFile(catalogPath) + data, err := os.ReadFile(catalogPath) // #nosec G304 -- catalogPath is controlled by application if err != nil { return nil, fmt.Errorf("failed to read catalog file %s: %w", catalogPath, err) } @@ -258,7 +258,7 @@ func ConvertDbtCatalogToWrenMDL(catalogPath string, data_source DataSource, mani var manifestData map[string]interface{} if manifestPath != "" { pterm.Info.Printf("Reading manifest.json for descriptions from: %s\n", manifestPath) - manifestBytes, err := os.ReadFile(manifestPath) + manifestBytes, err := os.ReadFile(manifestPath) // #nosec G304 -- manifestPath is controlled by application if err != nil { pterm.Warning.Printf("Warning: Failed to read manifest file %s: %v\n", manifestPath, err) } else { diff --git a/wren-launcher/commands/dbt/data_source.go b/wren-launcher/commands/dbt/data_source.go index de22be493f..b96d4e2b39 100644 --- a/wren-launcher/commands/dbt/data_source.go +++ b/wren-launcher/commands/dbt/data_source.go @@ -8,6 +8,35 @@ import ( "github.com/pterm/pterm" ) +// Constants for data types +const ( + integerType = "integer" + varcharType = "varchar" + dateType = "date" + timestampType = "timestamp" + doubleType = "double" + booleanType = "boolean" +) + +// Constants for SQL data types +const ( + integerSQL = "INTEGER" + intSQL = "INT" + bigintSQL = "BIGINT" + varcharSQL = "VARCHAR" + textSQL = "TEXT" + stringSQL = "STRING" + dateSQL = "DATE" + timestampSQL = "TIMESTAMP" + datetimeSQL = "DATETIME" + doubleSQL = "DOUBLE" + floatSQL = "FLOAT" + numericSQL = "NUMERIC" + decimalSQL = "DECIMAL" + booleanSQL = "BOOLEAN" + boolSQL = "BOOL" +) + // DataSource is a common interface for all data source types type DataSource interface { GetType() string @@ -140,18 +169,18 @@ func (ds *WrenLocalFileDataSource) MapType(sourceType string) string { sourceType = strings.ToUpper(sourceType) switch sourceType { - case "INTEGER", "INT", "BIGINT": - return "integer" - case "VARCHAR", "TEXT", "STRING": - return "varchar" - case "DATE": - return "date" - case "TIMESTAMP", "DATETIME": - return "timestamp" - case "DOUBLE", "FLOAT", "NUMERIC", "DECIMAL": - return "double" - case "BOOLEAN", "BOOL": - return "boolean" + case integerSQL, intSQL, bigintSQL: + return integerType + case varcharSQL, textSQL, stringSQL: + return varcharType + case dateSQL: + return dateType + case timestampSQL, datetimeSQL: + return timestampType + case doubleSQL, floatSQL, numericSQL, decimalSQL: + return doubleType + case booleanSQL, boolSQL: + return booleanType default: // Return the original type if no mapping is found return strings.ToLower(sourceType) diff --git a/wren-launcher/commands/dbt/data_source_test.go b/wren-launcher/commands/dbt/data_source_test.go index fe7b5f0d43..201d8c4430 100644 --- a/wren-launcher/commands/dbt/data_source_test.go +++ b/wren-launcher/commands/dbt/data_source_test.go @@ -4,6 +4,46 @@ import ( "testing" ) +// Test constants +const ( + testHost = "localhost" + testUser = "test_user" + testPassword = "test_pass" + pgType = "postgres" + duckdbType = "duckdb" +) + +// Helper function to validate PostgreSQL data source +func validatePostgresDataSource(t *testing.T, ds *WrenPostgresDataSource, expectedDB string) { + t.Helper() + + if ds.Host != testHost { + t.Errorf("Expected host '%s', got '%s'", testHost, ds.Host) + } + if ds.Port != 5432 { + t.Errorf("Expected port 5432, got %d", ds.Port) + } + if ds.Database != expectedDB { + t.Errorf("Expected database '%s', got '%s'", expectedDB, ds.Database) + } + if ds.User != testUser { + t.Errorf("Expected user '%s', got '%s'", testUser, ds.User) + } + if ds.Password != testPassword { + t.Errorf("Expected password '%s', got '%s'", testPassword, ds.Password) + } + + // Test validation + if err := ds.Validate(); err != nil { + t.Errorf("Validation failed: %v", err) + } + + // Test type + if ds.GetType() != pgType { + t.Errorf("Expected type '%s', got '%s'", pgType, ds.GetType()) + } +} + func TestFromDbtProfiles_Postgres(t *testing.T) { // Test PostgreSQL connection conversion profiles := &DbtProfiles{ @@ -12,12 +52,12 @@ func TestFromDbtProfiles_Postgres(t *testing.T) { Target: "dev", Outputs: map[string]DbtConnection{ "dev": { - Type: "postgres", - Host: "localhost", + Type: pgType, + Host: testHost, Port: 5432, Database: "test_db", - User: "test_user", - Password: "test_pass", + User: testUser, + Password: testPassword, }, }, }, @@ -38,31 +78,44 @@ func TestFromDbtProfiles_Postgres(t *testing.T) { t.Fatalf("Expected WrenPostgresDataSource, got %T", dataSources[0]) } - if ds.Host != "localhost" { - t.Errorf("Expected host 'localhost', got '%s'", ds.Host) - } - if ds.Port != 5432 { - t.Errorf("Expected port 5432, got %d", ds.Port) - } - if ds.Database != "test_db" { - t.Errorf("Expected database 'test_db', got '%s'", ds.Database) - } - if ds.User != "test_user" { - t.Errorf("Expected user 'test_user', got '%s'", ds.User) + validatePostgresDataSource(t, ds, "test_db") +} + +func TestFromDbtProfiles_PostgresWithDbName(t *testing.T) { + // Test PostgreSQL connection conversion with dbname field (PostgreSQL specific) + profiles := &DbtProfiles{ + Profiles: map[string]DbtProfile{ + "test_profile": { + Target: "dev", + Outputs: map[string]DbtConnection{ + "dev": { + Type: pgType, + Host: testHost, + Port: 5432, + Database: "jaffle_shop", + User: testUser, + Password: testPassword, + }, + }, + }, + }, } - if ds.Password != "test_pass" { - t.Errorf("Expected password 'test_pass', got '%s'", ds.Password) + + dataSources, err := FromDbtProfiles(profiles) + if err != nil { + t.Fatalf("FromDbtProfiles failed: %v", err) } - // Test validation - if err := ds.Validate(); err != nil { - t.Errorf("Validation failed: %v", err) + if len(dataSources) != 1 { + t.Fatalf("Expected 1 data source, got %d", len(dataSources)) } - // Test type - if ds.GetType() != "postgres" { - t.Errorf("Expected type 'postgres', got '%s'", ds.GetType()) + ds, ok := dataSources[0].(*WrenPostgresDataSource) + if !ok { + t.Fatalf("Expected WrenPostgresDataSource, got %T", dataSources[0]) } + + validatePostgresDataSource(t, ds, "jaffle_shop") } func TestFromDbtProfiles_LocalFile(t *testing.T) { @@ -73,7 +126,7 @@ func TestFromDbtProfiles_LocalFile(t *testing.T) { Target: "dev", Outputs: map[string]DbtConnection{ "dev": { - Type: "duckdb", + Type: duckdbType, Path: "/abs_path/jaffle_shop.duckdb", }, }, @@ -98,8 +151,8 @@ func TestFromDbtProfiles_LocalFile(t *testing.T) { if ds.Url != "/abs_path" { t.Errorf("Expected url '/abs_path', got '%s'", ds.Url) } - if ds.Format != "duckdb" { - t.Errorf("Expected format 'duckdb', got '%s'", ds.Format) + if ds.Format != duckdbType { + t.Errorf("Expected format '%s', got '%s'", duckdbType, ds.Format) } // Test validation @@ -148,61 +201,73 @@ func TestFromDbtProfiles_NilProfiles(t *testing.T) { } } +// Validator interface for data sources +type Validator interface { + Validate() error +} + +// Helper function to test data source validation +func testDataSourceValidation(t *testing.T, testName string, validDS Validator, invalidDSCases []struct { + name string + ds Validator +}) { + t.Helper() + + t.Run(testName+" valid", func(t *testing.T) { + if err := validDS.Validate(); err != nil { + t.Errorf("Valid data source validation failed: %v", err) + } + }) + + for _, tt := range invalidDSCases { + t.Run(testName+" "+tt.name, func(t *testing.T) { + if err := tt.ds.Validate(); err == nil { + t.Errorf("Expected validation error for %s, but got none", tt.name) + } + }) + } +} + func TestPostgresDataSourceValidation(t *testing.T) { - // Test PostgreSQL data source validation - tests := []struct { - name string - ds *WrenPostgresDataSource - wantErr bool + validDS := &WrenPostgresDataSource{ + Host: testHost, + Port: 5432, + Database: "test", + User: "user", + } + + invalidCases := []struct { + name string + ds Validator }{ { - name: "valid", - ds: &WrenPostgresDataSource{ - Host: "localhost", + "empty host", + &WrenPostgresDataSource{ Port: 5432, Database: "test", User: "user", }, - wantErr: false, }, { - name: "empty host", - ds: &WrenPostgresDataSource{ - Port: 5432, - Database: "test", - User: "user", - }, - wantErr: true, - }, - { - name: "empty database", - ds: &WrenPostgresDataSource{ - Host: "localhost", + "empty database", + &WrenPostgresDataSource{ + Host: testHost, Port: 5432, User: "user", }, - wantErr: true, }, { - name: "invalid port", - ds: &WrenPostgresDataSource{ - Host: "localhost", + "invalid port", + &WrenPostgresDataSource{ + Host: testHost, Port: 0, Database: "test", User: "user", }, - wantErr: true, }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := tt.ds.Validate() - if (err != nil) != tt.wantErr { - t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } + testDataSourceValidation(t, "postgres", validDS, invalidCases) } func TestGetActiveDataSources(t *testing.T) { @@ -285,7 +350,7 @@ func TestGetDataSourceByType(t *testing.T) { User: "user", }, "file_dev": { - Type: "duckdb", + Type: duckdbType, Path: "/data/test.csv", }, "postgres_prod": { diff --git a/wren-launcher/commands/dbt/profiles_analyzer.go b/wren-launcher/commands/dbt/profiles_analyzer.go index df004f1ec3..b01f79c1e0 100644 --- a/wren-launcher/commands/dbt/profiles_analyzer.go +++ b/wren-launcher/commands/dbt/profiles_analyzer.go @@ -12,7 +12,7 @@ import ( // AnalyzeDbtProfiles reads and analyzes a dbt profiles.yml file func AnalyzeDbtProfiles(profilesPath string) (*DbtProfiles, error) { // Read the profiles.yml file - data, err := os.ReadFile(profilesPath) + data, err := os.ReadFile(profilesPath) // #nosec G304 -- profilesPath is controlled by application if err != nil { return nil, fmt.Errorf("failed to read profiles file %s: %w", profilesPath, err) } diff --git a/wren-launcher/commands/launch.go b/wren-launcher/commands/launch.go index f54bfc4db2..4edb0f6d4c 100644 --- a/wren-launcher/commands/launch.go +++ b/wren-launcher/commands/launch.go @@ -29,7 +29,9 @@ func prepareProjectDir() string { projectDir := path.Join(homedir, ".wrenai") if _, err := os.Stat(projectDir); os.IsNotExist(err) { - os.Mkdir(projectDir, 0755) + if err := os.Mkdir(projectDir, 0750); err != nil { + return "" + } } return projectDir @@ -190,7 +192,8 @@ func Launch() { defer func() { if r := recover(); r != nil { pterm.Error.Println("An error occurred:", r) - fmt.Scanf("h") + var dummy string + _, _ = fmt.Scanf("%s", &dummy) } }() @@ -370,10 +373,11 @@ func Launch() { // open browser pterm.Info.Println("Opening browser") - utils.Openbrowser(uiUrl) + _ = utils.Openbrowser(uiUrl) pterm.Info.Println("You can now safely close this terminal window") - fmt.Scanf("h") + var dummy string + _, _ = fmt.Scanf("%s", &dummy) } func getOpenaiGenerationModel() (string, bool) { @@ -464,7 +468,7 @@ func validateOpenaiApiKey(apiKey string) bool { // insufficient credit balance error if err != nil { pterm.Error.Println("Invalid API key", err) - fmt.Scanln() + _, _ = fmt.Scanln() return true } @@ -507,7 +511,7 @@ func processDbtProject(projectDir string) (string, error) { // create target directory in project dir targetDir := filepath.Join(projectDir, "target") - err = os.MkdirAll(targetDir, 0755) + err = os.MkdirAll(targetDir, 0750) if err != nil { return "", fmt.Errorf("failed to create target directory: %w", err) } diff --git a/wren-launcher/config/config.go b/wren-launcher/config/config.go index 96fc8d3f32..d41552fe73 100644 --- a/wren-launcher/config/config.go +++ b/wren-launcher/config/config.go @@ -5,6 +5,12 @@ import ( "runtime" ) +// Platform constants +const ( + platformLinuxAmd64 = "linux/amd64" + platformLinuxArm64 = "linux/arm64" +) + // private variable within the config package var disableTelemetry bool var openaiAPIKey string @@ -33,18 +39,18 @@ func GetPlatform() string { switch runtime.GOOS { case "darwin": if runtime.GOARCH == "arm64" { - return "linux/arm64" + return platformLinuxArm64 } - return "linux/amd64" + return platformLinuxAmd64 case "linux": if runtime.GOARCH == "arm64" { - return "linux/arm64" + return platformLinuxArm64 } - return "linux/amd64" + return platformLinuxAmd64 case "windows": - return "linux/amd64" // Windows typically uses amd64 + return platformLinuxAmd64 // Windows typically uses amd64 default: - return "linux/amd64" // Default to amd64 for unknown platforms + return platformLinuxAmd64 // Default to amd64 for unknown platforms } } diff --git a/wren-launcher/go.mod b/wren-launcher/go.mod index 1283bd6f78..f8e9096633 100644 --- a/wren-launcher/go.mod +++ b/wren-launcher/go.mod @@ -1,6 +1,6 @@ module github.com/Canner/WrenAI/wren-launcher -go 1.23.8 +go 1.24 toolchain go1.24.1 diff --git a/wren-launcher/utils/docker.go b/wren-launcher/utils/docker.go index ac194a6c06..72794d980d 100644 --- a/wren-launcher/utils/docker.go +++ b/wren-launcher/utils/docker.go @@ -16,7 +16,6 @@ import ( cmdCompose "github.com/docker/compose/v2/cmd/compose" "github.com/docker/compose/v2/pkg/api" "github.com/docker/compose/v2/pkg/compose" - "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/google/uuid" "github.com/pterm/pterm" @@ -24,7 +23,7 @@ import ( const ( // please change the version when the version is updated - WREN_PRODUCT_VERSION string = "0.27.0" + WREN_PRODUCT_VERSION string = "0.27.0-rc.2" DOCKER_COMPOSE_YAML_URL string = "https://raw.githubusercontent.com/Canner/WrenAI/" + WREN_PRODUCT_VERSION + "/docker/docker-compose.yaml" DOCKER_COMPOSE_ENV_URL string = "https://raw.githubusercontent.com/Canner/WrenAI/" + WREN_PRODUCT_VERSION + "/docker/.env.example" AI_SERVICE_CONFIG_URL string = "https://raw.githubusercontent.com/Canner/WrenAI/" + WREN_PRODUCT_VERSION + "/docker/config.example.yaml" @@ -92,20 +91,19 @@ func replaceEnvFileContent(content string, projectDir string, openaiApiKey strin } func downloadFile(filepath string, url string) error { - // Get the data - resp, err := http.Get(url) + resp, err := http.Get(url) // #nosec G107 -- URL is from trusted source constants if err != nil { return err } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() // Create the file - out, err := os.Create(filepath) + out, err := os.Create(filepath) // #nosec G304 -- filepath is controlled by application if err != nil { return err } - defer out.Close() + defer func() { _ = out.Close() }() // Write the body to file _, err = io.Copy(out, resp.Body) @@ -157,7 +155,7 @@ func PrepareConfigFileForOpenAI(projectDir string, generationModel string) error } // read the config.yaml file - content, err := os.ReadFile(configPath) + content, err := os.ReadFile(configPath) // #nosec G304 -- configPath is controlled by application if err != nil { return err } @@ -170,7 +168,7 @@ func PrepareConfigFileForOpenAI(projectDir string, generationModel string) error } // write back to config.yaml - err = os.WriteFile(configPath, []byte(config), 0644) + err = os.WriteFile(configPath, []byte(config), 0600) if err != nil { return err } @@ -185,7 +183,7 @@ func mergeEnvContent(newEnvFile string, envFileContent string) (string, error) { } // File exists, read existing content - existingContent, err := os.ReadFile(newEnvFile) + existingContent, err := os.ReadFile(newEnvFile) // #nosec G304 -- newEnvFile is controlled by application if err != nil { return "", err } @@ -267,7 +265,7 @@ func PrepareDockerFiles(openaiApiKey string, openaiGenerationModel string, hostP } // read the file - envExampleFileContent, err := os.ReadFile(envExampleFile) + envExampleFileContent, err := os.ReadFile(envExampleFile) // #nosec G304 -- envExampleFile is controlled by application if err != nil { return err } @@ -294,7 +292,7 @@ func PrepareDockerFiles(openaiApiKey string, openaiGenerationModel string, hostP } // write the file - err = os.WriteFile(newEnvFile, []byte(envFileContent), 0644) + err = os.WriteFile(newEnvFile, []byte(envFileContent), 0600) if err != nil { return err } @@ -408,7 +406,7 @@ func RunDockerCompose(projectName string, projectDir string, llmProvider string) return nil } -func listProcess() ([]types.Container, error) { +func listProcess() ([]container.Summary, error) { ctx := context.Background() dockerCli, err := command.NewDockerCli() if err != nil { @@ -432,35 +430,35 @@ func listProcess() ([]types.Container, error) { return containers, nil } -func findWrenUIContainer() (types.Container, error) { +func findWrenUIContainer() (container.Summary, error) { containers, err := listProcess() if err != nil { - return types.Container{}, err + return container.Summary{}, err } - for _, container := range containers { + for _, cont := range containers { // return if com.docker.compose.project == wrenai && com.docker.compose.service=wren-ui - if container.Labels["com.docker.compose.project"] == "wrenai" && container.Labels["com.docker.compose.service"] == "wren-ui" { - return container, nil + if cont.Labels["com.docker.compose.project"] == "wrenai" && cont.Labels["com.docker.compose.service"] == "wren-ui" { + return cont, nil } } - return types.Container{}, fmt.Errorf("WrenUI container not found") + return container.Summary{}, fmt.Errorf("WrenUI container not found") } -func findAIServiceContainer() (types.Container, error) { +func findAIServiceContainer() (container.Summary, error) { containers, err := listProcess() if err != nil { - return types.Container{}, err + return container.Summary{}, err } - for _, container := range containers { - if container.Labels["com.docker.compose.project"] == "wrenai" && container.Labels["com.docker.compose.service"] == "wren-ai-service" { - return container, nil + for _, cont := range containers { + if cont.Labels["com.docker.compose.project"] == "wrenai" && cont.Labels["com.docker.compose.service"] == "wren-ai-service" { + return cont, nil } } - return types.Container{}, fmt.Errorf("WrenAI service container not found") + return container.Summary{}, fmt.Errorf("WrenAI service container not found") } func IfPortUsedByWrenUI(port int) bool { @@ -470,7 +468,7 @@ func IfPortUsedByWrenUI(port int) bool { } for _, containerPort := range container.Ports { - if containerPort.PublicPort == uint16(port) { + if port >= 0 && port <= 65535 && containerPort.PublicPort == uint16(port) { return true } } @@ -485,7 +483,7 @@ func IfPortUsedByAIService(port int) bool { } for _, containerPort := range container.Ports { - if containerPort.PublicPort == uint16(port) { + if port >= 0 && port <= 65535 && containerPort.PublicPort == uint16(port) { return true } } @@ -495,25 +493,25 @@ func IfPortUsedByAIService(port int) bool { func CheckUIServiceStarted(url string) error { // check response from localhost:3000 - resp, err := http.Get(url) + resp, err := http.Get(url) // #nosec G107 -- URL is validated by application logic if err != nil { return err } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != 200 { - return fmt.Errorf("Wren AI is not started yet") + return fmt.Errorf("wren AI is not started yet") } return nil } func CheckAIServiceStarted(url string) error { // health check - resp, err := http.Get(url) + resp, err := http.Get(url) // #nosec G107 -- URL is validated by application logic if err != nil { return err } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != 200 { return fmt.Errorf("AI service is not started yet") diff --git a/wren-launcher/utils/rc.go b/wren-launcher/utils/rc.go index 552a725ec8..75743a823d 100644 --- a/wren-launcher/utils/rc.go +++ b/wren-launcher/utils/rc.go @@ -26,7 +26,7 @@ func (w *WrenRC) getWrenRcFilePath() string { func (w *WrenRC) ensureRcFile() (string, error) { // ensure folder created - err := os.MkdirAll(w.rcFileDir, os.ModePerm) + err := os.MkdirAll(w.rcFileDir, 0750) if err != nil { return "", err } @@ -35,11 +35,11 @@ func (w *WrenRC) ensureRcFile() (string, error) { rcFilePath := w.getWrenRcFilePath() _, err = os.Stat(rcFilePath) if os.IsNotExist(err) { - f, err := os.Create(rcFilePath) + f, err := os.Create(rcFilePath) // #nosec G304 -- rcFilePath is controlled by application if err != nil { return "", err } - f.Close() + _ = f.Close() } return rcFilePath, nil } @@ -50,11 +50,11 @@ func (w *WrenRC) parseInto() (map[string]string, error) { return nil, err } - f, err := os.Open(rcFilePath) + f, err := os.Open(rcFilePath) // #nosec G304 -- rcFilePath is controlled by application if err != nil { return nil, err } - defer f.Close() + defer func() { _ = f.Close() }() // prepare a map to store the key value pairs m := make(map[string]string) @@ -123,11 +123,11 @@ func (w *WrenRC) Set(key string, value string, override bool) error { // overrite the rc file with the given key value pairs func (w *WrenRC) write(m map[string]string) error { rcFilePath := w.getWrenRcFilePath() - f, err := os.Create(rcFilePath) + f, err := os.Create(rcFilePath) // #nosec G304 -- rcFilePath is controlled by application if err != nil { return err } - defer f.Close() + defer func() { _ = f.Close() }() for k, v := range m { _, err = fmt.Fprintf(f, "%s=%s\n", k, v) diff --git a/wren-launcher/utils/rc_test.go b/wren-launcher/utils/rc_test.go index f6f6ff1646..5c8eec2c45 100644 --- a/wren-launcher/utils/rc_test.go +++ b/wren-launcher/utils/rc_test.go @@ -10,7 +10,7 @@ func TestReadWriteRcFile(t *testing.T) { if err != nil { t.Errorf("Error: %v", err) } - defer os.RemoveAll(dir) + defer func() { _ = os.RemoveAll(dir) }() // create a WrenRC struct w := WrenRC{dir} @@ -31,12 +31,13 @@ func TestReadWriteRcFile(t *testing.T) { } // read the value of a non-existent key from the rc file -func TestReadNonExistentKey(t *testing.T) { +func TestSet(t *testing.T) { + // create a temp directory dir, err := os.MkdirTemp("", "wrenrc") if err != nil { t.Errorf("Error: %v", err) } - defer os.RemoveAll(dir) + defer func() { _ = os.RemoveAll(dir) }() // create a WrenRC struct w := WrenRC{dir}