|  | #  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", "GoLibrary", "GoPath", "go_context") | 
|  |  | 
|  | 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, OutputGroupInfo(go_generated_srcs = depset(library.srcs))] | 
|  |  | 
|  | # 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"], | 
|  | ) |