golang.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517
  1. package golang
  2. import (
  3. "fmt"
  4. "os"
  5. "path/filepath"
  6. "strings"
  7. "git.linuxforward.com/byop/byop-engine/analyzer/templates"
  8. )
  9. type Golang struct {
  10. // Add fields as needed for Golang analysis
  11. }
  12. // Name returns the name of the Golang technology stack.
  13. func (g *Golang) Name() string {
  14. return "Golang"
  15. }
  16. // Analyze analyzes the codebase and returns a guessed technology stack for Golang.
  17. func (g *Golang) Analyze(codebasePath string) (bool, error) {
  18. // Verify if the codebasePath is valid and contains Go files
  19. if codebasePath == "" {
  20. return false, fmt.Errorf("codebase path cannot be empty")
  21. }
  22. // Check if the codebase exists and contains Go files
  23. _, err := os.Stat(codebasePath)
  24. if os.IsNotExist(err) {
  25. return false, fmt.Errorf("codebase path does not exist: %s", codebasePath)
  26. }
  27. // Enhanced detection logic inspired by Railway Railpack
  28. return g.isGoMod(codebasePath) || g.isGoWorkspace(codebasePath) || g.hasMainGo(codebasePath), nil
  29. }
  30. // GenerateDockerfile generates a Dockerfile for Golang projects based on analysis
  31. func (g *Golang) GenerateDockerfile(codebasePath string) (string, error) {
  32. // Analyze the Go project
  33. analysis, err := g.analyzeGoProject(codebasePath)
  34. if err != nil {
  35. return "", fmt.Errorf("failed to analyze Go project: %w", err)
  36. }
  37. // Check for file existence
  38. hasGoMod := g.fileExists(filepath.Join(codebasePath, "go.mod"))
  39. hasGoSum := g.fileExists(filepath.Join(codebasePath, "go.sum"))
  40. hasVendor := g.dirExists(filepath.Join(codebasePath, "vendor"))
  41. // Determine build tools needed
  42. buildTools := []string{"git"} // Always need git for go get
  43. needsBuildTools := true
  44. // Determine runtime dependencies
  45. runtimeDeps := []string{}
  46. needsRuntimeDeps := false
  47. runtimeImage := "alpine:latest"
  48. if analysis.RequiresCACerts {
  49. runtimeDeps = append(runtimeDeps, "ca-certificates")
  50. needsRuntimeDeps = true
  51. }
  52. if analysis.RequiresTimezone {
  53. runtimeDeps = append(runtimeDeps, "tzdata")
  54. needsRuntimeDeps = true
  55. }
  56. if analysis.CGOEnabled {
  57. runtimeDeps = append(runtimeDeps, "libc6-compat")
  58. needsRuntimeDeps = true
  59. buildTools = append(buildTools, "build-base")
  60. }
  61. // If no runtime deps needed, we can use scratch
  62. if !needsRuntimeDeps {
  63. runtimeImage = "scratch"
  64. }
  65. // Create template data
  66. templateData := DockerfileTemplateData{
  67. AppName: analysis.AppName,
  68. BinaryName: analysis.AppName,
  69. GoVersion: g.getGoVersion(codebasePath),
  70. Port: analysis.Port,
  71. BuildCommand: g.getBuildCommand(analysis, codebasePath),
  72. HasGoMod: hasGoMod,
  73. HasGoSum: hasGoSum,
  74. HasVendor: hasVendor,
  75. CGOEnabled: analysis.CGOEnabled,
  76. NeedsBuildTools: needsBuildTools,
  77. BuildTools: buildTools,
  78. NeedsRuntimeDeps: needsRuntimeDeps,
  79. RuntimeDeps: runtimeDeps,
  80. RuntimeImage: runtimeImage,
  81. HealthCheckEndpoint: "/health", // Default health check endpoint
  82. }
  83. // Create template engine
  84. engine, err := templates.NewTemplateEngine()
  85. if err != nil {
  86. return "", fmt.Errorf("failed to create template engine: %w", err)
  87. }
  88. // Render template
  89. dockerfile, err := engine.Render("golang", templateData)
  90. if err != nil {
  91. return "", fmt.Errorf("failed to render Golang template: %w", err)
  92. }
  93. return dockerfile, nil
  94. }
  95. // DockerfileTemplateData represents the data passed to Golang Dockerfile templates
  96. type DockerfileTemplateData struct {
  97. AppName string `json:"app_name"`
  98. BinaryName string `json:"binary_name"`
  99. GoVersion string `json:"go_version"`
  100. Port int `json:"port"`
  101. BuildCommand string `json:"build_command"`
  102. HasGoMod bool `json:"has_go_mod"`
  103. HasGoSum bool `json:"has_go_sum"`
  104. HasVendor bool `json:"has_vendor"`
  105. CGOEnabled bool `json:"cgo_enabled"`
  106. NeedsBuildTools bool `json:"needs_build_tools"`
  107. BuildTools []string `json:"build_tools"`
  108. NeedsRuntimeDeps bool `json:"needs_runtime_deps"`
  109. RuntimeDeps []string `json:"runtime_deps"`
  110. RuntimeImage string `json:"runtime_image"`
  111. HealthCheckEndpoint string `json:"health_check_endpoint,omitempty"`
  112. }
  113. // GoProjectAnalysis contains analysis results for a Go project
  114. type GoProjectAnalysis struct {
  115. GoVersion string `json:"go_version"`
  116. AppName string `json:"app_name"`
  117. MainPackage string `json:"main_package"`
  118. Port int `json:"port"`
  119. Modules []string `json:"modules"`
  120. BuildTags []string `json:"build_tags"`
  121. RequiresCACerts bool `json:"requires_ca_certs"`
  122. RequiresTimezone bool `json:"requires_timezone"`
  123. EnvVars map[string]string `json:"env_vars"`
  124. CGOEnabled bool `json:"cgo_enabled"` // New field for CGO
  125. }
  126. // analyzeGoProject analyzes a Go project to understand its structure and requirements
  127. func (g *Golang) analyzeGoProject(codebasePath string) (*GoProjectAnalysis, error) {
  128. analysis := &GoProjectAnalysis{
  129. Port: 8080, // Default
  130. EnvVars: make(map[string]string),
  131. Modules: []string{},
  132. CGOEnabled: os.Getenv("CGO_ENABLED") == "1", // Check CGO_ENABLED env var
  133. }
  134. // Read go.mod to get module name and Go version
  135. goModPath := filepath.Join(codebasePath, "go.mod")
  136. if content, err := os.ReadFile(goModPath); err == nil {
  137. lines := strings.Split(string(content), "\n")
  138. for _, line := range lines {
  139. line = strings.TrimSpace(line)
  140. if strings.HasPrefix(line, "module ") {
  141. parts := strings.Fields(line)
  142. if len(parts) > 1 {
  143. moduleName := parts[1]
  144. // Extract app name from module path
  145. pathParts := strings.Split(moduleName, "/")
  146. analysis.AppName = pathParts[len(pathParts)-1]
  147. }
  148. } else if strings.HasPrefix(line, "go ") {
  149. parts := strings.Fields(line)
  150. if len(parts) > 1 {
  151. analysis.GoVersion = parts[1]
  152. }
  153. }
  154. }
  155. }
  156. // Find main package
  157. analysis.MainPackage = g.findMainPackage(codebasePath)
  158. // Analyze imports to determine requirements
  159. g.analyzeImports(codebasePath, analysis)
  160. // Try to detect port from common patterns
  161. analysis.Port = g.detectPort(codebasePath)
  162. // Set default app name if not found
  163. if analysis.AppName == "" {
  164. analysis.AppName = filepath.Base(codebasePath)
  165. if analysis.AppName == "." || analysis.AppName == "" {
  166. analysis.AppName = "goapp"
  167. }
  168. }
  169. return analysis, nil
  170. }
  171. func (g *Golang) findMainPackage(codebasePath string) string {
  172. // Strategy inspired by Railway Railpack - multiple fallback approaches
  173. // 1. Check for main.go in root (most common simple case)
  174. if g.hasMainGo(codebasePath) && g.hasRootGoFiles(codebasePath) {
  175. return "."
  176. }
  177. // 2. Look for cmd directory structure (standard Go layout)
  178. if dirs, err := g.findCmdDirectories(codebasePath); err == nil && len(dirs) > 0 {
  179. // Try to find the first directory with a main function
  180. for _, dir := range dirs {
  181. if g.hasMainFunction(filepath.Join(codebasePath, dir)) {
  182. return "./" + dir // Add ./ prefix for local path
  183. }
  184. }
  185. // Fallback to first cmd directory if none have main function
  186. return "./" + dirs[0]
  187. }
  188. // 3. Check if it's a Go workspace with multiple modules
  189. if g.isGoWorkspace(codebasePath) {
  190. packages := g.getGoWorkspacePackages(codebasePath)
  191. for _, pkg := range packages {
  192. pkgPath := filepath.Join(codebasePath, pkg)
  193. if g.hasMainGo(pkgPath) || g.hasMainFunction(pkgPath) {
  194. return "./" + pkg
  195. }
  196. }
  197. }
  198. // 4. Look for any directory with a main function as final fallback
  199. if entries, err := os.ReadDir(codebasePath); err == nil {
  200. for _, entry := range entries {
  201. if entry.IsDir() && entry.Name() != ".git" && entry.Name() != "vendor" && entry.Name() != "configs" {
  202. dirPath := filepath.Join(codebasePath, entry.Name())
  203. if g.hasMainFunction(dirPath) {
  204. return "./" + entry.Name()
  205. }
  206. }
  207. }
  208. }
  209. // 5. Default fallback
  210. return "."
  211. }
  212. // hasMainFunction checks if a directory contains Go files with a main function
  213. func (g *Golang) hasMainFunction(dirPath string) bool {
  214. files, err := os.ReadDir(dirPath)
  215. if err != nil {
  216. return false
  217. }
  218. for _, file := range files {
  219. if strings.HasSuffix(file.Name(), ".go") && !file.IsDir() {
  220. content, err := os.ReadFile(filepath.Join(dirPath, file.Name()))
  221. if err != nil {
  222. continue
  223. }
  224. contentStr := string(content)
  225. // Look for main function or main.go file
  226. if strings.Contains(contentStr, "func main(") || file.Name() == "main.go" {
  227. return true
  228. }
  229. }
  230. }
  231. return false
  232. }
  233. func (g *Golang) analyzeImports(codebasePath string, analysis *GoProjectAnalysis) {
  234. // Walk through Go files and analyze imports
  235. filepath.Walk(codebasePath, func(path string, info os.FileInfo, err error) error {
  236. if err != nil || !strings.HasSuffix(path, ".go") {
  237. return nil
  238. }
  239. content, err := os.ReadFile(path)
  240. if err != nil {
  241. return nil
  242. }
  243. contentStr := string(content)
  244. // Check for common patterns that require CA certs
  245. if strings.Contains(contentStr, "crypto/tls") ||
  246. strings.Contains(contentStr, "net/http") ||
  247. strings.Contains(contentStr, "github.com/") {
  248. analysis.RequiresCACerts = true
  249. }
  250. // Check for timezone requirements
  251. if strings.Contains(contentStr, "time.LoadLocation") ||
  252. strings.Contains(contentStr, "time.ParseInLocation") {
  253. analysis.RequiresTimezone = true
  254. }
  255. // Add common modules
  256. if strings.Contains(contentStr, "github.com/gin-gonic/gin") {
  257. analysis.Modules = append(analysis.Modules, "gin")
  258. }
  259. if strings.Contains(contentStr, "github.com/gorilla/mux") {
  260. analysis.Modules = append(analysis.Modules, "gorilla-mux")
  261. }
  262. if strings.Contains(contentStr, "github.com/labstack/echo") {
  263. analysis.Modules = append(analysis.Modules, "echo")
  264. }
  265. return nil
  266. })
  267. }
  268. func (g *Golang) detectPort(codebasePath string) int {
  269. defaultPort := 8080
  270. // Common port detection patterns
  271. patterns := []string{
  272. ":8080",
  273. ":3000",
  274. ":8000",
  275. "PORT",
  276. "HTTP_PORT",
  277. }
  278. filepath.Walk(codebasePath, func(path string, info os.FileInfo, err error) error {
  279. if err != nil || !strings.HasSuffix(path, ".go") {
  280. return nil
  281. }
  282. content, err := os.ReadFile(path)
  283. if err != nil {
  284. return nil
  285. }
  286. contentStr := string(content)
  287. for _, pattern := range patterns {
  288. if strings.Contains(contentStr, pattern) {
  289. // Try to extract actual port number
  290. if strings.Contains(pattern, ":") {
  291. portStr := strings.TrimPrefix(pattern, ":")
  292. if port := g.parsePort(portStr); port > 0 {
  293. defaultPort = port
  294. return filepath.SkipAll
  295. }
  296. }
  297. }
  298. }
  299. return nil
  300. })
  301. return defaultPort
  302. }
  303. func (g *Golang) parsePort(portStr string) int {
  304. // Simple port parsing - in production you'd want more robust parsing
  305. switch portStr {
  306. case "8080":
  307. return 8080
  308. case "3000":
  309. return 3000
  310. case "8000":
  311. return 8000
  312. default:
  313. return 0
  314. }
  315. }
  316. func (g *Golang) fileExists(path string) bool {
  317. _, err := os.Stat(path)
  318. return err == nil
  319. }
  320. func (g *Golang) dirExists(dirpath string) bool {
  321. info, err := os.Stat(dirpath)
  322. return err == nil && info.IsDir()
  323. }
  324. // Helper methods inspired by Railway Railpack
  325. func (g *Golang) isGoMod(codebasePath string) bool {
  326. _, err := os.Stat(filepath.Join(codebasePath, "go.mod"))
  327. return err == nil
  328. }
  329. func (g *Golang) isGoWorkspace(codebasePath string) bool {
  330. _, err := os.Stat(filepath.Join(codebasePath, "go.work"))
  331. return err == nil
  332. }
  333. func (g *Golang) hasMainGo(codebasePath string) bool {
  334. _, err := os.Stat(filepath.Join(codebasePath, "main.go"))
  335. return err == nil
  336. }
  337. func (g *Golang) hasRootGoFiles(codebasePath string) bool {
  338. files, err := filepath.Glob(filepath.Join(codebasePath, "*.go"))
  339. if err != nil {
  340. return false
  341. }
  342. return len(files) > 0
  343. }
  344. func (g *Golang) findCmdDirectories(codebasePath string) ([]string, error) {
  345. cmdDir := filepath.Join(codebasePath, "cmd")
  346. entries, err := os.ReadDir(cmdDir)
  347. if err != nil {
  348. return nil, err
  349. }
  350. var dirs []string
  351. for _, entry := range entries {
  352. if entry.IsDir() {
  353. dirs = append(dirs, filepath.Join("cmd", entry.Name()))
  354. }
  355. }
  356. return dirs, nil
  357. }
  358. func (g *Golang) getGoWorkspacePackages(codebasePath string) []string {
  359. var packages []string
  360. err := filepath.Walk(codebasePath, func(path string, info os.FileInfo, err error) error {
  361. if err != nil {
  362. return nil // Continue walking even if there's an error
  363. }
  364. if info.Name() == "go.mod" {
  365. relPath, err := filepath.Rel(codebasePath, filepath.Dir(path))
  366. if err != nil {
  367. return nil
  368. }
  369. // Skip the root go.mod
  370. if relPath != "." {
  371. packages = append(packages, relPath)
  372. }
  373. }
  374. return nil
  375. })
  376. if err != nil {
  377. return packages
  378. }
  379. return packages
  380. }
  381. // Enhanced build strategy inspired by Railway Railpack
  382. func (g *Golang) getBuildCommand(analysis *GoProjectAnalysis, codebasePath string) string {
  383. appName := analysis.AppName
  384. if appName == "" {
  385. appName = "app"
  386. }
  387. flags := "-w -s"
  388. if !analysis.CGOEnabled {
  389. flags = "-w -s -extldflags \"-static\""
  390. }
  391. baseBuildCmd := fmt.Sprintf("go build -ldflags=\"%s\" -o %s", flags, appName)
  392. // Strategy 1: Use explicit main package if detected
  393. if analysis.MainPackage != "" && analysis.MainPackage != "." {
  394. return fmt.Sprintf("%s %s", baseBuildCmd, analysis.MainPackage)
  395. }
  396. // Strategy 2: Check for root Go files
  397. if g.hasRootGoFiles(codebasePath) && g.isGoMod(codebasePath) {
  398. return baseBuildCmd
  399. }
  400. // Strategy 3: Try cmd directories
  401. if dirs, err := g.findCmdDirectories(codebasePath); err == nil && len(dirs) > 0 {
  402. return fmt.Sprintf("%s ./%s", baseBuildCmd, dirs[0])
  403. }
  404. // Strategy 4: Go workspace
  405. if g.isGoWorkspace(codebasePath) {
  406. packages := g.getGoWorkspacePackages(codebasePath)
  407. for _, pkg := range packages {
  408. if g.hasMainGo(filepath.Join(codebasePath, pkg)) {
  409. return fmt.Sprintf("%s ./%s", baseBuildCmd, pkg)
  410. }
  411. }
  412. }
  413. // Strategy 5: Fallback to main.go if present
  414. if g.hasMainGo(codebasePath) {
  415. return fmt.Sprintf("%s main.go", baseBuildCmd)
  416. }
  417. // Default
  418. return baseBuildCmd
  419. }
  420. // Enhanced Go version detection
  421. func (g *Golang) getGoVersion(codebasePath string) string {
  422. // First check go.mod file
  423. if goModContents, err := os.ReadFile(filepath.Join(codebasePath, "go.mod")); err == nil {
  424. lines := strings.Split(string(goModContents), "\n")
  425. for _, line := range lines {
  426. line = strings.TrimSpace(line)
  427. if strings.HasPrefix(line, "go ") {
  428. parts := strings.Fields(line)
  429. if len(parts) > 1 {
  430. return parts[1]
  431. }
  432. }
  433. }
  434. }
  435. // Check environment variable
  436. if envVersion := os.Getenv("GO_VERSION"); envVersion != "" {
  437. return envVersion
  438. }
  439. // Default version
  440. return "1.23"
  441. }