From 5d7027dc3fc6a16e2023ffa1ebac93f1c667786e Mon Sep 17 00:00:00 2001
From: Ryan <me@hackerc.at>
Date: Tue, 22 Mar 2022 20:26:10 +0100
Subject: [PATCH] feat: add bug-report flag (#1056)

* feat: add bug-report flag

* fix: use docker host CPU count

* feat: add config files to bug-report

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
---
 cmd/root.go                 | 98 +++++++++++++++++++++++++++++++++++--
 pkg/container/docker_run.go | 21 ++++++--
 pkg/runner/runner.go        | 16 ++++--
 3 files changed, 124 insertions(+), 11 deletions(-)

diff --git a/cmd/root.go b/cmd/root.go
index ac57595..9567a42 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -3,6 +3,7 @@ package cmd
 import (
 	"bufio"
 	"context"
+	"fmt"
 	"os"
 	"path/filepath"
 	"regexp"
@@ -19,6 +20,7 @@ import (
 
 	"github.com/nektos/act/pkg/artifacts"
 	"github.com/nektos/act/pkg/common"
+	"github.com/nektos/act/pkg/container"
 	"github.com/nektos/act/pkg/model"
 	"github.com/nektos/act/pkg/runner"
 )
@@ -39,6 +41,8 @@ func Execute(ctx context.Context, version string) {
 	rootCmd.Flags().BoolP("list", "l", false, "list workflows")
 	rootCmd.Flags().BoolP("graph", "g", false, "draw workflows")
 	rootCmd.Flags().StringP("job", "j", "", "run job")
+	rootCmd.Flags().BoolP("bug-report", "", false, "Display system information for bug report")
+
 	rootCmd.Flags().StringArrayVarP(&input.secrets, "secret", "s", []string{}, "secret to make available to actions with optional value (e.g. -s mysecret=foo or -s mysecret)")
 	rootCmd.Flags().StringArrayVarP(&input.envs, "env", "", []string{}, "env to make available to actions with optional value (e.g. --env myenv=foo or --env myenv)")
 	rootCmd.Flags().StringArrayVarP(&input.platforms, "platform", "P", []string{}, "custom image to use per platform (e.g. -P ubuntu-18.04=nektos/act-environments-ubuntu:18.04)")
@@ -105,14 +109,94 @@ func args() []string {
 
 	args := make([]string, 0)
 	for _, f := range actrc {
-		args = append(args, readArgsFile(f)...)
+		args = append(args, readArgsFile(f, true)...)
 	}
 
 	args = append(args, os.Args[1:]...)
 	return args
 }
 
-func readArgsFile(file string) []string {
+func bugReport(ctx context.Context, version string) error {
+	var commonSocketPaths = []string{
+		"/var/run/docker.sock",
+		"/var/run/podman/podman.sock",
+		"$HOME/.colima/docker.sock",
+		"$XDG_RUNTIME_DIR/docker.sock",
+		`\\.\pipe\docker_engine`,
+	}
+
+	sprintf := func(key, val string) string {
+		return fmt.Sprintf("%-24s%s\n", key, val)
+	}
+
+	report := sprintf("act version:", version)
+	report += sprintf("GOOS:", runtime.GOOS)
+	report += sprintf("GOARCH:", runtime.GOARCH)
+	report += sprintf("NumCPU:", fmt.Sprint(runtime.NumCPU()))
+
+	var dockerHost string
+	if dockerHost = os.Getenv("DOCKER_HOST"); dockerHost == "" {
+		dockerHost = "DOCKER_HOST environment variable is unset/empty."
+	}
+
+	report += sprintf("Docker host:", dockerHost)
+	report += fmt.Sprintln("Sockets found:")
+	for _, p := range commonSocketPaths {
+		if strings.HasPrefix(p, `$`) {
+			v := strings.Split(p, `/`)[0]
+			p = strings.Replace(p, v, os.Getenv(strings.TrimPrefix(v, `$`)), 1)
+		}
+		if _, err := os.Stat(p); err != nil {
+			continue
+		} else {
+			report += fmt.Sprintf("\t%s\n", p)
+		}
+	}
+
+	info, err := container.GetHostInfo(ctx)
+	if err != nil {
+		fmt.Println(report)
+		return err
+	}
+
+	report += sprintf("Config files:", "")
+	for _, c := range configLocations() {
+		args := readArgsFile(c, false)
+		if len(args) > 0 {
+			report += fmt.Sprintf("\t%s:\n", c)
+			for _, l := range args {
+				report += fmt.Sprintf("\t\t%s\n", l)
+			}
+		}
+	}
+
+	report += fmt.Sprintln("Docker Engine:")
+
+	report += sprintf("\tEngine version:", info.ServerVersion)
+	report += sprintf("\tEngine runtime:", info.DefaultRuntime)
+	report += sprintf("\tCgroup version:", info.CgroupVersion)
+	report += sprintf("\tCgroup driver:", info.CgroupDriver)
+	report += sprintf("\tStorage driver:", info.Driver)
+	report += sprintf("\tRegistry URI:", info.IndexServerAddress)
+
+	report += sprintf("\tOS:", info.OperatingSystem)
+	report += sprintf("\tOS type:", info.OSType)
+	report += sprintf("\tOS version:", info.OSVersion)
+	report += sprintf("\tOS arch:", info.Architecture)
+	report += sprintf("\tOS kernel:", info.KernelVersion)
+	report += sprintf("\tOS CPU:", fmt.Sprint(info.NCPU))
+	report += sprintf("\tOS memory:", fmt.Sprintf("%d MB", info.MemTotal/1024/1024))
+
+	report += fmt.Sprintln("\tSecurity options:")
+	for _, secopt := range info.SecurityOptions {
+		report += fmt.Sprintf("\t\t%s\n", secopt)
+	}
+
+	fmt.Println(report)
+	return nil
+}
+
+func readArgsFile(file string, split bool) []string {
 	args := make([]string, 0)
 	f, err := os.Open(file)
 	if err != nil {
@@ -127,8 +211,10 @@ func readArgsFile(file string) []string {
 	scanner := bufio.NewScanner(f)
 	for scanner.Scan() {
 		arg := strings.TrimSpace(scanner.Text())
-		if strings.HasPrefix(arg, "-") {
+		if strings.HasPrefix(arg, "-") && split {
 			args = append(args, regexp.MustCompile(`\s`).Split(arg, 2)...)
+		} else if !split {
+			args = append(args, arg)
 		}
 	}
 	return args
@@ -162,6 +248,10 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str
 			log.SetFormatter(&log.JSONFormatter{})
 		}
 
+		if ok, _ := cmd.Flags().GetBool("bug-report"); ok {
+			return bugReport(ctx, cmd.Version)
+		}
+
 		if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" && input.containerArchitecture == "" {
 			l := log.New()
 			l.SetFormatter(&log.TextFormatter{
@@ -256,7 +346,7 @@ func newRunCommand(ctx context.Context, input *Input) func(*cobra.Command, []str
 				if err := defaultImageSurvey(cfgLocations[0]); err != nil {
 					log.Fatal(err)
 				}
-				input.platforms = readArgsFile(cfgLocations[0])
+				input.platforms = readArgsFile(cfgLocations[0], true)
 			}
 		}
 
diff --git a/pkg/container/docker_run.go b/pkg/container/docker_run.go
index 9fb8123..cc8335d 100644
--- a/pkg/container/docker_run.go
+++ b/pkg/container/docker_run.go
@@ -201,10 +201,7 @@ type containerReference struct {
 	input *NewContainerInput
 }
 
-func GetDockerClient(ctx context.Context) (*client.Client, error) {
-	var err error
-	var cli *client.Client
-
+func GetDockerClient(ctx context.Context) (cli *client.Client, err error) {
 	// TODO: this should maybe need to be a global option, not hidden in here?
 	//       though i'm not sure how that works out when there's another Executor :D
 	//		 I really would like something that works on OSX native for eg
@@ -232,6 +229,22 @@ func GetDockerClient(ctx context.Context) (*client.Client, error) {
 	return cli, err
 }
 
+func GetHostInfo(ctx context.Context) (info types.Info, err error) {
+	var cli *client.Client
+	cli, err = GetDockerClient(ctx)
+	if err != nil {
+		return info, err
+	}
+	defer cli.Close()
+
+	info, err = cli.Info(ctx)
+	if err != nil {
+		return info, err
+	}
+
+	return info, nil
+}
+
 func (cr *containerReference) connect() common.Executor {
 	return func(ctx context.Context) error {
 		if cr.cli != nil {
diff --git a/pkg/runner/runner.go b/pkg/runner/runner.go
index 5c9174c..105227b 100644
--- a/pkg/runner/runner.go
+++ b/pkg/runner/runner.go
@@ -9,9 +9,11 @@ import (
 	"runtime"
 	"strings"
 
-	"github.com/nektos/act/pkg/common"
-	"github.com/nektos/act/pkg/model"
 	log "github.com/sirupsen/logrus"
+
+	"github.com/nektos/act/pkg/common"
+	"github.com/nektos/act/pkg/container"
+	"github.com/nektos/act/pkg/model"
 )
 
 // Runner provides capabilities to run GitHub actions
@@ -171,7 +173,15 @@ func (runner *runnerImpl) NewPlanExecutor(plan *model.Plan) common.Executor {
 				}
 				pipeline = append(pipeline, common.NewParallelExecutor(maxParallel, stageExecutor...))
 			}
-			return common.NewParallelExecutor(runtime.NumCPU(), pipeline...)(ctx)
+			var ncpu int
+			info, err := container.GetHostInfo(ctx)
+			if err != nil {
+				log.Errorf("failed to obtain container engine info: %s", err)
+				ncpu = 1 // sane default?
+			} else {
+				ncpu = info.NCPU
+			}
+			return common.NewParallelExecutor(ncpu, pipeline...)(ctx)
 		})
 	}