123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517 |
- package golang
- import (
- "fmt"
- "os"
- "path/filepath"
- "strings"
- "git.linuxforward.com/byop/byop-engine/analyzer/templates"
- )
- type Golang struct {
- // Add fields as needed for Golang analysis
- }
- // Name returns the name of the Golang technology stack.
- func (g *Golang) Name() string {
- return "Golang"
- }
- // Analyze analyzes the codebase and returns a guessed technology stack for Golang.
- func (g *Golang) Analyze(codebasePath string) (bool, error) {
- // Verify if the codebasePath is valid and contains Go files
- if codebasePath == "" {
- return false, fmt.Errorf("codebase path cannot be empty")
- }
- // Check if the codebase exists and contains Go files
- _, err := os.Stat(codebasePath)
- if os.IsNotExist(err) {
- return false, fmt.Errorf("codebase path does not exist: %s", codebasePath)
- }
- // Enhanced detection logic inspired by Railway Railpack
- return g.isGoMod(codebasePath) || g.isGoWorkspace(codebasePath) || g.hasMainGo(codebasePath), nil
- }
- // GenerateDockerfile generates a Dockerfile for Golang projects based on analysis
- func (g *Golang) GenerateDockerfile(codebasePath string) (string, error) {
- // Analyze the Go project
- analysis, err := g.analyzeGoProject(codebasePath)
- if err != nil {
- return "", fmt.Errorf("failed to analyze Go project: %w", err)
- }
- // Check for file existence
- hasGoMod := g.fileExists(filepath.Join(codebasePath, "go.mod"))
- hasGoSum := g.fileExists(filepath.Join(codebasePath, "go.sum"))
- hasVendor := g.dirExists(filepath.Join(codebasePath, "vendor"))
- // Determine build tools needed
- buildTools := []string{"git"} // Always need git for go get
- needsBuildTools := true
- // Determine runtime dependencies
- runtimeDeps := []string{}
- needsRuntimeDeps := false
- runtimeImage := "alpine:latest"
- if analysis.RequiresCACerts {
- runtimeDeps = append(runtimeDeps, "ca-certificates")
- needsRuntimeDeps = true
- }
- if analysis.RequiresTimezone {
- runtimeDeps = append(runtimeDeps, "tzdata")
- needsRuntimeDeps = true
- }
- if analysis.CGOEnabled {
- runtimeDeps = append(runtimeDeps, "libc6-compat")
- needsRuntimeDeps = true
- buildTools = append(buildTools, "build-base")
- }
- // If no runtime deps needed, we can use scratch
- if !needsRuntimeDeps {
- runtimeImage = "scratch"
- }
- // Create template data
- templateData := DockerfileTemplateData{
- AppName: analysis.AppName,
- BinaryName: analysis.AppName,
- GoVersion: g.getGoVersion(codebasePath),
- Port: analysis.Port,
- BuildCommand: g.getBuildCommand(analysis, codebasePath),
- HasGoMod: hasGoMod,
- HasGoSum: hasGoSum,
- HasVendor: hasVendor,
- CGOEnabled: analysis.CGOEnabled,
- NeedsBuildTools: needsBuildTools,
- BuildTools: buildTools,
- NeedsRuntimeDeps: needsRuntimeDeps,
- RuntimeDeps: runtimeDeps,
- RuntimeImage: runtimeImage,
- HealthCheckEndpoint: "/health", // Default health check endpoint
- }
- // Create template engine
- engine, err := templates.NewTemplateEngine()
- if err != nil {
- return "", fmt.Errorf("failed to create template engine: %w", err)
- }
- // Render template
- dockerfile, err := engine.Render("golang", templateData)
- if err != nil {
- return "", fmt.Errorf("failed to render Golang template: %w", err)
- }
- return dockerfile, nil
- }
- // DockerfileTemplateData represents the data passed to Golang Dockerfile templates
- type DockerfileTemplateData struct {
- AppName string `json:"app_name"`
- BinaryName string `json:"binary_name"`
- GoVersion string `json:"go_version"`
- Port int `json:"port"`
- BuildCommand string `json:"build_command"`
- HasGoMod bool `json:"has_go_mod"`
- HasGoSum bool `json:"has_go_sum"`
- HasVendor bool `json:"has_vendor"`
- CGOEnabled bool `json:"cgo_enabled"`
- NeedsBuildTools bool `json:"needs_build_tools"`
- BuildTools []string `json:"build_tools"`
- NeedsRuntimeDeps bool `json:"needs_runtime_deps"`
- RuntimeDeps []string `json:"runtime_deps"`
- RuntimeImage string `json:"runtime_image"`
- HealthCheckEndpoint string `json:"health_check_endpoint,omitempty"`
- }
- // GoProjectAnalysis contains analysis results for a Go project
- type GoProjectAnalysis struct {
- GoVersion string `json:"go_version"`
- AppName string `json:"app_name"`
- MainPackage string `json:"main_package"`
- Port int `json:"port"`
- Modules []string `json:"modules"`
- BuildTags []string `json:"build_tags"`
- RequiresCACerts bool `json:"requires_ca_certs"`
- RequiresTimezone bool `json:"requires_timezone"`
- EnvVars map[string]string `json:"env_vars"`
- CGOEnabled bool `json:"cgo_enabled"` // New field for CGO
- }
- // analyzeGoProject analyzes a Go project to understand its structure and requirements
- func (g *Golang) analyzeGoProject(codebasePath string) (*GoProjectAnalysis, error) {
- analysis := &GoProjectAnalysis{
- Port: 8080, // Default
- EnvVars: make(map[string]string),
- Modules: []string{},
- CGOEnabled: os.Getenv("CGO_ENABLED") == "1", // Check CGO_ENABLED env var
- }
- // Read go.mod to get module name and Go version
- goModPath := filepath.Join(codebasePath, "go.mod")
- if content, err := os.ReadFile(goModPath); err == nil {
- lines := strings.Split(string(content), "\n")
- for _, line := range lines {
- line = strings.TrimSpace(line)
- if strings.HasPrefix(line, "module ") {
- parts := strings.Fields(line)
- if len(parts) > 1 {
- moduleName := parts[1]
- // Extract app name from module path
- pathParts := strings.Split(moduleName, "/")
- analysis.AppName = pathParts[len(pathParts)-1]
- }
- } else if strings.HasPrefix(line, "go ") {
- parts := strings.Fields(line)
- if len(parts) > 1 {
- analysis.GoVersion = parts[1]
- }
- }
- }
- }
- // Find main package
- analysis.MainPackage = g.findMainPackage(codebasePath)
- // Analyze imports to determine requirements
- g.analyzeImports(codebasePath, analysis)
- // Try to detect port from common patterns
- analysis.Port = g.detectPort(codebasePath)
- // Set default app name if not found
- if analysis.AppName == "" {
- analysis.AppName = filepath.Base(codebasePath)
- if analysis.AppName == "." || analysis.AppName == "" {
- analysis.AppName = "goapp"
- }
- }
- return analysis, nil
- }
- func (g *Golang) findMainPackage(codebasePath string) string {
- // Strategy inspired by Railway Railpack - multiple fallback approaches
- // 1. Check for main.go in root (most common simple case)
- if g.hasMainGo(codebasePath) && g.hasRootGoFiles(codebasePath) {
- return "."
- }
- // 2. Look for cmd directory structure (standard Go layout)
- if dirs, err := g.findCmdDirectories(codebasePath); err == nil && len(dirs) > 0 {
- // Try to find the first directory with a main function
- for _, dir := range dirs {
- if g.hasMainFunction(filepath.Join(codebasePath, dir)) {
- return "./" + dir // Add ./ prefix for local path
- }
- }
- // Fallback to first cmd directory if none have main function
- return "./" + dirs[0]
- }
- // 3. Check if it's a Go workspace with multiple modules
- if g.isGoWorkspace(codebasePath) {
- packages := g.getGoWorkspacePackages(codebasePath)
- for _, pkg := range packages {
- pkgPath := filepath.Join(codebasePath, pkg)
- if g.hasMainGo(pkgPath) || g.hasMainFunction(pkgPath) {
- return "./" + pkg
- }
- }
- }
- // 4. Look for any directory with a main function as final fallback
- if entries, err := os.ReadDir(codebasePath); err == nil {
- for _, entry := range entries {
- if entry.IsDir() && entry.Name() != ".git" && entry.Name() != "vendor" && entry.Name() != "configs" {
- dirPath := filepath.Join(codebasePath, entry.Name())
- if g.hasMainFunction(dirPath) {
- return "./" + entry.Name()
- }
- }
- }
- }
- // 5. Default fallback
- return "."
- }
- // hasMainFunction checks if a directory contains Go files with a main function
- func (g *Golang) hasMainFunction(dirPath string) bool {
- files, err := os.ReadDir(dirPath)
- if err != nil {
- return false
- }
- for _, file := range files {
- if strings.HasSuffix(file.Name(), ".go") && !file.IsDir() {
- content, err := os.ReadFile(filepath.Join(dirPath, file.Name()))
- if err != nil {
- continue
- }
- contentStr := string(content)
- // Look for main function or main.go file
- if strings.Contains(contentStr, "func main(") || file.Name() == "main.go" {
- return true
- }
- }
- }
- return false
- }
- func (g *Golang) analyzeImports(codebasePath string, analysis *GoProjectAnalysis) {
- // Walk through Go files and analyze imports
- filepath.Walk(codebasePath, func(path string, info os.FileInfo, err error) error {
- if err != nil || !strings.HasSuffix(path, ".go") {
- return nil
- }
- content, err := os.ReadFile(path)
- if err != nil {
- return nil
- }
- contentStr := string(content)
- // Check for common patterns that require CA certs
- if strings.Contains(contentStr, "crypto/tls") ||
- strings.Contains(contentStr, "net/http") ||
- strings.Contains(contentStr, "github.com/") {
- analysis.RequiresCACerts = true
- }
- // Check for timezone requirements
- if strings.Contains(contentStr, "time.LoadLocation") ||
- strings.Contains(contentStr, "time.ParseInLocation") {
- analysis.RequiresTimezone = true
- }
- // Add common modules
- if strings.Contains(contentStr, "github.com/gin-gonic/gin") {
- analysis.Modules = append(analysis.Modules, "gin")
- }
- if strings.Contains(contentStr, "github.com/gorilla/mux") {
- analysis.Modules = append(analysis.Modules, "gorilla-mux")
- }
- if strings.Contains(contentStr, "github.com/labstack/echo") {
- analysis.Modules = append(analysis.Modules, "echo")
- }
- return nil
- })
- }
- func (g *Golang) detectPort(codebasePath string) int {
- defaultPort := 8080
- // Common port detection patterns
- patterns := []string{
- ":8080",
- ":3000",
- ":8000",
- "PORT",
- "HTTP_PORT",
- }
- filepath.Walk(codebasePath, func(path string, info os.FileInfo, err error) error {
- if err != nil || !strings.HasSuffix(path, ".go") {
- return nil
- }
- content, err := os.ReadFile(path)
- if err != nil {
- return nil
- }
- contentStr := string(content)
- for _, pattern := range patterns {
- if strings.Contains(contentStr, pattern) {
- // Try to extract actual port number
- if strings.Contains(pattern, ":") {
- portStr := strings.TrimPrefix(pattern, ":")
- if port := g.parsePort(portStr); port > 0 {
- defaultPort = port
- return filepath.SkipAll
- }
- }
- }
- }
- return nil
- })
- return defaultPort
- }
- func (g *Golang) parsePort(portStr string) int {
- // Simple port parsing - in production you'd want more robust parsing
- switch portStr {
- case "8080":
- return 8080
- case "3000":
- return 3000
- case "8000":
- return 8000
- default:
- return 0
- }
- }
- func (g *Golang) fileExists(path string) bool {
- _, err := os.Stat(path)
- return err == nil
- }
- func (g *Golang) dirExists(dirpath string) bool {
- info, err := os.Stat(dirpath)
- return err == nil && info.IsDir()
- }
- // Helper methods inspired by Railway Railpack
- func (g *Golang) isGoMod(codebasePath string) bool {
- _, err := os.Stat(filepath.Join(codebasePath, "go.mod"))
- return err == nil
- }
- func (g *Golang) isGoWorkspace(codebasePath string) bool {
- _, err := os.Stat(filepath.Join(codebasePath, "go.work"))
- return err == nil
- }
- func (g *Golang) hasMainGo(codebasePath string) bool {
- _, err := os.Stat(filepath.Join(codebasePath, "main.go"))
- return err == nil
- }
- func (g *Golang) hasRootGoFiles(codebasePath string) bool {
- files, err := filepath.Glob(filepath.Join(codebasePath, "*.go"))
- if err != nil {
- return false
- }
- return len(files) > 0
- }
- func (g *Golang) findCmdDirectories(codebasePath string) ([]string, error) {
- cmdDir := filepath.Join(codebasePath, "cmd")
- entries, err := os.ReadDir(cmdDir)
- if err != nil {
- return nil, err
- }
- var dirs []string
- for _, entry := range entries {
- if entry.IsDir() {
- dirs = append(dirs, filepath.Join("cmd", entry.Name()))
- }
- }
- return dirs, nil
- }
- func (g *Golang) getGoWorkspacePackages(codebasePath string) []string {
- var packages []string
- err := filepath.Walk(codebasePath, func(path string, info os.FileInfo, err error) error {
- if err != nil {
- return nil // Continue walking even if there's an error
- }
- if info.Name() == "go.mod" {
- relPath, err := filepath.Rel(codebasePath, filepath.Dir(path))
- if err != nil {
- return nil
- }
- // Skip the root go.mod
- if relPath != "." {
- packages = append(packages, relPath)
- }
- }
- return nil
- })
- if err != nil {
- return packages
- }
- return packages
- }
- // Enhanced build strategy inspired by Railway Railpack
- func (g *Golang) getBuildCommand(analysis *GoProjectAnalysis, codebasePath string) string {
- appName := analysis.AppName
- if appName == "" {
- appName = "app"
- }
- flags := "-w -s"
- if !analysis.CGOEnabled {
- flags = "-w -s -extldflags \"-static\""
- }
- baseBuildCmd := fmt.Sprintf("go build -ldflags=\"%s\" -o %s", flags, appName)
- // Strategy 1: Use explicit main package if detected
- if analysis.MainPackage != "" && analysis.MainPackage != "." {
- return fmt.Sprintf("%s %s", baseBuildCmd, analysis.MainPackage)
- }
- // Strategy 2: Check for root Go files
- if g.hasRootGoFiles(codebasePath) && g.isGoMod(codebasePath) {
- return baseBuildCmd
- }
- // Strategy 3: Try cmd directories
- if dirs, err := g.findCmdDirectories(codebasePath); err == nil && len(dirs) > 0 {
- return fmt.Sprintf("%s ./%s", baseBuildCmd, dirs[0])
- }
- // Strategy 4: Go workspace
- if g.isGoWorkspace(codebasePath) {
- packages := g.getGoWorkspacePackages(codebasePath)
- for _, pkg := range packages {
- if g.hasMainGo(filepath.Join(codebasePath, pkg)) {
- return fmt.Sprintf("%s ./%s", baseBuildCmd, pkg)
- }
- }
- }
- // Strategy 5: Fallback to main.go if present
- if g.hasMainGo(codebasePath) {
- return fmt.Sprintf("%s main.go", baseBuildCmd)
- }
- // Default
- return baseBuildCmd
- }
- // Enhanced Go version detection
- func (g *Golang) getGoVersion(codebasePath string) string {
- // First check go.mod file
- if goModContents, err := os.ReadFile(filepath.Join(codebasePath, "go.mod")); err == nil {
- lines := strings.Split(string(goModContents), "\n")
- for _, line := range lines {
- line = strings.TrimSpace(line)
- if strings.HasPrefix(line, "go ") {
- parts := strings.Fields(line)
- if len(parts) > 1 {
- return parts[1]
- }
- }
- }
- }
- // Check environment variable
- if envVersion := os.Getenv("GO_VERSION"); envVersion != "" {
- return envVersion
- }
- // Default version
- return "1.23"
- }
|