Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions internal/gengapic/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ go_library(
"custom_operation.go",
"doc_file.go",
"example.go",
"feature.go",
"generator.go",
"gengapic.go",
"gengrpc.go",
Expand Down Expand Up @@ -69,6 +70,7 @@ go_test(
"custom_operation_test.go",
"doc_file_test.go",
"example_test.go",
"feature_test.go",
"generator_test.go",
"gengapic_test.go",
"genrest_test.go",
Expand Down
7 changes: 6 additions & 1 deletion internal/gengapic/client_init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,12 @@ func TestClientOpt(t *testing.T) {
{Name: "google.longrunning.Operations"},
},
},
gRPCServiceConfig: grpcConf},
gRPCServiceConfig: grpcConf,
// Showcase would enable MTLS if we went through legacy enablements, so add it explicitly here.
featureEnablement: map[featureID]bool{
MTLSHardBoundTokensFeature: true,
},
},
}

serv := &descriptorpb.ServiceDescriptorProto{
Expand Down
130 changes: 130 additions & 0 deletions internal/gengapic/feature.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// Copyright 2026 Google LLC
//
// 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
//
// https://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 gengapic

import "google.golang.org/protobuf/types/pluginpb"

// We use a custom type for feature ID to clarify that features are setup explicitly.
type featureID string

// featureInfo contains basic information about features, and provides an extensible mechanism
// for adding more infomation down the line (maturity level, warning about stale experiments, etc)
type featureInfo struct {
// Short summary of feature.
Description string

// Tracking ID for more information. Github issue, internal b/ issue, etc.
TrackingID string
}

// Define feature ID strings here. More details about features are kept in the featureRegistry map.
const (
WrapperTypesForPageSizeFeature featureID = "wrapper_types_for_page_size"
OrderedRoutingHeadersFeature featureID = "ordered_routing_headers"
MTLSHardBoundTokensFeature featureID = "mtls_hard_bound_tokens"
)

// featureRegistry contains the registry of defined features.
// Introducing a new capability to the generator generally starts here, as features
// must be registered to be enabled. This should not be modified at runtime. Those
// who attempt to do so will be given a stern talking to.
Comment thread
quartzmo marked this conversation as resolved.
var featureRegistry = map[featureID]*featureInfo{
MTLSHardBoundTokensFeature: {
Description: "support MTLS hard bound tokens",
Comment thread
shollyman marked this conversation as resolved.
TrackingID: "b/327916505",
},
OrderedRoutingHeadersFeature: {
Description: "Specify that routing headers are emitted in a deterministic fashion. Primarily used for firestore.",
},
WrapperTypesForPageSizeFeature: {
Description: "Allow List RPCs to generator with support for protobuf wrapper types (e.g. Int32Value, etc).",
TrackingID: "b/352331075",
},
}

// legacyFeatureEnablementByPackage is temporary bridge functionality. Features should be enabled via protoc flags, but
Comment thread
quartzmo marked this conversation as resolved.
// to bootstrap without breaking generation we keep the legacy definitions enabled here until we can move
// configuration upstream into tools like librarian/bazel/etc as needed.
var legacyFeatureEnablementByPackage = map[featureID][]string{
OrderedRoutingHeadersFeature: []string{
"google.firestore.v1",
"google.firestore.admin.v1",
},
WrapperTypesForPageSizeFeature: []string{
"google.cloud.bigquery.v2",
},
}

// similar to legacyFeatureEnablementByPackage, this is legacy feature enablement using the "name" field from the API
// service config.
var legacyFeatureEnablementByAPIName = map[featureID][]string{
MTLSHardBoundTokensFeature: []string{
"bigquery.googleapis.com",
"cloudasset.googleapis.com",
"clouderrorreporting.googleapis.com",
"cloudkms.googleapis.com",
"cloudresourcemanager.googleapis.com",
"cloudtasks.googleapis.com",
"cloudtrace.googleapis.com",
"dataflow.googleapis.com",
"datastore.googleapis.com",
"essentialcontacts.googleapis.com",
"firestore.googleapis.com",
"iam.googleapis.com",
"iamcredentials.googleapis.com",
"logging.googleapis.com",
"monitoring.googleapis.com",
"orgpolicy.googleapis.com",
"pubsub.googleapis.com",
"recommender.googleapis.com",
"secretmanager.googleapis.com",
"showcase.googleapis.com",
},
}

// This function consolidates legacy processing of feature enablements.
// Like the associated allowlists, it should go away once librarian and bazel can pass feature enablements directly.
func processLegacyEnablements(cfg *generatorConfig, req *pluginpb.CodeGeneratorRequest) {
// Use the first proto file in the FileDescriptorSet to handle legacy enablement by package name.
Comment thread
quartzmo marked this conversation as resolved.
if len(req.GetProtoFile()) > 0 {
probePackage := req.GetProtoFile()[0].GetPackage()
for f, packages := range legacyFeatureEnablementByPackage {
for _, v := range packages {
if probePackage == v { // matched
if cfg.featureEnablement == nil {
cfg.featureEnablement = make(map[featureID]bool)
}
cfg.featureEnablement[f] = true
Comment thread
shollyman marked this conversation as resolved.
Outdated
break
}
}
}
}
// Now, process legacy feature enablements based on API name.
if cfg.APIServiceConfig != nil {
probeName := cfg.APIServiceConfig.GetName()
for f, apis := range legacyFeatureEnablementByAPIName {
for _, v := range apis {
if probeName == v { // matched
if cfg.featureEnablement == nil {
cfg.featureEnablement = make(map[featureID]bool)
}
cfg.featureEnablement[f] = true
break
}
}
}
}
}
102 changes: 102 additions & 0 deletions internal/gengapic/feature_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// Copyright 2026 Google LLC
//
// 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
//
// https://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 gengapic

import (
"testing"

"google.golang.org/genproto/googleapis/api/serviceconfig"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/descriptorpb"
"google.golang.org/protobuf/types/pluginpb"
)

func TestProcessLegacyEnablements(t *testing.T) {
for _, tc := range []struct {
desc string
protoPkg string
apiName string
wantFeaturesEnabled []featureID
wantFeatureDisabled []featureID
}{
{
desc: "default",
wantFeatureDisabled: []featureID{
MTLSHardBoundTokensFeature,
OrderedRoutingHeadersFeature,
WrapperTypesForPageSizeFeature,
},
},
{
desc: "bigquery package",
protoPkg: "google.cloud.bigquery.v2",
wantFeaturesEnabled: []featureID{WrapperTypesForPageSizeFeature},
},
{
desc: "bigquery package only",
protoPkg: "google.cloud.bigquery.v2",
wantFeaturesEnabled: []featureID{WrapperTypesForPageSizeFeature},
wantFeatureDisabled: []featureID{OrderedRoutingHeadersFeature, MTLSHardBoundTokensFeature},
},
{
desc: "firestore admin package only",
protoPkg: "google.firestore.admin.v1",
wantFeaturesEnabled: []featureID{OrderedRoutingHeadersFeature},
wantFeatureDisabled: []featureID{WrapperTypesForPageSizeFeature, MTLSHardBoundTokensFeature},
},
{
desc: "cloud kms api name",
apiName: "cloudkms.googleapis.com",
wantFeaturesEnabled: []featureID{MTLSHardBoundTokensFeature},
wantFeatureDisabled: []featureID{WrapperTypesForPageSizeFeature, OrderedRoutingHeadersFeature},
},
} {
t.Run(tc.desc, func(t *testing.T) {
req := &pluginpb.CodeGeneratorRequest{
Parameter: proto.String("go-gapic-package=foo/bar/baz;baz"),
ProtoFile: []*descriptorpb.FileDescriptorProto{
{
Package: proto.String("foo"),
},
},
}
if tc.protoPkg != "" {
req.ProtoFile[0].Package = proto.String(tc.protoPkg)
}
cfg, err := configFromRequest(req.Parameter)
if err != nil {
t.Fatalf("configFromRequest err: %v", err)
}
if tc.apiName != "" {
cfg.APIServiceConfig = &serviceconfig.Service{
Name: tc.apiName,
}
}
g := &generator{cfg: cfg}
processLegacyEnablements(cfg, req)
for _, f := range tc.wantFeaturesEnabled {
if !g.featureEnabled(f) {
t.Errorf("expected feature %q enabled, was not", f)
}
}
for _, f := range tc.wantFeatureDisabled {
if g.featureEnabled(f) {
t.Errorf("expected feature %q to be disabled, was enabled", f)
}
}
})

}
}
49 changes: 16 additions & 33 deletions internal/gengapic/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,39 +31,6 @@ import (
"google.golang.org/protobuf/types/pluginpb"
)

// keyed by proto package name, e.g. "google.cloud.foo.v1".
var enableWrapperTypesForPageSize = map[string]bool{
"google.cloud.bigquery.v2": true,
}

var enableOrderedRoutingHeaders = map[string]bool{
"google.firestore.v1": true,
"google.firestore.admin.v1": true,
}

var enableMtlsHardBoundTokens = map[string]bool{
"bigquery.googleapis.com": true,
"cloudasset.googleapis.com": true,
"clouderrorreporting.googleapis.com": true,
"cloudkms.googleapis.com": true,
"cloudresourcemanager.googleapis.com": true,
"cloudtasks.googleapis.com": true,
"cloudtrace.googleapis.com": true,
"dataflow.googleapis.com": true,
"datastore.googleapis.com": true,
"essentialcontacts.googleapis.com": true,
"firestore.googleapis.com": true,
"iam.googleapis.com": true,
"iamcredentials.googleapis.com": true,
"logging.googleapis.com": true,
"monitoring.googleapis.com": true,
"orgpolicy.googleapis.com": true,
"pubsub.googleapis.com": true,
"recommender.googleapis.com": true,
"secretmanager.googleapis.com": true,
"showcase.googleapis.com": true,
}

type generator struct {
pt printer.P

Expand Down Expand Up @@ -131,6 +98,11 @@ func newGenerator(req *pluginpb.CodeGeneratorRequest) (*generator, error) {
if err != nil {
return nil, err
}
// Handle legacy enablement via hardcoded allowlists.
// This logic should be removed when legacy enablement is no longer needed.
Comment thread
shollyman marked this conversation as resolved.
Outdated
processLegacyEnablements(cfg, req)

// attach config to generator.
g.cfg = cfg

files := req.GetProtoFile()
Expand Down Expand Up @@ -171,6 +143,17 @@ func newGenerator(req *pluginpb.CodeGeneratorRequest) (*generator, error) {
return g, nil
}

// featureEnabled is a simple boolean checker for probing if a given feature has been enabled.
func (g *generator) featureEnabled(f featureID) bool {
if g.cfg == nil {
return false
}
if val, ok := g.cfg.featureEnablement[f]; ok && val {
return true
}
return false
}

// printf formatted-prints to sb, using the print syntax from fmt package.
//
// It automatically keeps track of indentation caused by curly-braces.
Expand Down
9 changes: 1 addition & 8 deletions internal/gengapic/gengapic.go
Original file line number Diff line number Diff line change
Expand Up @@ -456,14 +456,7 @@ func (g *generator) insertDynamicRequestHeaders(m *descriptorpb.MethodDescriptor
g.printf(" routingHeadersMap[%q] = %s", headerName, regexHelper)
g.printf("}")
}
var orderedHeaders bool
for p, ok := range enableOrderedRoutingHeaders {
if g.descInfo.ParentFile[m].GetPackage() == p && ok {
orderedHeaders = true
break
}
}
if orderedHeaders {
if g.featureEnabled(OrderedRoutingHeadersFeature) {
for i := range headers {
headerName := headers[i][2]
g.printf("if headerValue, ok := routingHeadersMap[%q]; ok {", headerName)
Expand Down
4 changes: 4 additions & 0 deletions internal/gengapic/gengapic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1425,6 +1425,7 @@ func TestInsertDynamicRequestHeaders_Ordering(t *testing.T) {
Type: make(map[string]pbinfo.ProtoType),
ParentFile: make(map[protoreflect.ProtoMessage]*descriptorpb.FileDescriptorProto),
},
cfg: &generatorConfig{},
}

m := &descriptorpb.MethodDescriptorProto{
Expand Down Expand Up @@ -1467,6 +1468,9 @@ func TestInsertDynamicRequestHeaders_Ordering(t *testing.T) {
for _, tc := range tests {
t.Run(tc.pkgName, func(t *testing.T) {
g.reset()
g.cfg.featureEnablement = map[featureID]bool{
OrderedRoutingHeadersFeature: tc.wantOrdered,
}

file := &descriptorpb.FileDescriptorProto{
Package: proto.String(tc.pkgName),
Expand Down
2 changes: 1 addition & 1 deletion internal/gengapic/gengrpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ func (g *generator) grpcClientOptions(serv *descriptorpb.ServiceDescriptorProto,
p(" internaloption.WithDefaultAudience(%q),", generateDefaultAudience(host))
p(" internaloption.WithDefaultScopes(DefaultAuthScopes()...),")
p(" internaloption.EnableJwtWithScope(),")
if _, ok := enableMtlsHardBoundTokens[g.cfg.APIServiceConfig.GetName()]; ok {
if g.featureEnabled(MTLSHardBoundTokensFeature) {
p("internaloption.AllowHardBoundTokens(\"MTLS_S2A\"),")
}
p(" internaloption.EnableNewAuthLibrary(),")
Expand Down
Loading
Loading