Files
2022-05-17 01:56:44 +03:00

394 lines
8.7 KiB
Go

package build
import (
"context"
"errors"
"fmt"
"github.com/markbates/pkger"
"io"
"io/ioutil"
"log"
"net"
"net/http"
"net/url"
"os"
"os/exec"
"path"
"path/filepath"
"regexp"
"runtime"
"strings"
"time"
)
const (
LatestVersion = "latest"
)
type Requirements struct {
BrowserSource BrowserSource
BrowserChannel string // "beta", "esr", "dev" and so on
DriverVersion string
Tags []string
NoCache bool
TestsDir string
RunTests bool
IgnoreTests bool
PushImage bool
}
type BrowserSource string
// Return regular file corresponding to this source and optionally download this file
func (bs *BrowserSource) Prepare() (string, string, error) {
src := string(*bs)
if src == "" {
return "", "", errors.New("empty browser source")
}
if _, err := os.Stat(src); err == nil {
pkgName := filepath.Base(src)
return src, extractVersion(pkgName), nil
} else if u, err := url.Parse(src); strings.HasPrefix(src, "http") && err == nil {
pkgName := path.Base(src)
data, err := downloadFile(u.String())
if err != nil {
return "", "", fmt.Errorf("download file: %v", err)
}
f, err := ioutil.TempFile("", "images")
if err != nil {
return "", "", fmt.Errorf("temporary file: %v", err)
}
outputFileName := f.Name()
err = ioutil.WriteFile(outputFileName, data, 0644)
if err != nil {
return "", "", fmt.Errorf("save downloaded file: %v", err)
}
return outputFileName, extractVersion(pkgName), nil
}
return "", src, nil
}
func extractVersion(name string) string {
pieces := strings.Split(name, "_")
version := name
if len(pieces) >= 2 {
version = pieces[1]
}
pieces = strings.Split(version, "+")
pieces = strings.Split(pieces[0], "-")
pieces = strings.Split(pieces[0], "~")
return pieces[0]
}
func versionN(pkgVersion string, n int) string {
buildVersion := pkgVersion
pieces := strings.Split(pkgVersion, ".")
if len(pieces) >= n {
buildVersion = strings.Join(pieces[0:n], ".")
}
return buildVersion
}
func majorVersion(pkgVersion string) string {
return versionN(pkgVersion, 1)
}
func majorMinorVersion(pkgVersion string) string {
return versionN(pkgVersion, 2)
}
func buildVersion(pkgVersion string) string {
return versionN(pkgVersion, 3)
}
type Image struct {
Dir string
BuildArgs []string
Labels []string
FileServer bool
Requirements
}
func NewImage(srcDir string, destDir string, req Requirements) (*Image, error) {
if !requireCommand("docker") {
return nil, fmt.Errorf("docker is not installed")
}
dir, err := copySourceFiles(srcDir, destDir)
if err != nil {
return nil, fmt.Errorf("copy source files: %v", err)
}
if len(req.Tags) == 0 {
return nil, errors.New("image tag is required")
}
return &Image{Dir: dir, Requirements: req}, nil
}
func requireCommand(cmd string) bool {
_, err := exec.LookPath(cmd)
return err == nil
}
func tmpDir() (string, error) {
dir, err := ioutil.TempDir("", "images")
if err != nil {
return "", fmt.Errorf("create temporary dir: %v", err)
}
return dir, nil
}
func copySourceFiles(srcDir string, destDir string) (string, error) {
const prefix = "/static"
walkDir := filepath.Join(prefix, srcDir)
err := pkger.Walk(walkDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
regex := regexp.MustCompile(`^.+:/static(.+)$`)
relativePath := regex.FindStringSubmatch(path)[1]
outputPath := filepath.Join(destDir, relativePath)
if info.IsDir() {
return os.MkdirAll(outputPath, info.Mode())
}
fileDir := filepath.Join(destDir, filepath.Dir(relativePath))
if !fileExists(fileDir) {
log.Printf("mkdir dir %s", fileDir)
return os.MkdirAll(fileDir, info.Mode())
}
src, err := pkger.Open(path)
if err != nil {
return err
}
defer src.Close()
dest, err := os.Create(outputPath)
if err != nil {
return err
}
defer dest.Close()
_, err = io.Copy(dest, src)
if err != nil {
return err
}
err = dest.Sync()
if err != nil {
return err
}
err = os.Chmod(outputPath, info.Mode())
if err != nil {
return err
}
return nil
})
if err != nil {
return "", err
}
return filepath.Join(destDir, srcDir), nil
}
func (i *Image) Build() error {
args := []string{"build"}
for _, tag := range i.Tags {
args = append(args, "-t", tag)
}
if len(i.BuildArgs) > 0 {
for _, arg := range i.BuildArgs {
args = append(args, "--build-arg", arg)
}
}
if httpProxy := os.Getenv("HTTP_PROXY"); httpProxy != "" {
args = append(args, "--build-arg", fmt.Sprintf("http_proxy=%s", httpProxy))
}
if httpsProxy := os.Getenv("HTTPS_PROXY"); httpsProxy != "" {
args = append(args, "--build-arg", fmt.Sprintf("https_proxy=%s", httpsProxy))
}
if len(i.Labels) > 0 {
for _, label := range i.Labels {
args = append(args, "--label", label)
}
}
if i.NoCache {
args = append(args, "--no-cache")
}
if i.FileServer {
server := &http.Server{
Handler: http.FileServer(http.Dir(i.Dir)),
}
ln, err := net.Listen("tcp", ":8080")
if err != nil {
return fmt.Errorf("failed to allocate free port: %v", err)
}
e := make(chan error)
go func() {
e <- server.Serve(ln)
}()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
defer server.Shutdown(ctx)
if runtime.GOOS == "linux" {
ip, err := dockerHostIP()
if err != nil {
return fmt.Errorf("failed to detect host machine IP: %v", err)
}
args = append(args, "--add-host", fmt.Sprintf("host.docker.internal:%s", ip))
}
}
args = append(args, i.Dir)
log.Printf("running command: docker %s", strings.Join(args, " "))
cmd := exec.Command("docker", args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Start()
if err != nil {
return fmt.Errorf("failed to start command: %v", err)
}
err = cmd.Wait()
if err != nil {
return fmt.Errorf("command execution error: %v", err)
}
return nil
}
func dockerHostIP() (string, error) {
ifaces, err := net.Interfaces()
if err != nil {
return "", err
}
for _, iface := range ifaces {
if iface.Name != "docker0" {
continue
}
if iface.Flags&net.FlagUp == 0 {
continue // interface down
}
if iface.Flags&net.FlagLoopback != 0 {
continue // loopback interface
}
addrs, err := iface.Addrs()
if err != nil {
return "", err
}
for _, addr := range addrs {
var ip net.IP
switch v := addr.(type) {
case *net.IPNet:
ip = v.IP
case *net.IPAddr:
ip = v.IP
}
if ip == nil || ip.IsLoopback() {
continue
}
ip = ip.To4()
if ip == nil {
continue // not an ipv4 address
}
return ip.String(), nil
}
}
return "", errors.New("no live network interfaces detected")
}
func (i *Image) Test(testsDir string, browserName string, browserVersion string) error {
if !i.RunTests {
log.Println("not running tests")
return nil
}
ref := i.Tags[0]
err := doTest(ref, testsDir, browserName, browserVersion)
if err != nil {
if i.IgnoreTests {
log.Printf("ignoring tests: %v", err)
return nil
}
return fmt.Errorf("tests error: %v", err)
}
log.Println("tests passed")
return nil
}
func doTest(ref string, testsDir string, browserName string, browserVersion string) error {
if !fileExists(testsDir) {
return fmt.Errorf("tests directory %s does not exist", testsDir)
}
seleniumUrl := "http://localhost:4445/"
if browserName == "firefox" || (browserName == "opera" && browserVersion == "12.16") {
seleniumUrl = "http://localhost:4445/wd/hub"
}
exec.Command("docker", "rm", "-f", "selenium").Output()
defer func() {
exec.Command("docker", "rm", "-f", "selenium").Output()
}()
output, err := exec.Command("docker", "run", "-d", "--name", "selenium", "--privileged", "-p", "4445:4444", ref).Output()
if err != nil {
return fmt.Errorf("failed to start docker image: %s %v", string(output), err)
}
if !requireCommand("mvn") {
return fmt.Errorf("maven is not installed")
}
mvnCmd := exec.Command("mvn", "clean", "test",
fmt.Sprintf("-Dgrid.connection.url=%s", seleniumUrl),
fmt.Sprintf("-Dgrid.browser.name=%s", browserName),
fmt.Sprintf("-Dgrid.browser.version=%s", browserVersion),
)
mvnCmd.Dir = testsDir
mvnCmd.Stdout = os.Stdout
mvnCmd.Stderr = os.Stderr
err = mvnCmd.Start()
if err != nil {
return fmt.Errorf("failed to start tests: %v", err)
}
err = mvnCmd.Wait()
if err != nil {
return fmt.Errorf("tests finished with error: %v", err)
}
return nil
}
func fileExists(p string) bool {
_, err := os.Stat(p)
return !os.IsNotExist(err)
}
func (i *Image) Push() error {
if i.PushImage {
for _, tag := range i.Tags {
cmd := exec.Command("docker", "push", tag)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Start()
if err != nil {
return fmt.Errorf("invalid docker push %s: %v", tag, err)
}
err = cmd.Wait()
if err != nil {
return fmt.Errorf("pushing failed: %v", err)
}
}
}
return nil
}