Adds `DAPR_GRPC_ENPOINT` support to client (#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 <me@joshvanl.dev>

* Remove errors.Join to have compatibility with Go 1.19

Signed-off-by: joshvanl <me@joshvanl.dev>

---------

Signed-off-by: joshvanl <me@joshvanl.dev>
This commit is contained in:
Josh van Leeuwen 2023-12-01 19:19:56 +01:00 committed by GitHub
parent ae8becfcd3
commit 224a159b98
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 510 additions and 11 deletions

View File

@ -15,6 +15,7 @@ package client
import ( import (
"context" "context"
"crypto/tls"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@ -28,9 +29,11 @@ import (
"github.com/dapr/go-sdk/actor" "github.com/dapr/go-sdk/actor"
"github.com/dapr/go-sdk/actor/config" "github.com/dapr/go-sdk/actor/config"
"github.com/dapr/go-sdk/client/internal"
"github.com/dapr/go-sdk/version" "github.com/dapr/go-sdk/version"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata" "google.golang.org/grpc/metadata"
@ -43,6 +46,7 @@ import (
const ( const (
daprPortDefault = "50001" daprPortDefault = "50001"
daprPortEnvVarName = "DAPR_GRPC_PORT" /* #nosec */ daprPortEnvVarName = "DAPR_GRPC_PORT" /* #nosec */
daprGRPCEndpointEnvVarName = "DAPR_GRPC_ENDPOINT"
traceparentKey = "traceparent" traceparentKey = "traceparent"
apiTokenKey = "dapr-api-token" /* #nosec */ apiTokenKey = "dapr-api-token" /* #nosec */
apiTokenEnvVarName = "DAPR_API_TOKEN" /* #nosec */ apiTokenEnvVarName = "DAPR_API_TOKEN" /* #nosec */
@ -219,18 +223,28 @@ type Client interface {
// NewClientWithConnection(conn *grpc.ClientConn) Client // NewClientWithConnection(conn *grpc.ClientConn) Client
// NewClientWithSocket(socket string) (client Client, err error) // NewClientWithSocket(socket string) (client Client, err error)
func NewClient() (client Client, err error) { func NewClient() (client Client, err error) {
port := os.Getenv(daprPortEnvVarName)
if port == "" {
port = daprPortDefault
}
if defaultClient != nil {
return defaultClient, nil
}
lock.Lock() lock.Lock()
defer lock.Unlock() defer lock.Unlock()
if defaultClient != nil { if defaultClient != nil {
return defaultClient, nil return defaultClient, nil
} }
addr, ok := os.LookupEnv(daprGRPCEndpointEnvVarName)
if ok {
client, err = NewClientWithAddress(addr)
if err != nil {
return nil, fmt.Errorf("error creating %q client: %w", daprGRPCEndpointEnvVarName, err)
}
defaultClient = client
return defaultClient, nil
}
port, ok := os.LookupEnv(daprPortEnvVarName)
if !ok {
port = daprPortDefault
}
c, err := NewClientWithPort(port) c, err := NewClientWithPort(port)
if err != nil { if err != nil {
return nil, fmt.Errorf("error creating default client: %w", err) return nil, fmt.Errorf("error creating default client: %w", err)
@ -266,13 +280,28 @@ func NewClientWithAddressContext(ctx context.Context, address string) (client Cl
if err != nil { if err != nil {
return nil, err return nil, err
} }
parsedAddress, err := internal.ParseGRPCEndpoint(address)
if err != nil {
return nil, fmt.Errorf("error parsing address '%s': %w", address, err)
}
opts := []grpc.DialOption{
grpc.WithUserAgent(userAgent()),
grpc.WithBlock(),
}
if parsedAddress.TLS {
opts = append(opts, grpc.WithTransportCredentials(credentials.NewTLS(new(tls.Config))))
} else {
opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))
}
ctx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second) ctx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSeconds)*time.Second)
conn, err := grpc.DialContext( conn, err := grpc.DialContext(
ctx, ctx,
address, parsedAddress.Target,
grpc.WithTransportCredentials(insecure.NewCredentials()), opts...,
grpc.WithUserAgent(userAgent()),
grpc.WithBlock(),
) )
cancel() cancel()
if err != nil { if err != nil {

177
client/internal/parse.go Normal file
View File

@ -0,0 +1,177 @@
/*
Copyright 2023 The Dapr Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package internal
import (
"errors"
"fmt"
"net"
"net/url"
"strings"
)
// Parsed represents a parsed gRPC endpoint.
type Parsed struct {
Target string
TLS bool
}
//nolint:revive
func ParseGRPCEndpoint(endpoint string) (Parsed, error) {
target := endpoint
if len(target) == 0 {
return Parsed{}, errors.New("target is required")
}
var dnsAuthority string
var hostname string
var tls bool
urlSplit := strings.Split(target, ":")
if len(urlSplit) == 3 && !strings.Contains(target, "://") {
target = strings.Replace(target, ":", "://", 1)
} else if len(urlSplit) >= 2 && !strings.Contains(target, "://") && schemeKnown(urlSplit[0]) {
target = strings.Replace(target, ":", "://", 1)
} else {
urlSplit = strings.Split(target, "://")
if len(urlSplit) == 1 {
target = "dns://" + target
} else {
scheme := urlSplit[0]
if !schemeKnown(scheme) {
return Parsed{}, fmt.Errorf(("unknown scheme: %q"), scheme)
}
if scheme == "dns" {
urlSplit = strings.Split(target, "/")
if len(urlSplit) < 4 {
return Parsed{}, fmt.Errorf("invalid dns scheme: %q", target)
}
dnsAuthority = urlSplit[2]
target = "dns://" + urlSplit[3]
}
}
}
ptarget, err := url.Parse(target)
if err != nil {
return Parsed{}, err
}
var errs []string
for k := range ptarget.Query() {
if k != "tls" {
errs = append(errs, fmt.Sprintf("unrecognized query parameter: %q", k))
}
}
if len(errs) > 0 {
return Parsed{}, fmt.Errorf("failed to parse target %q: %s", target, strings.Join(errs, "; "))
}
if ptarget.Query().Has("tls") {
if ptarget.Scheme == "http" || ptarget.Scheme == "https" {
return Parsed{}, errors.New("cannot use tls query parameter with http(s) scheme")
}
qtls := ptarget.Query().Get("tls")
if qtls != "true" && qtls != "false" {
return Parsed{}, fmt.Errorf("invalid value for tls query parameter: %q", qtls)
}
tls = qtls == "true"
}
scheme := ptarget.Scheme
if scheme == "https" {
tls = true
}
if scheme == "http" || scheme == "https" {
scheme = "dns"
}
hostname = ptarget.Host
host, port, err := net.SplitHostPort(hostname)
aerr, ok := err.(*net.AddrError)
if ok && aerr.Err == "missing port in address" {
port = "443"
} else if err != nil {
return Parsed{}, err
} else {
hostname = host
}
if len(hostname) == 0 {
if scheme == "dns" {
hostname = "localhost"
} else {
hostname = ptarget.Path
}
}
switch scheme {
case "unix":
separator := ":"
if strings.HasPrefix(endpoint, "unix://") {
separator = "://"
}
target = scheme + separator + hostname
case "vsock":
target = scheme + ":" + hostname + ":" + port
case "unix-abstract":
target = scheme + ":" + hostname
case "dns":
if len(ptarget.Path) > 0 {
return Parsed{}, fmt.Errorf("path is not allowed: %q", ptarget.Path)
}
if strings.Count(hostname, ":") == 7 && !strings.HasPrefix(hostname, "[") && !strings.HasSuffix(hostname, "]") {
hostname = "[" + hostname + "]"
}
if len(dnsAuthority) > 0 {
dnsAuthority = "//" + dnsAuthority + "/"
}
target = scheme + ":" + dnsAuthority + hostname + ":" + port
default:
return Parsed{}, fmt.Errorf("unsupported scheme: %q", scheme)
}
return Parsed{
Target: target,
TLS: tls,
}, nil
}
func schemeKnown(scheme string) bool {
for _, s := range []string{
"dns",
"unix",
"unix-abstract",
"vsock",
"http",
"https",
"grpc",
"grpcs",
} {
if scheme == s {
return true
}
}
return false
}

View File

@ -0,0 +1,293 @@
/*
Copyright 2023 The Dapr Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package internal
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestParse(t *testing.T) {
tests := map[string]struct {
expTarget string
expTLS bool
expError bool
}{
"": {
expTarget: "",
expTLS: false,
expError: true,
},
":5000": {
expTarget: "dns:localhost:5000",
expTLS: false,
expError: false,
},
":5000?tls=false": {
expTarget: "dns:localhost:5000",
expTLS: false,
expError: false,
},
":5000?tls=true": {
expTarget: "dns:localhost:5000",
expTLS: true,
expError: false,
},
"myhost": {
expTarget: "dns:myhost:443",
expTLS: false,
expError: false,
},
"myhost?tls=false": {
expTarget: "dns:myhost:443",
expTLS: false,
expError: false,
},
"myhost?tls=true": {
expTarget: "dns:myhost:443",
expTLS: true,
expError: false,
},
"myhost:443": {
expTarget: "dns:myhost:443",
expTLS: false,
expError: false,
},
"myhost:443?tls=false": {
expTarget: "dns:myhost:443",
expTLS: false,
expError: false,
},
"myhost:443?tls=true": {
expTarget: "dns:myhost:443",
expTLS: true,
expError: false,
},
"http://myhost": {
expTarget: "dns:myhost:443",
expTLS: false,
expError: false,
},
"http://myhost?tls=false": {
expTarget: "",
expTLS: false,
expError: true,
},
"http://myhost?tls=true": {
expTarget: "",
expTLS: false,
expError: true,
},
"http://myhost:443": {
expTarget: "dns:myhost:443",
expTLS: false,
expError: false,
},
"http://myhost:443?tls=false": {
expTarget: "",
expTLS: false,
expError: true,
},
"http://myhost:443?tls=true": {
expTarget: "",
expTLS: false,
expError: true,
},
"http://myhost:5000": {
expTarget: "dns:myhost:5000",
expTLS: false,
expError: false,
},
"http://myhost:5000?tls=false": {
expTarget: "",
expTLS: false,
expError: true,
},
"http://myhost:5000?tls=true": {
expTarget: "",
expTLS: false,
expError: true,
},
"https://myhost:443": {
expTarget: "dns:myhost:443",
expTLS: true,
expError: false,
},
"https://myhost:443/tls=false": {
expTarget: "",
expTLS: false,
expError: true,
},
"https://myhost:443?tls=true": {
expTarget: "",
expTLS: false,
expError: true,
},
"dns:myhost": {
expTarget: "dns:myhost:443",
expTLS: false,
expError: false,
},
"dns:myhost?tls=false": {
expTarget: "dns:myhost:443",
expTLS: false,
expError: false,
},
"dns:myhost?tls=true": {
expTarget: "dns:myhost:443",
expTLS: true,
expError: false,
},
"dns://myauthority:53/myhost": {
expTarget: "dns://myauthority:53/myhost:443",
expTLS: false,
expError: false,
},
"dns://myauthority:53/myhost?tls=false": {
expTarget: "dns://myauthority:53/myhost:443",
expTLS: false,
expError: false,
},
"dns://myauthority:53/myhost?tls=true": {
expTarget: "dns://myauthority:53/myhost:443",
expTLS: true,
expError: false,
},
"dns://myhost": {
expTarget: "",
expTLS: false,
expError: true,
},
"unix:my.sock": {
expTarget: "unix:my.sock",
expTLS: false,
expError: false,
},
"unix:my.sock?tls=true": {
expTarget: "unix:my.sock",
expTLS: true,
expError: false,
},
"unix://my.sock": {
expTarget: "unix://my.sock",
expTLS: false,
expError: false,
},
"unix:///my.sock": {
expTarget: "unix:///my.sock",
expTLS: false,
expError: false,
},
"unix://my.sock?tls=true": {
expTarget: "unix://my.sock",
expTLS: true,
expError: false,
},
"unix-abstract:my.sock": {
expTarget: "unix-abstract:my.sock",
expTLS: false,
expError: false,
},
"unix-abstract:my.sock?tls=false": {
expTarget: "unix-abstract:my.sock",
expTLS: false,
expError: false,
},
"unix-abstract:my.sock?tls=true": {
expTarget: "unix-abstract:my.sock",
expTLS: true,
expError: false,
},
"vsock:mycid:5000": {
expTarget: "vsock:mycid:5000",
expTLS: false,
expError: false,
},
"vsock:mycid:5000?tls=false": {
expTarget: "vsock:mycid:5000",
expTLS: false,
expError: false,
},
"vsock:mycid:5000?tls=true": {
expTarget: "vsock:mycid:5000",
expTLS: true,
expError: false,
},
"dns:1.2.3.4:443": {
expTarget: "dns:1.2.3.4:443",
expTLS: false,
expError: false,
},
"dns:[2001:db8:1f70::999:de8:7648:6e8]:443": {
expTarget: "dns:[2001:db8:1f70::999:de8:7648:6e8]:443",
expTLS: false,
expError: false,
},
"dns:[2001:db8:1f70::999:de8:7648:6e8]:5000": {
expTarget: "dns:[2001:db8:1f70::999:de8:7648:6e8]:5000",
expTLS: false,
expError: false,
},
"dns:[2001:db8:1f70::999:de8:7648:6e8]:5000?abc=[]": {
expTarget: "",
expTLS: false,
expError: true,
},
"dns://myauthority:53/[2001:db8:1f70::999:de8:7648:6e8]": {
expTarget: "dns://myauthority:53/[2001:db8:1f70::999:de8:7648:6e8]:443",
expTLS: false,
expError: false,
},
"https://[2001:db8:1f70::999:de8:7648:6e8]": {
expTarget: "dns:[2001:db8:1f70::999:de8:7648:6e8]:443",
expTLS: true,
expError: false,
},
"https://[2001:db8:1f70::999:de8:7648:6e8]:5000": {
expTarget: "dns:[2001:db8:1f70::999:de8:7648:6e8]:5000",
expTLS: true,
expError: false,
},
"host:5000/v1/dapr": {
expTarget: "",
expTLS: false,
expError: true,
},
"host:5000/?a=1": {
expTarget: "",
expTLS: false,
expError: true,
},
"inv-scheme://myhost": {
expTarget: "",
expTLS: false,
expError: true,
},
"inv-scheme:myhost:5000": {
expTarget: "",
expTLS: false,
expError: true,
},
}
for url, tc := range tests {
t.Run(url, func(t *testing.T) {
parsed, err := ParseGRPCEndpoint(url)
assert.Equalf(t, tc.expError, err != nil, "%v", err)
assert.Equal(t, tc.expTarget, parsed.Target)
assert.Equal(t, tc.expTLS, parsed.TLS)
})
}
}