Skip to content

Commit dffd080

Browse files
JoshVanLmikeee
authored andcommitted
Adds DAPR_GRPC_ENPOINT support to client (dapr#475)
* Adds `DAPR_GRPC_ENPOINT` support to client PR adds `DAPR_GRPC_ENPOINT` environment variable support to client. Address parser respects http[s] schemes, and TLS query options, as per [0008-S-sidecar-endpoint-tls.md](https://github.com/dapr/proposals/blob/main/0008-S-sidecar-endpoint-tls.md). `DAPR_GRPC_ENDPONT` takes precedence over `DAPR_GRPC_PORT`. Signed-off-by: joshvanl <[email protected]> * Remove errors.Join to have compatibility with Go 1.19 Signed-off-by: joshvanl <[email protected]> --------- Signed-off-by: joshvanl <[email protected]>
1 parent 61356f4 commit dffd080

File tree

3 files changed

+510
-11
lines changed

3 files changed

+510
-11
lines changed

client/client.go

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ package client
1515

1616
import (
1717
"context"
18+
"crypto/tls"
1819
"errors"
1920
"fmt"
2021
"io"
@@ -28,9 +29,11 @@ import (
2829

2930
"github.com/dapr/go-sdk/actor"
3031
"github.com/dapr/go-sdk/actor/config"
32+
"github.com/dapr/go-sdk/client/internal"
3133
"github.com/dapr/go-sdk/version"
3234

3335
"google.golang.org/grpc"
36+
"google.golang.org/grpc/credentials"
3437
"google.golang.org/grpc/credentials/insecure"
3538
"google.golang.org/grpc/metadata"
3639

@@ -43,6 +46,7 @@ import (
4346
const (
4447
daprPortDefault = "50001"
4548
daprPortEnvVarName = "DAPR_GRPC_PORT" /* #nosec */
49+
daprGRPCEndpointEnvVarName = "DAPR_GRPC_ENDPOINT"
4650
traceparentKey = "traceparent"
4751
apiTokenKey = "dapr-api-token" /* #nosec */
4852
apiTokenEnvVarName = "DAPR_API_TOKEN" /* #nosec */
@@ -219,18 +223,28 @@ type Client interface {
219223
// NewClientWithConnection(conn *grpc.ClientConn) Client
220224
// NewClientWithSocket(socket string) (client Client, err error)
221225
func NewClient() (client Client, err error) {
222-
port := os.Getenv(daprPortEnvVarName)
223-
if port == "" {
224-
port = daprPortDefault
225-
}
226-
if defaultClient != nil {
227-
return defaultClient, nil
228-
}
229226
lock.Lock()
230227
defer lock.Unlock()
228+
231229
if defaultClient != nil {
232230
return defaultClient, nil
233231
}
232+
233+
addr, ok := os.LookupEnv(daprGRPCEndpointEnvVarName)
234+
if ok {
235+
client, err = NewClientWithAddress(addr)
236+
if err != nil {
237+
return nil, fmt.Errorf("error creating %q client: %w", daprGRPCEndpointEnvVarName, err)
238+
}
239+
defaultClient = client
240+
return defaultClient, nil
241+
}
242+
243+
port, ok := os.LookupEnv(daprPortEnvVarName)
244+
if !ok {
245+
port = daprPortDefault
246+
}
247+
234248
c, err := NewClientWithPort(port)
235249
if err != nil {
236250
return nil, fmt.Errorf("error creating default client: %w", err)
@@ -266,13 +280,28 @@ func NewClientWithAddressContext(ctx context.Context, address string) (client Cl
266280
if err != nil {
267281
return nil, err
268282
}
283+
284+
parsedAddress, err := internal.ParseGRPCEndpoint(address)
285+
if err != nil {
286+
return nil, fmt.Errorf("error parsing address '%s': %w", address, err)
287+
}
288+
289+
opts := []grpc.DialOption{
290+
grpc.WithUserAgent(userAgent()),
291+
grpc.WithBlock(),
292+
}
293+
294+
if parsedAddress.TLS {
295+
opts = append(opts, grpc.WithTransportCredentials(credentials.NewTLS(new(tls.Config))))
296+
} else {
297+
opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))
298+
}
299+
269300
ctx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
270301
conn, err := grpc.DialContext(
271302
ctx,
272-
address,
273-
grpc.WithTransportCredentials(insecure.NewCredentials()),
274-
grpc.WithUserAgent(userAgent()),
275-
grpc.WithBlock(),
303+
parsedAddress.Target,
304+
opts...,
276305
)
277306
cancel()
278307
if err != nil {

client/internal/parse.go

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
/*
2+
Copyright 2023 The Dapr Authors
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
http://www.apache.org/licenses/LICENSE-2.0
7+
Unless required by applicable law or agreed to in writing, software
8+
distributed under the License is distributed on an "AS IS" BASIS,
9+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
14+
package internal
15+
16+
import (
17+
"errors"
18+
"fmt"
19+
"net"
20+
"net/url"
21+
"strings"
22+
)
23+
24+
// Parsed represents a parsed gRPC endpoint.
25+
type Parsed struct {
26+
Target string
27+
TLS bool
28+
}
29+
30+
//nolint:revive
31+
func ParseGRPCEndpoint(endpoint string) (Parsed, error) {
32+
target := endpoint
33+
if len(target) == 0 {
34+
return Parsed{}, errors.New("target is required")
35+
}
36+
37+
var dnsAuthority string
38+
var hostname string
39+
var tls bool
40+
41+
urlSplit := strings.Split(target, ":")
42+
if len(urlSplit) == 3 && !strings.Contains(target, "://") {
43+
target = strings.Replace(target, ":", "://", 1)
44+
} else if len(urlSplit) >= 2 && !strings.Contains(target, "://") && schemeKnown(urlSplit[0]) {
45+
target = strings.Replace(target, ":", "://", 1)
46+
} else {
47+
urlSplit = strings.Split(target, "://")
48+
if len(urlSplit) == 1 {
49+
target = "dns://" + target
50+
} else {
51+
scheme := urlSplit[0]
52+
if !schemeKnown(scheme) {
53+
return Parsed{}, fmt.Errorf(("unknown scheme: %q"), scheme)
54+
}
55+
56+
if scheme == "dns" {
57+
urlSplit = strings.Split(target, "/")
58+
if len(urlSplit) < 4 {
59+
return Parsed{}, fmt.Errorf("invalid dns scheme: %q", target)
60+
}
61+
dnsAuthority = urlSplit[2]
62+
target = "dns://" + urlSplit[3]
63+
}
64+
}
65+
}
66+
67+
ptarget, err := url.Parse(target)
68+
if err != nil {
69+
return Parsed{}, err
70+
}
71+
72+
var errs []string
73+
for k := range ptarget.Query() {
74+
if k != "tls" {
75+
errs = append(errs, fmt.Sprintf("unrecognized query parameter: %q", k))
76+
}
77+
}
78+
if len(errs) > 0 {
79+
return Parsed{}, fmt.Errorf("failed to parse target %q: %s", target, strings.Join(errs, "; "))
80+
}
81+
82+
if ptarget.Query().Has("tls") {
83+
if ptarget.Scheme == "http" || ptarget.Scheme == "https" {
84+
return Parsed{}, errors.New("cannot use tls query parameter with http(s) scheme")
85+
}
86+
87+
qtls := ptarget.Query().Get("tls")
88+
if qtls != "true" && qtls != "false" {
89+
return Parsed{}, fmt.Errorf("invalid value for tls query parameter: %q", qtls)
90+
}
91+
92+
tls = qtls == "true"
93+
}
94+
95+
scheme := ptarget.Scheme
96+
if scheme == "https" {
97+
tls = true
98+
}
99+
if scheme == "http" || scheme == "https" {
100+
scheme = "dns"
101+
}
102+
103+
hostname = ptarget.Host
104+
105+
host, port, err := net.SplitHostPort(hostname)
106+
aerr, ok := err.(*net.AddrError)
107+
if ok && aerr.Err == "missing port in address" {
108+
port = "443"
109+
} else if err != nil {
110+
return Parsed{}, err
111+
} else {
112+
hostname = host
113+
}
114+
115+
if len(hostname) == 0 {
116+
if scheme == "dns" {
117+
hostname = "localhost"
118+
} else {
119+
hostname = ptarget.Path
120+
}
121+
}
122+
123+
switch scheme {
124+
case "unix":
125+
separator := ":"
126+
if strings.HasPrefix(endpoint, "unix://") {
127+
separator = "://"
128+
}
129+
target = scheme + separator + hostname
130+
131+
case "vsock":
132+
target = scheme + ":" + hostname + ":" + port
133+
134+
case "unix-abstract":
135+
target = scheme + ":" + hostname
136+
137+
case "dns":
138+
if len(ptarget.Path) > 0 {
139+
return Parsed{}, fmt.Errorf("path is not allowed: %q", ptarget.Path)
140+
}
141+
142+
if strings.Count(hostname, ":") == 7 && !strings.HasPrefix(hostname, "[") && !strings.HasSuffix(hostname, "]") {
143+
hostname = "[" + hostname + "]"
144+
}
145+
if len(dnsAuthority) > 0 {
146+
dnsAuthority = "//" + dnsAuthority + "/"
147+
}
148+
target = scheme + ":" + dnsAuthority + hostname + ":" + port
149+
150+
default:
151+
return Parsed{}, fmt.Errorf("unsupported scheme: %q", scheme)
152+
}
153+
154+
return Parsed{
155+
Target: target,
156+
TLS: tls,
157+
}, nil
158+
}
159+
160+
func schemeKnown(scheme string) bool {
161+
for _, s := range []string{
162+
"dns",
163+
"unix",
164+
"unix-abstract",
165+
"vsock",
166+
"http",
167+
"https",
168+
"grpc",
169+
"grpcs",
170+
} {
171+
if scheme == s {
172+
return true
173+
}
174+
}
175+
176+
return false
177+
}

0 commit comments

Comments
 (0)