Skip to content

Commit 4b59acf

Browse files
authored
Merge branch 'trunk' into phillip/adbc-reauth-on-session-expiry
2 parents 9a03ffa + 418e439 commit 4b59acf

3 files changed

Lines changed: 127 additions & 17 deletions

File tree

.github/workflows/go.yml

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -77,32 +77,68 @@ jobs:
7777
- name: Init and start spice app (Windows)
7878
if: matrix.os == 'windows-latest'
7979
run: |
80+
spice install
8081
spice init spice_qs
8182
cd spice_qs
8283
spice add spiceai/quickstart
83-
Start-Process -FilePath "spice" -ArgumentList "run" -RedirectStandardOutput "spice.log" -RedirectStandardError "spice.err.log"
84-
# Wait for Spice to be ready
85-
Write-Host "Waiting for Spice to be ready..."
84+
shell: pwsh
85+
86+
- name: Test
87+
if: matrix.os != 'windows-latest'
88+
env:
89+
SPICE_API_KEY: ${{ secrets.SPICE_CLOUD_API_KEY }}
90+
run: go test -v ./...
91+
92+
- name: Start runtime and test (Windows)
93+
if: matrix.os == 'windows-latest'
94+
env:
95+
SPICE_API_KEY: ${{ secrets.SPICE_CLOUD_API_KEY }}
96+
run: |
97+
$spicedPath = Join-Path $HOME ".spice\bin\spiced"
98+
$spicedDir = Join-Path (Get-Location) "spice_qs"
99+
$logPath = Join-Path $spicedDir "spice.log"
100+
$errPath = Join-Path $spicedDir "spice.err.log"
101+
$proc = Start-Process -FilePath $spicedPath -WorkingDirectory $spicedDir -RedirectStandardOutput $logPath -RedirectStandardError $errPath -PassThru
102+
Write-Host "Started spiced PID=$($proc.Id)"
103+
# Wait for Spice HTTP to be ready
104+
Write-Host "Waiting for Spice HTTP to be ready..."
86105
for ($i = 1; $i -le 60; $i++) {
87106
try {
88-
$response = Invoke-WebRequest -Uri "http://localhost:8090/v1/ready" -UseBasicParsing -ErrorAction Stop
107+
$response = Invoke-WebRequest -Uri "http://127.0.0.1:8090/v1/ready" -UseBasicParsing -ErrorAction Stop
89108
if ($response.Content -match "ready") {
90-
Write-Host "Spice is ready!"
109+
Write-Host "Spice HTTP is ready!"
91110
break
92111
}
93-
} catch {
94-
# Ignore errors, keep waiting
95-
}
96-
Write-Host "Waiting... ($i/60)"
112+
} catch { }
113+
Write-Host "Waiting HTTP... ($i/60)"
97114
Start-Sleep -Seconds 1
98115
}
116+
# Wait for Spice Flight (gRPC) to be ready by checking the TCP port
117+
Write-Host "Waiting for Spice Flight to be ready..."
118+
for ($i = 1; $i -le 60; $i++) {
119+
$tcp = New-Object System.Net.Sockets.TcpClient
120+
try {
121+
$tcp.Connect("127.0.0.1", 50051)
122+
if ($tcp.Connected) {
123+
Write-Host "Spice Flight is ready!"
124+
$tcp.Close()
125+
break
126+
}
127+
} catch { }
128+
finally { $tcp.Close() }
129+
Write-Host "Waiting Flight... ($i/60)"
130+
Start-Sleep -Seconds 1
131+
}
132+
try {
133+
go test -v ./...
134+
$testExit = $LASTEXITCODE
135+
} finally {
136+
Write-Host "Stopping spiced PID=$($proc.Id)"
137+
Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue
138+
}
139+
exit $testExit
99140
shell: pwsh
100141

101-
- name: Test
102-
env:
103-
SPICE_API_KEY: ${{ secrets.SPICE_CLOUD_API_KEY }}
104-
run: go test -v ./...
105-
106142
- name: Print Spice logs (Unix)
107143
if: always() && matrix.os != 'windows-latest'
108144
run: |

client.go

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ package gospice
22

33
import (
44
"context"
5+
"crypto/tls"
56
"crypto/x509"
67
"fmt"
78
"io"
89
"math"
910
"net/http"
11+
"os"
1012
"strings"
1113
"sync"
1214
"time"
@@ -44,6 +46,10 @@ type SpiceClient struct {
4446
backoffPolicy backoff.BackOff
4547
maxRetries uint
4648
userAgent string
49+
50+
tlsClientCertFile string
51+
tlsClientKeyFile string
52+
tlsRootCertFile string
4753
}
4854

4955
// NewSpiceClient creates a new SpiceClient
@@ -116,6 +122,32 @@ func WithSpiceCloudAddress() SpiceClientModifier {
116122
}
117123
}
118124

