mirror of
https://github.com/hatchet-dev/hatchet.git
synced 2026-01-06 16:59:39 -06:00
451 lines
11 KiB
Go
451 lines
11 KiB
Go
package client
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
grpc_retry "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/retry"
|
|
"github.com/rs/zerolog"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/credentials"
|
|
"google.golang.org/grpc/credentials/insecure"
|
|
"google.golang.org/grpc/keepalive"
|
|
"google.golang.org/grpc/status"
|
|
|
|
"github.com/hatchet-dev/hatchet/pkg/client/loader"
|
|
"github.com/hatchet-dev/hatchet/pkg/client/rest"
|
|
|
|
cloudrest "github.com/hatchet-dev/hatchet/pkg/client/cloud/rest"
|
|
|
|
"github.com/hatchet-dev/hatchet/pkg/client/types"
|
|
"github.com/hatchet-dev/hatchet/pkg/config/client"
|
|
"github.com/hatchet-dev/hatchet/pkg/logger"
|
|
"github.com/hatchet-dev/hatchet/pkg/validator"
|
|
)
|
|
|
|
type Client interface {
|
|
Admin() AdminClient
|
|
Cron() CronClient
|
|
Schedule() ScheduleClient
|
|
Dispatcher() DispatcherClient
|
|
Event() EventClient
|
|
Subscribe() SubscribeClient
|
|
API() *rest.ClientWithResponses
|
|
CloudAPI() *cloudrest.ClientWithResponses
|
|
TenantId() string
|
|
Namespace() string
|
|
CloudRegisterID() *string
|
|
RunnableActions() []string
|
|
}
|
|
|
|
type clientImpl struct {
|
|
conn *grpc.ClientConn
|
|
|
|
admin AdminClient
|
|
cron CronClient
|
|
schedule ScheduleClient
|
|
dispatcher DispatcherClient
|
|
event EventClient
|
|
subscribe SubscribeClient
|
|
rest *rest.ClientWithResponses
|
|
cloudrest *cloudrest.ClientWithResponses
|
|
|
|
// the tenant id
|
|
tenantId string
|
|
|
|
namespace string
|
|
|
|
cloudRegisterID *string
|
|
runnableActions []string
|
|
|
|
l *zerolog.Logger
|
|
|
|
v validator.Validator
|
|
}
|
|
|
|
type ClientOpt func(*ClientOpts)
|
|
|
|
type filesLoaderFunc func() []*types.Workflow
|
|
|
|
type ClientOpts struct {
|
|
tenantId string
|
|
l *zerolog.Logger
|
|
v validator.Validator
|
|
tls *tls.Config
|
|
hostPort string
|
|
serverURL string
|
|
token string
|
|
namespace string
|
|
noGrpcRetry bool
|
|
sharedMeta map[string]string
|
|
|
|
cloudRegisterID *string
|
|
runnableActions []string
|
|
|
|
filesLoader filesLoaderFunc
|
|
initWorkflows bool
|
|
presetWorkerLabels map[string]string
|
|
}
|
|
|
|
func defaultClientOpts(token *string, cf *client.ClientConfigFile) *ClientOpts {
|
|
var clientConfig *client.ClientConfig
|
|
var err error
|
|
|
|
configLoader := &loader.ConfigLoader{}
|
|
|
|
if cf == nil {
|
|
// read from environment variables and hostname by default
|
|
|
|
clientConfig, err = configLoader.LoadClientConfig(token)
|
|
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
} else {
|
|
if token != nil {
|
|
cf.Token = *token
|
|
}
|
|
clientConfig, err = loader.GetClientConfigFromConfigFile(cf)
|
|
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
logger := logger.NewDefaultLogger("client")
|
|
|
|
return &ClientOpts{
|
|
tenantId: clientConfig.TenantId,
|
|
token: clientConfig.Token,
|
|
l: &logger,
|
|
v: validator.NewDefaultValidator(),
|
|
tls: clientConfig.TLSConfig,
|
|
hostPort: clientConfig.GRPCBroadcastAddress,
|
|
serverURL: clientConfig.ServerURL,
|
|
filesLoader: types.DefaultLoader,
|
|
namespace: clientConfig.Namespace,
|
|
cloudRegisterID: clientConfig.CloudRegisterID,
|
|
runnableActions: clientConfig.RunnableActions,
|
|
noGrpcRetry: clientConfig.NoGrpcRetry,
|
|
sharedMeta: make(map[string]string),
|
|
presetWorkerLabels: clientConfig.PresetWorkerLabels,
|
|
}
|
|
}
|
|
|
|
// Deprecated: use WithLogger instead
|
|
func WithLogLevel(lvl string) ClientOpt {
|
|
return func(opts *ClientOpts) {
|
|
logger := logger.NewDefaultLogger("client")
|
|
lvl, err := zerolog.ParseLevel(lvl)
|
|
|
|
if err == nil {
|
|
logger = logger.Level(lvl)
|
|
}
|
|
|
|
opts.l = &logger
|
|
}
|
|
}
|
|
|
|
func WithLogger(l *zerolog.Logger) ClientOpt {
|
|
return func(opts *ClientOpts) {
|
|
opts.l = l
|
|
}
|
|
}
|
|
|
|
func WithTenantId(tenantId string) ClientOpt {
|
|
return func(opts *ClientOpts) {
|
|
opts.tenantId = tenantId
|
|
}
|
|
}
|
|
|
|
func WithHostPort(host string, port int) ClientOpt {
|
|
return func(opts *ClientOpts) {
|
|
opts.hostPort = fmt.Sprintf("%s:%d", host, port)
|
|
}
|
|
}
|
|
|
|
func WithToken(token string) ClientOpt {
|
|
return func(opts *ClientOpts) {
|
|
opts.token = token
|
|
}
|
|
}
|
|
|
|
func WithNamespace(namespace string) ClientOpt {
|
|
return func(opts *ClientOpts) {
|
|
opts.namespace = namespace + "_"
|
|
}
|
|
}
|
|
|
|
func WithSharedMeta(meta map[string]string) ClientOpt {
|
|
return func(opts *ClientOpts) {
|
|
if opts.sharedMeta == nil {
|
|
opts.sharedMeta = make(map[string]string)
|
|
}
|
|
|
|
for k, v := range meta {
|
|
opts.sharedMeta[k] = v
|
|
}
|
|
}
|
|
}
|
|
|
|
func InitWorkflows() ClientOpt {
|
|
return func(opts *ClientOpts) {
|
|
opts.initWorkflows = true
|
|
}
|
|
}
|
|
|
|
// WithWorkflows sets the workflow files to use for the worker. If this is not passed in, the workflows files will be loaded
|
|
// from the .hatchet folder in the current directory.
|
|
func WithWorkflows(files []*types.Workflow) ClientOpt {
|
|
return func(opts *ClientOpts) {
|
|
opts.filesLoader = func() []*types.Workflow {
|
|
return files
|
|
}
|
|
}
|
|
}
|
|
|
|
type sharedClientOpts struct {
|
|
tenantId string
|
|
namespace string
|
|
l *zerolog.Logger
|
|
v validator.Validator
|
|
ctxLoader *contextLoader
|
|
sharedMeta map[string]string
|
|
}
|
|
|
|
// New creates a new client instance.
|
|
func New(fs ...ClientOpt) (Client, error) {
|
|
var token *string
|
|
initOpts := &ClientOpts{}
|
|
for _, f := range fs {
|
|
f(initOpts)
|
|
}
|
|
if initOpts.token != "" {
|
|
token = &initOpts.token
|
|
}
|
|
|
|
opts := defaultClientOpts(token, nil)
|
|
|
|
for _, f := range fs {
|
|
f(opts)
|
|
}
|
|
|
|
return newFromOpts(opts)
|
|
}
|
|
|
|
func NewFromConfigFile(cf *client.ClientConfigFile, fs ...ClientOpt) (Client, error) {
|
|
opts := defaultClientOpts(nil, cf)
|
|
|
|
for _, f := range fs {
|
|
f(opts)
|
|
}
|
|
|
|
return newFromOpts(opts)
|
|
}
|
|
|
|
func newFromOpts(opts *ClientOpts) (Client, error) {
|
|
if opts.token == "" {
|
|
return nil, fmt.Errorf("token is required")
|
|
}
|
|
|
|
var transportCreds credentials.TransportCredentials
|
|
|
|
if opts.tls == nil {
|
|
opts.l.Debug().Msgf("connecting to %s without TLS", opts.hostPort)
|
|
|
|
transportCreds = insecure.NewCredentials()
|
|
} else {
|
|
opts.l.Debug().Msgf("connecting to %s with TLS server name %s", opts.hostPort, opts.tls.ServerName)
|
|
|
|
transportCreds = credentials.NewTLS(opts.tls)
|
|
}
|
|
|
|
keepAliveParams := keepalive.ClientParameters{
|
|
Time: 10 * time.Second, // grpc.keepalive_time_ms: 10 * 1000
|
|
Timeout: 60 * time.Second, // grpc.keepalive_timeout_ms: 60 * 1000
|
|
PermitWithoutStream: true, // grpc.keepalive_permit_without_calls: 1
|
|
}
|
|
|
|
grpcOpts := []grpc.DialOption{
|
|
grpc.WithTransportCredentials(transportCreds),
|
|
grpc.WithKeepaliveParams(keepAliveParams),
|
|
}
|
|
|
|
if !opts.noGrpcRetry {
|
|
retryOnCodes := []codes.Code{
|
|
codes.ResourceExhausted,
|
|
codes.DeadlineExceeded,
|
|
codes.FailedPrecondition,
|
|
codes.Internal,
|
|
codes.Unavailable,
|
|
}
|
|
|
|
retryOpts := []grpc_retry.CallOption{
|
|
|
|
grpc_retry.WithBackoff(grpc_retry.BackoffExponentialWithJitter(5*time.Second, 0.10)),
|
|
grpc_retry.WithMax(5),
|
|
grpc_retry.WithPerRetryTimeout(30 * time.Second),
|
|
grpc_retry.WithCodes(retryOnCodes...),
|
|
grpc_retry.WithOnRetryCallback(grpc_retry.OnRetryCallback(func(ctx context.Context, attempt uint, err error) {
|
|
if contains(retryOnCodes, status.Code(err)) {
|
|
opts.l.Debug().Msgf("grpc_retry attempt: %d, backoff for %v", attempt, err)
|
|
}
|
|
})),
|
|
}
|
|
grpcOpts = append(grpcOpts, grpc.WithChainStreamInterceptor(grpc_retry.StreamClientInterceptor(retryOpts...)))
|
|
grpcOpts = append(grpcOpts, grpc.WithChainUnaryInterceptor(grpc_retry.UnaryClientInterceptor(retryOpts...)))
|
|
}
|
|
|
|
conn, err := grpc.NewClient(
|
|
opts.hostPort,
|
|
grpcOpts...,
|
|
)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
shared := &sharedClientOpts{
|
|
tenantId: opts.tenantId,
|
|
namespace: opts.namespace,
|
|
l: opts.l,
|
|
v: opts.v,
|
|
ctxLoader: newContextLoader(opts.token),
|
|
sharedMeta: opts.sharedMeta,
|
|
}
|
|
|
|
subscribe := newSubscribe(conn, shared)
|
|
admin := newAdmin(conn, shared, subscribe)
|
|
dispatcher := newDispatcher(conn, shared, opts.presetWorkerLabels)
|
|
event := newEvent(conn, shared)
|
|
|
|
rest, err := rest.NewClientWithResponses(opts.serverURL, rest.WithRequestEditorFn(func(ctx context.Context, req *http.Request) error {
|
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", opts.token))
|
|
return nil
|
|
}))
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not create rest client: %w", err)
|
|
}
|
|
|
|
cloudrest, err := cloudrest.NewClientWithResponses(opts.serverURL, cloudrest.WithRequestEditorFn(func(ctx context.Context, req *http.Request) error {
|
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", opts.token))
|
|
return nil
|
|
}))
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not create cloud REST client: %w", err)
|
|
}
|
|
|
|
cronClient, err := NewCronClient(rest, opts.l, opts.v, opts.tenantId, opts.namespace)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not create cron client: %w", err)
|
|
}
|
|
|
|
scheduleClient, err := NewScheduleClient(rest, opts.l, opts.v, opts.tenantId, opts.namespace)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not create schedule client: %w", err)
|
|
}
|
|
|
|
// if init workflows is set, then we need to initialize the workflows
|
|
if opts.initWorkflows {
|
|
if err := initWorkflows(opts.filesLoader, admin); err != nil {
|
|
return nil, fmt.Errorf("could not init workflows: %w", err)
|
|
}
|
|
}
|
|
|
|
return &clientImpl{
|
|
conn: conn,
|
|
tenantId: opts.tenantId,
|
|
l: opts.l,
|
|
admin: admin,
|
|
cron: cronClient,
|
|
schedule: scheduleClient,
|
|
dispatcher: dispatcher,
|
|
subscribe: subscribe,
|
|
event: event,
|
|
v: opts.v,
|
|
rest: rest,
|
|
cloudrest: cloudrest,
|
|
namespace: opts.namespace,
|
|
cloudRegisterID: opts.cloudRegisterID,
|
|
runnableActions: opts.runnableActions,
|
|
}, nil
|
|
}
|
|
|
|
func (c *clientImpl) Admin() AdminClient {
|
|
return c.admin
|
|
}
|
|
|
|
func (c *clientImpl) Cron() CronClient {
|
|
return c.cron
|
|
}
|
|
|
|
func (c *clientImpl) Schedule() ScheduleClient {
|
|
return c.schedule
|
|
}
|
|
|
|
func (c *clientImpl) Dispatcher() DispatcherClient {
|
|
return c.dispatcher
|
|
}
|
|
|
|
func (c *clientImpl) Event() EventClient {
|
|
return c.event
|
|
}
|
|
|
|
func (c *clientImpl) Subscribe() SubscribeClient {
|
|
return c.subscribe
|
|
}
|
|
|
|
func (c *clientImpl) API() *rest.ClientWithResponses {
|
|
return c.rest
|
|
}
|
|
|
|
func (c *clientImpl) CloudAPI() *cloudrest.ClientWithResponses {
|
|
return c.cloudrest
|
|
}
|
|
|
|
func (c *clientImpl) TenantId() string {
|
|
return c.tenantId
|
|
}
|
|
|
|
func (c *clientImpl) Namespace() string {
|
|
return c.namespace
|
|
}
|
|
|
|
func (c *clientImpl) CloudRegisterID() *string {
|
|
return c.cloudRegisterID
|
|
}
|
|
|
|
func (c *clientImpl) RunnableActions() []string {
|
|
return c.runnableActions
|
|
}
|
|
|
|
func initWorkflows(fl filesLoaderFunc, adminClient AdminClient) error {
|
|
files := fl()
|
|
|
|
for _, file := range files {
|
|
if err := adminClient.PutWorkflow(file); err != nil {
|
|
return fmt.Errorf("could not create workflow: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func contains(codes []codes.Code, code codes.Code) bool {
|
|
for _, c := range codes {
|
|
if c == code {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|