Skip to content

Commit

Permalink
feat: add imagerunner services (#839)
Browse files Browse the repository at this point in the history
* added imagerunner service
  • Loading branch information
sebv authored Oct 4, 2023
1 parent 76064b9 commit 5f17154
Show file tree
Hide file tree
Showing 9 changed files with 344 additions and 1 deletion.
12 changes: 12 additions & 0 deletions .sauce/imagerunner.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,15 @@ suites:
dst: "hello.txt"
env: # Arbitrary Key-Value pairs set as environment variables inside the container.
MY_FOO: bar
services:
- name: "service1"
image: "busybox:1.35.0"
imagePullAuth: # Credentials used to pull the container image
user: $DOCKER_USERNAME
token: $DOCKER_PASSWORD
entrypoint: "cat hello.txt" # What command to start the container with
files: # Which files should be uploaded and mounted within the container
- src: "tests/e2e/imagerunner/hello.txt"
dst: "hello.txt"
env: # Arbitrary Key-Value pairs set as environment variables inside the container.
MY_FOO: bar
72 changes: 72 additions & 0 deletions api/saucectl.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -2763,12 +2763,84 @@
"metadata": {
"description": "Supply additional metadata to your runner.",
"type": "object"
},
"services": {
"description": "List of services to run with the suite.",
"type": "array",
"items": {
"$ref": "#/allOf/9/then/definitions/service"
}
}
},
"required": [
"name",
"workload"
]
},
"service": {
"description": "The set of properties providing details about how to run the service container.",
"type": "object",
"properties": {
"name": {
"description": "The name of the service.",
"type": "string"
},
"image": {
"description": "The name of the service image.",
"type": "string"
},
"imagePullAuth": {
"description": "Container registry credentials for accessing the service image.",
"type": "object",
"properties": {
"user": {
"description": "The username.",
"type": "string"
},
"token": {
"description": "The access token.",
"type": "string"
}
}
},
"entrypoint": {
"description": "The command line arguments to launch the service image with.",
"type": "string"
},
"files": {
"description": "List of files that you'd like saucectl to upload and mount within the service container.",
"type": "array",
"items": {
"type": "object",
"properties": {
"src": {
"description": "Path to the local file.",
"type": "string"
},
"dst": {
"description": "Path within the container that the file should be mounted at.",
"type": "string"
}
}
}
},
"env": {
"description": "Set one or more environment variables for the service.",
"type": "object"
},
"resourceProfile": {
"description": "Sets the CPU/memory limits of the service container. Format is <CPU><level><mem><level>. Default to c1m1.",
"enum": [
"",
"c1m1",
"c2m2",
"c3m3"
]
}
},
"required": [
"name"
]
}
},
"properties": {
Expand Down
72 changes: 72 additions & 0 deletions api/v1alpha/framework/imagerunner.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -119,12 +119,84 @@
"metadata": {
"description": "Supply additional metadata to your runner.",
"type": "object"
},
"services": {
"description": "List of services to run with the suite.",
"type": "array",
"items": {
"$ref": "#/definitions/service"
}
}
},
"required": [
"name",
"workload"
]
},
"service": {
"description": "The set of properties providing details about how to run the service container.",
"type": "object",
"properties": {
"name": {
"description": "The name of the service.",
"type": "string"
},
"image": {
"description": "The name of the service image.",
"type": "string"
},
"imagePullAuth": {
"description": "Container registry credentials for accessing the service image.",
"type": "object",
"properties": {
"user": {
"description": "The username.",
"type": "string"
},
"token": {
"description": "The access token.",
"type": "string"
}
}
},
"entrypoint": {
"description": "The command line arguments to launch the service image with.",
"type": "string"
},
"files": {
"description": "List of files that you'd like saucectl to upload and mount within the service container.",
"type": "array",
"items": {
"type": "object",
"properties": {
"src": {
"description": "Path to the local file.",
"type": "string"
},
"dst": {
"description": "Path within the container that the file should be mounted at.",
"type": "string"
}
}
}
},
"env": {
"description": "Set one or more environment variables for the service.",
"type": "object"
},
"resourceProfile": {
"description": "Sets the CPU/memory limits of the service container. Format is <CPU><level><mem><level>. Default to c1m1.",
"enum": [
"",
"c1m1",
"c2m2",
"c3m3"
]
}
},
"required": [
"name"
]
}
},
"properties": {
Expand Down
46 changes: 46 additions & 0 deletions internal/imagerunner/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,17 @@ type Suite struct {
Workload string `yaml:"workload,omitempty" json:"workload,omitempty"`
ResourceProfile string `yaml:"resourceProfile,omitempty" json:"resourceProfile,omitempty"`
Metadata map[string]string `yaml:"metadata,omitempty" json:"metadata,omitempty"`
Services []SuiteService `yaml:"services,omitempty" json:"services,omitempty"`
}

type SuiteService struct {
Name string `yaml:"name,omitempty" json:"name"`
Image string `yaml:"image,omitempty" json:"image"`
ImagePullAuth ImagePullAuth `yaml:"imagePullAuth,omitempty" json:"imagePullAuth"`
EntryPoint string `yaml:"entrypoint,omitempty" json:"entrypoint"`
Files []File `yaml:"files,omitempty" json:"files"`
Env map[string]string `yaml:"env,omitempty" json:"env"`
ResourceProfile string `yaml:"resourceProfile,omitempty" json:"resourceProfile,omitempty"`
}

type ImagePullAuth struct {
Expand Down Expand Up @@ -130,6 +141,23 @@ func SetDefaults(p *Project) {
suite.Env[k] = v
}
}

for j := range suite.Services {
service := &suite.Services[j]
if service.ResourceProfile == "" {
service.ResourceProfile = "c1m1"
}
suite.Metadata[fmt.Sprintf("resourceProfile-%s", GetCanonicalServiceName(service.Name))] = suite.ResourceProfile
if service.Env == nil {
service.Env = make(map[string]string)
}
// Precedence: --env flag > root-level env vars > default env vars > service env vars.
for _, env := range []map[string]string{p.Defaults.Env, p.Env, p.EnvFlag} {
for k, v := range env {
service.Env[k] = v
}
}
}
}
}

