| # Copyright 2020 The Monogon Project Authors. |
| # |
| # SPDX-License-Identifier: Apache-2.0 |
| # |
| # 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 |
| # |
| # http://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. |
| |
| # Bazel rules for generating Kubernetes-style API types using |
| # github.com/kubernetes/code-generator. |
| # |
| # k8s.io/code-gen generators target a peculiar filesystem/package structure for |
| # generated code: |
| # |
| # example.com/project/apis/goodbye/v1/doc.go - hand written |
| # /types.go - hand written |
| # /zz_generated.go - generated |
| # example.com/project/apis/hello/v1/doc.go - hand written |
| # /types.go - hand written |
| # /zz_generated.go - generated |
| # example.com/project/generated/clientset/... - generated |
| # example.com/project/generated/informers/... - generated |
| # example.com/project/generated/listers/... - generated |
| # |
| # This means, that usually the generated files are both colocated directly |
| # with the package (for zz_generated.deepcopy.go, generated by deepcopy) |
| # and have their own package (for files generated by clientset, informers, |
| # listers). |
| # |
| # Most importantly, however, multiple Go packages (in the above example, |
| # goodbye/v1 and hello/v1) are used to in turn generate multiple output Go |
| # packages. This proves problematic when generating code for Bazel, as we have |
| # to consume the result of code generation from multiple Bazel targets (each |
| # representing a different generated Go package). |
| # |
| # To handle this, we split up the code generation into four main steps. These |
| # need to be manually instantiated by any use who wants to consume these rules. |
| # |
| # 1. Create a rules_go go_path that contains all hand-written API packages. |
| # 2. Parse hand written API packages using go_kubernetes_resource_bundle, via |
| # the generated go_path. This prepares outputs (ie., 'runs' generators), |
| # and creates an internal provider (KubeResourceBundle) that contains |
| # informations about generated Go packages. |
| # 3. For every output package, create a go_kubernetes_library target, |
| # specifying the bundle from which it's supposed to be created, and which |
| # particular library from that bundle should be used. |
| # 4. Next to every go_kubernetes_library, create a go_library with the same |
| # importpath which embeds the go_kubernetes_library. The split between |
| # go_kubernetes_library and go_library is required to let Gazelle know |
| # about the availability of given importpaths at given Bazel targets |
| # (unfortunately, it seems like Gazelle is unable to parse rules that emit |
| # the same providers as go_library does, instead requiring us to use a full |
| # go_library rule instead). |
| # |
| # Point 3. is somwhat different for the generated deepcopy code, which has to |
| # live alongside (in the same importpath) as the hand-written API go_library, |
| # and needs to be embedded into that. Special care has to be taken to not cause |
| # a cycle (handwritten API -> go_path -> bundle -> kubernetes_library -> |
| # go_library) in this case. This is done via a select() which selectively |
| # enables the inclusion of the generated deepcopy code within the hand-written |
| # API library, only enabling it for the target build, not the preprocessing |
| # done by go_kubernetes_resource_bundle. |
| # |
| # Or, in graphical form: |
| # |
| # .------------. .------------. |
| # | go_library | | go_library | |
| # |------------| |------------| |
| # | goodbye/v1 | | hello/v2 | |
| # '------------' '------------' |
| # '------. .--------' |
| # .---------. |
| # | go_path | |
| # '---------' |
| # | (preprocessing transition) |
| # .-------------------------------. |
| # | go_kubernetes_resource_bundle | |
| # '-------------------------------' |
| # | '--------------------------. |
| # .---------------------------. .--------------------------. |
| # | go_kubernetes_library | | go_kubernetes_library | |
| # |---------------------------| |--------------------------| ... others ... |
| # | clientset/verioned/typed | | clientset/verioned/fake | |
| # '---------------------------' '--------------------------' |
| # | | |
| # .---------------------------. .--------------------------. |
| # | go_library | | go_library | |
| # |---------------------------| |--------------------------| ... others ... |
| # | clientset/versioned/typed | | clientset/versioned/fake | |
| # '---------------------------' '--------------------------' |
| # |
| |
| load("@io_bazel_rules_go//go:def.bzl", "go_context", "GoPath", "GoLibrary") |
| |
| def _preprocessing_transition_impl(settings, attr): |
| return { "//metropolis/build/kube-code-generator:preprocessing": "yes" } |
| |
| |
| # preprocessing_transition is attached to the incoming go_path in |
| # go_kubernetes_resource_bundle, unsets the |
| # //metropolis/build/kube-code-generator:embed_deepcopy config setting. |
| # This allows go_libraries that make up the handwritten API libraries to only |
| # embed the generated deepcopy when they are pulled in for build reasons, not |
| # when the graph is being traversed in order to generate the deepcopy itself. |
| # This breaks up the cycle that would happen otherwise. |
| preprocessing_transition = transition( |
| implementation = _preprocessing_transition_impl, |
| inputs = [], |
| outputs = ["//metropolis/build/kube-code-generator:preprocessing"], |
| ) |
| |
| # KubeResourceBundle is emitted by go_kubernetes_resource_bundle and contains |
| # informations about libraries generated by the kubernetes code-generators. |
| KubeResourceBundle = provider( |
| "Information about the generated Go sources of a k8s.io/code-generator-built library.", |
| fields = { |
| "libraries": "Map from Go importpath to list of Files that make up this importpath.", |
| }, |
| ) |
| |
| def _go_kubernetes_library_impl(ctx): |
| go = go_context(ctx) |
| bundle = ctx.attr.bundle[KubeResourceBundle] |
| libraries = bundle.libraries |
| |
| found_importpaths = [l.importpath for l in libraries] |
| |
| libraries = [l for l in libraries if l.importpath == ctx.attr.importpath] |
| if len(libraries) < 1: |
| fail("importpath {} not found in bundle (have {})".format(ctx.attr.importpath, ", ".join(found_importpaths))) |
| if len(libraries) > 1: |
| fail("internal error: multiple libraries with importpath {} found in bundle".format(ctx.attr.importpath)) |
| library = libraries[0] |
| |
| source = go.library_to_source(go, ctx.attr, library, ctx.coverage_instrumented()) |
| return [library, source] |
| |
| |
| # go_kubernetes_library picks a single Go library from a kube_resource_bundle |
| # and prepares it for being embedded into a go_library. |
| go_kubernetes_library = rule( |
| implementation = _go_kubernetes_library_impl, |
| attrs = { |
| "bundle": attr.label( |
| mandatory = True, |
| providers = [KubeResourceBundle], |
| doc = "A go_kubernetes_resource_bundle that contains the result of a kubernetes code-generation run.", |
| ), |
| "importpath": attr.string( |
| mandatory = True, |
| doc = "The importpath of the library picked from the bundle, same as the importpath of the go_library that embeds it.", |
| ), |
| "deps": attr.label_list( |
| providers = [GoLibrary], |
| doc = "All build dependencies of this library.", |
| ), |
| |
| "_go_context_data": attr.label( |
| default = "@io_bazel_rules_go//:go_context_data", |
| ), |
| |
| }, |
| toolchains = ["@io_bazel_rules_go//go:toolchain"], |
| ) |
| |
| # _gotool_run is a helper function which runs an executable under |
| # //metropolis/build/gotoolwrap, effectively setting up everything required to |
| # use standard Go tooling on the monogon workspace (ie. GOPATH/GOROOT). This is |
| # required by generators to run 'go fmt'. |
| def _gotool_run(ctx, executable, arguments, **kwargs): |
| go = go_context(ctx) |
| gopath = ctx.attr.gopath[0][GoPath] |
| |
| inputs = [ |
| gopath.gopath_file, |
| ] + kwargs.get('inputs', []) |
| |
| tools = [ |
| executable, |
| go.sdk.go, |
| ] + go.sdk.tools + kwargs.get('tools', []) |
| |
| env = { |
| "GOTOOLWRAP_GOPATH": gopath.gopath_file.path, |
| "GOTOOLWRAP_GOROOT": go.sdk.root_file.dirname, |
| } |
| env.update(kwargs.get('env', {})) |
| |
| kwargs_ = dict([(k, v) for (k,v) in kwargs.items() if k not in [ |
| 'executable', 'arguments', 'inputs', 'env', 'tools', |
| ]]) |
| |
| ctx.actions.run( |
| executable = ctx.executable._gotoolwrap, |
| arguments = [ executable.path ] + arguments, |
| env = env, |
| inputs = inputs, |
| tools = tools, |
| **kwargs_, |
| ) |
| |
| |
| # _output_directory returns the relative path into which |
| # ctx.action.declare_file writes are rooted. This is used as code-generators |
| # require a root path for all outputted files, instead of a list of files to |
| # emit. |
| def _output_directory(ctx): |
| # We combine bin_dir, the BUILDfile path and the target name. This seems |
| # wrong. Is there no simpler way to do this? |
| buildfile_path = ctx.build_file_path |
| parts = buildfile_path.split('/') |
| if not parts[-1].startswith('BUILD'): |
| fail("internal error: unexpected BUILD file path: {}", parts[-1]) |
| package_path = '/'.join(parts[:-1]) |
| return '/'.join([ctx.bin_dir.path, package_path, ctx.attr.name]) |
| |
| |
| # _cg returns a 'codegen context', a struct that's used to accumulate the |
| # results of code generation. It assumes all output will be rooted in a |
| # generated importpath (with more 'shortened' importpaths underneath the root), |
| # and collects outputs to pass to the codegen execution action. It also |
| # collects a map of importpaths to outputs that make it up. |
| def _cg(ctx, importpath): |
| output_root = _output_directory(ctx) |
| |
| return struct( |
| # The 'root' importpath, under which 'shortened' importpaths reside. |
| importpath = importpath, |
| # The prefix into which all files will be emitted. We use the target |
| # name for convenience. |
| output_prefix = ctx.attr.name, |
| # The full relative path visible to the codegen, pointing to the same |
| # directory as output_prefix (just from the point of view of the |
| # runtime filesystem, not the ctx.actions filepath declaration API). |
| output_root = output_root, |
| # The list of outputs that have to be generated by the codegen. |
| outputs = [], |
| # A map of importpath to list of outputs (from the above list) that |
| # make up a generated Go package/library. |
| libraries = {}, |
| |
| ctx = ctx, |
| ) |
| |
| |
| # _declare_library adds a single Go package/library at importpath to the |
| # codegen context with the given file paths (rooted in the importpath). |
| def _declare_library(cg, importpath, files): |
| importpath = cg.importpath + "/" + importpath |
| cg.libraries[importpath] = [] |
| for f in files: |
| output = cg.ctx.actions.declare_file("{}/{}/{}".format( |
| cg.output_prefix, |
| importpath, |
| f, |
| )) |
| cg.outputs.append(output) |
| cg.libraries[importpath].append(output) |
| |
| |
| # _declare_libraries declares multiple Go package/libraries to the codegen |
| # context. The key of the dictionary is the importpath of the library, and the |
| # value are the file names of generated outputs. |
| def _declare_libraries(cg, libraries): |
| for k, v in libraries.items(): |
| _declare_library(cg, k, v) |
| |
| |
| # _codegen_clientset runs the clientset codegenerator. |
| def _codegen_clientset(ctx): |
| cg = _cg(ctx, ctx.attr.importpath) |
| |
| _declare_libraries(cg, { |
| "clientset/versioned": ["clientset.go", "doc.go"], |
| "clientset/versioned/fake": ["register.go", "clientset_generated.go"], |
| "clientset/versioned/scheme": ["register.go", "doc.go"], |
| }) |
| |
| for api, types in ctx.attr.apis.items(): |
| client_name = api.split("/")[-2] |
| _declare_libraries(cg, { |
| "clientset/versioned/typed/{}".format(api): [ |
| "doc.go", "generated_expansion.go", |
| "{}_client.go".format(client_name), |
| ] + [ |
| "{}.go".format(t) for t in types |
| ], |
| "clientset/versioned/typed/{}/fake".format(api): [ |
| "doc.go", |
| "fake_{}_client.go".format(client_name), |
| ], |
| }) |
| |
| _gotool_run(ctx, |
| mnemonic = "ClientsetGen", |
| executable = ctx.executable._client_gen, |
| arguments = [ |
| "--clientset-name", "versioned", |
| "--input-base", ctx.attr.apipath, |
| "--input", ",".join(ctx.attr.apis), |
| "--output-package", cg.importpath + "/clientset", |
| "--output-base", cg.output_root, |
| "--go-header-file", ctx.file.boilerplate.path, |
| ], |
| inputs = [ |
| ctx.file.boilerplate, |
| ], |
| outputs = cg.outputs, |
| ) |
| |
| return cg.libraries |
| |
| |
| # _codegen_deepcopy runs the deepcopy codegenerator (outputting to the apipath, |
| # not the importpath). |
| def _codegen_deepcopy(ctx): |
| cg = _cg(ctx, ctx.attr.apipath) |
| |
| for api, types in ctx.attr.apis.items(): |
| _declare_libraries(cg, { |
| api: ["zz_generated.deepcopy.go"], |
| }) |
| |
| _gotool_run( |
| ctx, |
| mnemonic = "DeepcopyGen", |
| executable = ctx.executable._deepcopy_gen, |
| arguments = [ |
| "--input-dirs", ",".join(["{}/{}".format(ctx.attr.apipath, api) for api in ctx.attr.apis]), |
| "--go-header-file", ctx.file.boilerplate.path, |
| "--stderrthreshold", "0", |
| "-O", "zz_generated.deepcopy", |
| "--output-base", cg.output_root, |
| ctx.attr.apipath, |
| ], |
| inputs = [ |
| ctx.file.boilerplate, |
| ], |
| outputs = cg.outputs, |
| ) |
| return cg.libraries |
| |
| |
| # _codegen_informer runs the informer codegenerator. |
| def _codegen_informer(ctx): |
| cg = _cg(ctx, ctx.attr.importpath) |
| |
| _declare_libraries(cg, { |
| "informers/externalversions": [ "factory.go", "generic.go" ], |
| "informers/externalversions/internalinterfaces": [ "factory_interfaces.go" ], |
| }) |
| |
| for api, types in ctx.attr.apis.items(): |
| client_name = api.split("/")[-2] |
| _declare_libraries(cg, { |
| "informers/externalversions/{}".format(client_name): [ "interface.go" ], |
| "informers/externalversions/{}".format(api): [ |
| "interface.go", |
| ] + [ |
| "{}.go".format(t) |
| for t in types |
| ], |
| }) |
| |
| _gotool_run( |
| ctx, |
| mnemonic = "InformerGen", |
| executable = ctx.executable._informer_gen, |
| arguments = [ |
| "--input-dirs", ",".join(["{}/{}".format(ctx.attr.apipath, api) for api in ctx.attr.apis]), |
| "--versioned-clientset-package", "{}/clientset/versioned".format(ctx.attr.importpath), |
| "--listers-package", "{}/listers".format(ctx.attr.importpath), |
| "--output-package", "{}/informers".format(ctx.attr.importpath), |
| "--output-base", cg.output_root, |
| "--go-header-file", ctx.file.boilerplate.path, |
| ], |
| inputs = [ |
| ctx.file.boilerplate, |
| ], |
| outputs = cg.outputs, |
| ) |
| |
| return cg.libraries |
| |
| |
| # _codegen_lister runs the lister codegenerator. |
| def _codegen_lister(ctx): |
| cg = _cg(ctx, ctx.attr.importpath) |
| |
| for api, types in ctx.attr.apis.items(): |
| client_name = api.split("/")[-2] |
| _declare_libraries(cg, { |
| "listers/{}".format(api): [ |
| "expansion_generated.go", |
| ] + [ |
| "{}.go".format(t) |
| for t in types |
| ] |
| }) |
| |
| _gotool_run( |
| ctx, |
| mnemonic = "ListerGen", |
| executable = ctx.executable._lister_gen, |
| arguments = [ |
| "--input-dirs", ",".join(["{}/{}".format(ctx.attr.apipath, api) for api in ctx.attr.apis]), |
| "--output-package", "{}/listers".format(ctx.attr.importpath), |
| "--output-base", cg.output_root, |
| "--go-header-file", ctx.file.boilerplate.path, |
| "-v", "10", |
| ], |
| inputs = [ |
| ctx.file.boilerplate, |
| ], |
| outputs = cg.outputs, |
| ) |
| |
| return cg.libraries |
| |
| |
| # _update_dict_check is a helper function that updates dict a with dict b, |
| # ensuring there's no overwritten keys. |
| def _update_dict_check(a, b): |
| for k in b.keys(): |
| if k in a: |
| fail("internal error: repeat importpath {}", k) |
| a.update(b) |
| |
| |
| def _go_kubernetes_resource_bundle_impl(ctx): |
| go = go_context(ctx) |
| |
| all_gens = {} |
| _update_dict_check(all_gens, _codegen_clientset(ctx)) |
| _update_dict_check(all_gens, _codegen_deepcopy(ctx)) |
| _update_dict_check(all_gens, _codegen_informer(ctx)) |
| _update_dict_check(all_gens, _codegen_lister(ctx)) |
| |
| libraries = [] |
| for importpath, srcs in all_gens.items(): |
| library = go.new_library( |
| go, |
| srcs = srcs, |
| importpath = importpath, |
| ) |
| libraries.append(library) |
| |
| return [KubeResourceBundle(libraries=libraries)] |
| |
| |
| # go_kubernetes_resource_bundle runs kubernetes code-generators on a codepath |
| # for some requested APIs, and whose output can be made into Go library targets |
| # via go_kubernetes_library. This bundle corresponds to a single Kubernetes API |
| # resource group. |
| go_kubernetes_resource_bundle = rule( |
| implementation = _go_kubernetes_resource_bundle_impl, |
| attrs = { |
| "gopath": attr.label( |
| mandatory = True, |
| providers = [GoPath], |
| cfg = preprocessing_transition, |
| doc = "A rules_go go_path that contains all the API libraries for which codegen should be run.", |
| ), |
| |
| "importpath": attr.string( |
| mandatory = True, |
| doc = """ |
| The root importpath of the generated code (apart from deepcopy |
| codegen). The Bazel target path corresponding to this |
| importpath needs to contain the go_kubernetes_library and |
| go_library targets that allow to actually build against the |
| generated code. |
| """, |
| ), |
| |
| "apipath": attr.string( |
| mandatory = True, |
| doc = "The root importpath of the APIs for which to generate code.", |
| ), |
| "apis": attr.string_list_dict( |
| mandatory = True, |
| doc = """ |
| The APIs underneath importpath for which to generated code, |
| eg. foo/v1, mapping into a list of lowercased types generated |
| from each (eg. widget for `type Widget struct`). |
| """, |
| ), |
| |
| "boilerplate": attr.label( |
| default = "//metropolis/build/kube-code-generator:boilerplate.go.txt", |
| allow_single_file = True, |
| doc = "Header that will be used in the generated code.", |
| ), |
| |
| "_go_context_data": attr.label( |
| default = "@io_bazel_rules_go//:go_context_data", |
| ), |
| |
| "_gotoolwrap": attr.label( |
| default = Label("//metropolis/build/gotoolwrap"), |
| allow_single_file = True, |
| executable = True, |
| cfg = "exec", |
| ), |
| |
| "_deepcopy_gen": attr.label( |
| default = Label("@io_k8s_code_generator//cmd/deepcopy-gen"), |
| allow_single_file = True, |
| executable = True, |
| cfg = "exec", |
| ), |
| "_client_gen": attr.label( |
| default = Label("@io_k8s_code_generator//cmd/client-gen"), |
| allow_single_file = True, |
| executable = True, |
| cfg = "exec", |
| ), |
| "_informer_gen": attr.label( |
| default = Label("@io_k8s_code_generator//cmd/informer-gen"), |
| allow_single_file = True, |
| executable = True, |
| cfg = "exec", |
| ), |
| "_lister_gen": attr.label( |
| default = Label("@io_k8s_code_generator//cmd/lister-gen"), |
| allow_single_file = True, |
| executable = True, |
| cfg = "exec", |
| ), |
| |
| "_allowlist_function_transition": attr.label( |
| default = "@bazel_tools//tools/allowlists/function_transition_allowlist" |
| ) |
| }, |
| toolchains = ["@io_bazel_rules_go//go:toolchain"], |
| ) |