diff --git a/Makefile b/Makefile index 5bca199..d3a3ab3 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ OUTPUT_PATH := $(OUTPUT_DIR)/$(BINARY_NAME) .PHONY: all build clean test test-verbose test-coverage test-race test-short test-clean benchmark test-auth test-crypto test-proxy test-utils -all: build +all: test build build: go build -o $(OUTPUT_PATH) $(CMD_DIR) diff --git a/cmd/main.go b/cmd/main.go index dce4589..3205819 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -149,9 +149,9 @@ func configureProxy(proxyConns *proxy.Conn, cfg *utils.Config) error { metricsHandler := metrics.NewMetricsHandler(metrics.MetricsHandlerOptions{ // Todo: do we need these many attributes? InitialAttributes: attribute.NewSet( - attribute.String("proxy-id", t.ProxyId), - attribute.String("target", t.Target), - attribute.String("namespace", t.Namespace), + attribute.String("proxy_id", t.ProxyId), + attribute.String("namespace", t.TemporalCloud.Namespace), + attribute.String("host_port", t.TemporalCloud.HostPort), attribute.String("auth_type", authType), attribute.String("encryption_key", t.EncryptionKey), ), @@ -172,12 +172,7 @@ func configureProxy(proxyConns *proxy.Conn, cfg *utils.Config) error { } err := proxyConns.AddConn(proxy.AddConnInput{ - ProxyId: t.ProxyId, - Target: t.Target, - TLSCertPath: t.TLS.CertFile, - TLSKeyPath: t.TLS.KeyFile, - EncryptionKeyID: t.EncryptionKey, - Namespace: t.Namespace, + Target: &t, AuthManager: authManager, AuthType: authType, MetricsHandler: metricsHandler, diff --git a/config.yaml.sample b/config.yaml.sample index 9e2fcc9..533ce17 100644 --- a/config.yaml.sample +++ b/config.yaml.sample @@ -1,21 +1,30 @@ server: port: 7233 host: "0.0.0.0" + metrics: port: 9090 + encryption: caching: max_cache: 100 max_age: "10m" max_usage: 100 + targets: - - proxy_id: "..internal" - target: "..tmprl.cloud:7233" - tls: - cert_file: "/path/to/./tls.crt" - key_file: "/path/to/./tls.key" + - proxy_id: "" + temporal_cloud: + namespace: "." + host_port: "..tmprl.cloud:7233" # endpoint when using mTLS + # host_port: "..api.temporal.io:7233" # endpoint when using API keys + authentication: # only set either tls or api_key, not both + tls: + cert_file: "/path/to/./tls.crt" + key_file: "/path/to/./tls.key" + api_key: # only set either value or env, not both + value: "" + env: encryption_key: "" - namespace: "." authentication: type: "spiffe" config: @@ -24,13 +33,19 @@ targets: audiences: - "temporal_cloud_proxy" - - source: "..internal" - target: "..tmprl.cloud:7233" - tls: - cert_file: "/path/to/./tls.crt" - key_file: "/path/to/./tls.key" + - proxy_id: "" + temporal_cloud: + namespace: "." + host_port: "..tmprl.cloud:7233" # endpoint when using mTLS + # host_port: "..api.temporal.io:7233" # endpoint when using API keys + authentication: # only set either tls or api_key, not both + tls: + cert_file: "/path/to/./tls.crt" + key_file: "/path/to/./tls.key" + api_key: # only set either value or env, not both + value: "" + env: encryption_key: "" - namespace: "." authentication: type: "spiffe" config: diff --git a/proxy/proxy.go b/proxy/proxy.go index 3b0fd13..5d7e8ab 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -5,24 +5,22 @@ import ( "crypto/tls" "errors" "fmt" - "go.temporal.io/sdk/converter" - "os" - "sync" - "temporal-sa/temporal-cloud-proxy/codec" - "temporal-sa/temporal-cloud-proxy/crypto" - "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/kms" - - "temporal-sa/temporal-cloud-proxy/auth" - "temporal-sa/temporal-cloud-proxy/metrics" - + "go.temporal.io/sdk/converter" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials" "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" + "os" + "sync" + "temporal-sa/temporal-cloud-proxy/auth" + "temporal-sa/temporal-cloud-proxy/codec" + "temporal-sa/temporal-cloud-proxy/crypto" + "temporal-sa/temporal-cloud-proxy/metrics" + "temporal-sa/temporal-cloud-proxy/utils" ) type Conn struct { @@ -59,12 +57,7 @@ func createKMSClient() *kms.KMS { // AddConnInput contains parameters for adding a new connection type AddConnInput struct { - ProxyId string - Target string - TLSCertPath string - TLSKeyPath string - EncryptionKeyID string - Namespace string + Target *utils.TargetConfig AuthManager *auth.AuthManager AuthType string MetricsHandler metrics.MetricsHandler @@ -73,18 +66,26 @@ type AddConnInput struct { // AddConn adds a new connection to the proxy func (mc *Conn) AddConn(input AddConnInput) error { - fmt.Printf("Adding connection id: %s target: %s\n", input.ProxyId, input.Target) + fmt.Printf("Adding connection id: %s namespace: %s hostport: %s\n", + input.Target.ProxyId, input.Target.TemporalCloud.Namespace, input.Target.TemporalCloud.HostPort) - cert, err := tls.LoadX509KeyPair(input.TLSCertPath, input.TLSKeyPath) - if err != nil { - return err + mc.mu.RLock() + _, exists := mc.namespace[input.Target.ProxyId] + mc.mu.RUnlock() + if exists { + return fmt.Errorf("proxy-id %s already exists", input.Target.ProxyId) + } + + if input.Target.TemporalCloud.Authentication.ApiKey != nil && input.Target.TemporalCloud.Authentication.TLS != nil { + return fmt.Errorf("%s: cannot have both api key and mtls authentication configured on a single target", + input.Target.ProxyId) } //Initialize AWS KMS client kmsClient := createKMSClient() codecContext := map[string]string{ - "namespace": input.Namespace, + "namespace": input.Target.TemporalCloud.Namespace, } clientInterceptor, err := converter.NewPayloadCodecGRPCClientInterceptor( @@ -92,7 +93,7 @@ func (mc *Conn) AddConn(input AddConnInput) error { Codecs: []converter.PayloadCodec{codec.NewEncryptionCodecWithCaching( kmsClient, codecContext, - input.EncryptionKeyID, + input.Target.EncryptionKey, input.MetricsHandler, input.CryptoCachingConfig, )}, @@ -102,21 +103,68 @@ func (mc *Conn) AddConn(input AddConnInput) error { return err } + tlsConfig := tls.Config{} + + grpcInterceptors := []grpc.UnaryClientInterceptor{ + clientInterceptor, + } + + if apiKeyConfig := input.Target.TemporalCloud.Authentication.ApiKey; apiKeyConfig != nil { + if apiKeyConfig.Value != "" && apiKeyConfig.EnvVar != "" { + // TODO proper logging + fmt.Printf("WARN - multiple values provided for api key, using value. proxy_id: %s\n", input.Target.ProxyId) + } + + apiKey := "" + if apiKeyConfig.Value != "" { + apiKey = apiKeyConfig.Value + } else if apiKeyConfig.EnvVar != "" { + apiKey = os.Getenv(apiKeyConfig.EnvVar) + } + + if apiKey == "" { + return fmt.Errorf("%s: no api key provided", input.Target.ProxyId) + } + + grpcInterceptors = append(grpcInterceptors, + func(ctx context.Context, method string, req any, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { + md, ok := metadata.FromIncomingContext(ctx) + + if ok { + md = md.Copy() + md.Delete("authorization") + md.Delete("temporal-namespace") + + ctx = metadata.NewOutgoingContext(ctx, md) + ctx = metadata.AppendToOutgoingContext(ctx, "temporal-namespace", input.Target.TemporalCloud.Namespace) + ctx = metadata.AppendToOutgoingContext(ctx, "authorization", "Bearer "+apiKey) + } + + return invoker(ctx, method, req, reply, cc, opts...) + }) + } else { + cert, err := tls.LoadX509KeyPair(input.Target.TemporalCloud.Authentication.TLS.CertFile, + input.Target.TemporalCloud.Authentication.TLS.KeyFile) + if err != nil { + return err + } + + tlsConfig.Certificates = []tls.Certificate{cert} + } + conn, err := grpc.NewClient( - input.Target, + input.Target.TemporalCloud.HostPort, grpc.WithTransportCredentials(credentials.NewTLS( - &tls.Config{ - Certificates: []tls.Certificate{cert}, - }, + &tlsConfig, )), - grpc.WithUnaryInterceptor(clientInterceptor), + grpc.WithChainUnaryInterceptor(grpcInterceptors...), ) if err != nil { return err } mc.mu.Lock() - mc.namespace[input.ProxyId] = NamespaceConn{ + mc.namespace[input.Target.ProxyId] = NamespaceConn{ conn: conn, authManager: input.AuthManager, authType: input.AuthType, @@ -137,8 +185,10 @@ func (mc *Conn) CloseAll() error { if err := namespace.conn.Close(); err != nil { errs = append(errs, err) } - if err := namespace.authManager.Close(); err != nil { - errs = append(errs, err) + if namespace.authManager != nil { + if err := namespace.authManager.Close(); err != nil { + errs = append(errs, err) + } } } diff --git a/proxy/proxy_test.go b/proxy/proxy_test.go index 5f96621..7e6420a 100644 --- a/proxy/proxy_test.go +++ b/proxy/proxy_test.go @@ -17,6 +17,8 @@ import ( "time" "temporal-sa/temporal-cloud-proxy/auth" + "temporal-sa/temporal-cloud-proxy/metrics" + "temporal-sa/temporal-cloud-proxy/utils" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -116,60 +118,150 @@ func TestConn_AddConn(t *testing.T) { errorMsg string }{ { - name: "successful connection addition", + name: "successful connection addition with TLS", input: AddConnInput{ - ProxyId: "test-proxy-id", - Target: "localhost:7233", - TLSCertPath: certPath, - TLSKeyPath: keyPath, - EncryptionKeyID: "test-key-id", - Namespace: "test-namespace", - AuthManager: nil, // Use nil for simplicity in tests - AuthType: "jwt", + Target: &utils.TargetConfig{ + ProxyId: "test-proxy-id", + TemporalCloud: utils.TemporalCloudConfig{ + Namespace: "test-namespace", + HostPort: "localhost:7233", + Authentication: utils.TemporalAuthConfig{ + TLS: &utils.TLSConfig{ + CertFile: certPath, + KeyFile: keyPath, + }, + }, + }, + EncryptionKey: "test-key-id", + }, + AuthManager: nil, // Use nil for simplicity in tests + AuthType: "jwt", + MetricsHandler: metrics.NewMetricsHandler(metrics.MetricsHandlerOptions{}), + CryptoCachingConfig: nil, }, expectError: false, }, + { + name: "successful connection addition with API key (value)", + input: AddConnInput{ + Target: &utils.TargetConfig{ + ProxyId: "test-proxy-id-api", + TemporalCloud: utils.TemporalCloudConfig{ + Namespace: "test-namespace", + HostPort: "localhost:7233", + Authentication: utils.TemporalAuthConfig{ + ApiKey: &utils.TemporalApiKeyConfig{ + Value: "test-api-key", + }, + }, + }, + EncryptionKey: "test-key-id", + }, + AuthManager: nil, + AuthType: "jwt", + MetricsHandler: metrics.NewMetricsHandler(metrics.MetricsHandlerOptions{}), + CryptoCachingConfig: nil, + }, + expectError: false, + }, + { + name: "successful connection addition with API key (env var)", + input: AddConnInput{ + Target: &utils.TargetConfig{ + ProxyId: "test-proxy-id-api-env", + TemporalCloud: utils.TemporalCloudConfig{ + Namespace: "test-namespace", + HostPort: "localhost:7233", + Authentication: utils.TemporalAuthConfig{ + ApiKey: &utils.TemporalApiKeyConfig{ + EnvVar: "TEST_TEMPORAL_API_KEY", + }, + }, + }, + EncryptionKey: "test-key-id", + }, + AuthManager: nil, + AuthType: "jwt", + MetricsHandler: metrics.NewMetricsHandler(metrics.MetricsHandlerOptions{}), + CryptoCachingConfig: nil, + }, + expectError: true, // Will fail because env var is not set + }, { name: "invalid certificate path", input: AddConnInput{ - ProxyId: "test-proxy-id", - Target: "localhost:7233", - TLSCertPath: "/nonexistent/cert.pem", - TLSKeyPath: keyPath, - EncryptionKeyID: "test-key-id", - Namespace: "test-namespace", - AuthManager: nil, - AuthType: "jwt", + Target: &utils.TargetConfig{ + ProxyId: "test-proxy-id", + TemporalCloud: utils.TemporalCloudConfig{ + Namespace: "test-namespace", + HostPort: "localhost:7233", + Authentication: utils.TemporalAuthConfig{ + TLS: &utils.TLSConfig{ + CertFile: "/nonexistent/cert.pem", + KeyFile: keyPath, + }, + }, + }, + EncryptionKey: "test-key-id", + }, + AuthManager: nil, + AuthType: "jwt", + MetricsHandler: metrics.NewMetricsHandler(metrics.MetricsHandlerOptions{}), + CryptoCachingConfig: nil, }, expectError: true, }, { name: "invalid key path", input: AddConnInput{ - ProxyId: "test-proxy-id", - Target: "localhost:7233", - TLSCertPath: certPath, - TLSKeyPath: "/nonexistent/key.pem", - EncryptionKeyID: "test-key-id", - Namespace: "test-namespace", - AuthManager: nil, - AuthType: "jwt", + Target: &utils.TargetConfig{ + ProxyId: "test-proxy-id", + TemporalCloud: utils.TemporalCloudConfig{ + Namespace: "test-namespace", + HostPort: "localhost:7233", + Authentication: utils.TemporalAuthConfig{ + TLS: &utils.TLSConfig{ + CertFile: certPath, + KeyFile: "/nonexistent/key.pem", + }, + }, + }, + EncryptionKey: "test-key-id", + }, + AuthManager: nil, + AuthType: "jwt", + MetricsHandler: metrics.NewMetricsHandler(metrics.MetricsHandlerOptions{}), + CryptoCachingConfig: nil, }, expectError: true, }, { - name: "connection without auth manager", + name: "both API key and TLS configured - should error", input: AddConnInput{ - ProxyId: "test-proxy-id-no-auth", - Target: "localhost:7233", - TLSCertPath: certPath, - TLSKeyPath: keyPath, - EncryptionKeyID: "test-key-id", - Namespace: "test-namespace", - AuthManager: nil, - AuthType: "", + Target: &utils.TargetConfig{ + ProxyId: "test-proxy-id", + TemporalCloud: utils.TemporalCloudConfig{ + Namespace: "test-namespace", + HostPort: "localhost:7233", + Authentication: utils.TemporalAuthConfig{ + ApiKey: &utils.TemporalApiKeyConfig{ + Value: "test-api-key", + }, + TLS: &utils.TLSConfig{ + CertFile: certPath, + KeyFile: keyPath, + }, + }, + }, + EncryptionKey: "test-key-id", + }, + AuthManager: nil, + AuthType: "jwt", + MetricsHandler: metrics.NewMetricsHandler(metrics.MetricsHandlerOptions{}), + CryptoCachingConfig: nil, }, - expectError: false, + expectError: true, + errorMsg: "cannot have both api key and mtls authentication", }, } @@ -188,7 +280,7 @@ func TestConn_AddConn(t *testing.T) { assert.Equal(t, 1, len(conn.namespace)) // Verify the connection was stored correctly - nsConn, exists := conn.namespace[tt.input.ProxyId] + nsConn, exists := conn.namespace[tt.input.Target.ProxyId] assert.True(t, exists) assert.NotNil(t, nsConn.conn) assert.Equal(t, tt.input.AuthManager, nsConn.authManager) @@ -430,14 +522,24 @@ func TestConn_ConcurrentAccess(t *testing.T) { defer wg.Done() input := AddConnInput{ - ProxyId: fmt.Sprintf("proxy-id-%d", id), - Target: "localhost:7233", - TLSCertPath: certPath, - TLSKeyPath: keyPath, - EncryptionKeyID: "test-key-id", - Namespace: fmt.Sprintf("namespace-%d", id), - AuthManager: nil, - AuthType: "jwt", + Target: &utils.TargetConfig{ + ProxyId: fmt.Sprintf("proxy-id-%d", id), + TemporalCloud: utils.TemporalCloudConfig{ + Namespace: fmt.Sprintf("namespace-%d", id), + HostPort: "localhost:7233", + Authentication: utils.TemporalAuthConfig{ + TLS: &utils.TLSConfig{ + CertFile: certPath, + KeyFile: keyPath, + }, + }, + }, + EncryptionKey: "test-key-id", + }, + AuthManager: nil, + AuthType: "jwt", + MetricsHandler: metrics.NewMetricsHandler(metrics.MetricsHandlerOptions{}), + CryptoCachingConfig: nil, } err := conn.AddConn(input) diff --git a/utils/config.go b/utils/config.go index 97c363d..cb32c2d 100644 --- a/utils/config.go +++ b/utils/config.go @@ -27,12 +27,26 @@ type CachingConfig struct { } type TargetConfig struct { - ProxyId string `yaml:"proxy_id"` - Target string `yaml:"target"` - TLS TLSConfig `yaml:"tls"` - EncryptionKey string `yaml:"encryption_key"` - Namespace string `yaml:"namespace"` - Authentication *AuthConfig `yaml:"authentication,omitempty"` + ProxyId string `yaml:"proxy_id"` + TemporalCloud TemporalCloudConfig `yaml:"temporal_cloud"` + EncryptionKey string `yaml:"encryption_key"` + Authentication *AuthConfig `yaml:"authentication,omitempty"` +} + +type TemporalCloudConfig struct { + Namespace string `yaml:"namespace"` + HostPort string `yaml:"host_port"` + Authentication TemporalAuthConfig `yaml:"authentication"` +} + +type TemporalAuthConfig struct { + TLS *TLSConfig `yaml:"tls,omitempty"` + ApiKey *TemporalApiKeyConfig `yaml:"api_key,omitempty"` +} + +type TemporalApiKeyConfig struct { + Value string `yaml:"value,omitempty"` + EnvVar string `yaml:"env,omitempty"` } type TLSConfig struct { diff --git a/utils/config_test.go b/utils/config_test.go index 53e1f30..6ef2639 100644 --- a/utils/config_test.go +++ b/utils/config_test.go @@ -14,19 +14,28 @@ func TestConfig_UnmarshalYAML(t *testing.T) { wantErr bool }{ { - name: "valid complete config", + name: "valid complete config with TLS authentication", yamlData: ` server: port: 7233 host: "0.0.0.0" +metrics: + port: 8080 +encryption: + caching: + max_cache: 100 + max_age: "1h" + max_usage: 1000 targets: - proxy_id: "test.namespace.internal" - target: "test.namespace.tmprl.cloud:7233" - tls: - cert_file: "/path/to/cert.crt" - key_file: "/path/to/key.key" + temporal_cloud: + namespace: "test.namespace" + host_port: "test.namespace.tmprl.cloud:7233" + authentication: + tls: + cert_file: "/path/to/cert.crt" + key_file: "/path/to/key.key" encryption_key: "test-key" - namespace: "test.namespace" authentication: type: "spiffe" config: @@ -40,16 +49,30 @@ targets: Port: 7233, Host: "0.0.0.0", }, + Metrics: MetricsConfig{ + Port: 8080, + }, + Encryption: EncryptionConfig{ + Caching: CachingConfig{ + MaxCache: 100, + MaxAge: "1h", + MaxUsage: 1000, + }, + }, Targets: []TargetConfig{ { - ProxyId: "test.namespace.internal", - Target: "test.namespace.tmprl.cloud:7233", - EncryptionKey: "test-key", - Namespace: "test.namespace", - TLS: TLSConfig{ - CertFile: "/path/to/cert.crt", - KeyFile: "/path/to/key.key", + ProxyId: "test.namespace.internal", + TemporalCloud: TemporalCloudConfig{ + Namespace: "test.namespace", + HostPort: "test.namespace.tmprl.cloud:7233", + Authentication: TemporalAuthConfig{ + TLS: &TLSConfig{ + CertFile: "/path/to/cert.crt", + KeyFile: "/path/to/key.key", + }, + }, }, + EncryptionKey: "test-key", Authentication: &AuthConfig{ Type: "spiffe", Config: map[string]interface{}{ @@ -64,35 +87,47 @@ targets: wantErr: false, }, { - name: "minimal config without authentication", + name: "valid config with API key authentication (value)", yamlData: ` server: port: 8080 host: "localhost" +metrics: + port: 9090 targets: - proxy_id: "simple.internal" - target: "simple.external:8080" - tls: - cert_file: "/cert.crt" - key_file: "/key.key" + temporal_cloud: + namespace: "simple" + host_port: "simple.external:8080" + authentication: + api_key: + value: "your-api-key-here" encryption_key: "simple-key" - namespace: "simple" `, want: Config{ Server: ServerConfig{ Port: 8080, Host: "localhost", }, + Metrics: MetricsConfig{ + Port: 9090, + }, + Encryption: EncryptionConfig{ + Caching: CachingConfig{}, + }, Targets: []TargetConfig{ { - ProxyId: "simple.internal", - Target: "simple.external:8080", - EncryptionKey: "simple-key", - Namespace: "simple", - TLS: TLSConfig{ - CertFile: "/cert.crt", - KeyFile: "/key.key", + ProxyId: "simple.internal", + TemporalCloud: TemporalCloudConfig{ + Namespace: "simple", + HostPort: "simple.external:8080", + Authentication: TemporalAuthConfig{ + ApiKey: &TemporalApiKeyConfig{ + Value: "your-api-key-here", + }, + }, }, + EncryptionKey: "simple-key", Authentication: nil, }, }, @@ -100,26 +135,82 @@ targets: wantErr: false, }, { - name: "multiple targets", + name: "valid config with API key authentication (env var)", + yamlData: ` +server: + port: 8080 + host: "localhost" +metrics: + port: 9090 +targets: + - proxy_id: "simple.internal" + temporal_cloud: + namespace: "simple" + host_port: "simple.external:8080" + authentication: + api_key: + env: "TEMPORAL_API_KEY" + encryption_key: "simple-key" +`, + want: Config{ + Server: ServerConfig{ + Port: 8080, + Host: "localhost", + }, + Metrics: MetricsConfig{ + Port: 9090, + }, + Encryption: EncryptionConfig{ + Caching: CachingConfig{}, + }, + Targets: []TargetConfig{ + { + ProxyId: "simple.internal", + TemporalCloud: TemporalCloudConfig{ + Namespace: "simple", + HostPort: "simple.external:8080", + Authentication: TemporalAuthConfig{ + ApiKey: &TemporalApiKeyConfig{ + EnvVar: "TEMPORAL_API_KEY", + }, + }, + }, + EncryptionKey: "simple-key", + Authentication: nil, + }, + }, + }, + wantErr: false, + }, + { + name: "multiple targets with mixed authentication", yamlData: ` server: port: 9090 host: "127.0.0.1" +metrics: + port: 8081 +encryption: + caching: + max_cache: 50 targets: - proxy_id: "target1.internal" - target: "target1.external:9090" - tls: - cert_file: "/target1.crt" - key_file: "/target1.key" + temporal_cloud: + namespace: "namespace1" + host_port: "target1.external:9090" + authentication: + tls: + cert_file: "/target1.crt" + key_file: "/target1.key" encryption_key: "key1" - namespace: "namespace1" - proxy_id: "target2.internal" - target: "target2.external:9091" - tls: - cert_file: "/target2.crt" - key_file: "/target2.key" + temporal_cloud: + namespace: "namespace2" + host_port: "target2.external:9091" + authentication: + api_key: + value: "target2-api-key" encryption_key: "key2" - namespace: "namespace2" authentication: type: "oauth" config: @@ -131,27 +222,42 @@ targets: Port: 9090, Host: "127.0.0.1", }, + Metrics: MetricsConfig{ + Port: 8081, + }, + Encryption: EncryptionConfig{ + Caching: CachingConfig{ + MaxCache: 50, + }, + }, Targets: []TargetConfig{ { - ProxyId: "target1.internal", - Target: "target1.external:9090", - EncryptionKey: "key1", - Namespace: "namespace1", - TLS: TLSConfig{ - CertFile: "/target1.crt", - KeyFile: "/target1.key", + ProxyId: "target1.internal", + TemporalCloud: TemporalCloudConfig{ + Namespace: "namespace1", + HostPort: "target1.external:9090", + Authentication: TemporalAuthConfig{ + TLS: &TLSConfig{ + CertFile: "/target1.crt", + KeyFile: "/target1.key", + }, + }, }, + EncryptionKey: "key1", Authentication: nil, }, { - ProxyId: "target2.internal", - Target: "target2.external:9091", - EncryptionKey: "key2", - Namespace: "namespace2", - TLS: TLSConfig{ - CertFile: "/target2.crt", - KeyFile: "/target2.key", + ProxyId: "target2.internal", + TemporalCloud: TemporalCloudConfig{ + Namespace: "namespace2", + HostPort: "target2.external:9091", + Authentication: TemporalAuthConfig{ + ApiKey: &TemporalApiKeyConfig{ + Value: "target2-api-key", + }, + }, }, + EncryptionKey: "key2", Authentication: &AuthConfig{ Type: "oauth", Config: map[string]interface{}{ @@ -244,14 +350,18 @@ func TestServerConfig_Validation(t *testing.T) { func TestTargetConfig_Structure(t *testing.T) { target := TargetConfig{ - ProxyId: "test.internal", - Target: "test.external:7233", - EncryptionKey: "test-key", - Namespace: "test-namespace", - TLS: TLSConfig{ - CertFile: "/path/to/cert.crt", - KeyFile: "/path/to/key.key", + ProxyId: "test.internal", + TemporalCloud: TemporalCloudConfig{ + Namespace: "test-namespace", + HostPort: "test.external:7233", + Authentication: TemporalAuthConfig{ + TLS: &TLSConfig{ + CertFile: "/path/to/cert.crt", + KeyFile: "/path/to/key.key", + }, + }, }, + EncryptionKey: "test-key", Authentication: &AuthConfig{ Type: "spiffe", Config: map[string]interface{}{ @@ -263,20 +373,20 @@ func TestTargetConfig_Structure(t *testing.T) { if target.ProxyId != "test.internal" { t.Errorf("Expected ProxyId to be 'test.internal', got %s", target.ProxyId) } - if target.Target != "test.external:7233" { - t.Errorf("Expected Target to be 'test.external:7233', got %s", target.Target) + if target.TemporalCloud.HostPort != "test.external:7233" { + t.Errorf("Expected TemporalCloud.HostPort to be 'test.external:7233', got %s", target.TemporalCloud.HostPort) } if target.EncryptionKey != "test-key" { t.Errorf("Expected EncryptionKey to be 'test-key', got %s", target.EncryptionKey) } - if target.Namespace != "test-namespace" { - t.Errorf("Expected Namespace to be 'test-namespace', got %s", target.Namespace) + if target.TemporalCloud.Namespace != "test-namespace" { + t.Errorf("Expected TemporalCloud.Namespace to be 'test-namespace', got %s", target.TemporalCloud.Namespace) } - if target.TLS.CertFile != "/path/to/cert.crt" { - t.Errorf("Expected TLS.CertFile to be '/path/to/cert.crt', got %s", target.TLS.CertFile) + if target.TemporalCloud.Authentication.TLS.CertFile != "/path/to/cert.crt" { + t.Errorf("Expected TemporalCloud.Authentication.TLS.CertFile to be '/path/to/cert.crt', got %s", target.TemporalCloud.Authentication.TLS.CertFile) } - if target.TLS.KeyFile != "/path/to/key.key" { - t.Errorf("Expected TLS.KeyFile to be '/path/to/key.key', got %s", target.TLS.KeyFile) + if target.TemporalCloud.Authentication.TLS.KeyFile != "/path/to/key.key" { + t.Errorf("Expected TemporalCloud.Authentication.TLS.KeyFile to be '/path/to/key.key', got %s", target.TemporalCloud.Authentication.TLS.KeyFile) } if target.Authentication == nil { t.Error("Expected Authentication to not be nil") @@ -342,6 +452,187 @@ func TestAuthConfig_Types(t *testing.T) { } } +func TestMetricsConfig_Structure(t *testing.T) { + tests := []struct { + name string + config MetricsConfig + want int + }{ + { + name: "default metrics port", + config: MetricsConfig{Port: 8080}, + want: 8080, + }, + { + name: "custom metrics port", + config: MetricsConfig{Port: 9090}, + want: 9090, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.config.Port != tt.want { + t.Errorf("Expected Port to be %d, got %d", tt.want, tt.config.Port) + } + }) + } +} + +func TestEncryptionConfig_Structure(t *testing.T) { + tests := []struct { + name string + config EncryptionConfig + want CachingConfig + }{ + { + name: "full caching config", + config: EncryptionConfig{ + Caching: CachingConfig{ + MaxCache: 100, + MaxAge: "1h", + MaxUsage: 1000, + }, + }, + want: CachingConfig{ + MaxCache: 100, + MaxAge: "1h", + MaxUsage: 1000, + }, + }, + { + name: "partial caching config", + config: EncryptionConfig{ + Caching: CachingConfig{ + MaxCache: 50, + }, + }, + want: CachingConfig{ + MaxCache: 50, + }, + }, + { + name: "empty caching config", + config: EncryptionConfig{ + Caching: CachingConfig{}, + }, + want: CachingConfig{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.config.Caching.MaxCache != tt.want.MaxCache { + t.Errorf("Expected MaxCache to be %d, got %d", tt.want.MaxCache, tt.config.Caching.MaxCache) + } + if tt.config.Caching.MaxAge != tt.want.MaxAge { + t.Errorf("Expected MaxAge to be %s, got %s", tt.want.MaxAge, tt.config.Caching.MaxAge) + } + if tt.config.Caching.MaxUsage != tt.want.MaxUsage { + t.Errorf("Expected MaxUsage to be %d, got %d", tt.want.MaxUsage, tt.config.Caching.MaxUsage) + } + }) + } +} + +func TestTemporalAuthConfig_Structure(t *testing.T) { + tests := []struct { + name string + config TemporalAuthConfig + desc string + }{ + { + name: "TLS authentication", + config: TemporalAuthConfig{ + TLS: &TLSConfig{ + CertFile: "/path/to/cert.crt", + KeyFile: "/path/to/key.key", + }, + }, + desc: "should have TLS config and no API key", + }, + { + name: "API key authentication with value", + config: TemporalAuthConfig{ + ApiKey: &TemporalApiKeyConfig{ + Value: "test-api-key", + }, + }, + desc: "should have API key and no TLS config", + }, + { + name: "API key authentication with env var", + config: TemporalAuthConfig{ + ApiKey: &TemporalApiKeyConfig{ + EnvVar: "TEMPORAL_API_KEY", + }, + }, + desc: "should have API key env var and no TLS config", + }, + { + name: "empty authentication", + config: TemporalAuthConfig{}, + desc: "should have neither TLS nor API key", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + switch tt.name { + case "TLS authentication": + if tt.config.TLS == nil { + t.Error("Expected TLS to not be nil") + } else { + if tt.config.TLS.CertFile != "/path/to/cert.crt" { + t.Errorf("Expected CertFile to be '/path/to/cert.crt', got %s", tt.config.TLS.CertFile) + } + if tt.config.TLS.KeyFile != "/path/to/key.key" { + t.Errorf("Expected KeyFile to be '/path/to/key.key', got %s", tt.config.TLS.KeyFile) + } + } + if tt.config.ApiKey != nil { + t.Error("Expected ApiKey to be nil") + } + case "API key authentication with value": + if tt.config.ApiKey == nil { + t.Error("Expected ApiKey to not be nil") + } else { + if tt.config.ApiKey.Value != "test-api-key" { + t.Errorf("Expected ApiKey.Value to be 'test-api-key', got %s", tt.config.ApiKey.Value) + } + if tt.config.ApiKey.EnvVar != "" { + t.Errorf("Expected ApiKey.EnvVar to be empty, got %s", tt.config.ApiKey.EnvVar) + } + } + if tt.config.TLS != nil { + t.Error("Expected TLS to be nil") + } + case "API key authentication with env var": + if tt.config.ApiKey == nil { + t.Error("Expected ApiKey to not be nil") + } else { + if tt.config.ApiKey.EnvVar != "TEMPORAL_API_KEY" { + t.Errorf("Expected ApiKey.EnvVar to be 'TEMPORAL_API_KEY', got %s", tt.config.ApiKey.EnvVar) + } + if tt.config.ApiKey.Value != "" { + t.Errorf("Expected ApiKey.Value to be empty, got %s", tt.config.ApiKey.Value) + } + } + if tt.config.TLS != nil { + t.Error("Expected TLS to be nil") + } + case "empty authentication": + if tt.config.TLS != nil { + t.Error("Expected TLS to be nil") + } + if tt.config.ApiKey != nil { + t.Error("Expected ApiKey to be nil") + } + } + }) + } +} + // Helper function to compare Config structs func configEqual(a, b Config) bool { if a.Server.Port != b.Server.Port || a.Server.Host != b.Server.Host { @@ -363,14 +654,40 @@ func configEqual(a, b Config) bool { } func targetConfigEqual(a, b TargetConfig) bool { - if a.ProxyId != b.ProxyId || a.Target != b.Target || a.EncryptionKey != b.EncryptionKey || a.Namespace != b.Namespace { + if a.ProxyId != b.ProxyId || a.EncryptionKey != b.EncryptionKey { + return false + } + + // Compare TemporalCloud configuration + if a.TemporalCloud.Namespace != b.TemporalCloud.Namespace || a.TemporalCloud.HostPort != b.TemporalCloud.HostPort { + return false + } + + // Compare TemporalCloud Authentication - API Key + if (a.TemporalCloud.Authentication.ApiKey == nil) != (b.TemporalCloud.Authentication.ApiKey == nil) { return false } - if a.TLS.CertFile != b.TLS.CertFile || a.TLS.KeyFile != b.TLS.KeyFile { + if a.TemporalCloud.Authentication.ApiKey != nil && b.TemporalCloud.Authentication.ApiKey != nil { + if a.TemporalCloud.Authentication.ApiKey.Value != b.TemporalCloud.Authentication.ApiKey.Value || + a.TemporalCloud.Authentication.ApiKey.EnvVar != b.TemporalCloud.Authentication.ApiKey.EnvVar { + return false + } + } + + // Compare TLS configuration + if (a.TemporalCloud.Authentication.TLS == nil) != (b.TemporalCloud.Authentication.TLS == nil) { return false } + if a.TemporalCloud.Authentication.TLS != nil && b.TemporalCloud.Authentication.TLS != nil { + if a.TemporalCloud.Authentication.TLS.CertFile != b.TemporalCloud.Authentication.TLS.CertFile || + a.TemporalCloud.Authentication.TLS.KeyFile != b.TemporalCloud.Authentication.TLS.KeyFile { + return false + } + } + + // Compare proxy Authentication (spiffe, oauth, etc.) if (a.Authentication == nil) != (b.Authentication == nil) { return false }