api_ruby.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473
  1. package buildpacks
  2. import (
  3. "bufio"
  4. "context"
  5. "fmt"
  6. "os"
  7. "regexp"
  8. "strings"
  9. "sync"
  10. "github.com/google/go-github/github"
  11. "github.com/pelletier/go-toml"
  12. )
  13. type apiRubyRuntime struct {
  14. wg sync.WaitGroup
  15. packs map[string]*BuildpackInfo
  16. }
  17. // FIXME: should be called once at the top-level somewhere in the backend
  18. func populateRubyPacks(client *github.Client) map[string]*BuildpackInfo {
  19. packs := make(map[string]*BuildpackInfo)
  20. repoRelease, _, err := client.Repositories.GetLatestRelease(context.Background(), "paketo-buildpacks", "ruby")
  21. if err != nil {
  22. fmt.Printf("Error fetching latest release for paketo-buildpacks/ruby: %v\n", err)
  23. return nil
  24. }
  25. fileContent, _, _, err := client.Repositories.GetContents(
  26. context.Background(), "paketo-buildpacks", "ruby", "buildpack.toml",
  27. &github.RepositoryContentGetOptions{
  28. Ref: *repoRelease.TagName,
  29. },
  30. )
  31. if err != nil {
  32. fmt.Printf("Error fetching contents of buildpack.toml for paketo-buildpacks/ruby: %v\n", err)
  33. return nil
  34. }
  35. data, err := fileContent.GetContent()
  36. if err != nil {
  37. fmt.Printf("Error calling GetContent() on buildpack.toml for paketo-buildpacks/ruby: %v\n", err)
  38. return nil
  39. }
  40. buildpackToml, err := toml.Load(data)
  41. if err != nil {
  42. fmt.Printf("Error while reading buildpack.toml from paketo-buildpacks/ruby: %v\n", err)
  43. os.Exit(1)
  44. }
  45. order := buildpackToml.Get("order").([]*toml.Tree)
  46. // puma
  47. packs[puma] = newBuildpackInfo()
  48. pumaGroup := order[0].GetArray("group").([]*toml.Tree)
  49. for i := 0; i < len(pumaGroup); i++ {
  50. packs[puma].addPack(
  51. buildpackOrderGroupInfo{
  52. ID: pumaGroup[i].Get("id").(string),
  53. Optional: pumaGroup[i].GetDefault("optional", false).(bool),
  54. Version: pumaGroup[i].Get("version").(string),
  55. },
  56. )
  57. }
  58. // thin
  59. packs[thin] = newBuildpackInfo()
  60. thinGroup := order[1].GetArray("group").([]*toml.Tree)
  61. for i := 0; i < len(thinGroup); i++ {
  62. packs[thin].addPack(
  63. buildpackOrderGroupInfo{
  64. ID: thinGroup[i].Get("id").(string),
  65. Optional: thinGroup[i].GetDefault("optional", false).(bool),
  66. Version: thinGroup[i].Get("version").(string),
  67. },
  68. )
  69. }
  70. // unicorn
  71. packs[unicorn] = newBuildpackInfo()
  72. unicornGroup := order[2].GetArray("group").([]*toml.Tree)
  73. for i := 0; i < len(unicornGroup); i++ {
  74. packs[unicorn].addPack(
  75. buildpackOrderGroupInfo{
  76. ID: unicornGroup[i].Get("id").(string),
  77. Optional: unicornGroup[i].GetDefault("optional", false).(bool),
  78. Version: unicornGroup[i].Get("version").(string),
  79. },
  80. )
  81. }
  82. // passenger
  83. packs[passenger] = newBuildpackInfo()
  84. passengerGroup := order[3].GetArray("group").([]*toml.Tree)
  85. for i := 0; i < len(passengerGroup); i++ {
  86. packs[passenger].addPack(
  87. buildpackOrderGroupInfo{
  88. ID: passengerGroup[i].Get("id").(string),
  89. Optional: passengerGroup[i].GetDefault("optional", false).(bool),
  90. Version: passengerGroup[i].Get("version").(string),
  91. },
  92. )
  93. }
  94. // rackup
  95. packs[rackup] = newBuildpackInfo()
  96. rackupGroup := order[4].GetArray("group").([]*toml.Tree)
  97. for i := 0; i < len(rackupGroup); i++ {
  98. packs[rackup].addPack(
  99. buildpackOrderGroupInfo{
  100. ID: rackupGroup[i].Get("id").(string),
  101. Optional: rackupGroup[i].GetDefault("optional", false).(bool),
  102. Version: rackupGroup[i].Get("version").(string),
  103. },
  104. )
  105. }
  106. // rake
  107. packs[rake] = newBuildpackInfo()
  108. rakeGroup := order[5].GetArray("group").([]*toml.Tree)
  109. for i := 0; i < len(rakeGroup); i++ {
  110. packs[rake].addPack(
  111. buildpackOrderGroupInfo{
  112. ID: rakeGroup[i].Get("id").(string),
  113. Optional: rakeGroup[i].GetDefault("optional", false).(bool),
  114. Version: rakeGroup[i].Get("version").(string),
  115. },
  116. )
  117. }
  118. return packs
  119. }
  120. func NewAPIRubyRuntime() APIRuntime {
  121. return &apiRubyRuntime{}
  122. }
  123. func (runtime *apiRubyRuntime) detectPuma(gemfileContent string, results chan struct {
  124. string
  125. bool
  126. }) {
  127. pumaFound := false
  128. quotes := `["']`
  129. pumaRe := regexp.MustCompile(fmt.Sprintf(`^\s*gem %spuma%s`, quotes, quotes))
  130. scanner := bufio.NewScanner(strings.NewReader(gemfileContent))
  131. for scanner.Scan() {
  132. line := []byte(scanner.Text())
  133. if pumaRe.Match(line) {
  134. pumaFound = true
  135. break
  136. }
  137. }
  138. if pumaFound {
  139. results <- struct {
  140. string
  141. bool
  142. }{puma, true}
  143. } else {
  144. results <- struct {
  145. string
  146. bool
  147. }{puma, false}
  148. }
  149. runtime.wg.Done()
  150. }
  151. func (runtime *apiRubyRuntime) detectThin(gemfileContent string, results chan struct {
  152. string
  153. bool
  154. }) {
  155. thinFound := false
  156. quotes := `["']`
  157. thinRe := regexp.MustCompile(fmt.Sprintf(`^\s*gem %sthin%s`, quotes, quotes))
  158. scanner := bufio.NewScanner(strings.NewReader(gemfileContent))
  159. for scanner.Scan() {
  160. line := []byte(scanner.Text())
  161. if thinRe.Match(line) {
  162. thinFound = true
  163. break
  164. }
  165. }
  166. if thinFound {
  167. results <- struct {
  168. string
  169. bool
  170. }{thin, true}
  171. } else {
  172. results <- struct {
  173. string
  174. bool
  175. }{thin, false}
  176. }
  177. runtime.wg.Done()
  178. }
  179. func (runtime *apiRubyRuntime) detectUnicorn(gemfileContent string, results chan struct {
  180. string
  181. bool
  182. }) {
  183. unicornFound := false
  184. quotes := `["']`
  185. unicornRe := regexp.MustCompile(fmt.Sprintf(`^\s*gem %sunicorn%s`, quotes, quotes))
  186. scanner := bufio.NewScanner(strings.NewReader(gemfileContent))
  187. for scanner.Scan() {
  188. line := []byte(scanner.Text())
  189. if unicornRe.Match(line) {
  190. unicornFound = true
  191. break
  192. }
  193. }
  194. if unicornFound {
  195. results <- struct {
  196. string
  197. bool
  198. }{unicorn, true}
  199. } else {
  200. results <- struct {
  201. string
  202. bool
  203. }{unicorn, false}
  204. }
  205. runtime.wg.Done()
  206. }
  207. func (runtime *apiRubyRuntime) detectPassenger(gemfileContent string, results chan struct {
  208. string
  209. bool
  210. }) {
  211. passengerFound := false
  212. quotes := `["']`
  213. passengerRe := regexp.MustCompile(fmt.Sprintf(`^\s*gem %spassenger%s`, quotes, quotes))
  214. scanner := bufio.NewScanner(strings.NewReader(gemfileContent))
  215. for scanner.Scan() {
  216. line := []byte(scanner.Text())
  217. if passengerRe.Match(line) {
  218. passengerFound = true
  219. break
  220. }
  221. }
  222. if passengerFound {
  223. results <- struct {
  224. string
  225. bool
  226. }{passenger, true}
  227. } else {
  228. results <- struct {
  229. string
  230. bool
  231. }{passenger, false}
  232. }
  233. runtime.wg.Done()
  234. }
  235. func (runtime *apiRubyRuntime) detectRackup(
  236. client *github.Client, owner, name string,
  237. repoContentOptions github.RepositoryContentGetOptions, results chan struct {
  238. string
  239. bool
  240. },
  241. ) {
  242. fileContent, _, _, err := client.Repositories.GetContents(context.Background(),
  243. owner, name, "Gemfile.lock", &repoContentOptions)
  244. if err != nil {
  245. fmt.Printf("Error fetching contents of Gemfile.lock for %s/%s: %v\n", owner, name, err)
  246. runtime.wg.Done()
  247. return
  248. }
  249. gemfileLockContent, err := fileContent.GetContent()
  250. if err != nil {
  251. fmt.Printf("Error calling GetContent() on Gemfile.lock for %s/%s: %v\n", owner, name, err)
  252. runtime.wg.Done()
  253. return
  254. }
  255. rackFound := false
  256. scanner := bufio.NewScanner(strings.NewReader(gemfileLockContent))
  257. for scanner.Scan() {
  258. if strings.TrimSpace(scanner.Text()) == "GEM" {
  259. for scanner.Scan() {
  260. if strings.Contains(scanner.Text(), "rack") {
  261. rackFound = true
  262. break
  263. }
  264. }
  265. }
  266. }
  267. if rackFound {
  268. results <- struct {
  269. string
  270. bool
  271. }{rackup, true}
  272. } else {
  273. results <- struct {
  274. string
  275. bool
  276. }{rackup, false}
  277. }
  278. runtime.wg.Done()
  279. }
  280. func (runtime *apiRubyRuntime) detectRake(gemfileContent string, results chan struct {
  281. string
  282. bool
  283. }) {
  284. rakeFound := false
  285. quotes := `["']`
  286. rakeRe := regexp.MustCompile(fmt.Sprintf(`^\s*gem %srake%s`, quotes, quotes))
  287. scanner := bufio.NewScanner(strings.NewReader(gemfileContent))
  288. for scanner.Scan() {
  289. line := []byte(scanner.Text())
  290. if rakeRe.Match(line) {
  291. rakeFound = true
  292. break
  293. }
  294. }
  295. if rakeFound {
  296. results <- struct {
  297. string
  298. bool
  299. }{rake, true}
  300. } else {
  301. results <- struct {
  302. string
  303. bool
  304. }{rake, false}
  305. }
  306. runtime.wg.Done()
  307. }
  308. func (runtime *apiRubyRuntime) Detect(
  309. client *github.Client,
  310. directoryContent []*github.RepositoryContent,
  311. owner, name, path string,
  312. repoContentOptions github.RepositoryContentGetOptions,
  313. ) *RuntimeResponse {
  314. runtime.packs = populateRubyPacks(client)
  315. gemfileFound := false
  316. gemfileLockFound := false
  317. configRuFound := false
  318. rakefileFound := false
  319. for i := range directoryContent {
  320. name := directoryContent[i].GetName()
  321. if name == "Gemfile" {
  322. gemfileFound = true
  323. } else if name == "Gemfile.lock" {
  324. gemfileLockFound = true
  325. } else if name == "config.ru" {
  326. configRuFound = true
  327. } else if name == "Rakefile" || name == "Rakefile.rb" || name == "rakefile" || name == "rakefile.rb" {
  328. rakefileFound = true
  329. }
  330. }
  331. if !gemfileFound {
  332. fmt.Printf("No Ruby runtime detected for %s/%s\n", owner, name)
  333. return nil
  334. }
  335. fileContent, _, _, err := client.Repositories.GetContents(context.Background(), owner, name, "Gemfile", &repoContentOptions)
  336. if err != nil {
  337. fmt.Printf("Error fetching contents of Gemfile for %s/%s: %v\n", owner, name, err)
  338. return nil
  339. }
  340. gemfileContent, err := fileContent.GetContent()
  341. if err != nil {
  342. fmt.Printf("Error calling GetContent() on Gemfile for %s/%s: %v\n", owner, name, err)
  343. return nil
  344. }
  345. count := 6
  346. if !configRuFound {
  347. // unicorn needs config.ru
  348. count -= 1
  349. if !gemfileLockFound {
  350. // rackup needs one of Gemfile.lock or config.ru
  351. count -= 1
  352. }
  353. }
  354. if !rakefileFound {
  355. count -= 1
  356. }
  357. results := make(chan struct {
  358. string
  359. bool
  360. }, count)
  361. fmt.Printf("Starting detection for a Ruby runtime for %s/%s\n", owner, name)
  362. runtime.wg.Add(count)
  363. fmt.Println("Checking for puma")
  364. go runtime.detectPuma(gemfileContent, results)
  365. fmt.Println("Checking for thin")
  366. go runtime.detectThin(gemfileContent, results)
  367. if configRuFound {
  368. {
  369. // FIXME: find a better, more readable way of doing this
  370. fmt.Printf("Ruby rackup runtime detected for %s/%s\n", owner, name)
  371. results <- struct {
  372. string
  373. bool
  374. }{rackup, true}
  375. runtime.wg.Done()
  376. }
  377. fmt.Println("Checking for unicorn")
  378. runtime.detectUnicorn(gemfileContent, results)
  379. }
  380. fmt.Println("Checking for passenger")
  381. go runtime.detectPassenger(gemfileContent, results)
  382. if !configRuFound && gemfileLockFound {
  383. fmt.Println("Checking for rackup")
  384. go runtime.detectRackup(client, owner, name, repoContentOptions, results)
  385. }
  386. if rakefileFound {
  387. fmt.Println("Checking for rake")
  388. runtime.detectRake(gemfileContent, results)
  389. }
  390. runtime.wg.Wait()
  391. close(results)
  392. detected := make(map[string]bool)
  393. for result := range results {
  394. detected[result.string] = result.bool
  395. }
  396. // TODO: how to access config values for Ruby projects
  397. if found, ok := detected[puma]; ok && found {
  398. fmt.Printf("Ruby puma runtime detected for %s/%s\n", owner, name)
  399. return &RuntimeResponse{
  400. Name: "Ruby",
  401. Runtime: puma,
  402. Buildpacks: runtime.packs[puma],
  403. }
  404. } else if found, ok := detected[thin]; ok && found {
  405. fmt.Printf("Ruby thin runtime detected for %s/%s\n", owner, name)
  406. return &RuntimeResponse{
  407. Name: "Ruby",
  408. Runtime: thin,
  409. Buildpacks: runtime.packs[thin],
  410. }
  411. } else if found, ok := detected[unicorn]; ok && found {
  412. fmt.Printf("Ruby unicorn runtime detected for %s/%s\n", owner, name)
  413. return &RuntimeResponse{
  414. Name: "Ruby",
  415. Runtime: unicorn,
  416. Buildpacks: runtime.packs[unicorn],
  417. }
  418. } else if found, ok := detected[passenger]; ok && found {
  419. fmt.Printf("Ruby passenger runtime detected for %s/%s\n", owner, name)
  420. return &RuntimeResponse{
  421. Name: "Ruby",
  422. Runtime: passenger,
  423. Buildpacks: runtime.packs[passenger],
  424. }
  425. } else if found, ok := detected[rackup]; ok && found {
  426. fmt.Printf("Ruby rackup runtime detected for %s/%s\n", owner, name)
  427. return &RuntimeResponse{
  428. Name: "Ruby",
  429. Runtime: rackup,
  430. Buildpacks: runtime.packs[rackup],
  431. }
  432. } else if found, ok := detected[rake]; ok && found {
  433. fmt.Printf("Ruby rake runtime detected for %s/%s\n", owner, name)
  434. return &RuntimeResponse{
  435. Name: "Ruby",
  436. Runtime: rake,
  437. Buildpacks: runtime.packs[rake],
  438. }
  439. }
  440. panic("[api_ruby.go] This should ne never reached")
  441. }