Skip to content

Commit

Permalink
Work on envconfig
Browse files Browse the repository at this point in the history
  • Loading branch information
cretz committed Feb 19, 2025
1 parent 9d4a99a commit a10d1ec
Show file tree
Hide file tree
Showing 6 changed files with 873 additions and 0 deletions.
158 changes: 158 additions & 0 deletions contrib/envconfig/client_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package envconfig

import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"net/http"
"os"

"go.temporal.io/sdk/client"
"go.temporal.io/sdk/converter"
)

type ClientConfig struct {
Profiles map[string]*ClientConfigProfile
}

type ClientConfigProfile struct {
Address string
Namespace string
APIKey string
TLS *ClientConfigTLS
Codec *ClientConfigCodec
GRPCMeta map[string]string
}

type ClientConfigTLS struct {
Disabled bool
ClientCertPath string
ClientCertData []byte
ClientKeyPath string
ClientKeyData []byte
ServerCACertPath string
ServerCACertData []byte
ServerName string
DisableHostVerification bool
}

type ClientConfigCodec struct {
Endpoint string
Auth string
}

type ToClientOptionsOptions struct {
// If true and a codec is configured, the data converter of the client will point to the codec remotely. Users
// should usually not set this and rather configure the codec locally. Users should especially not enable this for
// clients used by workers since they call the codec repeatedly even during workflow replay.
IncludeRemoteCodec bool
}

func (c *ClientConfig) ToClientOptions(profile string, options ToClientOptionsOptions) (client.Options, error) {
if profile == "" {
profile = "default"
}
prof, ok := c.Profiles[profile]
if !ok {
return client.Options{}, fmt.Errorf("profile not found")
}
return prof.ToClientOptions(options)
}

func (c *ClientConfigProfile) ToClientOptions(options ToClientOptionsOptions) (client.Options, error) {
opts := client.Options{
HostPort: c.Address,
Namespace: c.Namespace,
}
if c.APIKey != "" {
opts.Credentials = client.NewAPIKeyStaticCredentials(c.APIKey)
}
if c.TLS != nil {
var err error
if opts.ConnectionOptions.TLS, err = c.TLS.toTLSConfig(); err != nil {
return client.Options{}, fmt.Errorf("invalid TLS config: %w", err)
}
} else if c.APIKey != "" && (c.TLS == nil || !c.TLS.Disabled) {
opts.ConnectionOptions.TLS = &tls.Config{}
}
if c.Codec != nil && options.IncludeRemoteCodec {
var err error
if opts.DataConverter, err = c.Codec.toDataConverter(c.Namespace); err != nil {
return client.Options{}, fmt.Errorf("invalid codec: %w", err)
}
}
if len(c.GRPCMeta) > 0 {
opts.HeadersProvider = fixedHeaders(c.GRPCMeta)
}
return opts, nil
}

func (c *ClientConfigTLS) toTLSConfig() (*tls.Config, error) {
if c.Disabled {
return nil, nil
}
conf := &tls.Config{}

// Client cert
if len(c.ClientCertData) > 0 || len(c.ClientKeyData) > 0 {
if len(c.ClientCertData) == 0 || len(c.ClientKeyData) == 0 {
return nil, fmt.Errorf("if either client cert or key data is present, other must be present too")
} else if c.ClientCertPath != "" || c.ClientKeyPath != "" {
return nil, fmt.Errorf("cannot have client key/cert path with data")
}
cert, err := tls.X509KeyPair(c.ClientCertData, c.ClientKeyData)
if err != nil {
return nil, fmt.Errorf("failed loading client cert/key data: %w", err)
}
conf.Certificates = append(conf.Certificates, cert)
} else if c.ClientCertPath != "" || c.ClientKeyPath != "" {
if c.ClientCertPath == "" || c.ClientKeyPath == "" {
return nil, fmt.Errorf("if either client cert or key path is present, other must be present too")
}
cert, err := tls.LoadX509KeyPair(c.ClientCertPath, c.ClientKeyPath)
if err != nil {
return nil, fmt.Errorf("failed loading client cert/key path: %w", err)
}
conf.Certificates = append(conf.Certificates, cert)
}

// Server CA cert
if len(c.ServerCACertData) > 0 || c.ServerCACertPath == "" {
pool := x509.NewCertPool()
serverCAData := c.ServerCACertData
if len(serverCAData) == 0 {
var err error
if serverCAData, err = os.ReadFile(c.ServerCACertPath); err != nil {
return nil, fmt.Errorf("failed reading server CA cert path: %w", err)
}
} else if c.ServerCACertPath == "" {
return nil, fmt.Errorf("cannot have server CA cert path with data")
}
if !pool.AppendCertsFromPEM(serverCAData) {
return nil, fmt.Errorf("failed adding server CA to CA pool")
}
conf.RootCAs = pool
}

conf.ServerName = c.ServerName
conf.InsecureSkipVerify = c.DisableHostVerification
return conf, nil
}

func (c *ClientConfigCodec) toDataConverter(namespace string) (converter.DataConverter, error) {
return converter.NewRemoteDataConverter(converter.GetDefaultDataConverter(), converter.RemoteDataConverterOptions{
Endpoint: c.Endpoint,
ModifyRequest: func(req *http.Request) error {
req.Header.Set("X-Namespace", namespace)
if c.Auth != "" {
req.Header.Set("Authorization", c.Auth)
}
return nil
},
}), nil
}

type fixedHeaders map[string]string

func (f fixedHeaders) GetHeaders(context.Context) (map[string]string, error) { return f, nil }
Loading

1 comment on commit a10d1ec

@tomwheeler
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested this just now. Here are the setup steps:

  1. Clone/checkout a local copy of this Go SDK code
  2. Clone the money-transfer-project-template-go repository
  3. Update the go.mod file in the money transfer sample to reference the Go SDK module from step 1 (i.e., added replace go.temporal.io/sdk and replace go.temporal.io/sdk/contrib/envconfig)
  4. Update the starter/main.go and worker/main.go files to create a Temporal Client by calling client.Dial(envconfig.MustLoadDefaultClientOptions()).

For my initial test, I launched a local Temporal Service using temporal server start-dev. I opened two new terminal tabs and verified that no environment variables starting with TEMPORAL_ were set in either shell. I then launcher the Worker and Starter. Both ran as expected.

For my second test, I set four environment variables, using values that correspond to my Temporal Cloud account (which is configured to support mTLS authentication):

  • TEMPORAL_ADDRESS
  • TEMPORAL_NAMESPACE
  • TEMPORAL_TLS_CLIENT_CERT_PATH
  • TEMPORAL_TLS_CLIENT_KEY_PATH

I then attempted to run the Worker as before, but it failed:

$ go run worker/main.go
panic: invalid TLS config: failed reading server CA cert path: open : no such file or directory

goroutine 1 [running]:
go.temporal.io/sdk/contrib/envconfig.MustLoadDefaultClientOptions()
	/Users/twheeler/temp/env-config-go-module-test/chad-env-go-sdk/contrib/envconfig/client_config_load.go:16 +0xf4
main.main()
	/Users/twheeler/temp/env-config-go-module-test/money-transfer-project-template-go/worker/main.go:17 +0x2c
exit status 2

I gather that this is because the TEMPORAL_TLS_SERVER_CA_CERT_PATH variable was not set, although I wouldn't think that would be necessary. For reference, the OMS has environment-based config built into the application and it works with the same values I used (although the variable names differ slightly between these two implementations).

Please sign in to comment.