Skip to content
Merged
65 changes: 40 additions & 25 deletions internal/xds/bootstrap/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import (
"google.golang.org/grpc/credentials/tls/certprovider"
"google.golang.org/grpc/internal"
"google.golang.org/grpc/internal/envconfig"
xdscreds "google.golang.org/grpc/internal/xds/credentials"
"google.golang.org/grpc/xds/bootstrap"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/structpb"
Expand Down Expand Up @@ -127,17 +128,18 @@ type AllowedGRPCService struct {
// copied in during parsing so the service is self-describing.
targetURI string
// channelCreds is the list of channel-credential configs from the
// bootstrap JSON. Kept for Equal and MarshalJSON.
// bootstrap JSON. Kept for MarshalJSON.
channelCreds []ChannelCreds
// callCredsConfigs is the list of call-credential configs from the
// bootstrap JSON. Kept for Equal and MarshalJSON.
// bootstrap JSON. Kept for MarshalJSON.
callCredsConfigs []CallCredsConfig
// selectedChannelCreds is the first channel-creds entry whose type the
// client supports; it is the one used to build the side channel.
selectedChannelCreds ChannelCreds
// dialOptions are built from the selected channel and call credentials
// and passed to grpc.NewClient when creating the side channel.
dialOptions []grpc.DialOption
// sideChannelCreds is the credentials bundle built from the first
// channel-creds entry whose type the client supports, paired with its
// identity.
sideChannelCreds *xdscreds.ChannelCreds
// sideCallCreds are the call credentials built from the supported
// call-creds configs, paired with their identities, preserving order.
sideCallCreds []*xdscreds.CallCreds
// cleanups release resources (credential bundles, file watchers) built
// for this service; run when the owning Config is no longer needed.
cleanups []func()
Expand All @@ -148,18 +150,22 @@ func (a *AllowedGRPCService) TargetURI() string {
return a.targetURI
}

// DialOptions returns the dial options built from this service's selected
// channel and call credentials, for use when creating the side channel.
func (a *AllowedGRPCService) DialOptions() []grpc.DialOption {
return a.dialOptions
// SideChannelCredentials returns the channel and call credentials configured
// for this service, paired with their identities, for use when creating the
// side channel to it. The returned credentials are owned by the bootstrap
// config: their cleanups are nil, and the underlying resources are released
// via Cleanups when the config is no longer needed.
func (a *AllowedGRPCService) SideChannelCredentials() (*xdscreds.ChannelCreds, []*xdscreds.CallCreds) {
return a.sideChannelCreds, a.sideCallCreds
}

// Cleanups returns cleanups to run when the service is no longer needed.
func (a *AllowedGRPCService) Cleanups() []func() {
return a.cleanups
}

// Equal reports whether a and other are considered equal.
// Equal reports whether a and other are considered equal: the same target
// with the same built channel and call credential identities.
func (a *AllowedGRPCService) Equal(other *AllowedGRPCService) bool {
if a == nil && other == nil {
return true
Expand All @@ -170,10 +176,10 @@ func (a *AllowedGRPCService) Equal(other *AllowedGRPCService) bool {
if a.targetURI != other.targetURI {
return false
}
if !slices.EqualFunc(a.channelCreds, other.channelCreds, ChannelCreds.Equal) {
if !a.sideChannelCreds.Equal(other.sideChannelCreds) {
return false
}
return slices.EqualFunc(a.callCredsConfigs, other.callCredsConfigs, CallCredsConfig.Equal)
return slices.EqualFunc(a.sideCallCreds, other.sideCallCreds, (*xdscreds.CallCreds).Equal)
}

type allowedGRPCServiceJSON struct {
Expand Down Expand Up @@ -237,8 +243,10 @@ func (a *AllowedGRPCService) UnmarshalJSON(data []byte) (err error) {
}
}()

var credsDialOption grpc.DialOption
var selectedChannelCreds ChannelCreds
// The built credentials are paired with their (JSON) identities but the
// pairs carry no cleanups: the resources built here are owned by the
// bootstrap config and released via the cleanups collected below.
var sideChannelCreds *xdscreds.ChannelCreds
for _, cc := range jsonS.ChannelCreds {
c := bootstrap.GetChannelCredentials(cc.Type)
if c == nil {
Expand All @@ -248,19 +256,19 @@ func (a *AllowedGRPCService) UnmarshalJSON(data []byte) (err error) {
if err != nil {
return fmt.Errorf("xds: failed to build credentials bundle from bootstrap for allowed grpc service: type %q, err: %v", cc.Type, err)
}
selectedChannelCreds = cc
credsDialOption = grpc.WithCredentialsBundle(bundle)
identity := xdscreds.Identity{Type: cc.Type, Data: cc.Config}
sideChannelCreds = xdscreds.NewChannelCreds(bundle, identity, nil)
cleanups = append(cleanups, cancel)
break
}

// If no channel-creds type in the list was supported, credsDialOption is
// If no channel-creds type in the list was supported, sideChannelCreds is
// still nil after the loop; that is a validation error.
if credsDialOption == nil {
if sideChannelCreds == nil {
return fmt.Errorf("xds: no supported channel credentials found for allowed grpc service in config:\n%s", string(data))
}
dialOptions := []grpc.DialOption{credsDialOption}

var sideCallCreds []*xdscreds.CallCreds
for _, cfg := range jsonS.CallCredsConfigs {
c := bootstrap.GetCallCredentials(cfg.Type)
if c == nil {
Expand All @@ -270,14 +278,15 @@ func (a *AllowedGRPCService) UnmarshalJSON(data []byte) (err error) {
if err != nil {
return fmt.Errorf("xds: failed to build call credentials from bootstrap for allowed grpc service: type %q, err: %v", cfg.Type, err)
}
dialOptions = append(dialOptions, grpc.WithPerRPCCredentials(callCreds))
identity := xdscreds.Identity{Type: cfg.Type, Data: cfg.Config}
sideCallCreds = append(sideCallCreds, xdscreds.NewCallCreds(callCreds, identity, nil))
cleanups = append(cleanups, cancel)
}

a.channelCreds = jsonS.ChannelCreds
a.callCredsConfigs = jsonS.CallCredsConfigs
a.selectedChannelCreds = selectedChannelCreds
a.dialOptions = dialOptions
a.sideChannelCreds = sideChannelCreds
a.sideCallCreds = sideCallCreds
a.cleanups = cleanups
return nil
}
Expand Down Expand Up @@ -643,6 +652,12 @@ func (c *Config) AllowedGRPCServices() AllowedGRPCServices {
return c.allowedGRPCServices
}

// AllowedGRPCService returns the allowed gRPC service configured for the
// given target URI, or nil if there is none.
func (c *Config) AllowedGRPCService(targetURI string) *AllowedGRPCService {
return c.allowedGRPCServices[targetURI]
}

// XDSServers returns the top-level list of management servers to connect to,
// ordered by priority.
func (c *Config) XDSServers() ServerConfigs {
Expand Down
143 changes: 71 additions & 72 deletions internal/xds/bootstrap/bootstrap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import (
"google.golang.org/grpc/internal/envconfig"
"google.golang.org/grpc/internal/grpctest"
"google.golang.org/grpc/internal/testutils"
xdscreds "google.golang.org/grpc/internal/xds/credentials"
"google.golang.org/grpc/xds/bootstrap"
"google.golang.org/protobuf/testing/protocmp"
"google.golang.org/protobuf/types/known/structpb"
Expand Down Expand Up @@ -1148,6 +1149,13 @@ func (s) TestGetConfiguration_Federation(t *testing.T) {
Type: "jwt_token_file",
Config: json.RawMessage("{\n\"jwt_token_file\": \"/var/run/secrets/tokens/istio-token\"\n}"),
}},
// Equal compares the built credentials by identity, so
// the fixture carries identity-only pairs.
sideChannelCreds: xdscreds.NewChannelCreds(nil, xdscreds.Identity{Type: "insecure"}, nil),
sideCallCreds: []*xdscreds.CallCreds{xdscreds.NewCallCreds(nil, xdscreds.Identity{
Type: "jwt_token_file",
Data: json.RawMessage("{\n\"jwt_token_file\": \"/var/run/secrets/tokens/istio-token\"\n}"),
}, nil)},
},
},
},
Expand Down Expand Up @@ -1741,94 +1749,80 @@ func (s) TestAllowedGRPCServices_UnmarshalJSON(t *testing.T) {
tests := []struct {
name string
json string
want *AllowedGRPCService
// Fields deliberately excluded from Equal: the selected channel
// creds and the dial options built from the credentials.
wantSelectedChannelCredsType string
wantDialOptions int
// want carries the expected target and credential identities;
// comparisons use the Equal methods via cmp.Diff.
want AllowedGRPCServices
// wantCallCreds is the number of call credentials expected to be
// built for the target.
wantCallCreds int
}{
{
name: "insecure_channel_creds",
json: `{"dns:///sharding-service:443": {"channel_creds": [{"type": "insecure"}]}}`,
want: &AllowedGRPCService{
targetURI: target,
channelCreds: []ChannelCreds{{Type: "insecure"}},
},
wantSelectedChannelCredsType: "insecure",
wantDialOptions: 1,
want: AllowedGRPCServices{target: {
targetURI: target,
sideChannelCreds: xdscreds.NewChannelCreds(nil, xdscreds.Identity{Type: "insecure"}, nil),
}},
},
{
name: "with_call_creds",
json: `{"dns:///sharding-service:443": {"channel_creds": [{"type": "insecure"}], "call_creds": [{"type": "jwt_token_file", "config": {"jwt_token_file": "/var/run/secrets/tokens/istio-token"}}]}}`,
want: &AllowedGRPCService{
targetURI: target,
channelCreds: []ChannelCreds{{Type: "insecure"}},
callCredsConfigs: []CallCredsConfig{{
Type: "jwt_token_file",
Config: json.RawMessage(`{"jwt_token_file": "/var/run/secrets/tokens/istio-token"}`),
}},
},
wantSelectedChannelCredsType: "insecure",
// One channel-creds dial option plus one per-RPC call-creds
// option.
wantDialOptions: 2,
want: AllowedGRPCServices{target: {
targetURI: target,
sideChannelCreds: xdscreds.NewChannelCreds(nil, xdscreds.Identity{Type: "insecure"}, nil),
sideCallCreds: []*xdscreds.CallCreds{xdscreds.NewCallCreds(nil, xdscreds.Identity{
Type: "jwt_token_file",
Data: json.RawMessage(`{"jwt_token_file": "/var/run/secrets/tokens/istio-token"}`),
}, nil)},
}},
wantCallCreds: 1,
},
{
// Unsupported call-creds types are skipped without error, so no
// call credentials are built.
name: "unsupported_call_creds_skipped",
json: `{"dns:///sharding-service:443": {"channel_creds": [{"type": "insecure"}], "call_creds": [{"type": "unsupported_call_creds_type"}]}}`,
want: &AllowedGRPCService{
targetURI: target,
channelCreds: []ChannelCreds{{Type: "insecure"}},
callCredsConfigs: []CallCredsConfig{{
Type: "unsupported_call_creds_type",
}},
},
wantSelectedChannelCredsType: "insecure",
// Unsupported call-creds types are skipped without error, so
// only the channel-creds dial option is built.
wantDialOptions: 1,
want: AllowedGRPCServices{target: {
targetURI: target,
sideChannelCreds: xdscreds.NewChannelCreds(nil, xdscreds.Identity{Type: "insecure"}, nil),
}},
},
{
// One call credential is built for each supported call-creds
// config, preserving order.
name: "multiple_supported_call_creds",
json: `{"dns:///sharding-service:443": {"channel_creds": [{"type": "insecure"}], "call_creds": [{"type": "jwt_token_file", "config": {"jwt_token_file": "/tokens/token-one"}}, {"type": "jwt_token_file", "config": {"jwt_token_file": "/tokens/token-two"}}]}}`,
want: &AllowedGRPCService{
targetURI: target,
channelCreds: []ChannelCreds{{Type: "insecure"}},
callCredsConfigs: []CallCredsConfig{
{
Type: "jwt_token_file",
Config: json.RawMessage(`{"jwt_token_file": "/tokens/token-one"}`),
},
{
Type: "jwt_token_file",
Config: json.RawMessage(`{"jwt_token_file": "/tokens/token-two"}`),
},
want: AllowedGRPCServices{target: {
targetURI: target,
sideChannelCreds: xdscreds.NewChannelCreds(nil, xdscreds.Identity{Type: "insecure"}, nil),
sideCallCreds: []*xdscreds.CallCreds{
xdscreds.NewCallCreds(nil, xdscreds.Identity{
Type: "jwt_token_file",
Data: json.RawMessage(`{"jwt_token_file": "/tokens/token-one"}`),
}, nil),
xdscreds.NewCallCreds(nil, xdscreds.Identity{
Type: "jwt_token_file",
Data: json.RawMessage(`{"jwt_token_file": "/tokens/token-two"}`),
}, nil),
},
},
wantSelectedChannelCredsType: "insecure",
// One channel-creds dial option plus one per-RPC option for
// each supported call credential.
wantDialOptions: 3,
}},
wantCallCreds: 2,
},
{
name: "tls_channel_creds",
json: `{"dns:///sharding-service:443": {"channel_creds": [{"type": "tls", "config": {}}]}}`,
want: &AllowedGRPCService{
targetURI: target,
channelCreds: []ChannelCreds{{Type: "tls", Config: json.RawMessage("{}")}},
},
wantSelectedChannelCredsType: "tls",
wantDialOptions: 1,
want: AllowedGRPCServices{target: {
targetURI: target,
sideChannelCreds: xdscreds.NewChannelCreds(nil, xdscreds.Identity{Type: "tls", Data: json.RawMessage("{}")}, nil),
}},
},
{
name: "skips_unsupported_channel_creds",
json: `{"dns:///sharding-service:443": {"channel_creds": [{"type": "unsupported_cred_type"}, {"type": "insecure"}]}}`,
want: &AllowedGRPCService{
targetURI: target,
channelCreds: []ChannelCreds{{Type: "unsupported_cred_type"}, {Type: "insecure"}},
},
wantSelectedChannelCredsType: "insecure",
wantDialOptions: 1,
want: AllowedGRPCServices{target: {
targetURI: target,
sideChannelCreds: xdscreds.NewChannelCreds(nil, xdscreds.Identity{Type: "insecure"}, nil),
}},
},
}

Expand All @@ -1838,18 +1832,23 @@ func (s) TestAllowedGRPCServices_UnmarshalJSON(t *testing.T) {
if err := json.Unmarshal([]byte(test.json), &got); err != nil {
t.Fatalf("AllowedGRPCServices unmarshal failed: %v", err)
}
svc, ok := got[target]
if !ok {
t.Fatalf("AllowedGRPCServices missing key %q", target)
if diff := cmp.Diff(test.want, got); diff != "" {
t.Errorf("AllowedGRPCServices unmarshal returned unexpected diff (-want +got):\n%s", diff)
}
if !svc.Equal(test.want) {
t.Errorf("parsed service = %+v, want %+v", svc, test.want)
// Equal compares credentials by identity only, so it cannot tell
// a built credential from a nil one; verify the credentials were
// built.
chanCreds, callCreds := got[target].SideChannelCredentials()
if chanCreds == nil || chanCreds.Bundle() == nil {
t.Error("SideChannelCredentials() returned no built channel credentials")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: Maybe for completeness sake, add a field to the test table which says whether we expect call creds (or the number of call creds to expect) and verify here that call creds were in fact being built with the expected number.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

}
if got := svc.selectedChannelCreds.Type; got != test.wantSelectedChannelCredsType {
t.Errorf("selectedChannelCreds.Type = %q, want %q", got, test.wantSelectedChannelCredsType)
if len(callCreds) != test.wantCallCreds {
t.Errorf("SideChannelCredentials() returned %d call credentials, want %d", len(callCreds), test.wantCallCreds)
}
if got := len(svc.DialOptions()); got != test.wantDialOptions {
t.Errorf("len(DialOptions()) = %d, want %d", got, test.wantDialOptions)
for i, cc := range callCreds {
if cc.Credentials() == nil {
t.Errorf("SideChannelCredentials() call credentials[%d] have no built credentials", i)
}
}
})
}
Expand Down
Loading
Loading