blob: a1549321e3d9df2c3542815d2b69c3dcd00c8753 [file] [log] [blame]
Serge Bazanski7353e172021-03-31 22:09:22 +02001# Copyright 2020 The Monogon Project Authors.
2#
3# SPDX-License-Identifier: Apache-2.0
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17# Bazel rules for generating Kubernetes-style API types using
18# github.com/kubernetes/code-generator.
19#
20# k8s.io/code-gen generators target a peculiar filesystem/package structure for
21# generated code:
22#
23# example.com/project/apis/goodbye/v1/doc.go - hand written
24# /types.go - hand written
25# /zz_generated.go - generated
26# example.com/project/apis/hello/v1/doc.go - hand written
27# /types.go - hand written
28# /zz_generated.go - generated
29# example.com/project/generated/clientset/... - generated
30# example.com/project/generated/informers/... - generated
31# example.com/project/generated/listers/... - generated
32#
33# This means, that usually the generated files are both colocated directly
34# with the package (for zz_generated.deepcopy.go, generated by deepcopy)
35# and have their own package (for files generated by clientset, informers,
36# listers).
37#
38# Most importantly, however, multiple Go packages (in the above example,
39# goodbye/v1 and hello/v1) are used to in turn generate multiple output Go
40# packages. This proves problematic when generating code for Bazel, as we have
41# to consume the result of code generation from multiple Bazel targets (each
42# representing a different generated Go package).
43#
44# To handle this, we split up the code generation into four main steps. These
45# need to be manually instantiated by any use who wants to consume these rules.
46#
47# 1. Create a rules_go go_path that contains all hand-written API packages.
48# 2. Parse hand written API packages using go_kubernetes_resource_bundle, via
49# the generated go_path. This prepares outputs (ie., 'runs' generators),
50# and creates an internal provider (KubeResourceBundle) that contains
51# informations about generated Go packages.
52# 3. For every output package, create a go_kubernetes_library target,
53# specifying the bundle from which it's supposed to be created, and which
54# particular library from that bundle should be used.
55# 4. Next to every go_kubernetes_library, create a go_library with the same
56# importpath which embeds the go_kubernetes_library. The split between
57# go_kubernetes_library and go_library is required to let Gazelle know
58# about the availability of given importpaths at given Bazel targets
59# (unfortunately, it seems like Gazelle is unable to parse rules that emit
60# the same providers as go_library does, instead requiring us to use a full
61# go_library rule instead).
62#
63# Point 3. is somwhat different for the generated deepcopy code, which has to
64# live alongside (in the same importpath) as the hand-written API go_library,
65# and needs to be embedded into that. Special care has to be taken to not cause
66# a cycle (handwritten API -> go_path -> bundle -> kubernetes_library ->
67# go_library) in this case. This is done via a select() which selectively
68# enables the inclusion of the generated deepcopy code within the hand-written
69# API library, only enabling it for the target build, not the preprocessing
70# done by go_kubernetes_resource_bundle.
71#
72# Or, in graphical form:
73#
74# .------------. .------------.
75# | go_library | | go_library |
76# |------------| |------------|
77# | goodbye/v1 | | hello/v2 |
78# '------------' '------------'
79# '------. .--------'
80# .---------.
81# | go_path |
82# '---------'
83# | (preprocessing transition)
84# .-------------------------------.
85# | go_kubernetes_resource_bundle |
86# '-------------------------------'
87# | '--------------------------.
Lorenz Brun4e0dba62021-05-17 15:25:15 +020088# .---------------------------. .--------------------------.
Serge Bazanski7353e172021-03-31 22:09:22 +020089# | go_kubernetes_library | | go_kubernetes_library |
90# |---------------------------| |--------------------------| ... others ...
91# | clientset/verioned/typed | | clientset/verioned/fake |
92# '---------------------------' '--------------------------'
93# | |
Lorenz Brun4e0dba62021-05-17 15:25:15 +020094# .---------------------------. .--------------------------.
Serge Bazanski7353e172021-03-31 22:09:22 +020095# | go_library | | go_library |
96# |---------------------------| |--------------------------| ... others ...
97# | clientset/versioned/typed | | clientset/versioned/fake |
98# '---------------------------' '--------------------------'
99#
100
Lorenz Brun4e0dba62021-05-17 15:25:15 +0200101load("@io_bazel_rules_go//go:def.bzl", "GoLibrary", "GoPath", "go_context")
Serge Bazanski7353e172021-03-31 22:09:22 +0200102
103def _preprocessing_transition_impl(settings, attr):
Lorenz Brun4e0dba62021-05-17 15:25:15 +0200104 return {"//metropolis/build/kube-code-generator:preprocessing": "yes"}
Serge Bazanski7353e172021-03-31 22:09:22 +0200105
106# preprocessing_transition is attached to the incoming go_path in
107# go_kubernetes_resource_bundle, unsets the
108# //metropolis/build/kube-code-generator:embed_deepcopy config setting.
109# This allows go_libraries that make up the handwritten API libraries to only
110# embed the generated deepcopy when they are pulled in for build reasons, not
111# when the graph is being traversed in order to generate the deepcopy itself.
112# This breaks up the cycle that would happen otherwise.
113preprocessing_transition = transition(
114 implementation = _preprocessing_transition_impl,
115 inputs = [],
116 outputs = ["//metropolis/build/kube-code-generator:preprocessing"],
117)
118
119# KubeResourceBundle is emitted by go_kubernetes_resource_bundle and contains
120# informations about libraries generated by the kubernetes code-generators.
121KubeResourceBundle = provider(
122 "Information about the generated Go sources of a k8s.io/code-generator-built library.",
123 fields = {
124 "libraries": "Map from Go importpath to list of Files that make up this importpath.",
125 },
126)
127
128def _go_kubernetes_library_impl(ctx):
129 go = go_context(ctx)
130 bundle = ctx.attr.bundle[KubeResourceBundle]
131 libraries = bundle.libraries
132
133 found_importpaths = [l.importpath for l in libraries]
134
135 libraries = [l for l in libraries if l.importpath == ctx.attr.importpath]
136 if len(libraries) < 1:
137 fail("importpath {} not found in bundle (have {})".format(ctx.attr.importpath, ", ".join(found_importpaths)))
138 if len(libraries) > 1:
139 fail("internal error: multiple libraries with importpath {} found in bundle".format(ctx.attr.importpath))
140 library = libraries[0]
141
142 source = go.library_to_source(go, ctx.attr, library, ctx.coverage_instrumented())
Lorenz Brun4e0dba62021-05-17 15:25:15 +0200143 return [library, source, OutputGroupInfo(go_generated_srcs = depset(library.srcs))]
Serge Bazanski7353e172021-03-31 22:09:22 +0200144
145# go_kubernetes_library picks a single Go library from a kube_resource_bundle
146# and prepares it for being embedded into a go_library.
147go_kubernetes_library = rule(
148 implementation = _go_kubernetes_library_impl,
149 attrs = {
150 "bundle": attr.label(
151 mandatory = True,
152 providers = [KubeResourceBundle],
153 doc = "A go_kubernetes_resource_bundle that contains the result of a kubernetes code-generation run.",
154 ),
155 "importpath": attr.string(
156 mandatory = True,
157 doc = "The importpath of the library picked from the bundle, same as the importpath of the go_library that embeds it.",
158 ),
159 "deps": attr.label_list(
160 providers = [GoLibrary],
161 doc = "All build dependencies of this library.",
162 ),
Serge Bazanski7353e172021-03-31 22:09:22 +0200163 "_go_context_data": attr.label(
164 default = "@io_bazel_rules_go//:go_context_data",
165 ),
Serge Bazanski7353e172021-03-31 22:09:22 +0200166 },
167 toolchains = ["@io_bazel_rules_go//go:toolchain"],
168)
169
170# _gotool_run is a helper function which runs an executable under
171# //metropolis/build/gotoolwrap, effectively setting up everything required to
172# use standard Go tooling on the monogon workspace (ie. GOPATH/GOROOT). This is
173# required by generators to run 'go fmt'.
174def _gotool_run(ctx, executable, arguments, **kwargs):
175 go = go_context(ctx)
176 gopath = ctx.attr.gopath[0][GoPath]
177
178 inputs = [
179 gopath.gopath_file,
Lorenz Brun4e0dba62021-05-17 15:25:15 +0200180 ] + kwargs.get("inputs", [])
Serge Bazanski7353e172021-03-31 22:09:22 +0200181
182 tools = [
183 executable,
184 go.sdk.go,
Lorenz Brun4e0dba62021-05-17 15:25:15 +0200185 ] + go.sdk.tools + kwargs.get("tools", [])
Serge Bazanski7353e172021-03-31 22:09:22 +0200186
187 env = {
188 "GOTOOLWRAP_GOPATH": gopath.gopath_file.path,
189 "GOTOOLWRAP_GOROOT": go.sdk.root_file.dirname,
190 }
Lorenz Brun4e0dba62021-05-17 15:25:15 +0200191 env.update(kwargs.get("env", {}))
Serge Bazanski7353e172021-03-31 22:09:22 +0200192
Lorenz Brun4e0dba62021-05-17 15:25:15 +0200193 kwargs_ = dict([(k, v) for (k, v) in kwargs.items() if k not in [
194 "executable",
195 "arguments",
196 "inputs",
197 "env",
198 "tools",
Serge Bazanski7353e172021-03-31 22:09:22 +0200199 ]])
200
201 ctx.actions.run(
202 executable = ctx.executable._gotoolwrap,
Lorenz Brun4e0dba62021-05-17 15:25:15 +0200203 arguments = [executable.path] + arguments,
Serge Bazanski7353e172021-03-31 22:09:22 +0200204 env = env,
205 inputs = inputs,
206 tools = tools,
Lorenz Brun4e0dba62021-05-17 15:25:15 +0200207 **kwargs_
Serge Bazanski7353e172021-03-31 22:09:22 +0200208 )
209
Serge Bazanski7353e172021-03-31 22:09:22 +0200210# _output_directory returns the relative path into which
211# ctx.action.declare_file writes are rooted. This is used as code-generators
212# require a root path for all outputted files, instead of a list of files to
213# emit.
214def _output_directory(ctx):
215 # We combine bin_dir, the BUILDfile path and the target name. This seems
216 # wrong. Is there no simpler way to do this?
217 buildfile_path = ctx.build_file_path
Lorenz Brun4e0dba62021-05-17 15:25:15 +0200218 parts = buildfile_path.split("/")
219 if not parts[-1].startswith("BUILD"):
Serge Bazanski7353e172021-03-31 22:09:22 +0200220 fail("internal error: unexpected BUILD file path: {}", parts[-1])
Lorenz Brun4e0dba62021-05-17 15:25:15 +0200221 package_path = "/".join(parts[:-1])
222 return "/".join([ctx.bin_dir.path, package_path, ctx.attr.name])
Serge Bazanski7353e172021-03-31 22:09:22 +0200223
224# _cg returns a 'codegen context', a struct that's used to accumulate the
225# results of code generation. It assumes all output will be rooted in a
226# generated importpath (with more 'shortened' importpaths underneath the root),
227# and collects outputs to pass to the codegen execution action. It also
228# collects a map of importpaths to outputs that make it up.
229def _cg(ctx, importpath):
230 output_root = _output_directory(ctx)
231
232 return struct(
233 # The 'root' importpath, under which 'shortened' importpaths reside.
234 importpath = importpath,
235 # The prefix into which all files will be emitted. We use the target
236 # name for convenience.
237 output_prefix = ctx.attr.name,
238 # The full relative path visible to the codegen, pointing to the same
239 # directory as output_prefix (just from the point of view of the
240 # runtime filesystem, not the ctx.actions filepath declaration API).
241 output_root = output_root,
242 # The list of outputs that have to be generated by the codegen.
243 outputs = [],
244 # A map of importpath to list of outputs (from the above list) that
245 # make up a generated Go package/library.
246 libraries = {},
Serge Bazanski7353e172021-03-31 22:09:22 +0200247 ctx = ctx,
248 )
249
Serge Bazanski7353e172021-03-31 22:09:22 +0200250# _declare_library adds a single Go package/library at importpath to the
251# codegen context with the given file paths (rooted in the importpath).
252def _declare_library(cg, importpath, files):
253 importpath = cg.importpath + "/" + importpath
254 cg.libraries[importpath] = []
255 for f in files:
256 output = cg.ctx.actions.declare_file("{}/{}/{}".format(
257 cg.output_prefix,
258 importpath,
259 f,
260 ))
261 cg.outputs.append(output)
262 cg.libraries[importpath].append(output)
263
Serge Bazanski7353e172021-03-31 22:09:22 +0200264# _declare_libraries declares multiple Go package/libraries to the codegen
265# context. The key of the dictionary is the importpath of the library, and the
266# value are the file names of generated outputs.
267def _declare_libraries(cg, libraries):
268 for k, v in libraries.items():
269 _declare_library(cg, k, v)
270
Serge Bazanski7353e172021-03-31 22:09:22 +0200271# _codegen_clientset runs the clientset codegenerator.
272def _codegen_clientset(ctx):
273 cg = _cg(ctx, ctx.attr.importpath)
274
275 _declare_libraries(cg, {
276 "clientset/versioned": ["clientset.go", "doc.go"],
277 "clientset/versioned/fake": ["register.go", "clientset_generated.go"],
278 "clientset/versioned/scheme": ["register.go", "doc.go"],
279 })
280
281 for api, types in ctx.attr.apis.items():
282 client_name = api.split("/")[-2]
283 _declare_libraries(cg, {
284 "clientset/versioned/typed/{}".format(api): [
Lorenz Brun4e0dba62021-05-17 15:25:15 +0200285 "doc.go",
286 "generated_expansion.go",
Serge Bazanski7353e172021-03-31 22:09:22 +0200287 "{}_client.go".format(client_name),
288 ] + [
Lorenz Brun4e0dba62021-05-17 15:25:15 +0200289 "{}.go".format(t)
290 for t in types
Serge Bazanski7353e172021-03-31 22:09:22 +0200291 ],
292 "clientset/versioned/typed/{}/fake".format(api): [
293 "doc.go",
294 "fake_{}_client.go".format(client_name),
295 ],
296 })
297
Lorenz Brun4e0dba62021-05-17 15:25:15 +0200298 _gotool_run(
299 ctx,
Serge Bazanski7353e172021-03-31 22:09:22 +0200300 mnemonic = "ClientsetGen",
301 executable = ctx.executable._client_gen,
302 arguments = [
Lorenz Brun4e0dba62021-05-17 15:25:15 +0200303 "--clientset-name",
304 "versioned",
305 "--input-base",
306 ctx.attr.apipath,
307 "--input",
308 ",".join(ctx.attr.apis),
309 "--output-package",
310 cg.importpath + "/clientset",
311 "--output-base",
312 cg.output_root,
313 "--go-header-file",
314 ctx.file.boilerplate.path,
Serge Bazanski7353e172021-03-31 22:09:22 +0200315 ],
316 inputs = [
317 ctx.file.boilerplate,
318 ],
319 outputs = cg.outputs,
320 )
321
322 return cg.libraries
323
Serge Bazanski7353e172021-03-31 22:09:22 +0200324# _codegen_deepcopy runs the deepcopy codegenerator (outputting to the apipath,
325# not the importpath).
326def _codegen_deepcopy(ctx):
327 cg = _cg(ctx, ctx.attr.apipath)
328
329 for api, types in ctx.attr.apis.items():
330 _declare_libraries(cg, {
331 api: ["zz_generated.deepcopy.go"],
332 })
333
334 _gotool_run(
335 ctx,
336 mnemonic = "DeepcopyGen",
337 executable = ctx.executable._deepcopy_gen,
338 arguments = [
Lorenz Brun4e0dba62021-05-17 15:25:15 +0200339 "--input-dirs",
340 ",".join(["{}/{}".format(ctx.attr.apipath, api) for api in ctx.attr.apis]),
341 "--go-header-file",
342 ctx.file.boilerplate.path,
343 "--stderrthreshold",
344 "0",
345 "-O",
346 "zz_generated.deepcopy",
347 "--output-base",
348 cg.output_root,
Serge Bazanski7353e172021-03-31 22:09:22 +0200349 ctx.attr.apipath,
350 ],
351 inputs = [
352 ctx.file.boilerplate,
353 ],
354 outputs = cg.outputs,
355 )
356 return cg.libraries
357
Serge Bazanski7353e172021-03-31 22:09:22 +0200358# _codegen_informer runs the informer codegenerator.
359def _codegen_informer(ctx):
360 cg = _cg(ctx, ctx.attr.importpath)
361
362 _declare_libraries(cg, {
Lorenz Brun4e0dba62021-05-17 15:25:15 +0200363 "informers/externalversions": ["factory.go", "generic.go"],
364 "informers/externalversions/internalinterfaces": ["factory_interfaces.go"],
Serge Bazanski7353e172021-03-31 22:09:22 +0200365 })
366
367 for api, types in ctx.attr.apis.items():
368 client_name = api.split("/")[-2]
369 _declare_libraries(cg, {
Lorenz Brun4e0dba62021-05-17 15:25:15 +0200370 "informers/externalversions/{}".format(client_name): ["interface.go"],
Serge Bazanski7353e172021-03-31 22:09:22 +0200371 "informers/externalversions/{}".format(api): [
372 "interface.go",
373 ] + [
374 "{}.go".format(t)
375 for t in types
376 ],
377 })
378
379 _gotool_run(
380 ctx,
381 mnemonic = "InformerGen",
382 executable = ctx.executable._informer_gen,
383 arguments = [
Lorenz Brun4e0dba62021-05-17 15:25:15 +0200384 "--input-dirs",
385 ",".join(["{}/{}".format(ctx.attr.apipath, api) for api in ctx.attr.apis]),
386 "--versioned-clientset-package",
387 "{}/clientset/versioned".format(ctx.attr.importpath),
388 "--listers-package",
389 "{}/listers".format(ctx.attr.importpath),
390 "--output-package",
391 "{}/informers".format(ctx.attr.importpath),
392 "--output-base",
393 cg.output_root,
394 "--go-header-file",
395 ctx.file.boilerplate.path,
Serge Bazanski7353e172021-03-31 22:09:22 +0200396 ],
397 inputs = [
398 ctx.file.boilerplate,
399 ],
400 outputs = cg.outputs,
401 )
402
403 return cg.libraries
404
Serge Bazanski7353e172021-03-31 22:09:22 +0200405# _codegen_lister runs the lister codegenerator.
406def _codegen_lister(ctx):
407 cg = _cg(ctx, ctx.attr.importpath)
408
409 for api, types in ctx.attr.apis.items():
410 client_name = api.split("/")[-2]
411 _declare_libraries(cg, {
412 "listers/{}".format(api): [
413 "expansion_generated.go",
414 ] + [
415 "{}.go".format(t)
416 for t in types
Lorenz Brun4e0dba62021-05-17 15:25:15 +0200417 ],
Serge Bazanski7353e172021-03-31 22:09:22 +0200418 })
419
420 _gotool_run(
421 ctx,
422 mnemonic = "ListerGen",
423 executable = ctx.executable._lister_gen,
424 arguments = [
Lorenz Brun4e0dba62021-05-17 15:25:15 +0200425 "--input-dirs",
426 ",".join(["{}/{}".format(ctx.attr.apipath, api) for api in ctx.attr.apis]),
427 "--output-package",
428 "{}/listers".format(ctx.attr.importpath),
429 "--output-base",
430 cg.output_root,
431 "--go-header-file",
432 ctx.file.boilerplate.path,
433 "-v",
434 "10",
Serge Bazanski7353e172021-03-31 22:09:22 +0200435 ],
436 inputs = [
437 ctx.file.boilerplate,
438 ],
439 outputs = cg.outputs,
440 )
441
442 return cg.libraries
443
Serge Bazanski7353e172021-03-31 22:09:22 +0200444# _update_dict_check is a helper function that updates dict a with dict b,
445# ensuring there's no overwritten keys.
446def _update_dict_check(a, b):
447 for k in b.keys():
448 if k in a:
449 fail("internal error: repeat importpath {}", k)
450 a.update(b)
451
Serge Bazanski7353e172021-03-31 22:09:22 +0200452def _go_kubernetes_resource_bundle_impl(ctx):
453 go = go_context(ctx)
454
455 all_gens = {}
456 _update_dict_check(all_gens, _codegen_clientset(ctx))
457 _update_dict_check(all_gens, _codegen_deepcopy(ctx))
458 _update_dict_check(all_gens, _codegen_informer(ctx))
459 _update_dict_check(all_gens, _codegen_lister(ctx))
460
461 libraries = []
462 for importpath, srcs in all_gens.items():
463 library = go.new_library(
464 go,
465 srcs = srcs,
466 importpath = importpath,
467 )
468 libraries.append(library)
469
Lorenz Brun4e0dba62021-05-17 15:25:15 +0200470 return [KubeResourceBundle(libraries = libraries)]
Serge Bazanski7353e172021-03-31 22:09:22 +0200471
472# go_kubernetes_resource_bundle runs kubernetes code-generators on a codepath
473# for some requested APIs, and whose output can be made into Go library targets
474# via go_kubernetes_library. This bundle corresponds to a single Kubernetes API
475# resource group.
476go_kubernetes_resource_bundle = rule(
477 implementation = _go_kubernetes_resource_bundle_impl,
478 attrs = {
479 "gopath": attr.label(
480 mandatory = True,
481 providers = [GoPath],
482 cfg = preprocessing_transition,
483 doc = "A rules_go go_path that contains all the API libraries for which codegen should be run.",
484 ),
Serge Bazanski7353e172021-03-31 22:09:22 +0200485 "importpath": attr.string(
486 mandatory = True,
487 doc = """
488 The root importpath of the generated code (apart from deepcopy
489 codegen). The Bazel target path corresponding to this
490 importpath needs to contain the go_kubernetes_library and
491 go_library targets that allow to actually build against the
492 generated code.
493 """,
494 ),
Serge Bazanski7353e172021-03-31 22:09:22 +0200495 "apipath": attr.string(
496 mandatory = True,
497 doc = "The root importpath of the APIs for which to generate code.",
498 ),
499 "apis": attr.string_list_dict(
500 mandatory = True,
501 doc = """
502 The APIs underneath importpath for which to generated code,
503 eg. foo/v1, mapping into a list of lowercased types generated
504 from each (eg. widget for `type Widget struct`).
505 """,
506 ),
Serge Bazanski7353e172021-03-31 22:09:22 +0200507 "boilerplate": attr.label(
508 default = "//metropolis/build/kube-code-generator:boilerplate.go.txt",
509 allow_single_file = True,
510 doc = "Header that will be used in the generated code.",
511 ),
Serge Bazanski7353e172021-03-31 22:09:22 +0200512 "_go_context_data": attr.label(
513 default = "@io_bazel_rules_go//:go_context_data",
514 ),
Serge Bazanski7353e172021-03-31 22:09:22 +0200515 "_gotoolwrap": attr.label(
516 default = Label("//metropolis/build/gotoolwrap"),
517 allow_single_file = True,
518 executable = True,
519 cfg = "exec",
520 ),
Serge Bazanski7353e172021-03-31 22:09:22 +0200521 "_deepcopy_gen": attr.label(
522 default = Label("@io_k8s_code_generator//cmd/deepcopy-gen"),
523 allow_single_file = True,
524 executable = True,
525 cfg = "exec",
526 ),
527 "_client_gen": attr.label(
528 default = Label("@io_k8s_code_generator//cmd/client-gen"),
529 allow_single_file = True,
530 executable = True,
531 cfg = "exec",
532 ),
533 "_informer_gen": attr.label(
534 default = Label("@io_k8s_code_generator//cmd/informer-gen"),
535 allow_single_file = True,
536 executable = True,
537 cfg = "exec",
538 ),
539 "_lister_gen": attr.label(
540 default = Label("@io_k8s_code_generator//cmd/lister-gen"),
541 allow_single_file = True,
542 executable = True,
543 cfg = "exec",
544 ),
Serge Bazanski7353e172021-03-31 22:09:22 +0200545 "_allowlist_function_transition": attr.label(
Lorenz Brun4e0dba62021-05-17 15:25:15 +0200546 default = "@bazel_tools//tools/allowlists/function_transition_allowlist",
547 ),
Serge Bazanski7353e172021-03-31 22:09:22 +0200548 },
549 toolchains = ["@io_bazel_rules_go//go:toolchain"],
550)