Expand Down Expand Up @@ -159,6 +187,24 @@ func Validate(p Project) error {
if suite.ResourceProfile != "" && !ValidResourceProfilesValidator.MatchString(suite.ResourceProfile) {
return fmt.Errorf(msg.InvalidResourceProfile, suite.Name, ValidResourceProfilesFormat)
}
if err := ValidateServices(suite.Services, suite.Name); err != nil {
return err
}
}
return nil
}

func ValidateServices(service []SuiteService, suiteName string) error {
for _, service := range service {
if service.Name == "" {
return fmt.Errorf(msg.MissingServiceName, suiteName)
}
if service.Image == "" {
return fmt.Errorf(msg.MissingServiceImage, service.Name, suiteName)
}
if service.ResourceProfile != "" && !ValidResourceProfilesValidator.MatchString(service.ResourceProfile) {
return fmt.Errorf(msg.InvalidServiceResourceProfile, service.Name, suiteName, ValidResourceProfilesFormat)
}
}
return nil
}
Expand Down
71 changes: 71 additions & 0 deletions internal/imagerunner/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,77 @@ func TestValidate(t *testing.T) {
},
wantErr: `invalid resourceProfile for suite: Main Suite, resourceProfile should be of format cXmX`,
},
{
name: "Invalid serviceName",
args: args{
p: Project{
Sauce: config.SauceConfig{
Region: region.USWest1.String(),
},
Suites: []Suite{
{
Name: "Main Suite",
Image: "dummy/image",
Workload: "other",
Services: []SuiteService{
{
Image: "dummy/image",
},
},
},
},
},
},
wantErr: `missing "name" for service in suite: Main Suite`,
},
{
name: "Invalid serviceImage",
args: args{
p: Project{
Sauce: config.SauceConfig{
Region: region.USWest1.String(),
},
Suites: []Suite{
{
Name: "Main Suite",
Image: "dummy/image",
Workload: "other",
Services: []SuiteService{
{
Name: "myservice",
},
},
},
},
},
},
wantErr: `missing "image" for service: myservice in suite: Main Suite`,
},
{
name: "Invalid serviceResourceProfile",
args: args{
p: Project{
Sauce: config.SauceConfig{
Region: region.USWest1.String(),
},
Suites: []Suite{
{
Name: "Main Suite",
Image: "dummy/image",
Workload: "other",
Services: []SuiteService{
{
Name: "myservice",
Image: "dummy/image",
ResourceProfile: "test",
},
},
},
},
},
},
wantErr: `invalid resourceProfile for service: myservice in suite: Main Suite, resourceProfile should be of format cXmX`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down
9 changes: 9 additions & 0 deletions internal/imagerunner/imagerunner.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,15 @@ type RunnerSpec struct {
Artifacts []string `json:"artifacts,omitempty"`
WorkloadType string `json:"workload_type,omitempty"`
Tunnel *Tunnel `json:"tunnel,omitempty"`
Services []Service `json:"services,omitempty"`
}

type Service struct {
Name string `json:"name,omitempty"`
Container Container `json:"container,omitempty"`
EntryPoint string `json:"entrypoint,omitempty"`
Env []EnvItem `json:"env,omitempty"`
Files []FileData `json:"files,omitempty"`
}

type Tunnel struct {
Expand Down
17 changes: 17 additions & 0 deletions internal/imagerunner/utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package imagerunner

import (
"regexp"
"strings"
)

func GetCanonicalServiceName(serviceName string) string {
if serviceName == "" {
return ""
}
// make sure the service name has only lowercase letters, numbers, and underscores
canonicalName := strings.ToLower(regexp.MustCompile("[^a-z0-9-]").ReplaceAllString(serviceName, "-"))
// remove successives dashes
canonicalName = regexp.MustCompile("-+").ReplaceAllString(canonicalName, "-")
return canonicalName
}
6 changes: 6 additions & 0 deletions internal/msg/errormsg.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,12 @@ const (
ImageRunnerMaxConcurrency = "Maximum concurrency for imagerunner is 5. Replacing %d with 5."
// InvalidResourceProfile indicates the resourceProfile is not valid
InvalidResourceProfile = "invalid resourceProfile for suite: %s, resourceProfile should be of format %v"
// MissingServiceName indicates no service name provided
MissingServiceName = `missing "name" for service in suite: %s`
// MissingServiceImage indicates no docker image provided
MissingServiceImage = `missing "image" for service: %s in suite: %s`
// InvalidServiceResourceProfile indicates the service resourceProfile is not valid
InvalidServiceResourceProfile = "invalid resourceProfile for service: %s in suite: %s, resourceProfile should be of format %v"
)

// testcafe config settings
Expand Down
Loading

0 comments on commit 5f17154

Please sign in to comment.