feat(go-sdk): add ability to create workflows from the Go SDK more easily, quickstart improvements (#30)

* feat: add initial docs site

* feat: allow workflows to be defined from go sdk

* fix release action

* chore: remove server dependencies from client

* fix: use correct certificate for server

* chore: add port and bind address to grpc config

* fix: add env for grpc config

* fix: nil pointer when output is null

* chore: support variation in output args

* fix unresolve merge conflict

* fix: quickstart improvements

* temp remove database url

* fix: action id not required for event

* fix: actionid validation for events

* Remove deleted files
This commit is contained in:
abelanger5
2024-01-02 09:02:53 -05:00
committed by GitHub
parent ba5e043451
commit 5937b9fd98
50 changed files with 1332 additions and 449 deletions

View File

@@ -20,7 +20,7 @@ jobs:
- name: Pull and push hatchet-api
run: |
docker pull ghcr.io/hatchet-dev/hatchet/hatchet-api:${{steps.tag_name.outputs.tag}}
docker tag ghcr.io/hatchet-dev/hatchet/hatchet-api:${{steps.tag_name.outputs.tag}} ghcr.io/hatchet-dev/hatchet/hatchet-server:latest
docker tag ghcr.io/hatchet-dev/hatchet/hatchet-api:${{steps.tag_name.outputs.tag}} ghcr.io/hatchet-dev/hatchet/hatchet-api:latest
docker push ghcr.io/hatchet-dev/hatchet/hatchet-api:latest
- name: Pull and push hatchet-engine
run: |

1
.gitignore vendored
View File

@@ -24,6 +24,7 @@ dump.rdb
*.pfx
*.cert
generated
.next
node_modules

View File

@@ -23,6 +23,9 @@ message WorkerRegisterRequest {
// a list of actions that this worker can run
repeated string actions = 3;
// (optional) the services for this worker
repeated string services = 4;
}
message WorkerRegisterResponse {

View File

@@ -2,7 +2,6 @@ package cli
import (
_ "embed"
"io/ioutil"
"fmt"
"os"
@@ -189,22 +188,25 @@ func setupCerts(generated *generatedConfigFiles) error {
return fmt.Errorf("could not create worker-client-cert.conf file: %w", err)
}
// run openssl commands
c := exec.Command("bash", "-s", "-", fullPathCertDir)
// if CA files don't exists, run the script to regenerate all certs
if overwrite || (!fileExists(filepath.Join(fullPathCertDir, "./ca.key")) || !fileExists(filepath.Join(fullPathCertDir, "./ca.cert"))) {
// run openssl commands
c := exec.Command("bash", "-s", "-", fullPathCertDir)
c.Stdin = strings.NewReader(GenerateCertsScript)
c.Stdout = os.Stdout
c.Stderr = os.Stderr
c.Stdin = strings.NewReader(GenerateCertsScript)
c.Stdout = os.Stdout
c.Stderr = os.Stderr
err = c.Run()
err = c.Run()
if err != nil {
return err
if err != nil {
return err
}
}
generated.sc.TLS.TLSRootCAFile = filepath.Join(fullPathCertDir, "ca.cert")
generated.sc.TLS.TLSCertFile = filepath.Join(fullPathCertDir, "client-internal-admin.pem")
generated.sc.TLS.TLSKeyFile = filepath.Join(fullPathCertDir, "client-internal-admin.key")
generated.sc.TLS.TLSCertFile = filepath.Join(fullPathCertDir, "cluster.pem")
generated.sc.TLS.TLSKeyFile = filepath.Join(fullPathCertDir, "cluster.key")
return nil
}
@@ -273,7 +275,7 @@ func getFiles(name string) [][]byte {
basePath := filepath.Join(configDirectory, name)
if fileExists(basePath) {
configFileBytes, err := ioutil.ReadFile(basePath)
configFileBytes, err := os.ReadFile(basePath)
if err != nil {
panic(err)
@@ -285,7 +287,7 @@ func getFiles(name string) [][]byte {
generatedPath := filepath.Join(generatedConfigDir, name)
if fileExists(generatedPath) {
generatedFileBytes, err := ioutil.ReadFile(filepath.Join(generatedConfigDir, name))
generatedFileBytes, err := os.ReadFile(filepath.Join(generatedConfigDir, name))
if err != nil {
panic(err)
@@ -314,7 +316,7 @@ func writeGeneratedConfig(generated *generatedConfigFiles) error {
return err
}
err = ioutil.WriteFile(databasePath, databaseConfigBytes, 0666)
err = os.WriteFile(databasePath, databaseConfigBytes, 0666)
if err != nil {
return fmt.Errorf("could not write database.yaml file: %w", err)
@@ -328,7 +330,7 @@ func writeGeneratedConfig(generated *generatedConfigFiles) error {
return err
}
err = ioutil.WriteFile(serverPath, serverConfigBytes, 0666)
err = os.WriteFile(serverPath, serverConfigBytes, 0666)
if err != nil {
return fmt.Errorf("could not write server.yaml file: %w", err)

View File

@@ -5,8 +5,8 @@ import (
"os"
"github.com/hatchet-dev/hatchet/api/v1/server/run"
"github.com/hatchet-dev/hatchet/cmd/cmdutils"
"github.com/hatchet-dev/hatchet/internal/config/loader"
"github.com/hatchet-dev/hatchet/pkg/cmdutils"
"github.com/spf13/cobra"
)
@@ -62,7 +62,7 @@ func startServerOrDie(cf *loader.ConfigLoader, interruptCh <-chan interface{}) {
panic(err)
}
ctx, cancel := cmdutils.InterruptContext(interruptCh)
ctx, cancel := cmdutils.InterruptContextFromChan(interruptCh)
defer cancel()
runner := run.NewAPIServer(sc)

View File

@@ -5,7 +5,6 @@ import (
"os"
"sync"
"github.com/hatchet-dev/hatchet/cmd/cmdutils"
"github.com/hatchet-dev/hatchet/internal/config/loader"
"github.com/hatchet-dev/hatchet/internal/services/admin"
"github.com/hatchet-dev/hatchet/internal/services/dispatcher"
@@ -14,6 +13,7 @@ import (
"github.com/hatchet-dev/hatchet/internal/services/ingestor"
"github.com/hatchet-dev/hatchet/internal/services/jobscontroller"
"github.com/hatchet-dev/hatchet/internal/services/ticker"
"github.com/hatchet-dev/hatchet/pkg/cmdutils"
"github.com/spf13/cobra"
)
@@ -69,7 +69,7 @@ func startEngineOrDie(cf *loader.ConfigLoader, interruptCh <-chan interface{}) {
}
errCh := make(chan error)
ctx, cancel := cmdutils.InterruptContext(interruptCh)
ctx, cancel := cmdutils.InterruptContextFromChan(interruptCh)
wg := sync.WaitGroup{}
if sc.HasService("grpc") {
@@ -121,6 +121,8 @@ func startEngineOrDie(cf *loader.ConfigLoader, interruptCh <-chan interface{}) {
grpc.WithAdmin(adminSvc),
grpc.WithLogger(sc.Logger),
grpc.WithTLSConfig(sc.TLSConfig),
grpc.WithPort(sc.Runtime.GRPCPort),
grpc.WithBindAddress(sc.Runtime.GRPCBindAddress),
)
if err != nil {

View File

@@ -4,14 +4,21 @@ import (
"context"
"fmt"
"github.com/hatchet-dev/hatchet/cmd/cmdutils"
"github.com/hatchet-dev/hatchet/pkg/client"
"github.com/hatchet-dev/hatchet/pkg/cmdutils"
"github.com/hatchet-dev/hatchet/pkg/worker"
"github.com/joho/godotenv"
)
type printInput struct{}
func main() {
err := godotenv.Load()
if err != nil {
panic(err)
}
client, err := client.New(
client.InitWorkflows(),
)
@@ -23,8 +30,8 @@ func main() {
// Create a worker. This automatically reads in a TemporalClient from .env and workflow files from the .hatchet
// directory, but this can be customized with the `worker.WithTemporalClient` and `worker.WithWorkflowFiles` options.
worker, err := worker.NewWorker(
worker.WithDispatcherClient(
client.Dispatcher(),
worker.WithClient(
client,
),
)
@@ -42,7 +49,7 @@ func main() {
panic(err)
}
interruptCtx, cancel := cmdutils.InterruptContext(cmdutils.InterruptChan())
interruptCtx, cancel := cmdutils.InterruptContextFromChan(cmdutils.InterruptChan())
defer cancel()
err = worker.Start(interruptCtx)

View File

@@ -0,0 +1,135 @@
package main
import (
"context"
"time"
"github.com/hatchet-dev/hatchet/pkg/client"
"github.com/hatchet-dev/hatchet/pkg/cmdutils"
"github.com/hatchet-dev/hatchet/pkg/worker"
"github.com/joho/godotenv"
)
type userCreateEvent struct {
Username string `json:"username"`
UserId string `json:"user_id"`
Data map[string]string `json:"data"`
}
type stepOneOutput struct {
Message string `json:"message"`
}
func StepOne(ctx context.Context, input *userCreateEvent) (result *stepOneOutput, err error) {
return &stepOneOutput{
Message: "Username is: " + input.Username,
}, nil
}
func StepTwo(ctx context.Context, input *stepOneOutput) (result *stepOneOutput, err error) {
return &stepOneOutput{
Message: "Above message is: " + input.Message,
}, nil
}
func main() {
err := godotenv.Load()
if err != nil {
panic(err)
}
client, err := client.New()
if err != nil {
panic(err)
}
// Create a worker. This automatically reads in a TemporalClient from .env and workflow files from the .hatchet
// directory, but this can be customized with the `worker.WithTemporalClient` and `worker.WithWorkflowFiles` options.
w, err := worker.NewWorker(
worker.WithClient(
client,
),
)
if err != nil {
panic(err)
}
err = w.On(worker.Event("user:create"), &worker.WorkflowJob{
Name: "test-job",
Description: "This is a test job.",
Steps: []worker.WorkflowStep{
{
Function: StepOne,
},
{
Function: StepTwo,
},
},
})
if err != nil {
panic(err)
}
// err = worker.RegisterAction("echo:echo", func(ctx context.Context, input *actionInput) (result any, err error) {
// return map[string]interface{}{
// "message": input.Message,
// }, nil
// })
// if err != nil {
// panic(err)
// }
// err = worker.RegisterAction("echo:object", func(ctx context.Context, input *actionInput) (result any, err error) {
// return nil, nil
// })
// if err != nil {
// panic(err)
// }
interruptCtx, cancel := cmdutils.InterruptContextFromChan(cmdutils.InterruptChan())
defer cancel()
go func() {
err = w.Start(interruptCtx)
if err != nil {
panic(err)
}
cancel()
}()
testEvent := userCreateEvent{
Username: "echo-test",
UserId: "1234",
Data: map[string]string{
"test": "test",
},
}
// push an event
err = client.Event().Push(
context.Background(),
"user:create",
testEvent,
)
if err != nil {
panic(err)
}
for {
select {
case <-interruptCtx.Done():
return
default:
time.Sleep(time.Second)
}
}
}

View File

@@ -4,9 +4,10 @@ import (
"context"
"time"
"github.com/hatchet-dev/hatchet/cmd/cmdutils"
"github.com/hatchet-dev/hatchet/pkg/client"
"github.com/hatchet-dev/hatchet/pkg/cmdutils"
"github.com/hatchet-dev/hatchet/pkg/worker"
"github.com/joho/godotenv"
)
type sampleEvent struct{}
@@ -14,6 +15,12 @@ type sampleEvent struct{}
type requeueInput struct{}
func main() {
err := godotenv.Load()
if err != nil {
panic(err)
}
client, err := client.New(
client.InitWorkflows(),
)
@@ -25,8 +32,8 @@ func main() {
// Create a worker. This automatically reads in a TemporalClient from .env and workflow files from the .hatchet
// directory, but this can be customized with the `worker.WithTemporalClient` and `worker.WithWorkflowFiles` options.
worker, err := worker.NewWorker(
worker.WithDispatcherClient(
client.Dispatcher(),
worker.WithClient(
client,
),
)
@@ -42,7 +49,7 @@ func main() {
panic(err)
}
interruptCtx, cancel := cmdutils.InterruptContext(cmdutils.InterruptChan())
interruptCtx, cancel := cmdutils.InterruptContextFromChan(cmdutils.InterruptChan())
defer cancel()
event := sampleEvent{}

View File

@@ -6,6 +6,7 @@ import (
"time"
"github.com/hatchet-dev/hatchet/pkg/client"
"github.com/joho/godotenv"
)
type sampleEvent struct{}
@@ -13,6 +14,12 @@ type sampleEvent struct{}
type timeoutInput struct{}
func main() {
err := godotenv.Load()
if err != nil {
panic(err)
}
client, err := client.New(
client.InitWorkflows(),
)

View File

@@ -21,3 +21,8 @@ jobs:
timeout: 60s
with:
message: "Above message is: {{ .steps.echo2.message }}"
- id: testObject
action: echo:object
timeout: 60s
with:
object: "{{ .steps.echo3.json }}"

View File

@@ -4,9 +4,10 @@ import (
"context"
"time"
"github.com/hatchet-dev/hatchet/cmd/cmdutils"
"github.com/hatchet-dev/hatchet/pkg/client"
"github.com/hatchet-dev/hatchet/pkg/cmdutils"
"github.com/hatchet-dev/hatchet/pkg/worker"
"github.com/joho/godotenv"
)
type userCreateEvent struct {
@@ -20,6 +21,12 @@ type actionInput struct {
}
func main() {
err := godotenv.Load()
if err != nil {
panic(err)
}
client, err := client.New(
client.InitWorkflows(),
)
@@ -31,8 +38,8 @@ func main() {
// Create a worker. This automatically reads in a TemporalClient from .env and workflow files from the .hatchet
// directory, but this can be customized with the `worker.WithTemporalClient` and `worker.WithWorkflowFiles` options.
worker, err := worker.NewWorker(
worker.WithDispatcherClient(
client.Dispatcher(),
worker.WithClient(
client,
),
)
@@ -50,7 +57,15 @@ func main() {
panic(err)
}
interruptCtx, cancel := cmdutils.InterruptContext(cmdutils.InterruptChan())
err = worker.RegisterAction("echo:object", func(ctx context.Context, input *actionInput) (result any, err error) {
return nil, nil
})
if err != nil {
panic(err)
}
interruptCtx, cancel := cmdutils.InterruptContextFromChan(cmdutils.InterruptChan())
defer cancel()
go func() {

View File

@@ -8,11 +8,12 @@ import (
"context"
"time"
"github.com/hatchet-dev/hatchet/cmd/cmdutils"
"github.com/hatchet-dev/hatchet/pkg/client"
"github.com/hatchet-dev/hatchet/pkg/client/types"
"github.com/hatchet-dev/hatchet/pkg/cmdutils"
"github.com/hatchet-dev/hatchet/pkg/integrations/slack"
"github.com/hatchet-dev/hatchet/pkg/worker"
"github.com/joho/godotenv"
)
type userCreateEvent struct {
@@ -27,6 +28,12 @@ type actionInput struct {
var SlackChannelWorkflow []byte
func init() {
err := godotenv.Load()
if err != nil {
panic(err)
}
// initialize the slack channel workflow with SLACK_USER_ID
slackUserId := os.Getenv("SLACK_USER_ID")
@@ -75,8 +82,8 @@ func main() {
// Create a worker. This automatically reads in a TemporalClient from .env and workflow files from the .hatchet
// directory, but this can be customized with the `worker.WithTemporalClient` and `worker.WithWorkflowFiles` options.
worker, err := worker.NewWorker(
worker.WithDispatcherClient(
client.Dispatcher(),
worker.WithClient(
client,
),
worker.WithIntegration(
slackInt,
@@ -87,7 +94,7 @@ func main() {
panic(err)
}
interruptCtx, cancel := cmdutils.InterruptContext(cmdutils.InterruptChan())
interruptCtx, cancel := cmdutils.InterruptContextFromChan(cmdutils.InterruptChan())
defer cancel()
go worker.Start(interruptCtx)

View File

@@ -5,9 +5,10 @@ import (
"fmt"
"time"
"github.com/hatchet-dev/hatchet/cmd/cmdutils"
"github.com/hatchet-dev/hatchet/pkg/client"
"github.com/hatchet-dev/hatchet/pkg/cmdutils"
"github.com/hatchet-dev/hatchet/pkg/worker"
"github.com/joho/godotenv"
)
type sampleEvent struct{}
@@ -15,6 +16,12 @@ type sampleEvent struct{}
type timeoutInput struct{}
func main() {
err := godotenv.Load()
if err != nil {
panic(err)
}
client, err := client.New(
client.InitWorkflows(),
)
@@ -26,8 +33,8 @@ func main() {
// Create a worker. This automatically reads in a TemporalClient from .env and workflow files from the .hatchet
// directory, but this can be customized with the `worker.WithTemporalClient` and `worker.WithWorkflowFiles` options.
worker, err := worker.NewWorker(
worker.WithDispatcherClient(
client.Dispatcher(),
worker.WithClient(
client,
),
)
@@ -48,7 +55,7 @@ func main() {
panic(err)
}
interruptCtx, cancel := cmdutils.InterruptContext(cmdutils.InterruptChan())
interruptCtx, cancel := cmdutils.InterruptContextFromChan(cmdutils.InterruptChan())
defer cancel()
go func() {

View File

@@ -87,7 +87,7 @@ export function useTenantContext(): [
} else if (computedCurrTenant?.metadata.id) {
const newSearchParams = new URLSearchParams(searchParams);
newSearchParams.set('tenant', computedCurrTenant?.metadata.id);
setSearchParams(newSearchParams);
setSearchParams(newSearchParams, { replace: true });
}
}
}, [
@@ -116,7 +116,7 @@ export function useTenantContext(): [
const setTenant = (tenant: Tenant) => {
const newSearchParams = new URLSearchParams(searchParams);
newSearchParams.set('tenant', tenant.metadata.id);
setSearchParams(newSearchParams);
setSearchParams(newSearchParams, { replace: true });
};
return [currTenant || computedCurrTenant, setTenant];

View File

@@ -70,7 +70,7 @@ function Main() {
<div className="flex flex-row flex-1 w-full h-full">
<MainNav user={user} />
<Sidebar memberships={memberships} currTenant={currTenant} />
<div className="pt-12 flex-grow">
<div className="pt-12 flex-grow overflow-y-auto overflow-x-hidden">
<Outlet context={childCtx} />
</div>
</div>

View File

@@ -29,4 +29,4 @@
"@types/node": "18.11.10",
"typescript": "^4.9.3"
}
}
}

View File

@@ -55,4 +55,4 @@ The client to use to communicate with the Hatchet instance. This is required.
### `worker.WithName`
The name of the worker. This is used to identify the worker in the Hatchet UI.
The name of the worker. This is used to identify the worker in the Hatchet UI.

View File

@@ -159,6 +159,10 @@ func (store *UserSessionStore) New(r *http.Request, name string) (*sessions.Sess
} else {
session.IsNew = false
}
} else if strings.Contains(err.Error(), "the value is not valid") {
// this error occurs if the encryption keys have been rotated, in which case we'd like a new cookie
err = nil
session.IsNew = true
}
}

View File

@@ -1,24 +1,18 @@
// Adapted from: https://github.com/hatchet-dev/hatchet/blob/3c2c13168afa1af68d4baaf5ed02c9d49c5f0323/internal/config/loader/loader.go
// Adapted from: https://github.com/hatchet-dev/hatchet-v1-archived/blob/3c2c13168afa1af68d4baaf5ed02c9d49c5f0323/internal/config/loader/loader.go
package loader
import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/creasty/defaults"
"github.com/hatchet-dev/hatchet/internal/auth/cookie"
"github.com/hatchet-dev/hatchet/internal/config/client"
"github.com/hatchet-dev/hatchet/internal/config/database"
"github.com/hatchet-dev/hatchet/internal/config/loader/loaderutils"
"github.com/hatchet-dev/hatchet/internal/config/server"
"github.com/hatchet-dev/hatchet/internal/config/shared"
"github.com/hatchet-dev/hatchet/internal/repository/prisma"
"github.com/hatchet-dev/hatchet/internal/repository/prisma/db"
"github.com/hatchet-dev/hatchet/internal/services/ingestor"
@@ -26,7 +20,6 @@ import (
"github.com/hatchet-dev/hatchet/internal/validator"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/rs/zerolog"
"github.com/spf13/viper"
)
// LoadDatabaseConfigFile loads the database config file via viper
@@ -34,7 +27,7 @@ func LoadDatabaseConfigFile(files ...[]byte) (*database.ConfigFile, error) {
configFile := &database.ConfigFile{}
f := database.BindAllEnv
_, err := LoadConfigFromViper(f, configFile, files...)
_, err := loaderutils.LoadConfigFromViper(f, configFile, files...)
return configFile, err
}
@@ -44,45 +37,11 @@ func LoadServerConfigFile(files ...[]byte) (*server.ServerConfigFile, error) {
configFile := &server.ServerConfigFile{}
f := server.BindAllEnv
_, err := LoadConfigFromViper(f, configFile, files...)
_, err := loaderutils.LoadConfigFromViper(f, configFile, files...)
return configFile, err
}
// LoadClientConfigFile loads the worker config file via viper
func LoadClientConfigFile(files ...[]byte) (*client.ClientConfigFile, error) {
configFile := &client.ClientConfigFile{}
f := client.BindAllEnv
_, err := LoadConfigFromViper(f, configFile, files...)
return configFile, err
}
func LoadConfigFromViper(bindFunc func(v *viper.Viper), configFile interface{}, files ...[]byte) (*viper.Viper, error) {
v := viper.New()
v.SetConfigType("yaml")
bindFunc(v)
for _, f := range files {
err := v.MergeConfig(bytes.NewBuffer(f))
if err != nil {
return nil, fmt.Errorf("could not load viper config: %w", err)
}
}
defaults.Set(configFile)
err := v.Unmarshal(configFile)
if err != nil {
return nil, fmt.Errorf("could not unmarshal viper config: %w", err)
}
return v, nil
}
type ConfigLoader struct {
directory string
}
@@ -94,7 +53,7 @@ func NewConfigLoader(directory string) *ConfigLoader {
// LoadDatabaseConfig loads the database configuration
func (c *ConfigLoader) LoadDatabaseConfig() (res *database.Config, err error) {
sharedFilePath := filepath.Join(c.directory, "database.yaml")
configFileBytes, err := getConfigBytes(sharedFilePath)
configFileBytes, err := loaderutils.GetConfigBytes(sharedFilePath)
if err != nil {
return nil, err
@@ -112,7 +71,7 @@ func (c *ConfigLoader) LoadDatabaseConfig() (res *database.Config, err error) {
// LoadServerConfig loads the server configuration
func (c *ConfigLoader) LoadServerConfig() (res *server.ServerConfig, err error) {
sharedFilePath := filepath.Join(c.directory, "server.yaml")
configFileBytes, err := getConfigBytes(sharedFilePath)
configFileBytes, err := loaderutils.GetConfigBytes(sharedFilePath)
if err != nil {
return nil, err
@@ -126,54 +85,13 @@ func (c *ConfigLoader) LoadServerConfig() (res *server.ServerConfig, err error)
cf, err := LoadServerConfigFile(configFileBytes...)
if err != nil {
return nil, err
}
return GetServerConfigFromConfigfile(dc, cf)
}
// LoadClientConfig loads the client configuration
func (c *ConfigLoader) LoadClientConfig() (res *client.ClientConfig, err error) {
sharedFilePath := filepath.Join(c.directory, "client.yaml")
configFileBytes, err := getConfigBytes(sharedFilePath)
if err != nil {
return nil, err
}
cf, err := LoadClientConfigFile(configFileBytes...)
if err != nil {
return nil, err
}
return GetClientConfigFromConfigFile(cf)
}
func getConfigBytes(configFilePath string) ([][]byte, error) {
configFileBytes := make([][]byte, 0)
if fileExists(configFilePath) {
fileBytes, err := ioutil.ReadFile(configFilePath) // #nosec G304 -- config files are meant to be read from user-supplied directory
if err != nil {
return nil, fmt.Errorf("could not read config file at path %s: %w", configFilePath, err)
}
configFileBytes = append(configFileBytes, fileBytes)
}
return configFileBytes, nil
}
func fileExists(filename string) bool {
info, err := os.Stat(filename)
if err != nil && os.IsNotExist(err) {
return false
} else if err != nil {
return false
}
return !info.IsDir()
}
func GetDatabaseConfigFromConfigFile(cf *database.ConfigFile) (res *database.Config, err error) {
databaseUrl := fmt.Sprintf(
"postgresql://%s:%s@%s:%d/%s?sslmode=%s",
@@ -185,7 +103,7 @@ func GetDatabaseConfigFromConfigFile(cf *database.ConfigFile) (res *database.Con
cf.PostgresSSLMode,
)
os.Setenv("DATABASE_URL", databaseUrl)
// os.Setenv("DATABASE_URL", databaseUrl)
client := db.NewClient(
// db.WithDatasourceURL(databaseUrl),
@@ -211,17 +129,12 @@ func GetDatabaseConfigFromConfigFile(cf *database.ConfigFile) (res *database.Con
func GetServerConfigFromConfigfile(dc *database.Config, cf *server.ServerConfigFile) (res *server.ServerConfig, err error) {
l := zerolog.New(os.Stderr)
tls, err := loadServerTLSConfig(&cf.TLS)
tls, err := loaderutils.LoadServerTLSConfig(&cf.TLS)
if err != nil {
return nil, fmt.Errorf("could not load TLS config: %w", err)
}
runtime := server.ServerRuntimeConfig{
ServerURL: cf.Runtime.ServerURL,
Port: cf.Runtime.Port,
}
ss, err := cookie.NewUserSessionStore(
cookie.WithSessionRepository(dc.Repository.UserSession()),
cookie.WithCookieAllowInsecure(cf.Auth.Cookie.Insecure),
@@ -246,7 +159,7 @@ func GetServerConfigFromConfigfile(dc *database.Config, cf *server.ServerConfigF
}
return &server.ServerConfig{
Runtime: runtime,
Runtime: cf.Runtime,
Auth: cf.Auth,
Config: dc,
TaskQueue: rabbitmq.New(context.Background(), rabbitmq.WithURL(cf.TaskQueue.RabbitMQ.URL)),
@@ -259,78 +172,6 @@ func GetServerConfigFromConfigfile(dc *database.Config, cf *server.ServerConfigF
}, nil
}
func GetClientConfigFromConfigFile(cf *client.ClientConfigFile) (res *client.ClientConfig, err error) {
tlsConf, err := loadClientTLSConfig(&cf.TLS)
if err != nil {
return nil, fmt.Errorf("could not load TLS config: %w", err)
}
return &client.ClientConfig{
TenantId: cf.TenantId,
TLSConfig: tlsConf,
}, nil
}
func loadClientTLSConfig(tlsConfig *client.ClientTLSConfigFile) (*tls.Config, error) {
res, ca, err := LoadBaseTLSConfig(&tlsConfig.Base)
if err != nil {
return nil, err
}
res.ServerName = tlsConfig.TLSServerName
res.RootCAs = ca
return res, nil
}
func loadServerTLSConfig(tlsConfig *shared.TLSConfigFile) (*tls.Config, error) {
res, ca, err := LoadBaseTLSConfig(tlsConfig)
if err != nil {
return nil, err
}
res.ClientAuth = tls.RequireAndVerifyClientCert
res.ClientCAs = ca
return res, nil
}
func LoadBaseTLSConfig(tlsConfig *shared.TLSConfigFile) (*tls.Config, *x509.CertPool, error) {
var x509Cert tls.Certificate
var err error
if tlsConfig.TLSCert != "" && tlsConfig.TLSKey != "" {
x509Cert, err = tls.X509KeyPair([]byte(tlsConfig.TLSCert), []byte(tlsConfig.TLSKey))
} else if tlsConfig.TLSCertFile != "" && tlsConfig.TLSKeyFile != "" {
x509Cert, err = tls.LoadX509KeyPair(tlsConfig.TLSCertFile, tlsConfig.TLSKeyFile)
} else {
return nil, nil, fmt.Errorf("no cert or key provided")
}
var caBytes []byte
if tlsConfig.TLSRootCA != "" {
caBytes = []byte(tlsConfig.TLSRootCA)
} else if tlsConfig.TLSRootCAFile != "" {
caBytes, err = os.ReadFile(tlsConfig.TLSRootCAFile)
} else {
return nil, nil, fmt.Errorf("no root CA provided")
}
ca := x509.NewCertPool()
if ok := ca.AppendCertsFromPEM(caBytes); !ok {
return nil, nil, fmt.Errorf("could not append root CA to cert pool: %w", err)
}
return &tls.Config{
Certificates: []tls.Certificate{x509Cert},
}, ca, nil
}
func getStrArr(v string) []string {
return strings.Split(v, " ")
}

View File

@@ -0,0 +1,33 @@
package loaderutils
import (
"fmt"
"os"
)
func GetConfigBytes(configFilePath string) ([][]byte, error) {
configFileBytes := make([][]byte, 0)
if fileExists(configFilePath) {
fileBytes, err := os.ReadFile(configFilePath) // #nosec G304 -- config files are meant to be read from user-supplied directory
if err != nil {
return nil, fmt.Errorf("could not read config file at path %s: %w", configFilePath, err)
}
configFileBytes = append(configFileBytes, fileBytes)
}
return configFileBytes, nil
}
func fileExists(filename string) bool {
info, err := os.Stat(filename)
if err != nil && os.IsNotExist(err) {
return false
} else if err != nil {
return false
}
return !info.IsDir()
}

View File

@@ -0,0 +1,70 @@
package loaderutils
import (
"crypto/tls"
"crypto/x509"
"fmt"
"os"
"github.com/hatchet-dev/hatchet/internal/config/client"
"github.com/hatchet-dev/hatchet/internal/config/shared"
)
func LoadClientTLSConfig(tlsConfig *client.ClientTLSConfigFile) (*tls.Config, error) {
res, ca, err := LoadBaseTLSConfig(&tlsConfig.Base)
if err != nil {
return nil, err
}
res.ServerName = tlsConfig.TLSServerName
res.RootCAs = ca
return res, nil
}
func LoadServerTLSConfig(tlsConfig *shared.TLSConfigFile) (*tls.Config, error) {
res, ca, err := LoadBaseTLSConfig(tlsConfig)
if err != nil {
return nil, err
}
res.ClientAuth = tls.RequireAndVerifyClientCert
res.ClientCAs = ca
return res, nil
}
func LoadBaseTLSConfig(tlsConfig *shared.TLSConfigFile) (*tls.Config, *x509.CertPool, error) {
var x509Cert tls.Certificate
var err error
if tlsConfig.TLSCert != "" && tlsConfig.TLSKey != "" {
x509Cert, err = tls.X509KeyPair([]byte(tlsConfig.TLSCert), []byte(tlsConfig.TLSKey))
} else if tlsConfig.TLSCertFile != "" && tlsConfig.TLSKeyFile != "" {
x509Cert, err = tls.LoadX509KeyPair(tlsConfig.TLSCertFile, tlsConfig.TLSKeyFile)
} else {
return nil, nil, fmt.Errorf("no cert or key provided")
}
var caBytes []byte
if tlsConfig.TLSRootCA != "" {
caBytes = []byte(tlsConfig.TLSRootCA)
} else if tlsConfig.TLSRootCAFile != "" {
caBytes, err = os.ReadFile(tlsConfig.TLSRootCAFile)
} else {
return nil, nil, fmt.Errorf("no root CA provided")
}
ca := x509.NewCertPool()
if ok := ca.AppendCertsFromPEM(caBytes); !ok {
return nil, nil, fmt.Errorf("could not append root CA to cert pool: %w", err)
}
return &tls.Config{
Certificates: []tls.Certificate{x509Cert},
}, ca, nil
}

View File

@@ -0,0 +1,33 @@
package loaderutils
import (
"bytes"
"fmt"
"github.com/creasty/defaults"
"github.com/spf13/viper"
)
func LoadConfigFromViper(bindFunc func(v *viper.Viper), configFile interface{}, files ...[]byte) (*viper.Viper, error) {
v := viper.New()
v.SetConfigType("yaml")
bindFunc(v)
for _, f := range files {
err := v.MergeConfig(bytes.NewBuffer(f))
if err != nil {
return nil, fmt.Errorf("could not load viper config: %w", err)
}
}
defaults.Set(configFile)
err := v.Unmarshal(configFile)
if err != nil {
return nil, fmt.Errorf("could not unmarshal viper config: %w", err)
}
return v, nil
}

View File

@@ -32,6 +32,12 @@ type ConfigFileRuntime struct {
// ServerURL is the full server URL of the instance, including protocol.
ServerURL string `mapstructure:"url" json:"url,omitempty" default:"http://localhost:8080"`
// GRPCPort is the port that the grpc service listens on
GRPCPort int `mapstructure:"grpcPort" json:"grpcPort,omitempty" default:"7070"`
// GRPCBindAddress is the address that the grpc server binds to. Should set to 0.0.0.0 if binding in docker container.
GRPCBindAddress string `mapstructure:"grpcBindAddress" json:"grpcBindAddress,omitempty" default:"127.0.0.1"`
}
type ConfigFileAuth struct {
@@ -66,17 +72,12 @@ type RabbitMQConfigFile struct {
URL string `mapstructure:"url" json:"url,omitempty" validate:"required" default:"amqp://user:password@localhost:5672/"`
}
type ServerRuntimeConfig struct {
ServerURL string
Port int
}
type ServerConfig struct {
*database.Config
Auth ConfigFileAuth
Runtime ServerRuntimeConfig
Runtime ConfigFileRuntime
Services []string
@@ -109,6 +110,8 @@ func BindAllEnv(v *viper.Viper) {
// runtime options
v.BindEnv("runtime.port", "SERVER_PORT")
v.BindEnv("runtime.url", "SERVER_URL")
v.BindEnv("runtime.grpcPort", "SERVER_GRPC_PORT")
v.BindEnv("runtime.grpcBindAddress", "SERVER_GRPC_BIND_ADDRESS")
v.BindEnv("services", "SERVER_SERVICES")
// auth options

View File

@@ -21,10 +21,12 @@ func NewJobRunLookupDataFromInputBytes(input []byte) (JobRunLookupData, error) {
return JobRunLookupData{}, fmt.Errorf("failed to convert input to map: %w", err)
}
return NewJobRunLookupData(inputMap), nil
return NewJobRunLookupData(inputMap, input), nil
}
func NewJobRunLookupData(input map[string]interface{}) JobRunLookupData {
func NewJobRunLookupData(input map[string]interface{}, rawInput []byte) JobRunLookupData {
input["json"] = string(rawInput)
return JobRunLookupData{
Input: input,
}
@@ -62,6 +64,9 @@ func AddStepOutput(data *types.JSON, stepReadableId string, stepOutput []byte) (
return nil, fmt.Errorf("failed to convert step output to map: %w", err)
}
// add a "json" accessor to the output
outputMap["json"] = unquoted
currData := JobRunLookupData{}
err = FromJSONType(data, &currData)

View File

@@ -34,6 +34,10 @@ func jsonBytesToMap(jsonBytes []byte) (map[string]interface{}, error) {
return nil, err
}
if dataMap == nil {
return map[string]interface{}{}, nil
}
return dataMap, nil
}

View File

@@ -2,42 +2,63 @@ package datautils
import (
"bytes"
"encoding/json"
"fmt"
"reflect"
"text/template"
)
// RenderTemplateFields recursively processes the input map, rendering any string fields using the data map.
func RenderTemplateFields(data map[string]interface{}, input map[string]interface{}) error {
func RenderTemplateFields(data map[string]interface{}, input map[string]interface{}) (map[string]interface{}, error) {
output := map[string]interface{}{}
for key, val := range input {
switch v := val.(type) {
case string:
tmpl, err := template.New(key).Parse(v)
if err != nil {
return fmt.Errorf("error creating template for key %s: %v", key, err)
return nil, fmt.Errorf("error creating template for key %s: %v", key, err)
}
var tpl bytes.Buffer
err = tmpl.Execute(&tpl, data)
if err != nil {
return fmt.Errorf("error executing template for key %s: %v", key, err)
return nil, fmt.Errorf("error executing template for key %s: %v", key, err)
}
res := tpl.String()
// if the string can be unmarshalled into a map[string]interface{}, do so
resMap := map[string]interface{}{}
if err := json.Unmarshal(tpl.Bytes(), &resMap); err == nil {
output[key] = resMap
// if the key is "object", the entire input is replaced with the rendered value
if key == "object" {
// note we do not recursively render the new input, as it may contain untrusted data.
return resMap, nil
}
} else {
output[key] = res
}
input[key] = tpl.String()
case map[string]interface{}:
// if we hit a nested map[string]interface{}, render those recursively
err := RenderTemplateFields(data, v)
recOut, err := RenderTemplateFields(data, v)
if err != nil {
return err
return nil, err
}
output[key] = recOut
default:
if reflect.TypeOf(v).Kind() == reflect.Map {
// If it's a map but not map[string]interface{}, return an error
return fmt.Errorf("encountered a map that is not map[string]interface{}: %s", key)
return nil, fmt.Errorf("encountered a map that is not map[string]interface{}: %s", key)
}
}
}
return nil
return output, nil
}

View File

@@ -0,0 +1,88 @@
package datautils
import (
"encoding/json"
"testing"
)
func TestRenderTemplateFields(t *testing.T) {
tests := []struct {
name string
data map[string]interface{}
input map[string]interface{}
expected map[string]interface{}
wantErr bool
}{
{
name: "simple string template",
data: map[string]interface{}{"testing": "datavalue"},
input: map[string]interface{}{
"render": "{{ .testing }}",
},
expected: map[string]interface{}{
"render": "datavalue",
},
wantErr: false,
},
{
name: "nested map template",
data: map[string]interface{}{"testing": "nestedvalue"},
input: map[string]interface{}{
"nested": map[string]interface{}{
"render": "{{ .testing }}",
},
},
expected: map[string]interface{}{
"nested": map[string]interface{}{
"render": "nestedvalue",
},
},
wantErr: false,
},
{
name: "object template",
data: map[string]interface{}{"testing": `{ "nested": "nestedvalue" }`},
input: map[string]interface{}{
"nested": map[string]interface{}{
"render": "{{ .testing }}",
},
},
expected: map[string]interface{}{
"nested": map[string]interface{}{
"render": map[string]interface{}{
"nested": "nestedvalue",
},
},
},
wantErr: false,
},
{
name: "replace object",
data: map[string]interface{}{"testing": `{ "nested": "nestedvalue" }`},
input: map[string]interface{}{
"object": "{{ .testing }}",
},
expected: map[string]interface{}{
"nested": "nestedvalue",
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
output, err := RenderTemplateFields(tt.data, tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("RenderTemplateFields() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr {
jsonExpected, _ := json.Marshal(tt.expected)
jsonResult, _ := json.Marshal(output)
if string(jsonExpected) != string(jsonResult) {
t.Errorf("Expected %v, got %v", string(jsonExpected), string(jsonResult))
}
}
})
}
}

View File

@@ -9,8 +9,8 @@ type CreateEventOpts struct {
// (required) the tenant id
TenantId string `validate:"required,uuid"`
// (required) the event key - must be in actionId form
Key string `validate:"required,actionId"`
// (required) the event key
Key string `validate:"required"`
// (optional) the event data
Data *db.JSON

View File

@@ -157,7 +157,43 @@ func (w *workerRepository) CreateNewWorker(tenantId string, opts *repository.Cre
txs := []transaction.Param{}
optionals := []db.WorkerSetParam{}
workerId := uuid.New().String()
createTx := w.client.Worker.CreateOne(
db.Worker.Tenant.Link(
db.Tenant.ID.Equals(tenantId),
),
db.Worker.Name.Set(opts.Name),
db.Worker.Dispatcher.Link(
db.Dispatcher.ID.Equals(opts.DispatcherId),
),
db.Worker.ID.Set(workerId),
).Tx()
txs = append(txs, createTx)
for _, svc := range opts.Services {
upsertServiceTx := w.client.Service.UpsertOne(
db.Service.TenantIDName(
db.Service.TenantID.Equals(tenantId),
db.Service.Name.Equals(svc),
),
).Create(
db.Service.Name.Set(svc),
db.Service.Tenant.Link(
db.Tenant.ID.Equals(tenantId),
),
db.Service.Workers.Link(
db.Worker.ID.Equals(workerId),
),
).Update(
db.Service.Workers.Link(
db.Worker.ID.Equals(workerId),
),
).Tx()
txs = append(txs, upsertServiceTx)
}
if len(opts.Actions) > 0 {
for _, action := range opts.Actions {
@@ -173,28 +209,22 @@ func (w *workerRepository) CreateNewWorker(tenantId string, opts *repository.Cre
),
).Update().Tx())
optionals = append(optionals, db.Worker.Actions.Link(
db.Action.TenantIDID(
db.Action.TenantID.Equals(tenantId),
db.Action.ID.Equals(action),
// This is unfortunate but due to https://github.com/steebchen/prisma-client-go/issues/1095,
// we cannot set db.Worker.Actions.Link multiple times, and since Link required a unique action
// where clause, we have to do these in separate transactions
txs = append(txs, w.client.Worker.FindUnique(
db.Worker.ID.Equals(workerId),
).Update(
db.Worker.Actions.Link(
db.Action.TenantIDID(
db.Action.TenantID.Equals(tenantId),
db.Action.ID.Equals(action),
),
),
))
).Tx())
}
}
createTx := w.client.Worker.CreateOne(
db.Worker.Tenant.Link(
db.Tenant.ID.Equals(tenantId),
),
db.Worker.Name.Set(opts.Name),
db.Worker.Dispatcher.Link(
db.Dispatcher.ID.Equals(opts.DispatcherId),
),
optionals...,
).Tx()
txs = append(txs, createTx)
err := w.client.Prisma.Transaction(txs...).Exec(context.Background())
if err != nil {
@@ -235,12 +265,19 @@ func (w *workerRepository) UpdateWorker(tenantId, workerId string, opts *reposit
),
).Update().Tx())
optionals = append(optionals, db.Worker.Actions.Link(
db.Action.TenantIDID(
db.Action.TenantID.Equals(tenantId),
db.Action.ID.Equals(action),
// This is unfortunate but due to https://github.com/steebchen/prisma-client-go/issues/1095,
// we cannot set db.Worker.Actions.Link multiple times, and since Link required a unique action
// where clause, we have to do these in separate transactions
txs = append(txs, w.client.Worker.FindUnique(
db.Worker.ID.Equals(workerId),
).Update(
db.Worker.Actions.Link(
db.Action.TenantIDID(
db.Action.TenantID.Equals(tenantId),
db.Action.ID.Equals(action),
),
),
))
).Tx())
}
}

View File

@@ -13,6 +13,9 @@ type CreateWorkerOpts struct {
// The name of the worker
Name string `validate:"required,hatchetName"`
// The name of the service
Services []string `validate:"dive,hatchetName"`
// A list of actions this worker can run
Actions []string `validate:"dive,actionId"`
}

View File

@@ -17,7 +17,7 @@ type CreateWorkflowVersionOpts struct {
Version string `validate:"required"`
// (optional) event triggers for the workflow
EventTriggers []string `validate:"dive,actionId"`
EventTriggers []string
// (optional) cron triggers for the workflow
CronTriggers []string `validate:"dive,cron"`

View File

@@ -1,6 +1,7 @@
package repository
import (
"encoding/json"
"fmt"
"time"
@@ -29,14 +30,7 @@ func GetCreateWorkflowRunOptsFromEvent(event *db.EventModel, workflowVersion *db
eventId := event.ID
data := event.InnerEvent.Data
dataInputMap := make(map[string]interface{})
err := datautils.FromJSONType(data, &dataInputMap)
if err != nil {
return nil, fmt.Errorf("could not marshal event data: %w", err)
}
structuredJobRunData := datautils.NewJobRunLookupData(dataInputMap)
structuredJobRunData, err := datautils.NewJobRunLookupDataFromInputBytes([]byte(json.RawMessage(*data)))
if err != nil {
return nil, fmt.Errorf("could not create job run lookup data: %w", err)

View File

@@ -130,6 +130,8 @@ type WorkerRegisterRequest struct {
WorkerName string `protobuf:"bytes,2,opt,name=workerName,proto3" json:"workerName,omitempty"`
// a list of actions that this worker can run
Actions []string `protobuf:"bytes,3,rep,name=actions,proto3" json:"actions,omitempty"`
// (optional) the services for this worker
Services []string `protobuf:"bytes,4,rep,name=services,proto3" json:"services,omitempty"`
}
func (x *WorkerRegisterRequest) Reset() {
@@ -185,6 +187,13 @@ func (x *WorkerRegisterRequest) GetActions() []string {
return nil
}
func (x *WorkerRegisterRequest) GetServices() []string {
if x != nil {
return x.Services
}
return nil
}
type WorkerRegisterResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -733,115 +742,117 @@ var file_dispatcher_proto_rawDesc = []byte{
0x0a, 0x10, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x22, 0x6d, 0x0a, 0x15, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x52, 0x65, 0x67,
0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08,
0x74, 0x65, 0x6e, 0x61, 0x6e, 0x74, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08,
0x74, 0x65, 0x6e, 0x61, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x77, 0x6f, 0x72, 0x6b,
0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x77, 0x6f,
0x72, 0x6b, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x63, 0x74, 0x69,
0x6f, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x61, 0x63, 0x74, 0x69, 0x6f,
0x6e, 0x73, 0x22, 0x70, 0x0a, 0x16, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x52, 0x65, 0x67, 0x69,
0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08,
0x6f, 0x74, 0x6f, 0x22, 0x89, 0x01, 0x0a, 0x15, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x52, 0x65,
0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a,
0x08, 0x74, 0x65, 0x6e, 0x61, 0x6e, 0x74, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
0x08, 0x74, 0x65, 0x6e, 0x61, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x77, 0x6f, 0x72,
0x6b, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x77,
0x6f, 0x72, 0x6b, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x63, 0x74,
0x69, 0x6f, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x61, 0x63, 0x74, 0x69,
0x6f, 0x6e, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x18,
0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x22,
0x70, 0x0a, 0x16, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65,
0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x65, 0x6e,
0x61, 0x6e, 0x74, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x65, 0x6e,
0x61, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x49,
0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x49,
0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x18,
0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x4e, 0x61, 0x6d,
0x65, 0x22, 0x9d, 0x02, 0x0a, 0x0e, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x41, 0x63,
0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x65, 0x6e, 0x61, 0x6e, 0x74, 0x49, 0x64,
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x65, 0x6e, 0x61, 0x6e, 0x74, 0x49, 0x64,
0x12, 0x14, 0x0a, 0x05, 0x6a, 0x6f, 0x62, 0x49, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
0x05, 0x6a, 0x6f, 0x62, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x6a, 0x6f, 0x62, 0x4e, 0x61, 0x6d,
0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6a, 0x6f, 0x62, 0x4e, 0x61, 0x6d, 0x65,
0x12, 0x1a, 0x0a, 0x08, 0x6a, 0x6f, 0x62, 0x52, 0x75, 0x6e, 0x49, 0x64, 0x18, 0x04, 0x20, 0x01,
0x28, 0x09, 0x52, 0x08, 0x6a, 0x6f, 0x62, 0x52, 0x75, 0x6e, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06,
0x73, 0x74, 0x65, 0x70, 0x49, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74,
0x65, 0x70, 0x49, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x74, 0x65, 0x70, 0x52, 0x75, 0x6e, 0x49,
0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x74, 0x65, 0x70, 0x52, 0x75, 0x6e,
0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x18, 0x07,
0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x2b,
0x0a, 0x0a, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x18, 0x08, 0x20, 0x01,
0x28, 0x0e, 0x32, 0x0b, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x52,
0x0a, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x61,
0x63, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x09, 0x20, 0x01,
0x28, 0x09, 0x52, 0x0d, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61,
0x64, 0x22, 0x4d, 0x0a, 0x13, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x4c, 0x69, 0x73, 0x74, 0x65,
0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x65, 0x6e, 0x61,
0x6e, 0x74, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x65, 0x6e, 0x61,
0x6e, 0x74, 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x49, 0x64,
0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x49, 0x64,
0x22, 0x52, 0x0a, 0x18, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x55, 0x6e, 0x73, 0x75, 0x62, 0x73,
0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08,
0x74, 0x65, 0x6e, 0x61, 0x6e, 0x74, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08,
0x74, 0x65, 0x6e, 0x61, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x77, 0x6f, 0x72, 0x6b,
0x65, 0x72, 0x49, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x77, 0x6f, 0x72, 0x6b,
0x65, 0x72, 0x49, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x4e, 0x61,
0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72,
0x4e, 0x61, 0x6d, 0x65, 0x22, 0x9d, 0x02, 0x0a, 0x0e, 0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65,
0x64, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x65, 0x6e, 0x61, 0x6e,
0x74, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x65, 0x6e, 0x61, 0x6e,
0x74, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6a, 0x6f, 0x62, 0x49, 0x64, 0x18, 0x02, 0x20, 0x01,
0x28, 0x09, 0x52, 0x05, 0x6a, 0x6f, 0x62, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x6a, 0x6f, 0x62,
0x4e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6a, 0x6f, 0x62, 0x4e,
0x61, 0x6d, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x6a, 0x6f, 0x62, 0x52, 0x75, 0x6e, 0x49, 0x64, 0x18,
0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6a, 0x6f, 0x62, 0x52, 0x75, 0x6e, 0x49, 0x64, 0x12,
0x16, 0x0a, 0x06, 0x73, 0x74, 0x65, 0x70, 0x49, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52,
0x06, 0x73, 0x74, 0x65, 0x70, 0x49, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x74, 0x65, 0x70, 0x52,
0x75, 0x6e, 0x49, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x74, 0x65, 0x70,
0x52, 0x75, 0x6e, 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49,
0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49,
0x64, 0x12, 0x2b, 0x0a, 0x0a, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x18,
0x08, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0b, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79,
0x70, 0x65, 0x52, 0x0a, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x24,
0x0a, 0x0d, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18,
0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x61, 0x79,
0x6c, 0x6f, 0x61, 0x64, 0x22, 0x4d, 0x0a, 0x13, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x4c, 0x69,
0x73, 0x74, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x74,
0x65, 0x6e, 0x61, 0x6e, 0x74, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74,
0x65, 0x6e, 0x61, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x77, 0x6f, 0x72, 0x6b, 0x65,
0x72, 0x49, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x77, 0x6f, 0x72, 0x6b, 0x65,
0x72, 0x49, 0x64, 0x22, 0x52, 0x0a, 0x18, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x55, 0x6e, 0x73,
0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
0x1a, 0x0a, 0x08, 0x74, 0x65, 0x6e, 0x61, 0x6e, 0x74, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
0x09, 0x52, 0x08, 0x74, 0x65, 0x6e, 0x61, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x77,
0x6f, 0x72, 0x6b, 0x65, 0x72, 0x49, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x77,
0x6f, 0x72, 0x6b, 0x65, 0x72, 0x49, 0x64, 0x22, 0x53, 0x0a, 0x19, 0x57, 0x6f, 0x72, 0x6b, 0x65,
0x72, 0x55, 0x6e, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x73, 0x70,
0x65, 0x72, 0x49, 0x64, 0x22, 0x53, 0x0a, 0x19, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x55, 0x6e,
0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x65, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x65, 0x6e, 0x61, 0x6e, 0x74, 0x49, 0x64, 0x18, 0x01, 0x20,
0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x65, 0x6e, 0x61, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x1a, 0x0a,
0x08, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x49, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
0x08, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x49, 0x64, 0x22, 0xe1, 0x02, 0x0a, 0x0b, 0x41, 0x63,
0x74, 0x69, 0x6f, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x65, 0x6e,
0x61, 0x6e, 0x74, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x65, 0x6e,
0x61, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x49,
0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x49,
0x64, 0x12, 0x14, 0x0a, 0x05, 0x6a, 0x6f, 0x62, 0x49, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09,
0x52, 0x05, 0x6a, 0x6f, 0x62, 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x6a, 0x6f, 0x62, 0x52, 0x75,
0x6e, 0x49, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6a, 0x6f, 0x62, 0x52, 0x75,
0x6e, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x65, 0x70, 0x49, 0x64, 0x18, 0x05, 0x20,
0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x65, 0x70, 0x49, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x73,
0x74, 0x65, 0x70, 0x52, 0x75, 0x6e, 0x49, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09,
0x73, 0x74, 0x65, 0x70, 0x52, 0x75, 0x6e, 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x63, 0x74,
0x69, 0x6f, 0x6e, 0x49, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x63, 0x74,
0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x42, 0x0a, 0x0e, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x69,
0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e,
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e,
0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0e, 0x65, 0x76, 0x65, 0x6e, 0x74,
0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x2e, 0x0a, 0x09, 0x65, 0x76, 0x65,
0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x10, 0x2e, 0x41,
0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x52, 0x09,
0x65, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x65, 0x76, 0x65,
0x6e, 0x74, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52,
0x0c, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x22, 0x4d, 0x0a,
0x13, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70,
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x65, 0x6e, 0x61, 0x6e, 0x74, 0x49, 0x64,
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x65, 0x6e, 0x61, 0x6e, 0x74, 0x49, 0x64,
0x12, 0x1a, 0x0a, 0x08, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x49, 0x64, 0x18, 0x02, 0x20, 0x01,
0x28, 0x09, 0x52, 0x08, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x49, 0x64, 0x22, 0xe1, 0x02, 0x0a,
0x0b, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x1a, 0x0a, 0x08,
0x74, 0x65, 0x6e, 0x61, 0x6e, 0x74, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08,
0x74, 0x65, 0x6e, 0x61, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x77, 0x6f, 0x72, 0x6b,
0x65, 0x72, 0x49, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x77, 0x6f, 0x72, 0x6b,
0x65, 0x72, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x6a, 0x6f, 0x62, 0x49, 0x64, 0x18, 0x03, 0x20,
0x01, 0x28, 0x09, 0x52, 0x05, 0x6a, 0x6f, 0x62, 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x6a, 0x6f,
0x62, 0x52, 0x75, 0x6e, 0x49, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6a, 0x6f,
0x62, 0x52, 0x75, 0x6e, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x65, 0x70, 0x49, 0x64,
0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x65, 0x70, 0x49, 0x64, 0x12, 0x1c,
0x0a, 0x09, 0x73, 0x74, 0x65, 0x70, 0x52, 0x75, 0x6e, 0x49, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28,
0x09, 0x52, 0x09, 0x73, 0x74, 0x65, 0x70, 0x52, 0x75, 0x6e, 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08,
0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08,
0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x42, 0x0a, 0x0e, 0x65, 0x76, 0x65, 0x6e,
0x74, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b,
0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0e, 0x65, 0x76,
0x65, 0x6e, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x2e, 0x0a, 0x09,
0x65, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0e, 0x32,
0x10, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70,
0x65, 0x52, 0x09, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x22, 0x0a, 0x0c,
0x65, 0x76, 0x65, 0x6e, 0x74, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x0a, 0x20, 0x01,
0x28, 0x09, 0x52, 0x0c, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64,
0x22, 0x4d, 0x0a, 0x13, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x65, 0x6e, 0x61, 0x6e,
0x74, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x65, 0x6e, 0x61, 0x6e,
0x74, 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x49, 0x64, 0x18,
0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x49, 0x64, 0x2a,
0x35, 0x0a, 0x0a, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a,
0x0e, 0x53, 0x54, 0x41, 0x52, 0x54, 0x5f, 0x53, 0x54, 0x45, 0x50, 0x5f, 0x52, 0x55, 0x4e, 0x10,
0x00, 0x12, 0x13, 0x0a, 0x0f, 0x43, 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x5f, 0x53, 0x54, 0x45, 0x50,
0x5f, 0x52, 0x55, 0x4e, 0x10, 0x01, 0x2a, 0x86, 0x01, 0x0a, 0x0f, 0x41, 0x63, 0x74, 0x69, 0x6f,
0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1b, 0x0a, 0x17, 0x53, 0x54,
0x45, 0x50, 0x5f, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e,
0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x1b, 0x0a, 0x17, 0x53, 0x54, 0x45, 0x50, 0x5f,
0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x52, 0x54,
0x45, 0x44, 0x10, 0x01, 0x12, 0x1d, 0x0a, 0x19, 0x53, 0x54, 0x45, 0x50, 0x5f, 0x45, 0x56, 0x45,
0x4e, 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x43, 0x4f, 0x4d, 0x50, 0x4c, 0x45, 0x54, 0x45,
0x44, 0x10, 0x02, 0x12, 0x1a, 0x0a, 0x16, 0x53, 0x54, 0x45, 0x50, 0x5f, 0x45, 0x56, 0x45, 0x4e,
0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x03, 0x32,
0x81, 0x02, 0x0a, 0x0a, 0x44, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x65, 0x72, 0x12, 0x3d,
0x0a, 0x08, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x12, 0x16, 0x2e, 0x57, 0x6f, 0x72,
0x6b, 0x65, 0x72, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x1a, 0x17, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x52, 0x65, 0x67, 0x69, 0x73,
0x74, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a,
0x06, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x12, 0x14, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72,
0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0f, 0x2e,
0x41, 0x73, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x64, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x00,
0x30, 0x01, 0x12, 0x37, 0x0a, 0x0f, 0x53, 0x65, 0x6e, 0x64, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e,
0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x0c, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x76,
0x65, 0x6e, 0x74, 0x1a, 0x14, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x76, 0x65, 0x6e,
0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x46, 0x0a, 0x0b, 0x55,
0x6e, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x12, 0x19, 0x2e, 0x57, 0x6f, 0x72,
0x6b, 0x65, 0x72, 0x55, 0x6e, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x55, 0x6e,
0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x65, 0x22, 0x00, 0x42, 0x47, 0x5a, 0x45, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f,
0x6d, 0x2f, 0x68, 0x61, 0x74, 0x63, 0x68, 0x65, 0x74, 0x2d, 0x64, 0x65, 0x76, 0x2f, 0x68, 0x61,
0x74, 0x63, 0x68, 0x65, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x73,
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68,
0x65, 0x72, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x61, 0x63, 0x74, 0x73, 0x62, 0x06, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x33,
0x28, 0x09, 0x52, 0x08, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x49, 0x64, 0x2a, 0x35, 0x0a, 0x0a,
0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54,
0x41, 0x52, 0x54, 0x5f, 0x53, 0x54, 0x45, 0x50, 0x5f, 0x52, 0x55, 0x4e, 0x10, 0x00, 0x12, 0x13,
0x0a, 0x0f, 0x43, 0x41, 0x4e, 0x43, 0x45, 0x4c, 0x5f, 0x53, 0x54, 0x45, 0x50, 0x5f, 0x52, 0x55,
0x4e, 0x10, 0x01, 0x2a, 0x86, 0x01, 0x0a, 0x0f, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x76,
0x65, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1b, 0x0a, 0x17, 0x53, 0x54, 0x45, 0x50, 0x5f,
0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f,
0x57, 0x4e, 0x10, 0x00, 0x12, 0x1b, 0x0a, 0x17, 0x53, 0x54, 0x45, 0x50, 0x5f, 0x45, 0x56, 0x45,
0x4e, 0x54, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x52, 0x54, 0x45, 0x44, 0x10,
0x01, 0x12, 0x1d, 0x0a, 0x19, 0x53, 0x54, 0x45, 0x50, 0x5f, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f,
0x54, 0x59, 0x50, 0x45, 0x5f, 0x43, 0x4f, 0x4d, 0x50, 0x4c, 0x45, 0x54, 0x45, 0x44, 0x10, 0x02,
0x12, 0x1a, 0x0a, 0x16, 0x53, 0x54, 0x45, 0x50, 0x5f, 0x45, 0x56, 0x45, 0x4e, 0x54, 0x5f, 0x54,
0x59, 0x50, 0x45, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x03, 0x32, 0x81, 0x02, 0x0a,
0x0a, 0x44, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x65, 0x72, 0x12, 0x3d, 0x0a, 0x08, 0x52,
0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x12, 0x16, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72,
0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
0x17, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72,
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x33, 0x0a, 0x06, 0x4c, 0x69,
0x73, 0x74, 0x65, 0x6e, 0x12, 0x14, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x4c, 0x69, 0x73,
0x74, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x0f, 0x2e, 0x41, 0x73, 0x73,
0x69, 0x67, 0x6e, 0x65, 0x64, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x00, 0x30, 0x01, 0x12,
0x37, 0x0a, 0x0f, 0x53, 0x65, 0x6e, 0x64, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x76, 0x65,
0x6e, 0x74, 0x12, 0x0c, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74,
0x1a, 0x14, 0x2e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x65,
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x46, 0x0a, 0x0b, 0x55, 0x6e, 0x73, 0x75,
0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x12, 0x19, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72,
0x55, 0x6e, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x55, 0x6e, 0x73, 0x75, 0x62,
0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00,
0x42, 0x47, 0x5a, 0x45, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68,
0x61, 0x74, 0x63, 0x68, 0x65, 0x74, 0x2d, 0x64, 0x65, 0x76, 0x2f, 0x68, 0x61, 0x74, 0x63, 0x68,
0x65, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x73, 0x65, 0x72, 0x76,
0x69, 0x63, 0x65, 0x73, 0x2f, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x65, 0x72, 0x2f,
0x63, 0x6f, 0x6e, 0x74, 0x72, 0x61, 0x63, 0x74, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x33,
}
var (

View File

@@ -85,24 +85,18 @@ func (s *DispatcherImpl) Register(ctx context.Context, request *contracts.Worker
s.l.Debug().Msgf("Received register request from ID %s with actions %v", request.WorkerName, request.Actions)
// // get a list of step ids based on a list of action ids for the tenant
// steps, err := s.repo.Step().ListStepsByActions(request.TenantId, request.Actions)
svcs := request.Services
// if err != nil {
// return nil, err
// }
// stepIds := make([]string, len(steps))
// for i, step := range steps {
// stepIds[i] = step.ID
// }
if svcs == nil || len(svcs) == 0 {
svcs = []string{"default"}
}
// create a worker in the database
worker, err := s.repo.Worker().CreateNewWorker(request.TenantId, &repository.CreateWorkerOpts{
DispatcherId: s.dispatcherId,
Name: request.WorkerName,
Actions: request.Actions,
Services: svcs,
})
if err != nil {

View File

@@ -23,8 +23,9 @@ type Server struct {
dispatchercontracts.UnimplementedDispatcherServer
admincontracts.UnimplementedWorkflowServiceServer
l *zerolog.Logger
port int
l *zerolog.Logger
port int
bindAddress string
ingestor ingestor.Ingestor
dispatcher dispatcher.Dispatcher
@@ -35,20 +36,22 @@ type Server struct {
type ServerOpt func(*ServerOpts)
type ServerOpts struct {
l *zerolog.Logger
port int
ingestor ingestor.Ingestor
dispatcher dispatcher.Dispatcher
admin admin.AdminService
tls *tls.Config
l *zerolog.Logger
port int
bindAddress string
ingestor ingestor.Ingestor
dispatcher dispatcher.Dispatcher
admin admin.AdminService
tls *tls.Config
}
func defaultServerOpts() *ServerOpts {
logger := zerolog.New(os.Stderr)
return &ServerOpts{
l: &logger,
port: 7070,
l: &logger,
port: 7070,
bindAddress: "127.0.0.1",
}
}
@@ -58,6 +61,12 @@ func WithLogger(l *zerolog.Logger) ServerOpt {
}
}
func WithBindAddress(bindAddress string) ServerOpt {
return func(opts *ServerOpts) {
opts.bindAddress = bindAddress
}
}
func WithPort(port int) ServerOpt {
return func(opts *ServerOpts) {
opts.port = port
@@ -100,12 +109,13 @@ func NewServer(fs ...ServerOpt) (*Server, error) {
}
return &Server{
l: opts.l,
port: opts.port,
ingestor: opts.ingestor,
dispatcher: opts.dispatcher,
admin: opts.admin,
tls: opts.tls,
l: opts.l,
port: opts.port,
bindAddress: opts.bindAddress,
ingestor: opts.ingestor,
dispatcher: opts.dispatcher,
admin: opts.admin,
tls: opts.tls,
}, nil
}
@@ -114,9 +124,9 @@ func (s *Server) Start(ctx context.Context) error {
}
func (s *Server) startGRPC(ctx context.Context) error {
s.l.Debug().Msgf("starting grpc server on port %d", s.port)
s.l.Debug().Msgf("starting grpc server on %s:%d", s.bindAddress, s.port)
lis, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", s.port))
lis, err := net.Listen("tcp", fmt.Sprintf("%s:%d", s.bindAddress, s.port))
if err != nil {
return fmt.Errorf("failed to listen: %w", err)

View File

@@ -479,7 +479,7 @@ func (ec *JobsControllerImpl) queueStepRun(ctx context.Context, tenantId, stepId
return fmt.Errorf("could not get step inputs: %w", err)
}
err = datautils.RenderTemplateFields(lookupDataMap, inputDataMap)
inputDataMap, err = datautils.RenderTemplateFields(lookupDataMap, inputDataMap)
if err != nil {
return fmt.Errorf("could not render template fields: %w", err)

View File

@@ -5,8 +5,8 @@ import (
"fmt"
"os"
"github.com/hatchet-dev/hatchet/internal/config/loader"
"github.com/hatchet-dev/hatchet/internal/validator"
"github.com/hatchet-dev/hatchet/pkg/client/loader"
"github.com/hatchet-dev/hatchet/pkg/client/types"
"github.com/rs/zerolog"
"google.golang.org/grpc"

View File

@@ -32,6 +32,7 @@ const (
// TODO: add validator to client side
type GetActionListenerRequest struct {
WorkerName string
Services []string
Actions []string
}
@@ -157,6 +158,7 @@ func (d *dispatcherClientImpl) newActionListener(ctx context.Context, req *GetAc
TenantId: d.tenantId,
WorkerName: req.WorkerName,
Actions: req.Actions,
Services: req.Services,
})
if err != nil {

View File

@@ -0,0 +1,54 @@
package loader
import (
"fmt"
"path/filepath"
"github.com/hatchet-dev/hatchet/internal/config/client"
"github.com/hatchet-dev/hatchet/internal/config/loader/loaderutils"
)
type ConfigLoader struct {
directory string
}
// LoadClientConfig loads the client configuration
func (c *ConfigLoader) LoadClientConfig() (res *client.ClientConfig, err error) {
sharedFilePath := filepath.Join(c.directory, "client.yaml")
configFileBytes, err := loaderutils.GetConfigBytes(sharedFilePath)
if err != nil {
return nil, err
}
cf, err := LoadClientConfigFile(configFileBytes...)
if err != nil {
return nil, err
}
return GetClientConfigFromConfigFile(cf)
}
// LoadClientConfigFile loads the worker config file via viper
func LoadClientConfigFile(files ...[]byte) (*client.ClientConfigFile, error) {
configFile := &client.ClientConfigFile{}
f := client.BindAllEnv
_, err := loaderutils.LoadConfigFromViper(f, configFile, files...)
return configFile, err
}
func GetClientConfigFromConfigFile(cf *client.ClientConfigFile) (res *client.ClientConfig, err error) {
tlsConf, err := loaderutils.LoadClientTLSConfig(&cf.TLS)
if err != nil {
return nil, fmt.Errorf("could not load TLS config: %w", err)
}
return &client.ClientConfig{
TenantId: cf.TenantId,
TLSConfig: tlsConf,
}, nil
}

View File

@@ -35,6 +35,10 @@ func ParseActionID(actionID string) (Action, error) {
parts := strings.Split(actionID, ":")
numParts := len(parts)
if numParts < 2 || numParts > 3 {
return Action{}, fmt.Errorf("invalid action id %s, must have at least 2 strings separated : (colon)", actionID)
}
integrationId := firstToLower(parts[0])
verb := strings.ToLower(parts[1])

View File

@@ -1,4 +1,4 @@
// Adapted from: https://github.com/hatchet-dev/hatchet/blob/3c2c13168afa1af68d4baaf5ed02c9d49c5f0323/cmd/cmdutils/interrupt.go
// Adapted from: https://github.com/hatchet-dev/hatchet-v1-archived/blob/3c2c13168afa1af68d4baaf5ed02c9d49c5f0323/cmd/cmdutils/interrupt.go
package cmdutils
import (
@@ -22,7 +22,13 @@ func InterruptChan() <-chan interface{} {
return ret
}
func InterruptContext(interruptChan <-chan interface{}) (context.Context, context.CancelFunc) {
func NewInterruptContext() (context.Context, context.CancelFunc) {
interruptChan := InterruptChan()
return InterruptContextFromChan(interruptChan)
}
func InterruptContextFromChan(interruptChan <-chan interface{}) (context.Context, context.CancelFunc) {
ctx, cancel := context.WithCancel(context.Background())
go func() {

View File

@@ -32,3 +32,69 @@ func decodeArgsToInterface(fnType reflect.Type) (result interface{}, err error)
return reflect.New(secondArgElem).Interface(), nil
}
func decodeFnArgTypes(fnType reflect.Type) (result []reflect.Type, err error) {
if fnType.Kind() != reflect.Func {
return nil, fmt.Errorf("method must be a function")
}
// if not a function with two arguments, return error
if fnType.NumIn() != 2 {
return nil, fmt.Errorf("method must have exactly two arguments")
}
// if first argument is not a context, return error
firstArg := fnType.In(0)
if firstArg.Kind() != reflect.Interface || !firstArg.Implements(reflect.TypeOf((*context.Context)(nil)).Elem()) {
return nil, fmt.Errorf("first argument must be context.Context")
}
// if second argument is not a pointer to a struct, return error
secondArg := fnType.In(1)
if secondArg.Kind() != reflect.Ptr {
return nil, fmt.Errorf("second argument must be a pointer to a struct")
}
secondArgElem := secondArg.Elem()
if secondArgElem.Kind() != reflect.Struct {
return nil, fmt.Errorf("second argument must be a pointer to a struct")
}
return []reflect.Type{firstArg, secondArg}, nil
}
func decodeFnReturnTypes(fnType reflect.Type) (result []reflect.Type, err error) {
if fnType.NumOut() > 2 {
return nil, fmt.Errorf("fn cannot have more than 2 return values")
}
firstOut := fnType.Out(0)
// if there are two args, the first one should be a pointer to a struct
if fnType.NumOut() == 2 {
if firstOut.Kind() != reflect.Ptr {
return nil, fmt.Errorf("first argument must be a pointer to a struct when there are two return values")
}
firstOutElem := firstOut.Elem()
if firstOutElem.Kind() != reflect.Struct {
return nil, fmt.Errorf("first argument must be a pointer to a struct when there are two return values")
}
}
lastOut := fnType.Out(fnType.NumOut() - 1)
if lastOut.Kind() != reflect.Interface || !lastOut.Implements(reflect.TypeOf((*error)(nil)).Elem()) {
return nil, fmt.Errorf("last return value must be error")
}
if fnType.NumOut() == 1 {
return []reflect.Type{firstOut}, nil
}
return []reflect.Type{firstOut, lastOut}, nil
}

View File

@@ -40,28 +40,26 @@ func getFnFromMethod(method any) (result actionFunc, err error) {
}
// if function does not return two values, return error
if methodType.NumOut() != 2 {
return nil, fmt.Errorf("method must return exactly two values")
if methodType.NumOut() == 2 {
// if first return value is not a pointer to a struct, return error
firstReturn := methodType.Out(0)
if firstReturn.Kind() != reflect.Ptr {
return nil, fmt.Errorf("first return value must be a pointer to a struct")
}
firstReturnElem := firstReturn.Elem()
if firstReturnElem.Kind() != reflect.Struct {
return nil, fmt.Errorf("first return value must be a pointer to a struct")
}
}
// if first return value is not a pointer to a struct, return error
// firstReturn := methodType.Out(0)
// if last return value is not an error, return error
lastReturn := methodType.Out(methodType.NumOut() - 1)
// if firstReturn.Kind() != reflect.Ptr {
// return nil, fmt.Errorf("first return value must be a pointer to a struct")
// }
// firstReturnElem := firstReturn.Elem()
// if firstReturnElem.Kind() != reflect.Struct {
// return nil, fmt.Errorf("first return value must be a pointer to a struct")
// }
// if second return value is not an error, return error
secondReturn := methodType.Out(1)
if secondReturn.Kind() != reflect.Interface || !secondReturn.Implements(reflect.TypeOf((*error)(nil)).Elem()) {
return nil, fmt.Errorf("second return value must be an error")
if lastReturn.Kind() != reflect.Interface || !lastReturn.Implements(reflect.TypeOf((*error)(nil)).Elem()) {
return nil, fmt.Errorf("second return value must be of type error")
}
return func(args ...interface{}) []interface{} {
@@ -77,6 +75,12 @@ func getFnFromMethod(method any) (result actionFunc, err error) {
})
// Return the results as an interface slice
return []interface{}{values[0].Interface(), values[1].Interface()}
res := []interface{}{}
for i := range values {
res = append(res, values[i].Interface())
}
return res
}, nil
}

60
pkg/worker/service.go Normal file
View File

@@ -0,0 +1,60 @@
package worker
import (
"fmt"
"reflect"
"github.com/hatchet-dev/hatchet/pkg/client/types"
)
type Service struct {
Name string
worker *Worker
}
func (s *Service) On(t triggerConverter, workflow workflowConverter) error {
apiWorkflow := workflow.ToWorkflow(s.Name)
wt := &types.WorkflowTriggers{}
t.ToWorkflowTriggers(wt)
apiWorkflow.Triggers = *wt
// create the workflow via the API
err := s.worker.client.Admin().PutWorkflow(&apiWorkflow)
if err != nil {
return err
}
// register all steps as actions
for actionId, fn := range workflow.ToActionMap(s.Name) {
err := s.worker.registerAction(actionId, fn)
if err != nil {
return err
}
}
return nil
}
func (s *Service) RegisterAction(fn any) error {
fnType := reflect.TypeOf(fn)
if fnType.Kind() != reflect.Func {
return fmt.Errorf("method must be a function")
}
if fnType.Name() == "" {
return fmt.Errorf("function cannot be anonymous")
}
fnId := fnType.Name()
actionId := fmt.Sprintf("%s:%s", s.Name, fnId)
return s.worker.registerAction(actionId, fn)
}

View File

@@ -12,7 +12,6 @@ import (
"github.com/hatchet-dev/hatchet/pkg/client"
"github.com/hatchet-dev/hatchet/pkg/integrations"
"github.com/rs/zerolog"
"google.golang.org/grpc"
)
type actionFunc func(args ...any) []any
@@ -47,11 +46,7 @@ func (j *actionImpl) MethodFn() any {
}
type Worker struct {
conn *grpc.ClientConn
client client.DispatcherClient
// The worker id that gets assigned on register
workerId string
client client.Client
name string
@@ -60,12 +55,14 @@ type Worker struct {
l *zerolog.Logger
cancelMap sync.Map
services sync.Map
}
type WorkerOpt func(*WorkerOpts)
type WorkerOpts struct {
client client.DispatcherClient
client client.Client
name string
l *zerolog.Logger
@@ -88,7 +85,7 @@ func WithName(name string) WorkerOpt {
}
}
func WithDispatcherClient(client client.DispatcherClient) WorkerOpt {
func WithClient(client client.Client) WorkerOpt {
return func(opts *WorkerOpts) {
opts.client = client
}
@@ -123,7 +120,7 @@ func NewWorker(fs ...WorkerOpt) (*Worker, error) {
for _, integrationAction := range actions {
action := fmt.Sprintf("%s:%s", integrationId, integrationAction)
err := w.RegisterAction(action, integration.ActionHandler(integrationAction))
err := w.registerAction(action, integration.ActionHandler(integrationAction))
if err != nil {
return nil, fmt.Errorf("could not register integration action %s: %w", action, err)
@@ -131,10 +128,45 @@ func NewWorker(fs ...WorkerOpt) (*Worker, error) {
}
}
w.NewService("default")
return w, nil
}
func (w *Worker) NewService(name string) *Service {
svc := &Service{
Name: name,
worker: w,
}
w.services.Store(name, svc)
return svc
}
func (w *Worker) On(t triggerConverter, workflow workflowConverter) error {
// get the default service
svc, ok := w.services.Load("default")
if !ok {
return fmt.Errorf("could not load default service")
}
return svc.(*Service).On(t, workflow)
}
func (w *Worker) RegisterAction(name string, method any) error {
// get the default service
svc, ok := w.services.Load("default")
if !ok {
return fmt.Errorf("could not load default service")
}
return svc.(*Service).RegisterAction(method)
}
func (w *Worker) registerAction(name string, method any) error {
actionFunc, err := getFnFromMethod(method)
if err != nil {
@@ -163,7 +195,7 @@ func (w *Worker) Start(ctx context.Context) error {
actionNames = append(actionNames, job.Name())
}
listener, err := w.client.GetActionListener(ctx, &client.GetActionListenerRequest{
listener, err := w.client.Dispatcher().GetActionListener(ctx, &client.GetActionListenerRequest{
WorkerName: w.name,
Actions: actionNames,
})
@@ -195,13 +227,10 @@ RunWorker:
}
w.l.Debug().Msgf("action %s completed with result %v", action.ActionId, res)
return
}(action)
case <-ctx.Done():
w.l.Debug().Msgf("worker %s received context done, stopping", w.name)
break RunWorker
default:
}
}
@@ -228,7 +257,7 @@ func (w *Worker) executeAction(ctx context.Context, assignedAction *client.Actio
func (w *Worker) startStepRun(ctx context.Context, assignedAction *client.Action) (result any, err error) {
// send a message that the step run started
_, err = w.client.SendActionEvent(
_, err = w.client.Dispatcher().SendActionEvent(
ctx,
w.getActionEvent(assignedAction, client.ActionEventTypeStarted),
)
@@ -269,9 +298,11 @@ func (w *Worker) startStepRun(ctx context.Context, assignedAction *client.Action
default:
}
result = runResults[0]
if len(runResults) == 2 {
result = runResults[0]
}
if runResults[1] != nil {
if runResults[len(runResults)-1] != nil {
err = runResults[1].(error)
}
@@ -280,7 +311,7 @@ func (w *Worker) startStepRun(ctx context.Context, assignedAction *client.Action
failureEvent.EventPayload = err.Error()
_, err := w.client.SendActionEvent(
_, err := w.client.Dispatcher().SendActionEvent(
ctx,
failureEvent,
)
@@ -292,13 +323,6 @@ func (w *Worker) startStepRun(ctx context.Context, assignedAction *client.Action
return nil, err
}
// TODO: check last argument for error
// if err != nil {
// // TODO: send a message that the step run failed
// return nil, fmt.Errorf("could not run job: %w", err)
// }
// send a message that the step run completed
finishedEvent, err := w.getActionFinishedEvent(assignedAction, result)
@@ -306,7 +330,7 @@ func (w *Worker) startStepRun(ctx context.Context, assignedAction *client.Action
return nil, fmt.Errorf("could not create finished event: %w", err)
}
_, err = w.client.SendActionEvent(
_, err = w.client.Dispatcher().SendActionEvent(
ctx,
finishedEvent,
)

201
pkg/worker/workflow.go Normal file
View File

@@ -0,0 +1,201 @@
package worker
import (
"fmt"
"reflect"
"runtime"
"strings"
"github.com/hatchet-dev/hatchet/pkg/client/types"
)
type triggerConverter interface {
ToWorkflowTriggers(*types.WorkflowTriggers)
}
type Cron string
func (c Cron) ToWorkflowTriggers(wt *types.WorkflowTriggers) {
if wt.Cron == nil {
wt.Cron = []string{}
}
wt.Cron = append(wt.Cron, string(c))
}
type Event string
func (e Event) ToWorkflowTriggers(wt *types.WorkflowTriggers) {
if wt.Events == nil {
wt.Events = []string{}
}
wt.Events = append(wt.Events, string(e))
}
type workflowConverter interface {
ToWorkflow(svcName string) types.Workflow
ToActionMap(svcName string) map[string]any
}
type Workflow struct {
Jobs []WorkflowJob
}
type WorkflowJob struct {
// The name of the job
Name string
Description string
Timeout string
// The steps that are run in the job
Steps []WorkflowStep
}
func (j *WorkflowJob) ToWorkflow(svcName string) types.Workflow {
apiJob, err := j.ToWorkflowJob(svcName)
if err != nil {
panic(err)
}
jobs := map[string]types.WorkflowJob{
j.Name: *apiJob,
}
return types.Workflow{
Name: j.Name,
Version: "v0.1.0",
Jobs: jobs,
}
}
func (j *WorkflowJob) ToWorkflowJob(svcName string) (*types.WorkflowJob, error) {
apiJob := &types.WorkflowJob{
Description: j.Description,
Timeout: j.Timeout,
Steps: []types.WorkflowStep{},
}
var prevStep *step
for i, step := range j.Steps {
newStep, err := step.ToWorkflowStep(prevStep, svcName, i)
if err != nil {
return nil, err
}
apiJob.Steps = append(apiJob.Steps, newStep.APIStep)
prevStep = newStep
}
return apiJob, nil
}
func (j *WorkflowJob) ToActionMap(svcName string) map[string]any {
res := map[string]any{}
for i, step := range j.Steps {
actionId := step.GetActionId(svcName, i)
res[actionId] = step.Function
}
return res
}
type WorkflowStep struct {
// The step timeout
Timeout string
// The executed function
Function any
}
type step struct {
Id string
// non-ctx input is not optional
NonCtxInput reflect.Type
// non-err output is optional
NonErrOutput *reflect.Type
APIStep types.WorkflowStep
}
func (s *WorkflowStep) ToWorkflowStep(prevStep *step, svcName string, index int) (*step, error) {
fnType := reflect.TypeOf(s.Function)
res := &step{}
res.Id = s.GetStepId(index)
res.APIStep = types.WorkflowStep{
Name: res.Id,
ID: s.GetStepId(index),
Timeout: s.Timeout,
ActionID: s.GetActionId(svcName, index),
}
inputs, err := decodeFnArgTypes(fnType)
if err != nil {
return nil, err
}
res.NonCtxInput = inputs[1]
outputs, err := decodeFnReturnTypes(fnType)
if err != nil {
return nil, err
}
if len(outputs) > 1 {
res.NonErrOutput = &outputs[0]
}
// if the previous step's first output matches the last input of this step, then the data
// is passed through
if prevStep != nil && prevStep.NonErrOutput != nil {
if inputs[1] == *prevStep.NonErrOutput {
res.APIStep.With = map[string]interface{}{
"object": "{{ .steps." + prevStep.Id + ".json }}",
}
}
} else {
res.APIStep.With = map[string]interface{}{
"object": "{{ .input.json }}",
}
}
return res, nil
}
func (s *WorkflowStep) GetStepId(index int) string {
stepId := s.getFnName()
// this can happen if the function is anonymous
if stepId == "" {
stepId = fmt.Sprintf("step%d", index)
}
return stepId
}
func (s *WorkflowStep) GetActionId(svcName string, index int) string {
stepId := s.GetStepId(index)
return fmt.Sprintf("%s:%s", svcName, stepId)
}
func (s *WorkflowStep) getFnName() string {
fnName := runtime.FuncForPC(reflect.ValueOf(s.Function).Pointer()).Name()
return strings.Split(fnName, ".")[1]
}

View File

@@ -0,0 +1,48 @@
package worker
// import (
// "context"
// "testing"
// )
// // type actionInput struct {
// // Message string `json:"message"`
// // }
// // type stepOneOutput struct {
// // Message string `json:"message"`
// // }
// // type stepTwoOutput struct {
// // Message string `json:"message"`
// // }
// // func TestToWorkflowJob(t *testing.T) {
// // testJob := WorkflowJob{
// // Name: "test",
// // Description: "test",
// // Timeout: "1m",
// // Steps: []WorkflowStep{
// // {
// // ActionId: "test:test",
// // Function: func(ctx context.Context, input *actionInput) (result *stepOneOutput, err error) {
// // return nil, nil
// // },
// // },
// // {
// // ActionId: "test:test",
// // Function: func(ctx context.Context, input *stepOneOutput) (result *stepTwoOutput, err error) {
// // return nil, nil
// // },
// // },
// // },
// // }
// // job, err := testJob.ToWorkflowJob()
// // if err != nil {
// // t.Fatalf("could not convert workflow job: %v", err)
// // }
// // t.Fatalf("%v", job)
// // }

View File

@@ -318,6 +318,19 @@ CREATE TABLE "Worker" (
CONSTRAINT "Worker_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Service" (
"id" UUID NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deletedAt" TIMESTAMP(3),
"name" TEXT NOT NULL,
"description" TEXT,
"tenantId" UUID NOT NULL,
CONSTRAINT "Service_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "_WorkflowToWorkflowTag" (
"A" UUID NOT NULL,
@@ -330,6 +343,12 @@ CREATE TABLE "_ActionToWorker" (
"B" UUID NOT NULL
);
-- CreateTable
CREATE TABLE "_ServiceToWorker" (
"A" UUID NOT NULL,
"B" UUID NOT NULL
);
-- CreateIndex
CREATE UNIQUE INDEX "User_id_key" ON "User"("id");
@@ -444,6 +463,12 @@ CREATE UNIQUE INDEX "Ticker_id_key" ON "Ticker"("id");
-- CreateIndex
CREATE UNIQUE INDEX "Worker_id_key" ON "Worker"("id");
-- CreateIndex
CREATE UNIQUE INDEX "Service_id_key" ON "Service"("id");
-- CreateIndex
CREATE UNIQUE INDEX "Service_tenantId_name_key" ON "Service"("tenantId", "name");
-- CreateIndex
CREATE UNIQUE INDEX "_WorkflowToWorkflowTag_AB_unique" ON "_WorkflowToWorkflowTag"("A", "B");
@@ -456,6 +481,12 @@ CREATE UNIQUE INDEX "_ActionToWorker_AB_unique" ON "_ActionToWorker"("A", "B");
-- CreateIndex
CREATE INDEX "_ActionToWorker_B_index" ON "_ActionToWorker"("B");
-- CreateIndex
CREATE UNIQUE INDEX "_ServiceToWorker_AB_unique" ON "_ServiceToWorker"("A", "B");
-- CreateIndex
CREATE INDEX "_ServiceToWorker_B_index" ON "_ServiceToWorker"("B");
-- AddForeignKey
ALTER TABLE "UserPassword" ADD CONSTRAINT "UserPassword_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -579,6 +610,9 @@ ALTER TABLE "Worker" ADD CONSTRAINT "Worker_tenantId_fkey" FOREIGN KEY ("tenantI
-- AddForeignKey
ALTER TABLE "Worker" ADD CONSTRAINT "Worker_dispatcherId_fkey" FOREIGN KEY ("dispatcherId") REFERENCES "Dispatcher"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Service" ADD CONSTRAINT "Service_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_WorkflowToWorkflowTag" ADD CONSTRAINT "_WorkflowToWorkflowTag_A_fkey" FOREIGN KEY ("A") REFERENCES "Workflow"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -590,3 +624,9 @@ ALTER TABLE "_ActionToWorker" ADD CONSTRAINT "_ActionToWorker_A_fkey" FOREIGN KE
-- AddForeignKey
ALTER TABLE "_ActionToWorker" ADD CONSTRAINT "_ActionToWorker_B_fkey" FOREIGN KEY ("B") REFERENCES "Worker"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_ServiceToWorker" ADD CONSTRAINT "_ServiceToWorker_A_fkey" FOREIGN KEY ("A") REFERENCES "Service"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_ServiceToWorker" ADD CONSTRAINT "_ServiceToWorker_B_fkey" FOREIGN KEY ("B") REFERENCES "Worker"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -83,6 +83,7 @@ model Tenant {
members TenantMember[]
workflowTags WorkflowTag[]
actions Action[]
services Service[]
}
enum TenantMemberRole {
@@ -658,9 +659,33 @@ model Worker {
dispatcher Dispatcher @relation(fields: [dispatcherId], references: [id], onDelete: Cascade, onUpdate: Cascade)
dispatcherId String @db.Uuid
services Service[]
// the actions this worker can run
actions Action[]
// the jobs the worker has run
stepRuns StepRun[]
}
model Service {
// base fields
id String @id @unique @default(uuid()) @db.Uuid
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
deletedAt DateTime?
// the service name
name String
// the service description
description String?
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade, onUpdate: Cascade)
tenantId String @db.Uuid
// the service's workers
workers Worker[]
@@unique([tenantId, name])
}