xds/bootstrap: Add support for `grpc_server_resource_name_id`. (#4030)

diff --git a/xds/internal/client/bootstrap/bootstrap.go b/xds/internal/client/bootstrap/bootstrap.go
index ca7c96c..87abecd 100644
--- a/xds/internal/client/bootstrap/bootstrap.go
+++ b/xds/internal/client/bootstrap/bootstrap.go
@@ -78,6 +78,10 @@
 	// CertProviderConfigs contains a mapping from certificate provider plugin
 	// instance names to parsed buildable configs.
 	CertProviderConfigs map[string]*certprovider.BuildableConfig
+	// ServerResourceNameID contains the value to be used as the id in the
+	// resource name used to fetch the Listener resource on the xDS-enabled gRPC
+	// server.
+	ServerResourceNameID string
 }
 
 type channelCreds struct {
@@ -103,19 +107,20 @@
 //          "config": <JSON object containing config for the type>
 //        }
 //      ],
-//      "server_features": [ ... ]
-//		"certificate_providers" : {
-//			"default": {
-//				"plugin_name": "default-plugin-name",
-//				"config": { default plugin config in JSON }
-//			},
-//			"foo": {
-//				"plugin_name": "foo",
-//				"config": { foo plugin config in JSON }
-//			}
-//		}
 //    },
-//    "node": <JSON form of Node proto>
+//    "node": <JSON form of Node proto>,
+//    "server_features": [ ... ],
+//    "certificate_providers" : {
+//      "default": {
+//        "plugin_name": "default-plugin-name",
+//        "config": { default plugin config in JSON }
+//       },
+//      "foo": {
+//        "plugin_name": "foo",
+//        "config": { foo plugin config in JSON }
+//      }
+//    },
+//    "grpc_server_resource_name_id": "grpc/server"
 // }
 //
 // Currently, we support exactly one type of credential, which is
@@ -222,6 +227,10 @@
 				configs[instance] = bc
 			}
 			config.CertProviderConfigs = configs
+		case "grpc_server_resource_name_id":
+			if err := json.Unmarshal(v, &config.ServerResourceNameID); err != nil {
+				return nil, fmt.Errorf("xds: json.Unmarshal(%v) for field %q failed during bootstrap: %v", string(v), k, err)
+			}
 		}
 		// Do not fail the xDS bootstrap when an unknown field is seen. This can
 		// happen when an older version client reads a newer version bootstrap
diff --git a/xds/internal/client/bootstrap/bootstrap_test.go b/xds/internal/client/bootstrap/bootstrap_test.go
index 1b9deca..6691ff4 100644
--- a/xds/internal/client/bootstrap/bootstrap_test.go
+++ b/xds/internal/client/bootstrap/bootstrap_test.go
@@ -239,6 +239,9 @@
 	if diff := cmp.Diff(want.NodeProto, c.NodeProto, cmp.Comparer(proto.Equal)); diff != "" {
 		return fmt.Errorf("config.NodeProto diff (-want, +got):\n%s", diff)
 	}
+	if c.ServerResourceNameID != want.ServerResourceNameID {
+		return fmt.Errorf("config.ServerResourceNameID is %q, want %q", c.ServerResourceNameID, want.ServerResourceNameID)
+	}
 
 	// A vanilla cmp.Equal or cmp.Diff will not produce useful error message
 	// here. So, we iterate through the list of configs and compare them one at
@@ -699,3 +702,81 @@
 		})
 	}
 }
+
+func TestNewConfigWithServerResourceNameID(t *testing.T) {
+	cancel := setupBootstrapOverride(map[string]string{
+		"badServerResourceNameID": `
+		{
+			"node": {
+				"id": "ENVOY_NODE_ID",
+				"metadata": {
+				    "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector"
+			    }
+			},
+			"xds_servers" : [{
+				"server_uri": "trafficdirector.googleapis.com:443",
+				"channel_creds": [
+					{ "type": "google_default" }
+				]
+			}],
+			"grpc_server_resource_name_id": 123456789
+		}`,
+		"goodServerResourceNameID": `
+		{
+			"node": {
+				"id": "ENVOY_NODE_ID",
+				"metadata": {
+				    "TRAFFICDIRECTOR_GRPC_HOSTNAME": "trafficdirector"
+			    }
+			},
+			"xds_servers" : [{
+				"server_uri": "trafficdirector.googleapis.com:443",
+				"channel_creds": [
+					{ "type": "google_default" }
+				]
+			}],
+			"grpc_server_resource_name_id": "grpc/server"
+		}`,
+	})
+	defer cancel()
+
+	tests := []struct {
+		name       string
+		wantConfig *Config
+		wantErr    bool
+	}{
+		{
+			name:    "badServerResourceNameID",
+			wantErr: true,
+		},
+		{
+			name: "goodServerResourceNameID",
+			wantConfig: &Config{
+				BalancerName:         "trafficdirector.googleapis.com:443",
+				Creds:                grpc.WithCredentialsBundle(google.NewComputeEngineCredentials()),
+				TransportAPI:         version.TransportV2,
+				NodeProto:            v2NodeProto,
+				ServerResourceNameID: "grpc/server",
+			},
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			origBootstrapFileName := env.BootstrapFileName
+			env.BootstrapFileName = test.name
+			defer func() { env.BootstrapFileName = origBootstrapFileName }()
+
+			c, err := NewConfig()
+			if (err != nil) != test.wantErr {
+				t.Fatalf("NewConfig() returned (%+v, %v), wantErr: %v", c, err, test.wantErr)
+			}
+			if test.wantErr {
+				return
+			}
+			if err := c.compare(test.wantConfig); err != nil {
+				t.Fatal(err)
+			}
+		})
+	}
+}