Mohammed Nafees 4 лет назад
Родитель
Сommit
11385b4f59

+ 12 - 8
internal/integrations/buildpacks/api_python.go

@@ -255,26 +255,30 @@ func (runtime *apiPythonRuntime) Detect(
 	if detected[pipenv] {
 		fmt.Printf("Python pipenv runtime detected for %s/%s\n", owner, name)
 		return &RuntimeResponse{
-			Name:    "Python",
-			Runtime: pipenv,
+			Name:       "Python",
+			Runtime:    pipenv,
+			Buildpacks: runtime.packs[pipenv],
 		}
 	} else if detected[pip] {
 		fmt.Printf("Python pip runtime detected for %s/%s\n", owner, name)
 		return &RuntimeResponse{
-			Name:    "Python",
-			Runtime: pip,
+			Name:       "Python",
+			Runtime:    pip,
+			Buildpacks: runtime.packs[pip],
 		}
 	} else if detected[conda] {
 		fmt.Printf("Python conda runtime detected for %s/%s\n", owner, name)
 		return &RuntimeResponse{
-			Name:    "Python",
-			Runtime: conda,
+			Name:       "Python",
+			Runtime:    conda,
+			Buildpacks: runtime.packs[conda],
 		}
 	} else if detected[standalone] {
 		fmt.Printf("Python standalone runtime detected for %s/%s\n", owner, name)
 		return &RuntimeResponse{
-			Name:    "Python",
-			Runtime: standalone,
+			Name:       "Python",
+			Runtime:    standalone,
+			Buildpacks: runtime.packs[standalone],
 		}
 	}
 

+ 473 - 0
internal/integrations/buildpacks/api_ruby.go

@@ -0,0 +1,473 @@
+package buildpacks
+
+import (
+	"bufio"
+	"context"
+	"fmt"
+	"os"
+	"regexp"
+	"strings"
+	"sync"
+
+	"github.com/google/go-github/github"
+	"github.com/pelletier/go-toml"
+)
+
+type apiRubyRuntime struct {
+	wg    sync.WaitGroup
+	packs map[string]*BuildpackInfo
+}
+
+// FIXME: should be called once at the top-level somewhere in the backend
+func populateRubyPacks(client *github.Client) map[string]*BuildpackInfo {
+	packs := make(map[string]*BuildpackInfo)
+
+	repoRelease, _, err := client.Repositories.GetLatestRelease(context.Background(), "paketo-buildpacks", "ruby")
+	if err != nil {
+		fmt.Printf("Error fetching latest release for paketo-buildpacks/ruby: %v\n", err)
+		return nil
+	}
+	fileContent, _, _, err := client.Repositories.GetContents(
+		context.Background(), "paketo-buildpacks", "ruby", "buildpack.toml",
+		&github.RepositoryContentGetOptions{
+			Ref: *repoRelease.TagName,
+		},
+	)
+	if err != nil {
+		fmt.Printf("Error fetching contents of buildpack.toml for paketo-buildpacks/ruby: %v\n", err)
+		return nil
+	}
+
+	data, err := fileContent.GetContent()
+	if err != nil {
+		fmt.Printf("Error calling GetContent() on buildpack.toml for paketo-buildpacks/ruby: %v\n", err)
+		return nil
+	}
+
+	buildpackToml, err := toml.Load(data)
+	if err != nil {
+		fmt.Printf("Error while reading buildpack.toml from paketo-buildpacks/ruby: %v\n", err)
+		os.Exit(1)
+	}
+	order := buildpackToml.Get("order").([]*toml.Tree)
+
+	// puma
+	packs[puma] = newBuildpackInfo()
+	pumaGroup := order[0].GetArray("group").([]*toml.Tree)
+	for i := 0; i < len(pumaGroup); i++ {
+		packs[puma].addPack(
+			buildpackOrderGroupInfo{
+				ID:       pumaGroup[i].Get("id").(string),
+				Optional: pumaGroup[i].GetDefault("optional", false).(bool),
+				Version:  pumaGroup[i].Get("version").(string),
+			},
+		)
+	}
+
+	// thin
+	packs[thin] = newBuildpackInfo()
+	thinGroup := order[1].GetArray("group").([]*toml.Tree)
+	for i := 0; i < len(thinGroup); i++ {
+		packs[thin].addPack(
+			buildpackOrderGroupInfo{
+				ID:       thinGroup[i].Get("id").(string),
+				Optional: thinGroup[i].GetDefault("optional", false).(bool),
+				Version:  thinGroup[i].Get("version").(string),
+			},
+		)
+	}
+
+	// unicorn
+	packs[unicorn] = newBuildpackInfo()
+	unicornGroup := order[2].GetArray("group").([]*toml.Tree)
+	for i := 0; i < len(unicornGroup); i++ {
+		packs[unicorn].addPack(
+			buildpackOrderGroupInfo{
+				ID:       unicornGroup[i].Get("id").(string),
+				Optional: unicornGroup[i].GetDefault("optional", false).(bool),
+				Version:  unicornGroup[i].Get("version").(string),
+			},
+		)
+	}
+
+	// passenger
+	packs[passenger] = newBuildpackInfo()
+	passengerGroup := order[3].GetArray("group").([]*toml.Tree)
+	for i := 0; i < len(passengerGroup); i++ {
+		packs[passenger].addPack(
+			buildpackOrderGroupInfo{
+				ID:       passengerGroup[i].Get("id").(string),
+				Optional: passengerGroup[i].GetDefault("optional", false).(bool),
+				Version:  passengerGroup[i].Get("version").(string),
+			},
+		)
+	}
+
+	// rackup
+	packs[rackup] = newBuildpackInfo()
+	rackupGroup := order[4].GetArray("group").([]*toml.Tree)
+	for i := 0; i < len(rackupGroup); i++ {
+		packs[rackup].addPack(
+			buildpackOrderGroupInfo{
+				ID:       rackupGroup[i].Get("id").(string),
+				Optional: rackupGroup[i].GetDefault("optional", false).(bool),
+				Version:  rackupGroup[i].Get("version").(string),
+			},
+		)
+	}
+
+	// rake
+	packs[rake] = newBuildpackInfo()
+	rakeGroup := order[5].GetArray("group").([]*toml.Tree)
+	for i := 0; i < len(rakeGroup); i++ {
+		packs[rake].addPack(
+			buildpackOrderGroupInfo{
+				ID:       rakeGroup[i].Get("id").(string),
+				Optional: rakeGroup[i].GetDefault("optional", false).(bool),
+				Version:  rakeGroup[i].Get("version").(string),
+			},
+		)
+	}
+
+	return packs
+}
+
+func NewAPIRubyRuntime() APIRuntime {
+	return &apiRubyRuntime{}
+}
+
+func (runtime *apiRubyRuntime) detectPuma(gemfileContent string, results chan struct {
+	string
+	bool
+}) {
+	pumaFound := false
+	quotes := `["']`
+	pumaRe := regexp.MustCompile(fmt.Sprintf(`^\s*gem %spuma%s`, quotes, quotes))
+	scanner := bufio.NewScanner(strings.NewReader(gemfileContent))
+	for scanner.Scan() {
+		line := []byte(scanner.Text())
+		if pumaRe.Match(line) {
+			pumaFound = true
+			break
+		}
+	}
+	if pumaFound {
+		results <- struct {
+			string
+			bool
+		}{puma, true}
+	} else {
+		results <- struct {
+			string
+			bool
+		}{puma, false}
+	}
+	runtime.wg.Done()
+}
+
+func (runtime *apiRubyRuntime) detectThin(gemfileContent string, results chan struct {
+	string
+	bool
+}) {
+	thinFound := false
+	quotes := `["']`
+	thinRe := regexp.MustCompile(fmt.Sprintf(`^\s*gem %sthin%s`, quotes, quotes))
+	scanner := bufio.NewScanner(strings.NewReader(gemfileContent))
+	for scanner.Scan() {
+		line := []byte(scanner.Text())
+		if thinRe.Match(line) {
+			thinFound = true
+			break
+		}
+	}
+	if thinFound {
+		results <- struct {
+			string
+			bool
+		}{thin, true}
+	} else {
+		results <- struct {
+			string
+			bool
+		}{thin, false}
+	}
+	runtime.wg.Done()
+}
+
+func (runtime *apiRubyRuntime) detectUnicorn(gemfileContent string, results chan struct {
+	string
+	bool
+}) {
+	unicornFound := false
+	quotes := `["']`
+	unicornRe := regexp.MustCompile(fmt.Sprintf(`^\s*gem %sunicorn%s`, quotes, quotes))
+	scanner := bufio.NewScanner(strings.NewReader(gemfileContent))
+	for scanner.Scan() {
+		line := []byte(scanner.Text())
+		if unicornRe.Match(line) {
+			unicornFound = true
+			break
+		}
+	}
+	if unicornFound {
+		results <- struct {
+			string
+			bool
+		}{unicorn, true}
+	} else {
+		results <- struct {
+			string
+			bool
+		}{unicorn, false}
+	}
+	runtime.wg.Done()
+}
+
+func (runtime *apiRubyRuntime) detectPassenger(gemfileContent string, results chan struct {
+	string
+	bool
+}) {
+	passengerFound := false
+	quotes := `["']`
+	passengerRe := regexp.MustCompile(fmt.Sprintf(`^\s*gem %spassenger%s`, quotes, quotes))
+	scanner := bufio.NewScanner(strings.NewReader(gemfileContent))
+	for scanner.Scan() {
+		line := []byte(scanner.Text())
+		if passengerRe.Match(line) {
+			passengerFound = true
+			break
+		}
+	}
+	if passengerFound {
+		results <- struct {
+			string
+			bool
+		}{passenger, true}
+	} else {
+		results <- struct {
+			string
+			bool
+		}{passenger, false}
+	}
+	runtime.wg.Done()
+}
+
+func (runtime *apiRubyRuntime) detectRackup(
+	client *github.Client, owner, name string,
+	repoContentOptions github.RepositoryContentGetOptions, results chan struct {
+		string
+		bool
+	},
+) {
+	fileContent, _, _, err := client.Repositories.GetContents(context.Background(),
+		owner, name, "Gemfile.lock", &repoContentOptions)
+	if err != nil {
+		fmt.Printf("Error fetching contents of Gemfile.lock for %s/%s: %v\n", owner, name, err)
+		runtime.wg.Done()
+		return
+	}
+	gemfileLockContent, err := fileContent.GetContent()
+	if err != nil {
+		fmt.Printf("Error calling GetContent() on Gemfile.lock for %s/%s: %v\n", owner, name, err)
+		runtime.wg.Done()
+		return
+	}
+
+	rackFound := false
+	scanner := bufio.NewScanner(strings.NewReader(gemfileLockContent))
+	for scanner.Scan() {
+		if strings.TrimSpace(scanner.Text()) == "GEM" {
+			for scanner.Scan() {
+				if strings.Contains(scanner.Text(), "rack") {
+					rackFound = true
+					break
+				}
+			}
+		}
+	}
+	if rackFound {
+		results <- struct {
+			string
+			bool
+		}{rackup, true}
+	} else {
+		results <- struct {
+			string
+			bool
+		}{rackup, false}
+	}
+	runtime.wg.Done()
+}
+
+func (runtime *apiRubyRuntime) detectRake(gemfileContent string, results chan struct {
+	string
+	bool
+}) {
+	rakeFound := false
+	quotes := `["']`
+	rakeRe := regexp.MustCompile(fmt.Sprintf(`^\s*gem %srake%s`, quotes, quotes))
+	scanner := bufio.NewScanner(strings.NewReader(gemfileContent))
+	for scanner.Scan() {
+		line := []byte(scanner.Text())
+		if rakeRe.Match(line) {
+			rakeFound = true
+			break
+		}
+	}
+	if rakeFound {
+		results <- struct {
+			string
+			bool
+		}{rake, true}
+	} else {
+		results <- struct {
+			string
+			bool
+		}{rake, false}
+	}
+	runtime.wg.Done()
+}
+
+func (runtime *apiRubyRuntime) Detect(
+	client *github.Client,
+	directoryContent []*github.RepositoryContent,
+	owner, name, path string,
+	repoContentOptions github.RepositoryContentGetOptions,
+) *RuntimeResponse {
+	runtime.packs = populateRubyPacks(client)
+
+	gemfileFound := false
+	gemfileLockFound := false
+	configRuFound := false
+	rakefileFound := false
+	for i := range directoryContent {
+		name := directoryContent[i].GetName()
+		if name == "Gemfile" {
+			gemfileFound = true
+		} else if name == "Gemfile.lock" {
+			gemfileLockFound = true
+		} else if name == "config.ru" {
+			configRuFound = true
+		} else if name == "Rakefile" || name == "Rakefile.rb" || name == "rakefile" || name == "rakefile.rb" {
+			rakefileFound = true
+		}
+	}
+
+	if !gemfileFound {
+		fmt.Printf("No Ruby runtime detected for %s/%s\n", owner, name)
+		return nil
+	}
+
+	fileContent, _, _, err := client.Repositories.GetContents(context.Background(), owner, name, "Gemfile", &repoContentOptions)
+	if err != nil {
+		fmt.Printf("Error fetching contents of Gemfile for %s/%s: %v\n", owner, name, err)
+		return nil
+	}
+	gemfileContent, err := fileContent.GetContent()
+	if err != nil {
+		fmt.Printf("Error calling GetContent() on Gemfile for %s/%s: %v\n", owner, name, err)
+		return nil
+	}
+
+	count := 6
+	if !configRuFound {
+		// unicorn needs config.ru
+		count -= 1
+		if !gemfileLockFound {
+			// rackup needs one of Gemfile.lock or config.ru
+			count -= 1
+		}
+	}
+	if !rakefileFound {
+		count -= 1
+	}
+	results := make(chan struct {
+		string
+		bool
+	}, count)
+
+	fmt.Printf("Starting detection for a Ruby runtime for %s/%s\n", owner, name)
+	runtime.wg.Add(count)
+	fmt.Println("Checking for puma")
+	go runtime.detectPuma(gemfileContent, results)
+	fmt.Println("Checking for thin")
+	go runtime.detectThin(gemfileContent, results)
+	if configRuFound {
+		{
+			// FIXME: find a better, more readable way of doing this
+			fmt.Printf("Ruby rackup runtime detected for %s/%s\n", owner, name)
+			results <- struct {
+				string
+				bool
+			}{rackup, true}
+			runtime.wg.Done()
+		}
+
+		fmt.Println("Checking for unicorn")
+		runtime.detectUnicorn(gemfileContent, results)
+	}
+	fmt.Println("Checking for passenger")
+	go runtime.detectPassenger(gemfileContent, results)
+	if !configRuFound && gemfileLockFound {
+		fmt.Println("Checking for rackup")
+		go runtime.detectRackup(client, owner, name, repoContentOptions, results)
+	}
+	if rakefileFound {
+		fmt.Println("Checking for rake")
+		runtime.detectRake(gemfileContent, results)
+	}
+	runtime.wg.Wait()
+	close(results)
+
+	detected := make(map[string]bool)
+	for result := range results {
+		detected[result.string] = result.bool
+	}
+
+	// TODO: how to access config values for Ruby projects
+	if found, ok := detected[puma]; ok && found {
+		fmt.Printf("Ruby puma runtime detected for %s/%s\n", owner, name)
+		return &RuntimeResponse{
+			Name:       "Ruby",
+			Runtime:    puma,
+			Buildpacks: runtime.packs[puma],
+		}
+	} else if found, ok := detected[thin]; ok && found {
+		fmt.Printf("Ruby thin runtime detected for %s/%s\n", owner, name)
+		return &RuntimeResponse{
+			Name:       "Ruby",
+			Runtime:    thin,
+			Buildpacks: runtime.packs[thin],
+		}
+	} else if found, ok := detected[unicorn]; ok && found {
+		fmt.Printf("Ruby unicorn runtime detected for %s/%s\n", owner, name)
+		return &RuntimeResponse{
+			Name:       "Ruby",
+			Runtime:    unicorn,
+			Buildpacks: runtime.packs[unicorn],
+		}
+	} else if found, ok := detected[passenger]; ok && found {
+		fmt.Printf("Ruby passenger runtime detected for %s/%s\n", owner, name)
+		return &RuntimeResponse{
+			Name:       "Ruby",
+			Runtime:    passenger,
+			Buildpacks: runtime.packs[passenger],
+		}
+	} else if found, ok := detected[rackup]; ok && found {
+		fmt.Printf("Ruby rackup runtime detected for %s/%s\n", owner, name)
+		return &RuntimeResponse{
+			Name:       "Ruby",
+			Runtime:    rackup,
+			Buildpacks: runtime.packs[rackup],
+		}
+	} else if found, ok := detected[rake]; ok && found {
+		fmt.Printf("Ruby rake runtime detected for %s/%s\n", owner, name)
+		return &RuntimeResponse{
+			Name:       "Ruby",
+			Runtime:    rake,
+			Buildpacks: runtime.packs[rake],
+		}
+	}
+
+	panic("[api_ruby.go] This should ne never reached")
+}

+ 29 - 13
internal/integrations/buildpacks/shared.go

@@ -8,13 +8,28 @@ import (
 )
 
 const (
-	yarn       = "yarn"
-	npm        = "npm"
-	mod        = "mod"
-	dep        = "dep"
-	pipenv     = "pipenv"
-	pip        = "pip"
-	conda      = "conda"
+	// NodeJS
+	yarn = "yarn"
+	npm  = "npm"
+
+	// Go
+	mod = "mod"
+	dep = "dep"
+
+	// Python
+	pipenv = "pipenv"
+	pip    = "pip"
+	conda  = "conda"
+
+	// Ruby
+	puma      = "puma"
+	thin      = "thin"
+	unicorn   = "unicorn"
+	passenger = "passenger"
+	rackup    = "rackup"
+	rake      = "rake"
+
+	// Common
 	standalone = "standalone"
 )
 
@@ -66,12 +81,12 @@ type CLIRuntime interface {
 
 type APIRuntime interface {
 	Detect(
-		*github.Client,
-		[]*github.RepositoryContent,
-		string,
-		string,
-		string,
-		github.RepositoryContentGetOptions,
+		*github.Client, // github client to pull contents of files
+		[]*github.RepositoryContent, // the root folder structure of the git repo
+		string, // owner
+		string, // name
+		string, // path
+		github.RepositoryContentGetOptions, // SHA, branch or tag
 	) *RuntimeResponse
 }
 
@@ -80,4 +95,5 @@ var APIRuntimes = []APIRuntime{
 	NewAPIGoRuntime(),
 	NewAPINodeRuntime(),
 	NewAPIPythonRuntime(),
+	NewAPIRubyRuntime(),
 }