From 205b2fdb581a4d1b7ca4b6bdcf89bcb49fa3e1ec Mon Sep 17 00:00:00 2001 From: Felix P Date: Thu, 6 Apr 2023 10:52:37 +0200 Subject: [PATCH 1/5] Add init for imagerunner --- internal/cmd/ini/cmd.go | 5 + internal/cmd/ini/common.go | 25 +++-- internal/cmd/ini/common_test.go | 54 ++++++++++ internal/cmd/ini/imagerunner.go | 32 ++++++ internal/cmd/ini/initializer.go | 54 ++++++++++ internal/cmd/ini/initializer_test.go | 146 +++++++++++++++++++++++++++ internal/msg/errormsg.go | 2 + 7 files changed, 312 insertions(+), 6 deletions(-) create mode 100644 internal/cmd/ini/imagerunner.go diff --git a/internal/cmd/ini/cmd.go b/internal/cmd/ini/cmd.go index f533f302a..f9279fa9a 100644 --- a/internal/cmd/ini/cmd.go +++ b/internal/cmd/ini/cmd.go @@ -13,6 +13,7 @@ import ( "github.com/saucelabs/saucectl/internal/cypress" "github.com/saucelabs/saucectl/internal/espresso" "github.com/saucelabs/saucectl/internal/flags" + "github.com/saucelabs/saucectl/internal/imagerunner" "github.com/saucelabs/saucectl/internal/msg" "github.com/saucelabs/saucectl/internal/playwright" "github.com/saucelabs/saucectl/internal/puppeteer" @@ -39,6 +40,7 @@ type initConfig struct { frameworkName string frameworkVersion string cypressJSON string + dockerImage string app string testApp string otherApps []string @@ -95,6 +97,7 @@ func Command() *cobra.Command { cmd.Flags().StringVarP(&initCfg.frameworkName, "framework", "f", "", "framework to configure") cmd.Flags().StringVarP(&initCfg.frameworkVersion, "frameworkVersion", "v", "", "framework version to be used") cmd.Flags().StringVar(&initCfg.cypressJSON, "cypress.config", "", "path to cypress.json file (cypress only)") + cmd.Flags().StringVar(&initCfg.dockerImage, "dockerImage", "", "docker image to use (imagerunner only)") cmd.Flags().StringVar(&initCfg.app, "app", "", "path to application to test (espresso/xcuitest only)") cmd.Flags().StringVarP(&initCfg.testApp, "testApp", "t", "", "path to test application (espresso/xcuitest only)") cmd.Flags().StringSliceVarP(&initCfg.otherApps, "otherApps", "o", []string{}, "path to other applications (espresso/xcuitest only)") @@ -183,6 +186,8 @@ func batchMode(cmd *cobra.Command, initCfg *initConfig) error { initCfg, errs = ini.initializeBatchTestcafe(initCfg) case xcuitest.Kind: initCfg, errs = ini.initializeBatchXcuitest(cmd.Flags(), initCfg) + case imagerunner.Kind: + initCfg, errs = ini.initializeBatchImageRunner(initCfg) default: println() color.HiRed("Invalid framework selected") diff --git a/internal/cmd/ini/common.go b/internal/cmd/ini/common.go index 12963ef50..633886426 100644 --- a/internal/cmd/ini/common.go +++ b/internal/cmd/ini/common.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "regexp" "sort" "strconv" "strings" @@ -19,12 +20,13 @@ import ( ) var configurators = map[string]func(cfg *initConfig) interface{}{ - "cypress": configureCypress, - "espresso": configureEspresso, - "playwright": configurePlaywright, - "puppeteer": configurePuppeteer, - "testcafe": configureTestcafe, - "xcuitest": configureXCUITest, + "cypress": configureCypress, + "espresso": configureEspresso, + "playwright": configurePlaywright, + "puppeteer": configurePuppeteer, + "testcafe": configureTestcafe, + "xcuitest": configureXCUITest, + "imagerunner": configureImageRunner, } var sauceignores = map[string]string{ @@ -136,6 +138,17 @@ func extValidator(framework, frameworkVersion string) survey.Validator { } } +func dockerImageValidator() survey.Validator { + re := regexp.MustCompile("^([\\w.\\-_]+((:\\d+|)(/[a-z0-9._-]+/[a-z0-9._-]+))|)(/|)([a-z0-9.\\-_]+(/[a-z0-9.\\-_]+|))(:([\\w.\\-_]{1,127})|)$") + return func(s interface{}) error { + str := s.(string) + if re.MatchString(str) { + return nil + } + return fmt.Errorf("%s is not a valid docker image", str) + } +} + func getMajorVersion(frameworkVersion string) int { version := strings.Split(frameworkVersion, ".")[0] v, err := strconv.Atoi(version) diff --git a/internal/cmd/ini/common_test.go b/internal/cmd/ini/common_test.go index cea5ef79a..f5e93ae60 100644 --- a/internal/cmd/ini/common_test.go +++ b/internal/cmd/ini/common_test.go @@ -624,3 +624,57 @@ func TestCommon_getMajorVersion(t *testing.T) { }) } } + +func Test_dockerImageValidator(t *testing.T) { + tests := []struct { + image string + want error + }{ + { + image: "alpine", + want: nil, + }, + { + image: "alpine:latest", + want: nil, + }, + { + image: "_/alpine", + want: nil, + }, + { + image: "_/alpine:latest", + want: nil, + }, + { + image: "alpine:3.7", + want: nil, + }, + { + image: "docker.example.com/gmr/alpine:3.7", + want: nil, + }, + { + image: "docker.example.com:5000/gmr/alpine:latest", + want: nil, + }, + { + image: "pse/anabroker:latest", + want: nil, + }, + { + image: "buggy::latest", + want: errors.New("buggy::latest is not a valid docker image"), + }, + { + image: "buggy:", + want: errors.New("buggy: is not a valid docker image"), + }, + } + val := dockerImageValidator() + for _, tt := range tests { + t.Run(tt.image, func(t *testing.T) { + assert.Equalf(t, tt.want, val(tt.image), "dockerImageValidator()") + }) + } +} diff --git a/internal/cmd/ini/imagerunner.go b/internal/cmd/ini/imagerunner.go new file mode 100644 index 000000000..1a00866f3 --- /dev/null +++ b/internal/cmd/ini/imagerunner.go @@ -0,0 +1,32 @@ +package ini + +import ( + "fmt" + "github.com/saucelabs/saucectl/internal/config" + "github.com/saucelabs/saucectl/internal/imagerunner" +) + +func configureImageRunner(cfg *initConfig) interface{} { + return imagerunner.Project{ + TypeDef: config.TypeDef{ + APIVersion: imagerunner.APIVersion, + Kind: imagerunner.Kind, + }, + Sauce: config.SauceConfig{ + Region: cfg.region, + }, + Suites: []imagerunner.Suite{ + { + Name: fmt.Sprintf("imagerunner - %s", cfg.dockerImage), + Image: cfg.dockerImage, + }, + }, + Artifacts: config.Artifacts{ + Download: config.ArtifactDownload{ + When: cfg.artifactWhen, + Directory: "./artifacts", + Match: []string{"*"}, + }, + }, + } +} diff --git a/internal/cmd/ini/initializer.go b/internal/cmd/ini/initializer.go index fc01d044d..7038402ce 100644 --- a/internal/cmd/ini/initializer.go +++ b/internal/cmd/ini/initializer.go @@ -18,6 +18,7 @@ import ( "github.com/saucelabs/saucectl/internal/framework" "github.com/saucelabs/saucectl/internal/http" "github.com/saucelabs/saucectl/internal/iam" + "github.com/saucelabs/saucectl/internal/imagerunner" "github.com/saucelabs/saucectl/internal/msg" "github.com/saucelabs/saucectl/internal/playwright" "github.com/saucelabs/saucectl/internal/puppeteer" @@ -83,6 +84,8 @@ func (ini *initializer) configure() (*initConfig, error) { return ini.initializeEspresso() case xcuitest.Kind: return ini.initializeXCUITest() + case imagerunner.Kind: + return ini.initializeImageRunner() default: return &initConfig{}, fmt.Errorf("unsupported framework %v", fName) } @@ -414,6 +417,18 @@ func (ini *initializer) askFile(message string, val survey.Validator, comp compl survey.WithStdio(ini.stdio.In, ini.stdio.Out, ini.stdio.Err)) } +func (ini *initializer) askDockerImage(message string, val survey.Validator, targetValue *string) error { + q := &survey.Input{ + Message: message, + } + + return survey.AskOne(q, targetValue, + survey.WithShowCursor(true), + survey.WithValidator(survey.Required), + survey.WithValidator(val), + survey.WithStdio(ini.stdio.In, ini.stdio.Out, ini.stdio.Err)) +} + func (ini *initializer) initializeCypress() (*initConfig, error) { cfg := &initConfig{frameworkName: cypress.Kind} @@ -585,6 +600,22 @@ func (ini *initializer) initializeXCUITest() (*initConfig, error) { return cfg, nil } +func (ini *initializer) initializeImageRunner() (*initConfig, error) { + cfg := &initConfig{frameworkName: imagerunner.Kind} + + err := ini.askDockerImage("Docker Image to use:", dockerImageValidator(), &cfg.dockerImage) + if err != nil { + return &initConfig{}, err + } + + err = ini.askDownloadWhen(cfg) + if err != nil { + return &initConfig{}, err + } + + return cfg, nil +} + func checkFrameworkVersion(metadatas []framework.Metadata, frameworkName, frameworkVersion string) error { var supported []string for _, fm := range metadatas { @@ -927,3 +958,26 @@ func (ini *initializer) initializeBatchXcuitest(f *pflag.FlagSet, initCfg *initC } return initCfg, errs } + +func (ini *initializer) initializeBatchImageRunner(initCfg *initConfig) (*initConfig, []error) { + initCfg.frameworkName = imagerunner.Kind + var errs []error + var err error + + if initCfg.dockerImage == "" { + errs = append(errs, errors.New(msg.MissingDockerImage)) + } + if initCfg.dockerImage != "" { + verifier := dockerImageValidator() + if err = verifier(initCfg.dockerImage); err != nil { + errs = append(errs, fmt.Errorf("dockerImage: %s", err)) + } + } + if initCfg.artifactWhenStr != "" { + initCfg.artifactWhenStr = strings.ToLower(initCfg.artifactWhenStr) + if initCfg.artifactWhen, err = checkArtifactDownloadSetting(initCfg.artifactWhenStr); err != nil { + errs = append(errs, err) + } + } + return initCfg, errs +} diff --git a/internal/cmd/ini/initializer_test.go b/internal/cmd/ini/initializer_test.go index a87a648bf..0238e1b68 100644 --- a/internal/cmd/ini/initializer_test.go +++ b/internal/cmd/ini/initializer_test.go @@ -13,6 +13,8 @@ import ( "github.com/saucelabs/saucectl/internal/flags" "github.com/saucelabs/saucectl/internal/iam" + "github.com/saucelabs/saucectl/internal/imagerunner" + "github.com/saucelabs/saucectl/internal/msg" "github.com/spf13/pflag" "github.com/AlecAivazis/survey/v2/terminal" @@ -1015,6 +1017,12 @@ func Test_initializers(t *testing.T) { Platforms: []framework.Platform{}, }, }, + imagerunner.Kind: { + { + FrameworkName: imagerunner.Kind, + FrameworkVersion: "1.0.0", + }, + }, } ir := &mocks.FakeFrameworkInfoReader{ VersionsFn: func(ctx context.Context, frameworkName string) ([]framework.Metadata, error) { @@ -1024,6 +1032,7 @@ func Test_initializers(t *testing.T) { return []framework.Framework{ {Name: cypress.Kind}, {Name: espresso.Kind}, + {Name: imagerunner.Kind}, {Name: playwright.Kind}, {Name: "puppeteer"}, {Name: testcafe.Kind}, @@ -1486,6 +1495,47 @@ func Test_initializers(t *testing.T) { artifactWhen: config.WhenPass, }, }, + { + name: "ImageRunner - DockerImage", + procedure: func(c *expect.Console) error { + _, err := c.ExpectString("Docker Image to use:") + if err != nil { + return err + } + _, err = c.SendLine("ubuntu:latest") + if err != nil { + return err + } + _, err = c.ExpectString("Download artifacts:") + if err != nil { + return err + } + _, err = c.SendLine("when tests are passing") + if err != nil { + return err + } + _, err = c.ExpectEOF() + if err != nil { + return err + } + return nil + }, + ini: &initializer{infoReader: ir}, + execution: func(i *initializer, cfg *initConfig) error { + newCfg, err := i.initializeImageRunner() + if err != nil { + return err + } + *cfg = *newCfg + return nil + }, + startState: &initConfig{}, + expectedState: &initConfig{ + frameworkName: imagerunner.Kind, + dockerImage: "ubuntu:latest", + artifactWhen: config.WhenPass, + }, + }, } for _, tt := range testCases { t.Run(tt.name, func(lt *testing.T) { @@ -2827,3 +2877,99 @@ func Test_initializer_initializeBatchEspresso(t *testing.T) { }) } } + +func Test_initializer_initializeBatchImageRunner(t *testing.T) { + ini := &initializer{ + infoReader: &mocks.FakeFrameworkInfoReader{VersionsFn: func(ctx context.Context, frameworkName string) ([]framework.Metadata, error) { + return []framework.Metadata{ + { + FrameworkName: "imagerunner", + FrameworkVersion: "", + }, + }, nil + }}, + userService: &mocks.UserService{ConcurrencyFn: func(ctx context.Context) (iam.Concurrency, error) { + return iam.Concurrency{ + Org: iam.OrgConcurrency{ + Allowed: iam.CloudConcurrency{ + VDC: 2, + }, + }, + }, nil + }}, + } + var emptyErr []error + + type args struct { + initCfg *initConfig + } + tests := []struct { + name string + args args + want *initConfig + wantErrs []error + }{ + { + name: "Basic", + args: args{ + initCfg: &initConfig{ + frameworkName: "imagerunner", + dockerImage: "ubuntu:latest", + region: "us-west-1", + artifactWhen: "fail", + }, + }, + want: &initConfig{ + frameworkName: "imagerunner", + dockerImage: "ubuntu:latest", + region: "us-west-1", + artifactWhen: config.WhenFail, + }, + wantErrs: emptyErr, + }, + { + name: "invalid browser/platform", + args: args{ + initCfg: &initConfig{ + frameworkName: "imagerunner", + dockerImage: "ubuntu::buggy", + artifactWhenStr: "dummy", + }, + }, + want: &initConfig{ + frameworkName: "imagerunner", + dockerImage: "ubuntu::buggy", + artifactWhenStr: "dummy", + }, + wantErrs: []error{ + errors.New("dockerImage: ubuntu::buggy is not a valid docker image"), + errors.New("dummy: unknown download condition"), + }, + }, + { + name: "no flags", + args: args{ + initCfg: &initConfig{ + frameworkName: "imagerunner", + }, + }, + want: &initConfig{ + frameworkName: "imagerunner", + }, + wantErrs: []error{ + errors.New(msg.MissingDockerImage), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, errs := ini.initializeBatchImageRunner(tt.args.initCfg) + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("initializeBatchImageRunner() got = %v, want %v", got, tt.want) + } + if !reflect.DeepEqual(errs, tt.wantErrs) { + t.Errorf("initializeBatchImageRunner() got1 = %v, want %v", errs, tt.wantErrs) + } + }) + } +} diff --git a/internal/msg/errormsg.go b/internal/msg/errormsg.go index 1fbb7cb8e..1db04cb57 100644 --- a/internal/msg/errormsg.go +++ b/internal/msg/errormsg.go @@ -36,6 +36,8 @@ const ( MissingPlatformName = "no platform name specified" // MissingBrowserName indicates no browser name MissingBrowserName = "no browser name specified" + // MissingDockerImage indicates no app + MissingDockerImage = "no image provided" // MissingApp indicates no app MissingApp = "no app provided" // MissingTestApp indicates no testApp From 8b144d02b304be4c3c00ca8e4561a48f7e6a8c51 Mon Sep 17 00:00:00 2001 From: Felix P Date: Thu, 6 Apr 2023 10:55:30 +0200 Subject: [PATCH 2/5] Use backticks for regexp --- internal/cmd/ini/common.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/cmd/ini/common.go b/internal/cmd/ini/common.go index 633886426..30f3eed61 100644 --- a/internal/cmd/ini/common.go +++ b/internal/cmd/ini/common.go @@ -139,7 +139,7 @@ func extValidator(framework, frameworkVersion string) survey.Validator { } func dockerImageValidator() survey.Validator { - re := regexp.MustCompile("^([\\w.\\-_]+((:\\d+|)(/[a-z0-9._-]+/[a-z0-9._-]+))|)(/|)([a-z0-9.\\-_]+(/[a-z0-9.\\-_]+|))(:([\\w.\\-_]{1,127})|)$") + re := regexp.MustCompile(`^([\w.\-_]+((:\d+|)(/[a-z0-9._-]+/[a-z0-9._-]+))|)(/|)([a-z0-9.\-_]+(/[a-z0-9.\-_]+|))(:([\w.\-_]{1,127})|)$`) return func(s interface{}) error { str := s.(string) if re.MatchString(str) { From 599677b461596da9858e0462ad5fd5c4e83066ba Mon Sep 17 00:00:00 2001 From: Felix P Date: Thu, 6 Apr 2023 18:42:28 +0200 Subject: [PATCH 3/5] Add var credentials for DOCKER Image --- internal/cmd/ini/imagerunner.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/cmd/ini/imagerunner.go b/internal/cmd/ini/imagerunner.go index 1a00866f3..61f42b018 100644 --- a/internal/cmd/ini/imagerunner.go +++ b/internal/cmd/ini/imagerunner.go @@ -19,6 +19,10 @@ func configureImageRunner(cfg *initConfig) interface{} { { Name: fmt.Sprintf("imagerunner - %s", cfg.dockerImage), Image: cfg.dockerImage, + ImagePullAuth: imagerunner.ImagePullAuth{ + User: "${DOCKER_USERNAME}", + Token: "${DOCKER_PASSWORD}", + }, }, }, Artifacts: config.Artifacts{ From 97114493cbd6c053c461f0cd152c059af24a638a Mon Sep 17 00:00:00 2001 From: Felix P Date: Fri, 7 Apr 2023 11:47:35 +0200 Subject: [PATCH 4/5] Add display info for ImageRunner --- internal/cmd/ini/cmd.go | 2 ++ internal/cmd/ini/common.go | 12 ++++++++++++ internal/cmd/ini/imagerunner.go | 10 ++++++++++ 3 files changed, 24 insertions(+) diff --git a/internal/cmd/ini/cmd.go b/internal/cmd/ini/cmd.go index f9279fa9a..8b4a37713 100644 --- a/internal/cmd/ini/cmd.go +++ b/internal/cmd/ini/cmd.go @@ -159,6 +159,7 @@ func Run(cmd *cobra.Command, initCfg *initConfig) error { return err } displaySummary(files) + displayExtraInfo(initCfg.frameworkName) return nil } @@ -217,5 +218,6 @@ func batchMode(cmd *cobra.Command, initCfg *initConfig) error { return err } displaySummary(files) + displayExtraInfo(initCfg.frameworkName) return nil } diff --git a/internal/cmd/ini/common.go b/internal/cmd/ini/common.go index 30f3eed61..363253eea 100644 --- a/internal/cmd/ini/common.go +++ b/internal/cmd/ini/common.go @@ -29,6 +29,10 @@ var configurators = map[string]func(cfg *initConfig) interface{}{ "imagerunner": configureImageRunner, } +var extraInfoDisplay = map[string]func(){ + "imagerunner": displayExtraInfoImageRunner, +} + var sauceignores = map[string]string{ "cypress": sauceignoreCypress, "playwright": sauceignorePlaywright, @@ -100,6 +104,14 @@ func displaySummary(files []string) { println() } +func displayExtraInfo(framework string) { + fn, present := extraInfoDisplay[framework] + if !present { + return + } + fn() +} + func completeBasic(toComplete string) []string { files, _ := filepath.Glob(fmt.Sprintf("%s%s", toComplete, "*")) return files diff --git a/internal/cmd/ini/imagerunner.go b/internal/cmd/ini/imagerunner.go index 61f42b018..3367a358e 100644 --- a/internal/cmd/ini/imagerunner.go +++ b/internal/cmd/ini/imagerunner.go @@ -2,6 +2,8 @@ package ini import ( "fmt" + + "github.com/fatih/color" "github.com/saucelabs/saucectl/internal/config" "github.com/saucelabs/saucectl/internal/imagerunner" ) @@ -34,3 +36,11 @@ func configureImageRunner(cfg *initConfig) interface{} { }, } } + +func displayExtraInfoImageRunner() { + println() + color.HiGreen("Before running your pipeline, you need to set the following environment variables:") + color.Green(" - DOCKER_USERNAME") + color.Green(" - DOCKER_PASSWORD") + println() +} From 79c45c3e373033bc80b3cca98c04265bf286c2bf Mon Sep 17 00:00:00 2001 From: Felix P Date: Fri, 7 Apr 2023 11:49:26 +0200 Subject: [PATCH 5/5] Update word --- internal/cmd/ini/imagerunner.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/cmd/ini/imagerunner.go b/internal/cmd/ini/imagerunner.go index 3367a358e..fe60c9f41 100644 --- a/internal/cmd/ini/imagerunner.go +++ b/internal/cmd/ini/imagerunner.go @@ -39,7 +39,7 @@ func configureImageRunner(cfg *initConfig) interface{} { func displayExtraInfoImageRunner() { println() - color.HiGreen("Before running your pipeline, you need to set the following environment variables:") + color.HiGreen("Before running your tests, you need to set the following environment variables:") color.Green(" - DOCKER_USERNAME") color.Green(" - DOCKER_PASSWORD") println()