Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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]struct{}{
MTLSHardBoundTokensFeature: struct{}{},
},
},
}

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]struct{})
}
cfg.featureEnablement[f] = struct{}{}
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]struct{})
}
cfg.featureEnablement[f] = struct{}{}
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. (b/264668184)
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 _, ok := g.cfg.featureEnablement[f]; ok {
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
5 changes: 5 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,10 @@ func TestInsertDynamicRequestHeaders_Ordering(t *testing.T) {
for _, tc := range tests {
t.Run(tc.pkgName, func(t *testing.T) {
g.reset()
g.cfg.featureEnablement = make(map[featureID]struct{})
if tc.wantOrdered {
g.cfg.featureEnablement[OrderedRoutingHeadersFeature] = struct{}{}
}

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