blob: 294fc1147f3ec662634134e61be462c80d0e9c19 [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# | '--------------------------.
88# .---------------------------. .--------------------------.
89# | go_kubernetes_library | | go_kubernetes_library |
90# |---------------------------| |--------------------------| ... others ...
91# | clientset/verioned/typed | | clientset/verioned/fake |
92# '---------------------------' '--------------------------'
93# | |
94# .---------------------------. .--------------------------.
95# | go_library | | go_library |
96# |---------------------------| |--------------------------| ... others ...
97# | clientset/versioned/typed | | clientset/versioned/fake |
98# '---------------------------' '--------------------------'
99#
100
101load("@io_bazel_rules_go//go:def.bzl", "go_context", "GoPath", "GoLibrary")
102
103def _preprocessing_transition_impl(settings, attr):
104 return { "//metropolis/build/kube-code-generator:preprocessing": "yes" }
105
106
107# preprocessing_transition is attached to the incoming go_path in
108# go_kubernetes_resource_bundle, unsets the
109# //metropolis/build/kube-code-generator:embed_deepcopy config setting.
110# This allows go_libraries that make up the handwritten API libraries to only
111# embed the generated deepcopy when they are pulled in for build reasons, not
112# when the graph is being traversed in order to generate the deepcopy itself.
113# This breaks up the cycle that would happen otherwise.
114preprocessing_transition = transition(
115 implementation = _preprocessing_transition_impl,
116 inputs = [],
117 outputs = ["//metropolis/build/kube-code-generator:preprocessing"],
118)
119
120# KubeResourceBundle is emitted by go_kubernetes_resource_bundle and contains
121# informations about libraries generated by the kubernetes code-generators.
122KubeResourceBundle = provider(
123 "Information about the generated Go sources of a k8s.io/code-generator-built library.",
124 fields = {
125 "libraries": "Map from Go importpath to list of Files that make up this importpath.",
126 },
127)
128
129def _go_kubernetes_library_impl(ctx):
130 go = go_context(ctx)
131 bundle = ctx.attr.bundle[KubeResourceBundle]
132 libraries = bundle.libraries
133
134 found_importpaths = [l.importpath for l in libraries]
135
136 libraries = [l for l in libraries if l.importpath == ctx.attr.importpath]
137 if len(libraries) < 1:
138 fail("importpath {} not found in bundle (have {})".format(ctx.attr.importpath, ", ".join(found_importpaths)))
139 if len(libraries) > 1:
140 fail("internal error: multiple libraries with importpath {} found in bundle".format(ctx.attr.importpath))
141 library = libraries[0]
142
143 source = go.library_to_source(go, ctx.attr, library, ctx.coverage_instrumented())
144 return [library, source]
145
146
147# go_kubernetes_library picks a single Go library from a kube_resource_bundle
148# and prepares it for being embedded into a go_library.
149go_kubernetes_library = rule(
150 implementation = _go_kubernetes_library_impl,
151 attrs = {
152 "bundle": attr.label(
153 mandatory = True,
154 providers = [KubeResourceBundle],
155 doc = "A go_kubernetes_resource_bundle that contains the result of a kubernetes code-generation run.",
156 ),
157 "importpath": attr.string(
158 mandatory = True,
159 doc = "The importpath of the library picked from the bundle, same as the importpath of the go_library that embeds it.",
160 ),
161 "deps": attr.label_list(
162 providers = [GoLibrary],
163 doc = "All build dependencies of this library.",
164 ),
165
166 "_go_context_data": attr.label(
167 default = "@io_bazel_rules_go//:go_context_data",
168 ),
169
170 },
171 toolchains = ["@io_bazel_rules_go//go:toolchain"],
172)
173
174# _gotool_run is a helper function which runs an executable under
175# //metropolis/build/gotoolwrap, effectively setting up everything required to
176# use standard Go tooling on the monogon workspace (ie. GOPATH/GOROOT). This is
177# required by generators to run 'go fmt'.
178def _gotool_run(ctx, executable, arguments, **kwargs):
179 go = go_context(ctx)
180 gopath = ctx.attr.gopath[0][GoPath]
181
182 inputs = [
183 gopath.gopath_file,
184 ] + kwargs.get('inputs', [])
185
186 tools = [
187 executable,
188 go.sdk.go,
189 ] + go.sdk.tools + kwargs.get('tools', [])
190
191 env = {
192 "GOTOOLWRAP_GOPATH": gopath.gopath_file.path,
193 "GOTOOLWRAP_GOROOT": go.sdk.root_file.dirname,
194 }
195 env.update(kwargs.get('env', {}))
196
197 kwargs_ = dict([(k, v) for (k,v) in kwargs.items() if k not in [
198 'executable', 'arguments', 'inputs', 'env', 'tools',
199 ]])
200
201 ctx.actions.run(
202 executable = ctx.executable._gotoolwrap,
203 arguments = [ executable.path ] + arguments,
204 env = env,
205 inputs = inputs,
206 tools = tools,
207 **kwargs_,
208 )
209
210
211# _output_directory returns the relative path into which
212# ctx.action.declare_file writes are rooted. This is used as code-generators
213# require a root path for all outputted files, instead of a list of files to
214# emit.
215def _output_directory(ctx):
216 # We combine bin_dir, the BUILDfile path and the target name. This seems
217 # wrong. Is there no simpler way to do this?
218 buildfile_path = ctx.build_file_path
219 parts = buildfile_path.split('/')
220 if not parts[-1].startswith('BUILD'):
221 fail("internal error: unexpected BUILD file path: {}", parts[-1])
222 package_path = '/'.join(parts[:-1])
223 return '/'.join([ctx.bin_dir.path, package_path, ctx.attr.name])
224
225
226# _cg returns a 'codegen context', a struct that's used to accumulate the
227# results of code generation. It assumes all output will be rooted in a
228# generated importpath (with more 'shortened' importpaths underneath the root),
229# and collects outputs to pass to the codegen execution action. It also
230# collects a map of importpaths to outputs that make it up.
231def _cg(ctx, importpath):
232 output_root = _output_directory(ctx)
233
234 return struct(
235 # The 'root' importpath, under which 'shortened' importpaths reside.
236 importpath = importpath,
237 # The prefix into which all files will be emitted. We use the target
238 # name for convenience.
239 output_prefix = ctx.attr.name,
240 # The full relative path visible to the codegen, pointing to the same
241 # directory as output_prefix (just from the point of view of the
242 # runtime filesystem, not the ctx.actions filepath declaration API).
243 output_root = output_root,
244 # The list of outputs that have to be generated by the codegen.
245 outputs = [],
246 # A map of importpath to list of outputs (from the above list) that
247 # make up a generated Go package/library.
248 libraries = {},
249
250 ctx = ctx,
251 )
252
253
254# _declare_library adds a single Go package/library at importpath to the
255# codegen context with the given file paths (rooted in the importpath).
256def _declare_library(cg, importpath, files):
257 importpath = cg.importpath + "/" + importpath
258 cg.libraries[importpath] = []
259 for f in files:
260 output = cg.ctx.actions.declare_file("{}/{}/{}".format(
261 cg.output_prefix,
262 importpath,
263 f,
264 ))
265 cg.outputs.append(output)
266 cg.libraries[importpath].append(output)
267
268
269# _declare_libraries declares multiple Go package/libraries to the codegen
270# context. The key of the dictionary is the importpath of the library, and the
271# value are the file names of generated outputs.
272def _declare_libraries(cg, libraries):
273 for k, v in libraries.items():
274 _declare_library(cg, k, v)
275
276
277# _codegen_clientset runs the clientset codegenerator.
278def _codegen_clientset(ctx):
279 cg = _cg(ctx, ctx.attr.importpath)
280
281 _declare_libraries(cg, {
282 "clientset/versioned": ["clientset.go", "doc.go"],
283 "clientset/versioned/fake": ["register.go", "clientset_generated.go"],
284 "clientset/versioned/scheme": ["register.go", "doc.go"],
285 })
286
287 for api, types in ctx.attr.apis.items():
288 client_name = api.split("/")[-2]
289 _declare_libraries(cg, {
290 "clientset/versioned/typed/{}".format(api): [
291 "doc.go", "generated_expansion.go",
292 "{}_client.go".format(client_name),
293 ] + [
294 "{}.go".format(t) for t in types
295 ],
296 "clientset/versioned/typed/{}/fake".format(api): [
297 "doc.go",
298 "fake_{}_client.go".format(client_name),
299 ],
300 })
301
302 _gotool_run(ctx,
303 mnemonic = "ClientsetGen",
304 executable = ctx.executable._client_gen,
305 arguments = [
306 "--clientset-name", "versioned",
307 "--input-base", ctx.attr.apipath,
308 "--input", ",".join(ctx.attr.apis),
309 "--output-package", cg.importpath + "/clientset",
310 "--output-base", cg.output_root,
311 "--go-header-file", ctx.file.boilerplate.path,
312 ],
313 inputs = [
314 ctx.file.boilerplate,
315 ],
316 outputs = cg.outputs,
317 )
318
319 return cg.libraries
320
321
322# _codegen_deepcopy runs the deepcopy codegenerator (outputting to the apipath,
323# not the importpath).
324def _codegen_deepcopy(ctx):
325 cg = _cg(ctx, ctx.attr.apipath)
326
327 for api, types in ctx.attr.apis.items():
328 _declare_libraries(cg, {
329 api: ["zz_generated.deepcopy.go"],
330 })
331
332 _gotool_run(
333 ctx,
334 mnemonic = "DeepcopyGen",
335 executable = ctx.executable._deepcopy_gen,
336 arguments = [
337 "--input-dirs", ",".join(["{}/{}".format(ctx.attr.apipath, api) for api in ctx.attr.apis]),
338 "--go-header-file", ctx.file.boilerplate.path,
339 "--stderrthreshold", "0",
340 "-O", "zz_generated.deepcopy",
341 "--output-base", cg.output_root,
342 ctx.attr.apipath,
343 ],
344 inputs = [
345 ctx.file.boilerplate,
346 ],
347 outputs = cg.outputs,
348 )
349 return cg.libraries
350
351
352# _codegen_informer runs the informer codegenerator.
353def _codegen_informer(ctx):
354 cg = _cg(ctx, ctx.attr.importpath)
355
356 _declare_libraries(cg, {
357 "informers/externalversions": [ "factory.go", "generic.go" ],
358 "informers/externalversions/internalinterfaces": [ "factory_interfaces.go" ],
359 })
360
361 for api, types in ctx.attr.apis.items():
362 client_name = api.split("/")[-2]
363 _declare_libraries(cg, {
364 "informers/externalversions/{}".format(client_name): [ "interface.go" ],
365 "informers/externalversions/{}".format(api): [
366 "interface.go",
367 ] + [
368 "{}.go".format(t)
369 for t in types
370 ],
371 })
372
373 _gotool_run(
374 ctx,
375 mnemonic = "InformerGen",
376 executable = ctx.executable._informer_gen,
377 arguments = [
378 "--input-dirs", ",".join(["{}/{}".format(ctx.attr.apipath, api) for api in ctx.attr.apis]),
379 "--versioned-clientset-package", "{}/clientset/versioned".format(ctx.attr.importpath),
380 "--listers-package", "{}/listers".format(ctx.attr.importpath),
381 "--output-package", "{}/informers".format(ctx.attr.importpath),
382 "--output-base", cg.output_root,
383 "--go-header-file", ctx.file.boilerplate.path,
384 ],
385 inputs = [
386 ctx.file.boilerplate,
387 ],
388 outputs = cg.outputs,
389 )
390
391 return cg.libraries
392
393
394# _codegen_lister runs the lister codegenerator.
395def _codegen_lister(ctx):
396 cg = _cg(ctx, ctx.attr.importpath)
397
398 for api, types in ctx.attr.apis.items():
399 client_name = api.split("/")[-2]
400 _declare_libraries(cg, {
401 "listers/{}".format(api): [
402 "expansion_generated.go",
403 ] + [
404 "{}.go".format(t)
405 for t in types
406 ]
407 })
408
409 _gotool_run(
410 ctx,
411 mnemonic = "ListerGen",
412 executable = ctx.executable._lister_gen,
413 arguments = [
414 "--input-dirs", ",".join(["{}/{}".format(ctx.attr.apipath, api) for api in ctx.attr.apis]),
415 "--output-package", "{}/listers".format(ctx.attr.importpath),
416 "--output-base", cg.output_root,
417 "--go-header-file", ctx.file.boilerplate.path,
418 "-v", "10",
419 ],
420 inputs = [
421 ctx.file.boilerplate,
422 ],
423 outputs = cg.outputs,
424 )
425
426 return cg.libraries
427
428
429# _update_dict_check is a helper function that updates dict a with dict b,
430# ensuring there's no overwritten keys.
431def _update_dict_check(a, b):
432 for k in b.keys():
433 if k in a:
434 fail("internal error: repeat importpath {}", k)
435 a.update(b)
436
437
438def _go_kubernetes_resource_bundle_impl(ctx):
439 go = go_context(ctx)
440
441 all_gens = {}
442 _update_dict_check(all_gens, _codegen_clientset(ctx))
443 _update_dict_check(all_gens, _codegen_deepcopy(ctx))
444 _update_dict_check(all_gens, _codegen_informer(ctx))
445 _update_dict_check(all_gens, _codegen_lister(ctx))
446
447 libraries = []
448 for importpath, srcs in all_gens.items():
449 library = go.new_library(
450 go,
451 srcs = srcs,
452 importpath = importpath,
453 )
454 libraries.append(library)
455
456 return [KubeResourceBundle(libraries=libraries)]
457
458
459# go_kubernetes_resource_bundle runs kubernetes code-generators on a codepath
460# for some requested APIs, and whose output can be made into Go library targets
461# via go_kubernetes_library. This bundle corresponds to a single Kubernetes API
462# resource group.
463go_kubernetes_resource_bundle = rule(
464 implementation = _go_kubernetes_resource_bundle_impl,
465 attrs = {
466 "gopath": attr.label(
467 mandatory = True,
468 providers = [GoPath],
469 cfg = preprocessing_transition,
470 doc = "A rules_go go_path that contains all the API libraries for which codegen should be run.",
471 ),
472
473 "importpath": attr.string(
474 mandatory = True,
475 doc = """
476 The root importpath of the generated code (apart from deepcopy
477 codegen). The Bazel target path corresponding to this
478 importpath needs to contain the go_kubernetes_library and
479 go_library targets that allow to actually build against the
480 generated code.
481 """,
482 ),
483
484 "apipath": attr.string(
485 mandatory = True,
486 doc = "The root importpath of the APIs for which to generate code.",
487 ),
488 "apis": attr.string_list_dict(
489 mandatory = True,
490 doc = """
491 The APIs underneath importpath for which to generated code,
492 eg. foo/v1, mapping into a list of lowercased types generated
493 from each (eg. widget for `type Widget struct`).
494 """,
495 ),
496
497 "boilerplate": attr.label(
498 default = "//metropolis/build/kube-code-generator:boilerplate.go.txt",
499 allow_single_file = True,
500 doc = "Header that will be used in the generated code.",
501 ),
502
503 "_go_context_data": attr.label(
504 default = "@io_bazel_rules_go//:go_context_data",
505 ),
506
507 "_gotoolwrap": attr.label(
508 default = Label("//metropolis/build/gotoolwrap"),
509 allow_single_file = True,
510 executable = True,
511 cfg = "exec",
512 ),
513
514 "_deepcopy_gen": attr.label(
515 default = Label("@io_k8s_code_generator//cmd/deepcopy-gen"),
516 allow_single_file = True,
517 executable = True,
518 cfg = "exec",
519 ),
520 "_client_gen": attr.label(
521 default = Label("@io_k8s_code_generator//cmd/client-gen"),
522 allow_single_file = True,
523 executable = True,
524 cfg = "exec",
525 ),
526 "_informer_gen": attr.label(
527 default = Label("@io_k8s_code_generator//cmd/informer-gen"),
528 allow_single_file = True,
529 executable = True,
530 cfg = "exec",
531 ),
532 "_lister_gen": attr.label(
533 default = Label("@io_k8s_code_generator//cmd/lister-gen"),
534 allow_single_file = True,
535 executable = True,
536 cfg = "exec",
537 ),
538
539 "_allowlist_function_transition": attr.label(
540 default = "@bazel_tools//tools/allowlists/function_transition_allowlist"
541 )
542 },
543 toolchains = ["@io_bazel_rules_go//go:toolchain"],
544)