Skip to content

Commit

Permalink
codegen: enable CRD schema generation using protoc-gen-openapi (#539)
Browse files Browse the repository at this point in the history
* codegen: enable CRD schema generation using protoc-gen-openapi

Allows generating CRDs schema using protoc-gen-openapi instead
of Cue.

This is a part of a larger effort to customize schema generation
that is harder to accomplish using Cue:
solo-io/gloo-mesh-enterprise#16049

Signed-off-by: Shashank Ram <[email protected]>

* update deps and codegen

* add test

---------

Signed-off-by: Shashank Ram <[email protected]>
  • Loading branch information
shashankram authored Apr 5, 2024
1 parent aaa4fae commit b550b99
Show file tree
Hide file tree
Showing 14 changed files with 1,188 additions and 367 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/pull_request.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- name: Set up Go 1.21
- name: Set up Go 1.22
uses: actions/setup-go@v4
with:
go-version-file: "go.mod"
Expand Down
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export PATH:=$(GOBIN):$(PATH)
install-go-tools: mod-download
mkdir -p $(DEPSGOBIN)
go install github.com/golang/protobuf/[email protected]
go install github.com/solo-io/[email protected]
go install github.com/pseudomuto/protoc-gen-doc/cmd/protoc-gen-doc
go install github.com/solo-io/[email protected]
go install github.com/golang/mock/[email protected]
Expand Down Expand Up @@ -57,7 +58,7 @@ generated-code: install-tools update-licenses
go run api/generate.go
# the api/generate.go command is separated out to enable us to run go generate on the generated files (used for mockgen)
# this re-gens test protos
go test ./codegen
PATH=$(DEPSGOBIN):$(PATH) go test ./codegen
go generate -v ./...
$(DEPSGOBIN)/goimports -w .
go mod tidy
Expand Down
7 changes: 7 additions & 0 deletions changelog/v0.37.0/protoc-gen.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
changelog:
- type: BREAKING_CHANGE
issueLink: https://github.com/solo-io/gloo-mesh-enterprise/issues/16049
resolvesIssue: false
description: >
"Enable CRD schema generation using protoc-gen-openapi"
skipCI: "false"
38 changes: 19 additions & 19 deletions ci/oss_compliance/osa_provided.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@ Name|Version|License
[go-spew/spew](https://github.com/davecgh/go-spew)|v1.1.1|ISC License
[v3/log](https://github.com/emicklei/go-restful)|v3.11.0|MIT License
[json-patch/v5](https://github.com/evanphx/json-patch)|v5.6.0|BSD 3-clause "New" or "Revised" License
[fsnotify/fsnotify](https://github.com/fsnotify/fsnotify)|v1.6.0|BSD 3-clause "New" or "Revised" License
[go-logr/logr](https://github.com/go-logr/logr)|v1.2.4|Apache License 2.0
[go-openapi/jsonpointer](https://github.com/go-openapi/jsonpointer)|v0.19.6|Apache License 2.0
[fsnotify/fsnotify](https://github.com/fsnotify/fsnotify)|v1.7.0|BSD 3-clause "New" or "Revised" License
[logr/slogr](https://github.com/go-logr/logr)|v1.3.0|Apache License 2.0
[go-openapi/jsonpointer](https://github.com/go-openapi/jsonpointer)|v0.20.2|Apache License 2.0
[jsonreference/internal](https://github.com/go-openapi/jsonreference)|v0.20.2|Apache License 2.0
[go-openapi/swag](https://github.com/go-openapi/swag)|v0.22.3|Apache License 2.0
[go-openapi/swag](https://github.com/go-openapi/swag)|v0.22.8|Apache License 2.0
[gogo/protobuf](https://github.com/gogo/protobuf)|v1.3.2|BSD 3-clause "New" or "Revised" License
[groupcache/lru](https://github.com/golang/groupcache)|v0.0.0-20210331224755-41bb18bfe9da|Apache License 2.0
[golang/protobuf](https://github.com/golang/protobuf)|v1.5.3|BSD 3-clause "New" or "Revised" License
[google/gnostic-models](https://github.com/google/gnostic-models)|v0.6.8|Apache License 2.0
[cmp/internal](https://github.com/google/go-cmp)|v0.5.9|BSD 3-clause "New" or "Revised" License
[cmp/internal](https://github.com/google/go-cmp)|v0.6.0|BSD 3-clause "New" or "Revised" License
[gofuzz/bytesource](https://github.com/google/gofuzz)|v1.2.0|Apache License 2.0
[google/uuid](https://github.com/google/uuid)|v1.3.0|BSD 3-clause "New" or "Revised" License
[josharian/intern](https://github.com/josharian/intern)|v1.0.0|MIT License
Expand All @@ -33,27 +33,27 @@ Name|Version|License
[go.uber.org/multierr](https://go.uber.org/multierr)|v1.11.0|MIT License
[go.uber.org/zap](https://go.uber.org/zap)|v1.25.0|MIT License
[exp/maps](https://golang.org/x/exp/maps)|v0.0.0-20220921164117-439092de6870|BSD 3-clause "New" or "Revised" License
[x/net](https://golang.org/x/net)|v0.17.0|BSD 3-clause "New" or "Revised" License
[oauth2/internal](https://golang.org/x/oauth2/internal)|v0.8.0|BSD 3-clause "New" or "Revised" License
[x/term](https://golang.org/x/term)|v0.13.0|BSD 3-clause "New" or "Revised" License
[x/text](https://golang.org/x/text)|v0.13.0|BSD 3-clause "New" or "Revised" License
[x/net](https://golang.org/x/net)|v0.19.0|BSD 3-clause "New" or "Revised" License
[oauth2/internal](https://golang.org/x/oauth2/internal)|v0.10.0|BSD 3-clause "New" or "Revised" License
[x/term](https://golang.org/x/term)|v0.15.0|BSD 3-clause "New" or "Revised" License
[x/text](https://golang.org/x/text)|v0.14.0|BSD 3-clause "New" or "Revised" License
[time/rate](https://golang.org/x/time/rate)|v0.3.0|BSD 3-clause "New" or "Revised" License
[jsonpatch/v2](https://gomodules.xyz/jsonpatch/v2)|v2.4.0|Apache License 2.0
[google.golang.org/protobuf](https://google.golang.org/protobuf)|v1.30.0|BSD 3-clause "New" or "Revised" License
[google.golang.org/protobuf](https://google.golang.org/protobuf)|v1.31.0|BSD 3-clause "New" or "Revised" License
[gopkg.in/inf.v0](https://gopkg.in/inf.v0)|v0.9.1|BSD 3-clause "New" or "Revised" License
[gopkg.in/yaml.v2](https://gopkg.in/yaml.v2)|v2.4.0|Apache License 2.0
[gopkg.in/yaml.v3](https://gopkg.in/yaml.v3)|v3.0.1|MIT License
[k8s.io/api](https://k8s.io/api)|v0.28.3|Apache License 2.0
[k8s.io/apimachinery](https://k8s.io/apimachinery)|v0.28.3|Apache License 2.0
[k8s.io/client-go](https://k8s.io/client-go)|v0.28.3|Apache License 2.0
[config/v1alpha1](https://k8s.io/component-base/config/v1alpha1)|v0.28.3|Apache License 2.0
[v2/internal](https://k8s.io/klog/v2/internal)|v2.100.1|Apache License 2.0
[kube-openapi/pkg](https://k8s.io/kube-openapi/pkg)|v0.0.0-20230717233707-2695361300d9|Apache License 2.0
[k8s.io/utils](https://k8s.io/utils)|v0.0.0-20230406110748-d93618cff8a2|Apache License 2.0
[k8s.io/api](https://k8s.io/api)|v0.29.0|Apache License 2.0
[k8s.io/apimachinery](https://k8s.io/apimachinery)|v0.29.0|Apache License 2.0
[k8s.io/client-go](https://k8s.io/client-go)|v0.29.0|Apache License 2.0
[config/v1alpha1](https://k8s.io/component-base/config/v1alpha1)|v0.29.0|Apache License 2.0
[v2/internal](https://k8s.io/klog/v2/internal)|v2.110.1|Apache License 2.0
[kube-openapi/pkg](https://k8s.io/kube-openapi/pkg)|v0.0.0-20231010175941-2dd684a91f00|Apache License 2.0
[k8s.io/utils](https://k8s.io/utils)|v0.0.0-20230726121419-3b25d923346b|Apache License 2.0
[controller-runtime/pkg](https://sigs.k8s.io/controller-runtime/pkg)|v0.16.3|Apache License 2.0
[encoding/json](https://sigs.k8s.io/json/internal/golang/encoding/json)|v0.0.0-20221116044647-bc3834ca7abd|Apache License 2.0
[structured-merge-diff/v4](https://sigs.k8s.io/structured-merge-diff/v4)|v4.2.3|Apache License 2.0
[sigs.k8s.io/yaml](https://sigs.k8s.io/yaml)|v1.3.0|MIT License
[structured-merge-diff/v4](https://sigs.k8s.io/structured-merge-diff/v4)|v4.4.1|Apache License 2.0
[yaml/goyaml.v2](https://sigs.k8s.io/yaml/goyaml.v2)|v1.4.0|Apache License 2.0
[cmd/goimports](https://golang.org/x/tools/cmd/goimports)|latest|MIT License
[gogo/protobuf](https://github.com/gogo/protobuf)|latest|MIT License
[envoyproxy/envoy](https://github.com/envoyproxy/envoy)|latest|Apache License 2.0
Expand Down
1 change: 1 addition & 0 deletions ci/tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
_ "github.com/golang/protobuf/protoc-gen-go"
_ "github.com/pseudomuto/protoc-gen-doc/cmd/protoc-gen-doc"
_ "github.com/solo-io/protoc-gen-ext"
_ "github.com/solo-io/protoc-gen-openapi"
_ "golang.org/x/tools/cmd/goimports"
_ "k8s.io/code-generator"
)
126 changes: 126 additions & 0 deletions codegen/collector/executor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package collector

import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"

"github.com/rotisserie/eris"
)

type ProtocExecutor interface {
Execute(protoFile string, toFile string, imports []string) error
}

type DefaultProtocExecutor struct {
// The output directory
OutputDir string
// whether or not to do a regular go-proto generate while collecting descriptors
ShouldCompileFile func(string) bool
// arguments for go_out=
CustomGoArgs []string
// custom plugins
// each will append a <plugin>_out= directive to protoc command
CustomPlugins []string
}

var defaultGoArgs = []string{
"plugins=grpc",
}

func (d *DefaultProtocExecutor) Execute(protoFile string, toFile string, imports []string) error {
cmd := exec.Command("protoc")

for _, i := range imports {
cmd.Args = append(cmd.Args, fmt.Sprintf("-I%s", i))
}

if d.ShouldCompileFile(protoFile) {
goArgs := append(defaultGoArgs, d.CustomGoArgs...)
goArgsJoined := strings.Join(goArgs, ",")
cmd.Args = append(cmd.Args,
fmt.Sprintf("--go_out=%s:%s", goArgsJoined, d.OutputDir),
fmt.Sprintf("--ext_out=%s:%s", goArgsJoined, d.OutputDir),
)

for _, pluginName := range d.CustomPlugins {
cmd.Args = append(cmd.Args,
fmt.Sprintf("--%s_out=%s:%s", pluginName, goArgsJoined, d.OutputDir),
)
}
}

cmd.Args = append(cmd.Args,
"-o",
toFile,
"--include_imports",
"--include_source_info",
protoFile)

out, err := cmd.CombinedOutput()
if err != nil {
return eris.Wrapf(err, "%v failed: %s", cmd.Args, out)
}
return nil
}

type OpenApiProtocExecutor struct {
OutputDir string

// Whether to include descriptions in validation schemas
IncludeDescriptionsInSchema bool

// Whether to assign Enum fields the `x-kubernetes-int-or-string` property
// which allows the value to either be an integer or a string
EnumAsIntOrString bool

// A list of messages (core.solo.io.Status) whose validation schema should
// not be generated
MessagesWithEmptySchema []string
}

func (o *OpenApiProtocExecutor) Execute(protoFile string, toFile string, imports []string) error {
cmd := exec.Command("protoc")

for _, i := range imports {
cmd.Args = append(cmd.Args, fmt.Sprintf("-I%s", i))
}

// The way that --openapi_out works, is that it produces a file in an output directory,
// with the name of the file matching the proto package (ie gloo.solo.io).
// Therefore, if you have multiple protos in a single package, they will all be output
// to the same file, and overwrite one another.
// To avoid this, we generate a directory with the name of the proto file.
// For example my_resource.proto in the gloo.solo.io package will produce the following file:
// my_resource/gloo.solo.io.yaml

// The directoryName is created by taking the name of the file and removing the extension
_, fileName := filepath.Split(protoFile)
directoryName := fileName[0 : len(fileName)-len(filepath.Ext(fileName))]

// Create the directory
directoryPath := filepath.Join(o.OutputDir, directoryName)
_ = os.Mkdir(directoryPath, os.ModePerm)

cmd.Args = append(cmd.Args,
fmt.Sprintf("--openapi_out=yaml=true,single_file=false,include_description=true,multiline_description=true,enum_as_int_or_string=%v,proto_oneof=true,int_native=true,additional_empty_schema=%v:%s",
o.EnumAsIntOrString,
strings.Join(o.MessagesWithEmptySchema, "+"),
directoryPath),
)

cmd.Args = append(cmd.Args,
"-o",
toFile,
"--include_imports",
"--include_source_info",
protoFile)

out, err := cmd.CombinedOutput()
if err != nil {
return eris.Wrapf(err, "%v failed: %s", cmd.Args, out)
}
return nil
}
6 changes: 6 additions & 0 deletions codegen/model/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

"github.com/golang/protobuf/proto"
"github.com/solo-io/skv2/codegen/collector"
"github.com/solo-io/skv2/codegen/proto/schemagen"
apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
)
Expand Down Expand Up @@ -130,6 +131,11 @@ type Group struct {
type GroupOptions struct {
// Required when using crds in the templates directory
EscapeGoTemplateOperators bool

// Options for generating validation schemas
SchemaValidationOpts schemagen.ValidationSchemaOptions

SchemaGenerator schemagen.GeneratorKind
}

func (g Group) HasProtos() bool {
Expand Down
44 changes: 44 additions & 0 deletions codegen/proto/schemagen/generator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package schemagen

type GeneratorKind string

const (
Cue GeneratorKind = "cue"
ProtocGenOpenAPI GeneratorKind = "protoc-gen-openapi"
)

type ValidationSchemaOptions struct {
// Whether to assign Enum fields the `x-kubernetes-int-or-string` property
// which allows the value to either be an integer or a string
// If this is false, only string values are allowed
// Default: false
EnumAsIntOrString bool

// A list of messages (e.g. ratelimit.api.solo.io.Descriptor) whose validation schema should
// not be generated
MessagesWithEmptySchema []string
}

// prevent k8s from validating proto.Any fields (since it's unstructured)
func removeProtoAnyValidation(d map[string]interface{}, propertyField string) {
for _, v := range d {
values, ok := v.(map[string]interface{})
if !ok {
continue
}
desc, ok := values["properties"]
properties, isObj := desc.(map[string]interface{})
// detect proto.Any field from presence of [propertyField] as field under "properties"
if !ok || !isObj || properties[propertyField] == nil {
removeProtoAnyValidation(values, propertyField)
continue
}
// remove "properties" value
delete(values, "properties")
// remove "required" value
delete(values, "required")
// x-kubernetes-preserve-unknown-fields allows for unknown fields from a particular node
// see https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#specifying-a-structural-schema
values["x-kubernetes-preserve-unknown-fields"] = true
}
}
Loading

0 comments on commit b550b99

Please sign in to comment.