-
Notifications
You must be signed in to change notification settings - Fork 4.6k
xds bootstrap: enable using JWT Call Credentials (part 2 for A97) #8536
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
@easwars I think this is ready for another review :) |
Apologies for the delay in the review. I'm a bit swamped with other things. I will get to this asap. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wasn't able to make a complete pass. But some comments to get you going. I will try to make another pass asap.
internal/envconfig/xds.go
Outdated
// https://github.com/grpc/proposal/blob/master/A86-xds-http-connect.md | ||
XDSHTTPConnectEnabled = boolFromEnv("GRPC_EXPERIMENTAL_XDS_HTTP_CONNECT", false) | ||
|
||
// XDSBootstrapCallCredsEnabled controls if JWT call credentials can be used |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: This environment variable guards the newly added bootstrap field call_creds
. I agree that the only call credentials that we currently support via this mechanism is the JWT call creds, but that could change any day (before we get rid of this env var). So, I would prefer we more explicitly call out what this env var guards.
|
||
// NewCallCredentials returns a credentials.PerRPCCredentials The input config | ||
// should match the structure specified in gRFC A97 structure. | ||
// See gRFC A97: https://github.com/grpc/proposal/blob/master/A97-xds-jwt-call-creds.md |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some nits here.
- There needs to be a period after the first sentence.
- I think the next two lines can be shortened. (this is an internal API, so we can omit the link to the actual gRFC)
- There is no mention about the second return value,
func()
So, maybe something like
// NewCallCredentials returns a new JWT token based call credentials. The input
// config must match the structure specified in gRFC A97. The caller is expected
// to invoke the second return value when they are done using the returned call creds.
"google.golang.org/grpc/credentials" | ||
) | ||
|
||
func TestNewCallCredentials(t *testing.T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use the Tester
defined in the grpctest
package here:
grpc-go/internal/grpctest/grpctest.go
Line 52 in d0ebcdf
type Tester struct{} |
It has some setup and teardown magic that we find useful for all of our tests.
|
||
if !tt.wantSuccess { | ||
if err == nil { | ||
t.Fatal("Expected error, got nil") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please identify the function being tested in the test error message. See: https://google.github.io/styleguide/go/decisions#identify-the-function
internal/xds/bootstrap/bootstrap.go
Outdated
// CallCreds contains the call credentials configuration for individual RPCs. | ||
// It implements gRFC A97 call credentials structure. | ||
type CallCreds struct { | ||
// Type contains a unique name identifying the call credentials type. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm .. The gRFC does not state any uniqueness requirements for this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, you're right, it doesn't. I was influenced by the ChannelCreds.Type but there it makes more sense given that only one gets picked up. I've updated the comment
internal/xds/bootstrap/bootstrap.go
Outdated
} | ||
|
||
// CallCreds returns the call credentials configuration for this server. | ||
func (sc *ServerConfig) CallCreds() []CallCreds { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we call this CallCredsConfig
and the next method CallCreds
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was trying to keep symmetry with ChannelCreds
and SelectedChannelCreds
. However, SelectedCallCreds
now returns []credentials.PerRPCCredentials
rather than the config, still, which breaks this symmetry.
Realistically, I'm not sure even if the public methods are needed -- the PerRPCCredentials are added to the ServerConfig.extraDialOptions
and used via ServerConfig.DialOptions()
WDYT, shall I remove the public methods? I've renamed them in the meantime. I've used CallCredsConfigs
, plural, because I also renamed the Go type to CallCredsConfig
and the method returns a collection of those. I've also renamed the relevant fields in ServerConfig
(and ServerConfigTestingOptions
) in similar way.
internal/xds/bootstrap/bootstrap.go
Outdated
if envconfig.XDSBootstrapCallCredsEnabled { | ||
// Process call credentials - unlike channel creds, we use ALL supported | ||
// types. Also, call credentials are optional as per gRFC A97. | ||
for _, callCredConfig := range server.CallCreds { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Can we use a shorter name for the loop variable. Maybe cfg
?
internal/xds/bootstrap/bootstrap.go
Outdated
// Skip unsupported call credential types (don't fail bootstrap). | ||
continue | ||
} | ||
callCred, cancel, err := c.Build(callCredConfig.Config) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We refer to them as credentials
and not credential
even when there is a single one. This was a major source of confusion to me when I joined the team as well :)
So, s/callCred/callCreds/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Haha, yes, this did confuse me a bit when starting. Thanks for spotting this
} | ||
} | ||
|
||
func TestServerConfigCallCredsIntegration(t *testing.T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs to have the s
receiver on it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the review @easwars ; I think it's ready for another pass
internal/xds/bootstrap/bootstrap.go
Outdated
// CallCreds contains the call credentials configuration for individual RPCs. | ||
// It implements gRFC A97 call credentials structure. | ||
type CallCreds struct { | ||
// Type contains a unique name identifying the call credentials type. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, you're right, it doesn't. I was influenced by the ChannelCreds.Type but there it makes more sense given that only one gets picked up. I've updated the comment
internal/xds/bootstrap/bootstrap.go
Outdated
} | ||
|
||
// CallCreds returns the call credentials configuration for this server. | ||
func (sc *ServerConfig) CallCreds() []CallCreds { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was trying to keep symmetry with ChannelCreds
and SelectedChannelCreds
. However, SelectedCallCreds
now returns []credentials.PerRPCCredentials
rather than the config, still, which breaks this symmetry.
Realistically, I'm not sure even if the public methods are needed -- the PerRPCCredentials are added to the ServerConfig.extraDialOptions
and used via ServerConfig.DialOptions()
WDYT, shall I remove the public methods? I've renamed them in the meantime. I've used CallCredsConfigs
, plural, because I also renamed the Go type to CallCredsConfig
and the method returns a collection of those. I've also renamed the relevant fields in ServerConfig
(and ServerConfigTestingOptions
) in similar way.
internal/xds/bootstrap/bootstrap.go
Outdated
// Skip unsupported call credential types (don't fail bootstrap). | ||
continue | ||
} | ||
callCred, cancel, err := c.Build(callCredConfig.Config) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Haha, yes, this did confuse me a bit when starting. Thanks for spotting this
if err != nil { | ||
t.Fatalf("NewCallCredentials failed: %v", err) | ||
} | ||
if callCreds == nil { | ||
t.Fatal("Expected non-nil bundle") | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, much better, thanks!
originalJWTEnabled := envconfig.XDSBootstrapCallCredsEnabled | ||
envconfig.XDSBootstrapCallCredsEnabled = true | ||
defer func() { | ||
envconfig.XDSBootstrapCallCredsEnabled = originalJWTEnabled | ||
}() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have a helper function for this:
grpc-go/internal/testutils/envconfig.go
Line 26 in 9ff80a7
func SetEnvConfig[T any](t *testing.T, variable *T, value T) { |
// NewCallCredentials returns a new JWT token based call credentials. The input | ||
// config must match the structure specified in gRFC A97. The caller is expected | ||
// to invoke the second return value when they are done using the returned call creds. | ||
// The cancel function is idempotent. | ||
func NewCallCredentials(configJSON json.RawMessage) (credentials.PerRPCCredentials, func(), error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about adding named return values to help with the documentation.
// NewCallCredentials creates and returns JWT token-based call credentials
// using the input config (which must match gRFC A97).
//
// The caller must invoke the returned cancel function (which is idempotent),
// when they are done using the returned call credentials.
func NewCallCredentials(configJSON json.RawMessage) (c credentials.PerRPCCredentials, cancel func(), err error) {
"google.golang.org/grpc/internal/grpctest" | ||
) | ||
|
||
const defaultCtxTimeout = 5 * time.Second |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: s/defaultCtxTimeout/defaultTestTimeout/
callCreds, cleanup, err := NewCallCredentials(json.RawMessage(tt.config)) | ||
|
||
if err == nil { | ||
t.Fatal("NewCallCredentials: expected error, got nil") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please consider adding the config to the error message:
t.Fatal("NewCallCredentials(%s) succeeded when expected to fail", tt.config)
t.Fatal("NewCallCredentials: expected error, got nil") | ||
} | ||
if callCreds != nil { | ||
t.Error("NewCallCredentials: Expected nil bundle to be returned") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: there is no bundle anymore.
t.Error("NewCallCredentials(%s) returned non-nil credentials", tt.config)
if cleanup == nil { | ||
t.Error("NewCallCredentials: Expected non-nil cleanup function to be returned") | ||
} else { | ||
defer cleanup() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be nicer to always return a non-nil cancel func (even in error cases), to avoid such checks at callsites.
} | ||
|
||
// Test that call credentials get used | ||
ctx, cancel := context.WithTimeout(context.Background(), defaultCtxTimeout) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we really need a 5s
timeout here? And this const is only used here, you could remove the global and just inline the value here. Maybe a second at best should be good enough?
func writeTempFile(t *testing.T, content string) string { | ||
t.Helper() | ||
tempDir := t.TempDir() | ||
filePath := filepath.Join(tempDir, "tempfile") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably doesn't matter. But could be rename the file to something more appropriate. Maybe "jwt_token"?
if cc.Config == nil { | ||
return cc.Type | ||
} | ||
// We do not expect the Marshal call to fail since we wrote to cc.Config |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Here and in a few other places, please terminate comment lines with periods. https://google.github.io/styleguide/go/decisions#comment-sentences
channelCreds []ChannelCreds | ||
serverFeatures []string | ||
serverURI string | ||
channelCreds []ChannelCreds |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While you are here, could you please add a TODO to rename ChannelCreds
to ChannelCredsConfig
to match what we have with the call creds? Thanks.
return cc.Type + "-" + string(b) | ||
} | ||
|
||
// CallCredsConfig contains the call credentials configuration for individual RPCs. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it might be better to clarify in this docstring that these call creds are used when talking to the xDS management server. The docstring on the ChannelCreds
type does that. So, what do you think about something like:
CallCredsConfig contains the call credentials configuration to be used on
RPCs to the management server.
I think the part about A97 is not that important and could be skipped. Thanks.
case !slices.Equal(sc.serverFeatures, other.serverFeatures): | ||
return false | ||
case !sc.selectedCreds.Equal(other.selectedCreds): | ||
case !sc.selectedChannelCreds.Equal(other.selectedChannelCreds): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know this is existing code, but I'm wondering if we can remove this field from the equality check for a couple of reasons:
- we are already checking the channel credentials configuration (from which we selected this credentials) for equality
- this type is supposed to semantically represent configuration corresponding to an xDS server and not any runtime state. We are already abusing it a little (or optimizing) by storing the selected credentials in it, so that we don't have to parse the configuration again when we actually want to use the credentials.
} | ||
features := strings.Join(sc.serverFeatures, "-") | ||
return strings.Join([]string{sc.serverURI, sc.selectedCreds.String(), features}, "-") | ||
return strings.Join([]string{sc.serverURI, sc.selectedChannelCreds.String(), features, sc.CallCredsConfigs().String()}, "-") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I remember you asking me if we want to include the channel creds here and I said yes. But thinking about this now, I'm wondering if the returned string is going to be too verbose, especially since the String()
method of the call credentials config returns a JSON (as does the channel credentials). Have you see what this string actually looks like in any of your testing?
// CallCreds returns the built call credentials that are ready to use. | ||
// These are the credentials that were successfully built from the call_creds | ||
// configuration. | ||
func (sc *ServerConfig) CallCreds() []credentials.PerRPCCredentials { | ||
return sc.selectedCallCreds | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need this method at all (and the selectedCallCreds
field)? We are currently storing the selected call credentials directly as dial options in the extraDialOptions
field.
original := envconfig.XDSBootstrapCallCredsEnabled | ||
envconfig.XDSBootstrapCallCredsEnabled = true | ||
defer func() { | ||
envconfig.XDSBootstrapCallCredsEnabled = original | ||
}() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could use the helper function here as well testutils.SetEnvConfig
.
// Build returns a PerRPCCredentials created from the provided | ||
// configuration, and a function to clean up any additional resources | ||
// associated with them when they are no longer needed. | ||
Build(config json.RawMessage) (credentials.PerRPCCredentials, func(), error) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I realize that I added a comment about possibly using named return values for this method on the JWT call creds implementation. But it looks like we haven't done that for the existing channel credentials interface which has a similar set of return values. So, I'm fine with leaving this as is. Thanks.
builder Credentials | ||
typename string | ||
builder ChannelCredentials | ||
minimumRequiredConfig json.RawMessage |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we really need this new field when it is always set to nil
?
if bundle == nil { | ||
t.Errorf("%T.Build returned nil bundle, expected non-nil", test.builder) | ||
} | ||
stop() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we defer
this call to stop
right after the line where we call Build
?
if bundle == nil { | ||
t.Errorf("%T.Build returned nil bundle, expected non-nil", test.builder) | ||
} | ||
stop() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here.
} | ||
|
||
func TestJwtCallCredentials_DisabledIfFeatureNotEnabled(t *testing.T) { | ||
builder := GetCallCredentials("jwt_call_creds") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we explicitly set the value of the env var to false
instead of running this test under the assumption that it is false (which it currently is). But if we explicitly set it here, then we wont have to change the test when we change the default value of the env var to true
.
@dimpavloff This PR is mostly looking good. Most of the comments in this pass are nits. We should be able to resolve them quickly and move on. Thank you very much! |
Part two for grpc/proposal#492 (A97), following #8431 .
What this PR does is:
internal/xds/bootstrap
with support for loading multiple PerRPCCallCredentials specifed in a newcall_creds
field in the boostrap file as per A97xds/internal/xdsclient/clientimpl.go
to use the call credentials when constructing the clientxds/bootstrap
to register thejwtcreds
call credentials and make them available ifGRPC_EXPERIMENTAL_XDS_BOOTSTRAP_CALL_CREDS
is enabledI have added
DialOptionsWithCallCredsForTransport
because, even though current and future call credentials are likely to all expect secure transport, I thought it would be safer to check of insecure transport just in case. If you prefer, I can just updateDialOptions
to use all call credentials regardless of the transport.Relates to istio/istio#53532
RELEASE NOTES: