diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..c41b6ef --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,14 @@ +name: CI +on: [push] +jobs: + tests: + name: Unit Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup Go + uses: actions/setup-go@v4 + with: + go-version: '1.21.x' + - name: Client tests + run: make test diff --git a/Makefile b/Makefile index e4b747a..435ae52 100644 --- a/Makefile +++ b/Makefile @@ -18,3 +18,7 @@ proto: .PHONY: run-server run-server: proto reflex -s -t 120s -r '\.go' -- bash -c "make bin/alfred-server && bin/alfred-server" + +.PHONY: test +test: + go test -v ./... diff --git a/client/job/jobfile.go b/client/job/jobfile.go index 7ff7677..000fd94 100644 --- a/client/job/jobfile.go +++ b/client/job/jobfile.go @@ -2,11 +2,17 @@ package job import ( "fmt" + "os" + "path" + "regexp" + "strconv" + "time" ) const JobfileVersion = "1" type Jobfile struct { + Path string Version string Name string Image JobfileImage @@ -27,7 +33,10 @@ type JobfileService struct { } type JobfileServiceHealth struct { - Cmd []string + Cmd []string + Timeout string + Interval string + Retries string } func (jobfile Jobfile) Validate() error { @@ -36,16 +45,68 @@ func (jobfile Jobfile) Validate() error { } if jobfile.Name == "" { - return fmt.Errorf("job name is required") + return fmt.Errorf("name is required") } if jobfile.Image.Dockerfile == "" { return fmt.Errorf("image.dockerfile is required") } + if _, err := os.Stat(path.Join(jobfile.Path, jobfile.Image.Dockerfile)); os.IsNotExist(err) { + return fmt.Errorf("image.dockerfile must be an existing file on disk") + } if jobfile.Image.Context == "" { return fmt.Errorf("image.context is required") } + if _, err := os.Stat(path.Join(jobfile.Path, jobfile.Image.Context)); os.IsNotExist(err) { + return fmt.Errorf("image.context must be an existing folder on disk") + } + + serviceNameRegex := regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9._-]+$`) + serviceEnvKeyRegex := regexp.MustCompile(`^[A-Z][A-Z0-9_]+$`) + + for name, service := range jobfile.Services { + if !serviceNameRegex.MatchString(name) { + return fmt.Errorf("services names must be valid identifiers") + } + + if service.Image == "" { + return fmt.Errorf("services[%s].image is required", name) + } + + for key, _ := range service.Env { + if !serviceEnvKeyRegex.MatchString(key) { + return fmt.Errorf("services[%s].env[%s] must be a valid environment variable identifier", name, key) + } + } + + // If none of the health fields are specified, skip validation + if service.Health.Cmd == nil && service.Health.Timeout == "" && service.Health.Interval == "" && service.Health.Retries == "" { + continue + } + + if len(service.Health.Cmd) < 1 { + return fmt.Errorf("services[%s].health.cmd is required", name) + } + + if service.Health.Interval != "" { + if _, err := time.ParseDuration(service.Health.Interval); err != nil { + return fmt.Errorf("services[%s].health.interval is not a valid duration: %w", name, err) + } + } + + if service.Health.Timeout != "" { + if _, err := time.ParseDuration(service.Health.Timeout); err != nil { + return fmt.Errorf("services[%s].health.timeout is not a valid duration: %w", name, err) + } + } + + if service.Health.Retries != "" { + if _, err := strconv.ParseInt(service.Health.Retries, 10, 64); err != nil { + return fmt.Errorf("services[%s].health.retries is not a valid numeric: %w", name, err) + } + } + } return nil } diff --git a/client/job/jobfile_test.go b/client/job/jobfile_test.go new file mode 100644 index 0000000..8e4a584 --- /dev/null +++ b/client/job/jobfile_test.go @@ -0,0 +1,55 @@ +package job + +import ( + "github.com/samber/lo" + "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v3" + "os" + "path" + "testing" +) + +var flagtests = []struct { + file string + expected string +}{ + {"tests/jobfile/valid_minimalist.yaml", ""}, + {"tests/jobfile/valid_full_featured.yaml", ""}, + + {"tests/jobfile/invalid_missing_name.yaml", "name is required"}, + {"tests/jobfile/invalid_missing_image_dockerfile.yaml", "image.dockerfile is required"}, + {"tests/jobfile/invalid_missing_image_context.yaml", "image.context is required"}, + {"tests/jobfile/invalid_missing_services_image.yaml", "services[mysql].image is required"}, + {"tests/jobfile/invalid_missing_services_health_cmd.yaml", "services[mysql].health.cmd is required"}, + + {"tests/jobfile/invalid_version.yaml", "unsupported version '42'"}, + {"tests/jobfile/invalid_image_dockerfile.yaml", "image.dockerfile must be an existing file on disk"}, + {"tests/jobfile/invalid_image_context.yaml", "image.context must be an existing folder on disk"}, + {"tests/jobfile/invalid_services_map.yaml", "yaml: unmarshal errors:\n line 7: cannot unmarshal !!seq into map[string]job.JobfileService"}, + {"tests/jobfile/invalid_services_keys.yaml", "services names must be valid identifiers"}, + {"tests/jobfile/invalid_services_env_keys.yaml", "services[mysql].env[0/2] must be a valid environment variable identifier"}, +} + +func TestJobValidate(t *testing.T) { + var buf []byte + var err error + + for _, tt := range flagtests { + t.Run(tt.file, func(t *testing.T) { + buf = lo.Must(os.ReadFile(tt.file)) + + var jobfile Jobfile + if err = yaml.Unmarshal(buf, &jobfile); err != nil { + assert.Equal(t, tt.expected, err.Error()) + return + } + jobfile.Path = path.Dir(tt.file) + if err = jobfile.Validate(); err != nil { + assert.Equal(t, tt.expected, err.Error()) + return + } + + assert.Equal(t, "", tt.expected) + }) + } +} diff --git a/client/job/reader.go b/client/job/reader.go index f1390c3..9d24059 100644 --- a/client/job/reader.go +++ b/client/job/reader.go @@ -2,14 +2,16 @@ package job import ( "fmt" + "github.com/gammadia/alfred/proto" + "github.com/samber/lo" + "google.golang.org/protobuf/types/known/durationpb" + "gopkg.in/yaml.v3" "os" "os/exec" "path" + "strconv" "strings" - - "github.com/gammadia/alfred/proto" - "github.com/samber/lo" - "gopkg.in/yaml.v3" + "time" ) func Read(p string, overrides Overrides) (job *proto.Job, err error) { @@ -28,6 +30,7 @@ func Read(p string, overrides Overrides) (job *proto.Job, err error) { err = fmt.Errorf("unmarshal: %w", err) return } + jobfile.Path = path.Dir(p) if err = jobfile.Validate(); err != nil { err = fmt.Errorf("validate: %w", err) return @@ -62,7 +65,7 @@ func Read(p string, overrides Overrides) (job *proto.Job, err error) { // Services for name, service := range jobfile.Services { - job.Services = append(job.Services, &proto.Job_Service{ + jobService := &proto.Job_Service{ Name: name, Image: service.Image, Env: lo.MapToSlice(service.Env, func(key string, value string) *proto.Job_Env { @@ -75,7 +78,20 @@ func Read(p string, overrides Overrides) (job *proto.Job, err error) { Cmd: service.Health.Cmd[0], Args: service.Health.Cmd[1:], }), - }) + } + + if service.Health.Timeout != "" { + jobService.Health.Timeout = durationpb.New(lo.Must(time.ParseDuration(service.Health.Timeout))) + } + if service.Health.Interval != "" { + jobService.Health.Interval = durationpb.New(lo.Must(time.ParseDuration(service.Health.Interval))) + } + if service.Health.Retries != "" { + retries := lo.Must(strconv.ParseUint(service.Health.Retries, 10, 64)) + jobService.Health.Retries = &retries + } + + job.Services = append(job.Services, jobService) } return diff --git a/client/job/tests/jobfile/a.dockerfile b/client/job/tests/jobfile/a.dockerfile new file mode 100644 index 0000000..e69de29 diff --git a/client/job/tests/jobfile/invalid_image_context.yaml b/client/job/tests/jobfile/invalid_image_context.yaml new file mode 100644 index 0000000..43b9f65 --- /dev/null +++ b/client/job/tests/jobfile/invalid_image_context.yaml @@ -0,0 +1,5 @@ +version: "1" +name: "test" +image: + dockerfile: "a.dockerfile" + context: "./toto" diff --git a/client/job/tests/jobfile/invalid_image_dockerfile.yaml b/client/job/tests/jobfile/invalid_image_dockerfile.yaml new file mode 100644 index 0000000..235af75 --- /dev/null +++ b/client/job/tests/jobfile/invalid_image_dockerfile.yaml @@ -0,0 +1,5 @@ +version: "1" +name: "test" +image: + dockerfile: "toto.dockerfile" + context: "." diff --git a/client/job/tests/jobfile/invalid_missing_image_context.yaml b/client/job/tests/jobfile/invalid_missing_image_context.yaml new file mode 100644 index 0000000..b9fd757 --- /dev/null +++ b/client/job/tests/jobfile/invalid_missing_image_context.yaml @@ -0,0 +1,4 @@ +version: "1" +name: "test" +image: + dockerfile: "a.dockerfile" diff --git a/client/job/tests/jobfile/invalid_missing_image_dockerfile.yaml b/client/job/tests/jobfile/invalid_missing_image_dockerfile.yaml new file mode 100644 index 0000000..81a380e --- /dev/null +++ b/client/job/tests/jobfile/invalid_missing_image_dockerfile.yaml @@ -0,0 +1,4 @@ +version: "1" +name: "test" +image: + context: "." diff --git a/client/job/tests/jobfile/invalid_missing_name.yaml b/client/job/tests/jobfile/invalid_missing_name.yaml new file mode 100644 index 0000000..86f7c0f --- /dev/null +++ b/client/job/tests/jobfile/invalid_missing_name.yaml @@ -0,0 +1,4 @@ +version: "1" +image: + dockerfile: "a.dockerfile" + context: "." diff --git a/client/job/tests/jobfile/invalid_missing_services_health_cmd.yaml b/client/job/tests/jobfile/invalid_missing_services_health_cmd.yaml new file mode 100644 index 0000000..b280c00 --- /dev/null +++ b/client/job/tests/jobfile/invalid_missing_services_health_cmd.yaml @@ -0,0 +1,10 @@ +version: "1" +name: "test" +image: + dockerfile: "a.dockerfile" + context: "." +services: + mysql: + image: mysql + health: + retries: 1 diff --git a/client/job/tests/jobfile/invalid_missing_services_image.yaml b/client/job/tests/jobfile/invalid_missing_services_image.yaml new file mode 100644 index 0000000..43be3ee --- /dev/null +++ b/client/job/tests/jobfile/invalid_missing_services_image.yaml @@ -0,0 +1,9 @@ +version: "1" +name: "test" +image: + dockerfile: "a.dockerfile" + context: "." +services: + mysql: + env: + TOTO: toto diff --git a/client/job/tests/jobfile/invalid_services_env_keys.yaml b/client/job/tests/jobfile/invalid_services_env_keys.yaml new file mode 100644 index 0000000..6113cd6 --- /dev/null +++ b/client/job/tests/jobfile/invalid_services_env_keys.yaml @@ -0,0 +1,10 @@ +version: "1" +name: "test" +image: + dockerfile: "a.dockerfile" + context: "." +services: + mysql: + image: mysql + env: + 0/2: foo diff --git a/client/job/tests/jobfile/invalid_services_health_timeout.yaml b/client/job/tests/jobfile/invalid_services_health_timeout.yaml new file mode 100644 index 0000000..b2a21bc --- /dev/null +++ b/client/job/tests/jobfile/invalid_services_health_timeout.yaml @@ -0,0 +1,10 @@ +version: "1" +name: "test" +image: + dockerfile: "a.dockerfile" + context: "." +services: + mysql: + image: mysql + health: + timeout: diff --git a/client/job/tests/jobfile/invalid_services_keys.yaml b/client/job/tests/jobfile/invalid_services_keys.yaml new file mode 100644 index 0000000..2114f68 --- /dev/null +++ b/client/job/tests/jobfile/invalid_services_keys.yaml @@ -0,0 +1,8 @@ +version: "1" +name: "test" +image: + dockerfile: "a.dockerfile" + context: "." +services: + 0/2: + image: mysql diff --git a/client/job/tests/jobfile/invalid_services_map.yaml b/client/job/tests/jobfile/invalid_services_map.yaml new file mode 100644 index 0000000..2b88d35 --- /dev/null +++ b/client/job/tests/jobfile/invalid_services_map.yaml @@ -0,0 +1,8 @@ +version: "1" +name: "test" +image: + dockerfile: "a.dockerfile" + context: "." +services: + - mysql + - redis diff --git a/client/job/tests/jobfile/invalid_version.yaml b/client/job/tests/jobfile/invalid_version.yaml new file mode 100644 index 0000000..80dd0db --- /dev/null +++ b/client/job/tests/jobfile/invalid_version.yaml @@ -0,0 +1,5 @@ +version: "42" +name: "test" +image: + dockerfile: "a.dockerfile" + context: "." diff --git a/client/job/tests/jobfile/valid_full_featured.yaml b/client/job/tests/jobfile/valid_full_featured.yaml new file mode 100644 index 0000000..7f18c3e --- /dev/null +++ b/client/job/tests/jobfile/valid_full_featured.yaml @@ -0,0 +1,24 @@ +version: "1" +name: "test" +image: + dockerfile: "a.dockerfile" + context: "." +services: + redis: + image: "redis:7.0.5-alpine" + health: + cmd: ["redis-cli", "ping"] + timeout: "5s" + wait: "1s" + retries: 10 + mysql: + image: "mysql:8.0.31" + env: + MYSQL_ROOT_PASSWORD: "root" + health: + cmd: ["mysqladmin", "-u", "root", "-proot", "-h", "127.0.0.1", "ping"] + timeout: "10s" + interval: "2s" + retries: 20 +tasks: | + sloth instance --app tipee list -o json | jq -r '.[] | select(.features.tests) | .id' | tail -n 1 diff --git a/client/job/tests/jobfile/valid_minimalist.yaml b/client/job/tests/jobfile/valid_minimalist.yaml new file mode 100644 index 0000000..d727418 --- /dev/null +++ b/client/job/tests/jobfile/valid_minimalist.yaml @@ -0,0 +1,5 @@ +version: "1" +name: "test" +image: + dockerfile: "a.dockerfile" + context: "." diff --git a/go.mod b/go.mod index bbc5e43..bbcaf53 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.17.0 + github.com/stretchr/testify v1.8.4 golang.org/x/crypto v0.14.0 google.golang.org/grpc v1.59.0 google.golang.org/protobuf v1.31.0 @@ -21,6 +22,7 @@ require ( require ( github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/distribution/reference v0.5.0 // indirect github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/go-connections v0.4.0 // indirect @@ -44,6 +46,7 @@ require ( github.com/opencontainers/image-spec v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rivo/uniseg v0.4.4 // indirect github.com/sagikazarmark/locafero v0.3.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect diff --git a/hack/job.yaml b/hack/job.yaml index 7d457ae..9c8ee91 100644 --- a/hack/job.yaml +++ b/hack/job.yaml @@ -1,18 +1,24 @@ version: "1" -name: test +name: "test" image: - dockerfile: job.dockerfile - context: . + dockerfile: "job.dockerfile" + context: "." services: - #redis: - #image: redis:7.0.5-alpine - #health: - # cmd: [redis-cli, ping] + redis: + image: "redis:7.0.5-alpine" + health: + cmd: ["redis-cli", "ping"] + timeout: "5s" + wait: "1s" + retries: 10 mysql: - image: mysql:8.0.31 + image: "mysql:8.0.31" env: MYSQL_ROOT_PASSWORD: "root" health: cmd: ["mysqladmin", "-u", "root", "-proot", "-h", "127.0.0.1", "ping"] + timeout: "10s" + interval: "2s" + retries: 20 tasks: | sloth instance --app tipee list -o json | jq -r '.[] | select(.features.tests) | .id' | tail -n 1 diff --git a/proto/alfred.pb.go b/proto/alfred.pb.go index 800fc18..f384144 100644 --- a/proto/alfred.pb.go +++ b/proto/alfred.pb.go @@ -9,6 +9,7 @@ package proto import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" + durationpb "google.golang.org/protobuf/types/known/durationpb" reflect "reflect" sync "sync" ) @@ -599,8 +600,11 @@ type Job_Service_Health struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Cmd string `protobuf:"bytes,1,opt,name=cmd,proto3" json:"cmd,omitempty"` - Args []string `protobuf:"bytes,2,rep,name=args,proto3" json:"args,omitempty"` + Cmd string `protobuf:"bytes,1,opt,name=cmd,proto3" json:"cmd,omitempty"` + Args []string `protobuf:"bytes,2,rep,name=args,proto3" json:"args,omitempty"` + Timeout *durationpb.Duration `protobuf:"bytes,3,opt,name=timeout,proto3,oneof" json:"timeout,omitempty"` + Interval *durationpb.Duration `protobuf:"bytes,4,opt,name=interval,proto3,oneof" json:"interval,omitempty"` + Retries *uint64 `protobuf:"varint,5,opt,name=retries,proto3,oneof" json:"retries,omitempty"` } func (x *Job_Service_Health) Reset() { @@ -649,6 +653,27 @@ func (x *Job_Service_Health) GetArgs() []string { return nil } +func (x *Job_Service_Health) GetTimeout() *durationpb.Duration { + if x != nil { + return x.Timeout + } + return nil +} + +func (x *Job_Service_Health) GetInterval() *durationpb.Duration { + if x != nil { + return x.Interval + } + return nil +} + +func (x *Job_Service_Health) GetRetries() uint64 { + if x != nil && x.Retries != nil { + return *x.Retries + } + return 0 +} + type LoadImageMessage_Init struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -793,7 +818,9 @@ var File_proto_alfred_proto protoreflect.FileDescriptor var file_proto_alfred_proto_rawDesc = []byte{ 0x0a, 0x12, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x61, 0x6c, 0x66, 0x72, 0x65, 0x64, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xef, 0x02, 0x0a, 0x03, + 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x75, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xaa, 0x04, 0x0a, 0x03, 0x4a, 0x6f, 0x62, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x2e, 0x0a, @@ -801,7 +828,7 @@ var file_proto_alfred_proto_rawDesc = []byte{ 0x12, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4a, 0x6f, 0x62, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x52, 0x08, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x61, 0x73, 0x6b, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x74, 0x61, - 0x73, 0x6b, 0x73, 0x1a, 0xc8, 0x01, 0x0a, 0x07, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, + 0x73, 0x6b, 0x73, 0x1a, 0x83, 0x03, 0x0a, 0x07, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x20, 0x0a, 0x03, 0x65, 0x6e, 0x76, @@ -810,67 +837,79 @@ var file_proto_alfred_proto_rawDesc = []byte{ 0x65, 0x61, 0x6c, 0x74, 0x68, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4a, 0x6f, 0x62, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x48, 0x00, 0x52, 0x06, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, - 0x88, 0x01, 0x01, 0x1a, 0x2e, 0x0a, 0x06, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x12, 0x10, 0x0a, - 0x03, 0x63, 0x6d, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x63, 0x6d, 0x64, 0x12, - 0x12, 0x0a, 0x04, 0x61, 0x72, 0x67, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x61, - 0x72, 0x67, 0x73, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x1a, 0x2d, - 0x0a, 0x03, 0x45, 0x6e, 0x76, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x9a, 0x02, - 0x0a, 0x10, 0x4c, 0x6f, 0x61, 0x64, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x4d, 0x65, 0x73, 0x73, 0x61, - 0x67, 0x65, 0x12, 0x32, 0x0a, 0x04, 0x69, 0x6e, 0x69, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4c, 0x6f, 0x61, 0x64, 0x49, 0x6d, 0x61, - 0x67, 0x65, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x48, 0x00, - 0x52, 0x04, 0x69, 0x6e, 0x69, 0x74, 0x12, 0x32, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4c, 0x6f, 0x61, - 0x64, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x44, 0x61, - 0x74, 0x61, 0x48, 0x00, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x12, 0x32, 0x0a, 0x04, 0x64, 0x6f, - 0x6e, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x2e, 0x4c, 0x6f, 0x61, 0x64, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, - 0x65, 0x2e, 0x44, 0x6f, 0x6e, 0x65, 0x48, 0x00, 0x52, 0x04, 0x64, 0x6f, 0x6e, 0x65, 0x1a, 0x21, - 0x0a, 0x04, 0x49, 0x6e, 0x69, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x5f, - 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x49, - 0x64, 0x1a, 0x34, 0x0a, 0x04, 0x44, 0x61, 0x74, 0x61, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x68, 0x75, - 0x6e, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x63, 0x68, 0x75, 0x6e, 0x6b, 0x12, - 0x16, 0x0a, 0x06, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, - 0x06, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x1a, 0x06, 0x0a, 0x04, 0x44, 0x6f, 0x6e, 0x65, 0x42, - 0x09, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0xb0, 0x01, 0x0a, 0x11, 0x4c, - 0x6f, 0x61, 0x64, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x37, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, - 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4c, 0x6f, 0x61, 0x64, 0x49, 0x6d, 0x61, - 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x22, 0x0a, 0x0a, 0x63, 0x68, 0x75, - 0x6e, 0x6b, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x00, 0x52, - 0x09, 0x63, 0x68, 0x75, 0x6e, 0x6b, 0x53, 0x69, 0x7a, 0x65, 0x88, 0x01, 0x01, 0x22, 0x2f, 0x0a, - 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0f, 0x0a, 0x0b, 0x55, 0x4e, 0x53, 0x50, 0x45, - 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x43, 0x4f, 0x4e, 0x54, - 0x49, 0x4e, 0x55, 0x45, 0x10, 0x01, 0x12, 0x06, 0x0a, 0x02, 0x4f, 0x4b, 0x10, 0x02, 0x42, 0x0d, - 0x0a, 0x0b, 0x5f, 0x63, 0x68, 0x75, 0x6e, 0x6b, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x22, 0x32, 0x0a, - 0x12, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x03, 0x6a, 0x6f, 0x62, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x0a, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4a, 0x6f, 0x62, 0x52, 0x03, 0x6a, 0x6f, - 0x62, 0x22, 0x15, 0x0a, 0x13, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x4a, 0x6f, 0x62, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x0d, 0x0a, 0x0b, 0x50, 0x69, 0x6e, 0x67, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x40, 0x0a, 0x0c, 0x50, 0x69, 0x6e, 0x67, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, - 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, - 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x06, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x32, 0xc3, 0x01, 0x0a, 0x06, 0x41, 0x6c, - 0x66, 0x72, 0x65, 0x64, 0x12, 0x42, 0x0a, 0x09, 0x4c, 0x6f, 0x61, 0x64, 0x49, 0x6d, 0x61, 0x67, - 0x65, 0x12, 0x17, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4c, 0x6f, 0x61, 0x64, 0x49, 0x6d, - 0x61, 0x67, 0x65, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x18, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x2e, 0x4c, 0x6f, 0x61, 0x64, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x30, 0x01, 0x12, 0x44, 0x0a, 0x0b, 0x53, 0x63, 0x68, 0x65, - 0x64, 0x75, 0x6c, 0x65, 0x4a, 0x6f, 0x62, 0x12, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, - 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x64, - 0x75, 0x6c, 0x65, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, - 0x0a, 0x04, 0x50, 0x69, 0x6e, 0x67, 0x12, 0x12, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x50, - 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, - 0x22, 0x5a, 0x20, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x61, - 0x6d, 0x6d, 0x61, 0x64, 0x69, 0x61, 0x2f, 0x61, 0x6c, 0x66, 0x72, 0x65, 0x64, 0x2f, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x88, 0x01, 0x01, 0x1a, 0xe8, 0x01, 0x0a, 0x06, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x12, 0x10, + 0x0a, 0x03, 0x63, 0x6d, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x63, 0x6d, 0x64, + 0x12, 0x12, 0x0a, 0x04, 0x61, 0x72, 0x67, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, + 0x61, 0x72, 0x67, 0x73, 0x12, 0x38, 0x0a, 0x07, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x48, 0x00, 0x52, 0x07, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x88, 0x01, 0x01, 0x12, 0x3a, + 0x0a, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x01, 0x52, 0x08, 0x69, + 0x6e, 0x74, 0x65, 0x72, 0x76, 0x61, 0x6c, 0x88, 0x01, 0x01, 0x12, 0x1d, 0x0a, 0x07, 0x72, 0x65, + 0x74, 0x72, 0x69, 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x48, 0x02, 0x52, 0x07, 0x72, + 0x65, 0x74, 0x72, 0x69, 0x65, 0x73, 0x88, 0x01, 0x01, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x74, 0x69, + 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x76, + 0x61, 0x6c, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x72, 0x65, 0x74, 0x72, 0x69, 0x65, 0x73, 0x42, 0x09, + 0x0a, 0x07, 0x5f, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x1a, 0x2d, 0x0a, 0x03, 0x45, 0x6e, 0x76, + 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, + 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x9a, 0x02, 0x0a, 0x10, 0x4c, 0x6f, 0x61, + 0x64, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x32, 0x0a, + 0x04, 0x69, 0x6e, 0x69, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x2e, 0x4c, 0x6f, 0x61, 0x64, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x4d, 0x65, 0x73, + 0x73, 0x61, 0x67, 0x65, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x48, 0x00, 0x52, 0x04, 0x69, 0x6e, 0x69, + 0x74, 0x12, 0x32, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4c, 0x6f, 0x61, 0x64, 0x49, 0x6d, 0x61, 0x67, + 0x65, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x48, 0x00, 0x52, + 0x04, 0x64, 0x61, 0x74, 0x61, 0x12, 0x32, 0x0a, 0x04, 0x64, 0x6f, 0x6e, 0x65, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4c, 0x6f, 0x61, 0x64, + 0x49, 0x6d, 0x61, 0x67, 0x65, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x44, 0x6f, 0x6e, + 0x65, 0x48, 0x00, 0x52, 0x04, 0x64, 0x6f, 0x6e, 0x65, 0x1a, 0x21, 0x0a, 0x04, 0x49, 0x6e, 0x69, + 0x74, 0x12, 0x19, 0x0a, 0x08, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x07, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x49, 0x64, 0x1a, 0x34, 0x0a, 0x04, + 0x44, 0x61, 0x74, 0x61, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x68, 0x75, 0x6e, 0x6b, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x05, 0x63, 0x68, 0x75, 0x6e, 0x6b, 0x12, 0x16, 0x0a, 0x06, 0x6c, 0x65, + 0x6e, 0x67, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x6c, 0x65, 0x6e, 0x67, + 0x74, 0x68, 0x1a, 0x06, 0x0a, 0x04, 0x44, 0x6f, 0x6e, 0x65, 0x42, 0x09, 0x0a, 0x07, 0x6d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0xb0, 0x01, 0x0a, 0x11, 0x4c, 0x6f, 0x61, 0x64, 0x49, 0x6d, + 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x06, 0x73, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1f, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x2e, 0x4c, 0x6f, 0x61, 0x64, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x12, 0x22, 0x0a, 0x0a, 0x63, 0x68, 0x75, 0x6e, 0x6b, 0x5f, 0x73, 0x69, + 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x00, 0x52, 0x09, 0x63, 0x68, 0x75, 0x6e, + 0x6b, 0x53, 0x69, 0x7a, 0x65, 0x88, 0x01, 0x01, 0x22, 0x2f, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x12, 0x0f, 0x0a, 0x0b, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, + 0x44, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x43, 0x4f, 0x4e, 0x54, 0x49, 0x4e, 0x55, 0x45, 0x10, + 0x01, 0x12, 0x06, 0x0a, 0x02, 0x4f, 0x4b, 0x10, 0x02, 0x42, 0x0d, 0x0a, 0x0b, 0x5f, 0x63, 0x68, + 0x75, 0x6e, 0x6b, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x22, 0x32, 0x0a, 0x12, 0x53, 0x63, 0x68, 0x65, + 0x64, 0x75, 0x6c, 0x65, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, + 0x0a, 0x03, 0x6a, 0x6f, 0x62, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0a, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x2e, 0x4a, 0x6f, 0x62, 0x52, 0x03, 0x6a, 0x6f, 0x62, 0x22, 0x15, 0x0a, 0x13, + 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x0d, 0x0a, 0x0b, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x22, 0x40, 0x0a, 0x0c, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x16, 0x0a, 0x06, + 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x63, 0x6f, + 0x6d, 0x6d, 0x69, 0x74, 0x32, 0xc3, 0x01, 0x0a, 0x06, 0x41, 0x6c, 0x66, 0x72, 0x65, 0x64, 0x12, + 0x42, 0x0a, 0x09, 0x4c, 0x6f, 0x61, 0x64, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x17, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4c, 0x6f, 0x61, 0x64, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x4d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x18, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4c, 0x6f, + 0x61, 0x64, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, + 0x01, 0x30, 0x01, 0x12, 0x44, 0x0a, 0x0b, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x4a, + 0x6f, 0x62, 0x12, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x64, + 0x75, 0x6c, 0x65, 0x4a, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x4a, 0x6f, + 0x62, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x04, 0x50, 0x69, 0x6e, + 0x67, 0x12, 0x12, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x50, 0x69, 0x6e, 0x67, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x50, 0x69, + 0x6e, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x22, 0x5a, 0x20, 0x67, 0x69, + 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x61, 0x6d, 0x6d, 0x61, 0x64, 0x69, + 0x61, 0x2f, 0x61, 0x6c, 0x66, 0x72, 0x65, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -902,6 +941,7 @@ var file_proto_alfred_proto_goTypes = []interface{}{ (*LoadImageMessage_Init)(nil), // 11: proto.LoadImageMessage.Init (*LoadImageMessage_Data)(nil), // 12: proto.LoadImageMessage.Data (*LoadImageMessage_Done)(nil), // 13: proto.LoadImageMessage.Done + (*durationpb.Duration)(nil), // 14: google.protobuf.Duration } var file_proto_alfred_proto_depIdxs = []int32{ 8, // 0: proto.Job.services:type_name -> proto.Job.Service @@ -912,17 +952,19 @@ var file_proto_alfred_proto_depIdxs = []int32{ 1, // 5: proto.ScheduleJobRequest.job:type_name -> proto.Job 9, // 6: proto.Job.Service.env:type_name -> proto.Job.Env 10, // 7: proto.Job.Service.health:type_name -> proto.Job.Service.Health - 2, // 8: proto.Alfred.LoadImage:input_type -> proto.LoadImageMessage - 4, // 9: proto.Alfred.ScheduleJob:input_type -> proto.ScheduleJobRequest - 6, // 10: proto.Alfred.Ping:input_type -> proto.PingRequest - 3, // 11: proto.Alfred.LoadImage:output_type -> proto.LoadImageResponse - 5, // 12: proto.Alfred.ScheduleJob:output_type -> proto.ScheduleJobResponse - 7, // 13: proto.Alfred.Ping:output_type -> proto.PingResponse - 11, // [11:14] is the sub-list for method output_type - 8, // [8:11] is the sub-list for method input_type - 8, // [8:8] is the sub-list for extension type_name - 8, // [8:8] is the sub-list for extension extendee - 0, // [0:8] is the sub-list for field type_name + 14, // 8: proto.Job.Service.Health.timeout:type_name -> google.protobuf.Duration + 14, // 9: proto.Job.Service.Health.interval:type_name -> google.protobuf.Duration + 2, // 10: proto.Alfred.LoadImage:input_type -> proto.LoadImageMessage + 4, // 11: proto.Alfred.ScheduleJob:input_type -> proto.ScheduleJobRequest + 6, // 12: proto.Alfred.Ping:input_type -> proto.PingRequest + 3, // 13: proto.Alfred.LoadImage:output_type -> proto.LoadImageResponse + 5, // 14: proto.Alfred.ScheduleJob:output_type -> proto.ScheduleJobResponse + 7, // 15: proto.Alfred.Ping:output_type -> proto.PingResponse + 13, // [13:16] is the sub-list for method output_type + 10, // [10:13] is the sub-list for method input_type + 10, // [10:10] is the sub-list for extension type_name + 10, // [10:10] is the sub-list for extension extendee + 0, // [0:10] is the sub-list for field type_name } func init() { file_proto_alfred_proto_init() } @@ -1095,6 +1137,7 @@ func file_proto_alfred_proto_init() { } file_proto_alfred_proto_msgTypes[2].OneofWrappers = []interface{}{} file_proto_alfred_proto_msgTypes[7].OneofWrappers = []interface{}{} + file_proto_alfred_proto_msgTypes[9].OneofWrappers = []interface{}{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ diff --git a/proto/alfred.proto b/proto/alfred.proto index 8bc1583..5385e7c 100644 --- a/proto/alfred.proto +++ b/proto/alfred.proto @@ -4,6 +4,8 @@ option go_package = "github.com/gammadia/alfred/proto"; package proto; +import "google/protobuf/duration.proto"; + service Alfred { rpc LoadImage (stream LoadImageMessage) returns (stream LoadImageResponse); rpc ScheduleJob (ScheduleJobRequest) returns (ScheduleJobResponse); @@ -25,6 +27,10 @@ message Job { message Health { string cmd = 1; repeated string args = 2; + + optional google.protobuf.Duration timeout = 3; + optional google.protobuf.Duration interval = 4; + optional uint64 retries = 5; } } diff --git a/provisioner/internal/docker.go b/provisioner/internal/docker.go index 4ca8709..359e675 100644 --- a/provisioner/internal/docker.go +++ b/provisioner/internal/docker.go @@ -162,13 +162,14 @@ func RunContainer(ctx context.Context, log *slog.Logger, docker *client.Client, return } - // TODO: move to job definition - interval := 10 * time.Second - timeout := 5 * time.Second - retries := 3 + interval := lo.Ternary(service.Health.Interval != nil, service.Health.Interval.AsDuration(), 10*time.Second) + timeout := lo.Ternary(service.Health.Timeout != nil, service.Health.Timeout.AsDuration(), 5*time.Second) + retries := lo.Ternary(service.Health.Retries != nil, int(*service.Health.Retries), 3) for i := 0; i < retries; i++ { - time.Sleep(interval) + // Always wait 1 second before running the health check, and potentially more between retries + time.Sleep(lo.Ternary(i > 0, interval, 1*time.Second)) + healthCheckLog := serviceLog.With(slog.Group("retry", "attempt", i+1, "interval", interval)) exec, err := docker.ContainerExecCreate(ctx, containerId, types.ExecConfig{