125+
// WithTLSClientCertificate configures the client to present a client certificate
126+
// during the TLS handshake for mutual TLS (mTLS) authentication.
127+
// Both certFile and keyFile must be PEM-encoded.
128+
func WithTLSClientCertificate(certFile, keyFile string) SpiceClientModifier {
129+
return func(c *SpiceClient) error {
130+
if certFile == "" || keyFile == "" {
131+
return fmt.Errorf("both certFile and keyFile are required for mTLS")
132+
}
133+
c.tlsClientCertFile = certFile
134+
c.tlsClientKeyFile = keyFile
135+
return nil
136+
}
137+
}
138+
139+
// WithTLSRootCertificate configures the client to use a custom CA certificate
140+
// file for server verification instead of (in addition to) the system certificate store.
141+
func WithTLSRootCertificate(caFile string) SpiceClientModifier {
142+
return func(c *SpiceClient) error {
143+
if caFile == "" {
144+
return fmt.Errorf("caFile is required")
145+
}
146+
c.tlsRootCertFile = caFile
147+
return nil
148+
}
149+
}
150+
119151
// Init initializes the SpiceClient
120152
func (c *SpiceClient) Init(opts ...SpiceClientModifier) error {
121153
for _, opt := range opts {
@@ -130,13 +162,42 @@ func (c *SpiceClient) Init(opts ...SpiceClientModifier) error {
130162
return fmt.Errorf("error getting system cert pool: %w", err)
131163
}
132164

165+
if c.tlsRootCertFile != "" {
166+
caPem, err := os.ReadFile(c.tlsRootCertFile)
167+
if err != nil {
168+
return fmt.Errorf("error reading TLS root certificate '%s': %w", c.tlsRootCertFile, err)
169+
}
170+
if !systemCertPool.AppendCertsFromPEM(caPem) {
171+
return fmt.Errorf("failed to append CA certificate from '%s'", c.tlsRootCertFile)
172+
}
173+
}
174+
133175
flightClient, err := c.createClient(c.flightAddress, systemCertPool)
134176
if err != nil {
135177
return fmt.Errorf("error creating Spice Flight client: %w", err)
136178
}
137179

138180
c.flightClient = flightClient
139181

182+
// Update the HTTP client transport with the same TLS configuration
183+
// (custom CA and/or client certificate) used by the Flight client.
184+
httpTlsConfig := &tls.Config{
185+
RootCAs: systemCertPool,
186+
MinVersion: tls.VersionTLS12,
187+
}
188+
if c.tlsClientCertFile != "" && c.tlsClientKeyFile != "" {
189+
clientCert, err := tls.LoadX509KeyPair(c.tlsClientCertFile, c.tlsClientKeyFile)
190+
if err != nil {
191+
return fmt.Errorf("error loading client certificate for HTTP mTLS: %w", err)
192+
}
193+
httpTlsConfig.Certificates = []tls.Certificate{clientCert}
194+
}
195+
c.httpClient.Transport = &http.Transport{
196+
MaxIdleConnsPerHost: 10,
197+
DisableCompression: false,
198+
TLSClientConfig: httpTlsConfig,
199+
}
200+
140201
// Initialize ADBC client - non-fatal, will be initialized lazily if needed
141202
// This allows health checks to work even if ADBC connection fails initially
142203
_ = c.initADBC()
@@ -228,7 +289,20 @@ func (c *SpiceClient) createClient(address string, systemCertPool *x509.CertPool
228289
address = strings.TrimPrefix(address, "grpc://")
229290
grpcDialOpts = append(grpcDialOpts, grpc.WithTransportCredentials(insecure.NewCredentials()))
230291
} else {
231-
grpcDialOpts = append(grpcDialOpts, grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(systemCertPool, "")))
292+
tlsConfig := &tls.Config{
293+
RootCAs: systemCertPool,
294+
MinVersion: tls.VersionTLS12,
295+
}
296+
297+
if c.tlsClientCertFile != "" && c.tlsClientKeyFile != "" {
298+
clientCert, err := tls.LoadX509KeyPair(c.tlsClientCertFile, c.tlsClientKeyFile)
299+
if err != nil {
300+
return nil, fmt.Errorf("error loading client certificate for mTLS: %w", err)
301+
}
302+
tlsConfig.Certificates = []tls.Certificate{clientCert}
303+
}
304+
305+
grpcDialOpts = append(grpcDialOpts, grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)))
232306
}
233307

234308
client, err := flight.NewClientWithMiddleware(

config.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ func LoadConfig() ClientConfig {
2323

2424
func LoadLocalConfig() ClientConfig {
2525
return ClientConfig{
26-
HttpUrl: getEnvOrDefault("SPICE_LOCAL_HTTP_URL", "http://localhost:8090"),
27-
FlightUrl: getEnvOrDefault("SPICE_LOCAL_FLIGHT_URL", "grpc://localhost:50051"),
26+
HttpUrl: getEnvOrDefault("SPICE_LOCAL_HTTP_URL", "http://127.0.0.1:8090"),
27+
FlightUrl: getEnvOrDefault("SPICE_LOCAL_FLIGHT_URL", "grpc://127.0.0.1:50051"),
2828
}
2929
}
3030

0 commit comments

Comments
 (0)