Skip to content

Commit 79cd73a

Browse files
authored
Experimental cloud operations client (#1462)
1 parent c0af8a4 commit 79cd73a

File tree

6 files changed

+260
-15
lines changed

6 files changed

+260
-15
lines changed

.github/workflows/ci.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ jobs:
104104
TEMPORAL_NAMESPACE: sdk-ci.a2dd6
105105
TEMPORAL_CLIENT_CERT: ${{ secrets.TEMPORAL_CLIENT_CERT }}
106106
TEMPORAL_CLIENT_KEY: ${{ secrets.TEMPORAL_CLIENT_KEY }}
107+
TEMPORAL_CLIENT_CLOUD_API_KEY: ${{ secrets.TEMPORAL_CLIENT_CLOUD_API_KEY }}
108+
TEMPORAL_CLIENT_CLOUD_API_VERSION: 2024-05-13-00
107109
steps:
108110
- uses: actions/checkout@v4
109111
with:
@@ -114,6 +116,9 @@ jobs:
114116
- name: Single integration test against cloud
115117
run: 'go test -v --count 1 -p 1 . -run "TestIntegrationSuite/TestBasic$"'
116118
working-directory: test
119+
- name: Cloud operations tests
120+
run: 'go test -v --count 1 -p 1 . -run "TestCloudOperationsSuite/.*" -cloud-operations-tests'
121+
working-directory: test
117122

118123
features-test:
119124
uses: temporalio/features/.github/workflows/go.yaml@main

client/client.go

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import (
3434
"crypto/tls"
3535
"io"
3636

37+
"go.temporal.io/api/cloud/cloudservice/v1"
3738
commonpb "go.temporal.io/api/common/v1"
3839
enumspb "go.temporal.io/api/enums/v1"
3940
historypb "go.temporal.io/api/history/v1"
@@ -146,6 +147,11 @@ type (
146147
// Options are optional parameters for Client creation.
147148
Options = internal.ClientOptions
148149

150+
// CloudOperationsClientOptions are parameters for CloudOperationsClient creation.
151+
//
152+
// WARNING: Cloud operations client is currently experimental.
153+
CloudOperationsClientOptions = internal.CloudOperationsClientOptions
154+
149155
// ConnectionOptions are optional parameters that can be specified in ClientOptions
150156
ConnectionOptions = internal.ConnectionOptions
151157

@@ -830,6 +836,17 @@ type (
830836
Close()
831837
}
832838

839+
// CloudOperationsClient is the client for cloud operations.
840+
//
841+
// WARNING: Cloud operations client is currently experimental.
842+
CloudOperationsClient interface {
843+
// CloudService provides access to the underlying gRPC service.
844+
CloudService() cloudservice.CloudServiceClient
845+
846+
// Close client and clean up underlying resources.
847+
Close()
848+
}
849+
833850
// NamespaceClient is the client for managing operations on the namespace.
834851
// CLI, tools, ... can use this layer to manager operations on namespace.
835852
NamespaceClient interface {
@@ -946,6 +963,14 @@ func NewClientFromExistingWithContext(ctx context.Context, existingClient Client
946963
return internal.NewClientFromExisting(ctx, existingClient, options)
947964
}
948965

966+
// DialCloudOperationsClient creates a cloud client to perform cloud-management
967+
// operations. Users should provide Credentials in the options.
968+
//
969+
// WARNING: Cloud operations client is currently experimental.
970+
func DialCloudOperationsClient(ctx context.Context, options CloudOperationsClientOptions) (CloudOperationsClient, error) {
971+
return internal.DialCloudOperationsClient(ctx, options)
972+
}
973+
949974
// NewNamespaceClient creates an instance of a namespace client, to manage
950975
// lifecycle of namespaces. This will not attempt to connect to the server
951976
// eagerly and therefore may not fail for an unreachable server until a call is
@@ -956,10 +981,12 @@ func NewNamespaceClient(options Options) (NamespaceClient, error) {
956981

957982
// make sure if new methods are added to internal.Client they are also added to public Client.
958983
var (
959-
_ Client = internal.Client(nil)
960-
_ internal.Client = Client(nil)
961-
_ NamespaceClient = internal.NamespaceClient(nil)
962-
_ internal.NamespaceClient = NamespaceClient(nil)
984+
_ Client = internal.Client(nil)
985+
_ internal.Client = Client(nil)
986+
_ CloudOperationsClient = internal.CloudOperationsClient(nil)
987+
_ internal.CloudOperationsClient = CloudOperationsClient(nil)
988+
_ NamespaceClient = internal.NamespaceClient(nil)
989+
_ internal.NamespaceClient = NamespaceClient(nil)
963990
)
964991

965992
// NewValue creates a new [converter.EncodedValue] which can be used to decode binary data returned by Temporal. For example:

internal/client.go

Lines changed: 104 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
"sync/atomic"
3232
"time"
3333

34+
"go.temporal.io/api/cloud/cloudservice/v1"
3435
commonpb "go.temporal.io/api/common/v1"
3536
enumspb "go.temporal.io/api/enums/v1"
3637
"go.temporal.io/api/operatorservice/v1"
@@ -489,6 +490,48 @@ type (
489490
DisableErrorCodeMetricTags bool
490491
}
491492

493+
CloudOperationsClient interface {
494+
CloudService() cloudservice.CloudServiceClient
495+
Close()
496+
}
497+
498+
// CloudOperationsClientOptions are parameters for CloudOperationsClient creation.
499+
//
500+
// WARNING: Cloud operations client is currently experimental.
501+
CloudOperationsClientOptions struct {
502+
// Optional: The credentials for this client. This is essentially required.
503+
// See [go.temporal.io/sdk/client.NewAPIKeyStaticCredentials],
504+
// [go.temporal.io/sdk/client.NewAPIKeyDynamicCredentials], and
505+
// [go.temporal.io/sdk/client.NewMTLSCredentials].
506+
// Default: No credentials.
507+
Credentials Credentials
508+
509+
// Optional: Version header for safer mutations. May or may not be required
510+
// depending on cloud settings.
511+
// Default: No header.
512+
Version string
513+
514+
// Optional: Advanced server connection options such as TLS settings. Not
515+
// usually needed.
516+
ConnectionOptions ConnectionOptions
517+
518+
// Optional: Logger framework can use to log.
519+
// Default: Default logger provided.
520+
Logger log.Logger
521+
522+
// Optional: Metrics handler for reporting metrics.
523+
// Default: No metrics
524+
MetricsHandler metrics.Handler
525+
526+
// Optional: Overrides the specific host to connect to. Not usually needed.
527+
// Default: saas-api.tmprl.cloud:443
528+
HostPort string
529+
530+
// Optional: Disable TLS.
531+
// Default: false (i.e. TLS enabled)
532+
DisableTLS bool
533+
}
534+
492535
// HeadersProvider returns a map of gRPC headers that should be used on every request.
493536
HeadersProvider interface {
494537
GetHeaders(ctx context.Context) (map[string]string, error)
@@ -728,7 +771,7 @@ type (
728771

729772
// Credentials are optional credentials that can be specified in ClientOptions.
730773
type Credentials interface {
731-
applyToOptions(*ClientOptions) error
774+
applyToOptions(*ConnectionOptions) error
732775
// Can return nil to have no interceptor
733776
gRPCInterceptor() grpc.UnaryClientInterceptor
734777
}
@@ -783,7 +826,7 @@ func newClient(ctx context.Context, options ClientOptions, existing *WorkflowCli
783826
}
784827

785828
if options.Credentials != nil {
786-
if err := options.Credentials.applyToOptions(&options); err != nil {
829+
if err := options.Credentials.applyToOptions(&options.ConnectionOptions); err != nil {
787830
return nil, err
788831
}
789832
}
@@ -897,6 +940,59 @@ func NewServiceClient(workflowServiceClient workflowservice.WorkflowServiceClien
897940
return client
898941
}
899942

943+
// DialCloudOperationsClient creates a cloud client to perform cloud-management
944+
// operations.
945+
func DialCloudOperationsClient(ctx context.Context, options CloudOperationsClientOptions) (CloudOperationsClient, error) {
946+
// Set defaults
947+
if options.MetricsHandler == nil {
948+
options.MetricsHandler = metrics.NopHandler
949+
}
950+
if options.Logger == nil {
951+
options.Logger = ilog.NewDefaultLogger()
952+
}
953+
if options.HostPort == "" {
954+
options.HostPort = "saas-api.tmprl.cloud:443"
955+
}
956+
if options.Version != "" {
957+
options.ConnectionOptions.DialOptions = append(
958+
options.ConnectionOptions.DialOptions,
959+
grpc.WithChainUnaryInterceptor(func(
960+
ctx context.Context, method string, req, reply any,
961+
cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption,
962+
) error {
963+
ctx = metadata.AppendToOutgoingContext(ctx, "temporal-cloud-api-version", options.Version)
964+
return invoker(ctx, method, req, reply, cc, opts...)
965+
}),
966+
)
967+
}
968+
if options.Credentials != nil {
969+
if err := options.Credentials.applyToOptions(&options.ConnectionOptions); err != nil {
970+
return nil, err
971+
}
972+
}
973+
if options.ConnectionOptions.TLS == nil && !options.DisableTLS {
974+
options.ConnectionOptions.TLS = &tls.Config{}
975+
}
976+
// Exclude internal from retry by default
977+
options.ConnectionOptions.excludeInternalFromRetry = &atomic.Bool{}
978+
options.ConnectionOptions.excludeInternalFromRetry.Store(true)
979+
// TODO(cretz): Pass through context on dial
980+
conn, err := dial(newDialParameters(&ClientOptions{
981+
HostPort: options.HostPort,
982+
ConnectionOptions: options.ConnectionOptions,
983+
MetricsHandler: options.MetricsHandler,
984+
Credentials: options.Credentials,
985+
}, options.ConnectionOptions.excludeInternalFromRetry))
986+
if err != nil {
987+
return nil, err
988+
}
989+
return &cloudOperationsClient{
990+
conn: conn,
991+
logger: options.Logger,
992+
cloudServiceClient: cloudservice.NewCloudServiceClient(conn),
993+
}, nil
994+
}
995+
900996
// NewNamespaceClient creates an instance of a namespace client, to manager lifecycle of namespaces.
901997
func NewNamespaceClient(options ClientOptions) (NamespaceClient, error) {
902998
// Initialize root tags
@@ -964,7 +1060,7 @@ func NewAPIKeyDynamicCredentials(apiKeyCallback func(context.Context) (string, e
9641060
return apiKeyCredentials(apiKeyCallback)
9651061
}
9661062

967-
func (apiKeyCredentials) applyToOptions(*ClientOptions) error { return nil }
1063+
func (apiKeyCredentials) applyToOptions(*ConnectionOptions) error { return nil }
9681064

9691065
func (a apiKeyCredentials) gRPCInterceptor() grpc.UnaryClientInterceptor { return a.gRPCIntercept }
9701066

@@ -992,13 +1088,13 @@ type mTLSCredentials tls.Certificate
9921088

9931089
func NewMTLSCredentials(certificate tls.Certificate) Credentials { return mTLSCredentials(certificate) }
9941090

995-
func (m mTLSCredentials) applyToOptions(opts *ClientOptions) error {
996-
if opts.ConnectionOptions.TLS == nil {
997-
opts.ConnectionOptions.TLS = &tls.Config{}
998-
} else if len(opts.ConnectionOptions.TLS.Certificates) != 0 {
1091+
func (m mTLSCredentials) applyToOptions(opts *ConnectionOptions) error {
1092+
if opts.TLS == nil {
1093+
opts.TLS = &tls.Config{}
1094+
} else if len(opts.TLS.Certificates) != 0 {
9991095
return fmt.Errorf("cannot apply mTLS credentials, certificates already exist on TLS options")
10001096
}
1001-
opts.ConnectionOptions.TLS.Certificates = append(opts.ConnectionOptions.TLS.Certificates, tls.Certificate(m))
1097+
opts.TLS.Certificates = append(opts.TLS.Certificates, tls.Certificate(m))
10021098
return nil
10031099
}
10041100

internal/grpc_dialer_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -536,14 +536,14 @@ func TestCredentialsMTLS(t *testing.T) {
536536
// No TLS set
537537
var clientOptions ClientOptions
538538
creds := NewMTLSCredentials(tls.Certificate{Certificate: [][]byte{[]byte("somedata1")}})
539-
require.NoError(t, creds.applyToOptions(&clientOptions))
539+
require.NoError(t, creds.applyToOptions(&clientOptions.ConnectionOptions))
540540
require.Equal(t, "somedata1", string(clientOptions.ConnectionOptions.TLS.Certificates[0].Certificate[0]))
541541

542542
// TLS already set
543543
clientOptions = ClientOptions{}
544544
clientOptions.ConnectionOptions.TLS = &tls.Config{ServerName: "my-server-name"}
545545
creds = NewMTLSCredentials(tls.Certificate{Certificate: [][]byte{[]byte("somedata2")}})
546-
require.NoError(t, creds.applyToOptions(&clientOptions))
546+
require.NoError(t, creds.applyToOptions(&clientOptions.ConnectionOptions))
547547
require.Equal(t, "my-server-name", clientOptions.ConnectionOptions.TLS.ServerName)
548548
require.Equal(t, "somedata2", string(clientOptions.ConnectionOptions.TLS.Certificates[0].Certificate[0]))
549549

@@ -553,7 +553,7 @@ func TestCredentialsMTLS(t *testing.T) {
553553
Certificates: []tls.Certificate{{Certificate: [][]byte{[]byte("somedata3")}}},
554554
}
555555
creds = NewMTLSCredentials(tls.Certificate{Certificate: [][]byte{[]byte("somedata4")}})
556-
require.Error(t, creds.applyToOptions(&clientOptions))
556+
require.Error(t, creds.applyToOptions(&clientOptions.ConnectionOptions))
557557
}
558558

559559
type testGRPCServer struct {

internal/internal_workflow_client.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import (
4242
"google.golang.org/grpc/status"
4343
"google.golang.org/protobuf/types/known/durationpb"
4444

45+
"go.temporal.io/api/cloud/cloudservice/v1"
4546
commonpb "go.temporal.io/api/common/v1"
4647
enumspb "go.temporal.io/api/enums/v1"
4748
historypb "go.temporal.io/api/history/v1"
@@ -100,6 +101,13 @@ type (
100101
unclosedClients *int32
101102
}
102103

104+
// cloudOperationsClient is the client for managing cloud.
105+
cloudOperationsClient struct {
106+
conn *grpc.ClientConn
107+
logger log.Logger
108+
cloudServiceClient cloudservice.CloudServiceClient
109+
}
110+
103111
// namespaceClient is the client for managing namespaces.
104112
namespaceClient struct {
105113
workflowService workflowservice.WorkflowServiceClient
@@ -1289,6 +1297,16 @@ func (wc *WorkflowClient) Close() {
12891297
}
12901298
}
12911299

1300+
func (c *cloudOperationsClient) CloudService() cloudservice.CloudServiceClient {
1301+
return c.cloudServiceClient
1302+
}
1303+
1304+
func (c *cloudOperationsClient) Close() {
1305+
if err := c.conn.Close(); err != nil {
1306+
c.logger.Warn("unable to close connection", tagError, err)
1307+
}
1308+
}
1309+
12921310
// Register a namespace with temporal server
12931311
// The errors it can throw:
12941312
// - NamespaceAlreadyExistsError

0 commit comments

Comments
 (0)