Skip to content

Commit e8ffccb

Browse files
committed
lib: Add autogeneration for some resource* functionality
Protecting us from [1,2]: converting (v1beta1.Role) to (v1.Role): unknown conversion Because merging and application are also version-dependent, it's hard to paper over this in resourceread with automatic type conversion. Although from [3]: Promotes the rbac.authorization.k8s.io/v1beta1 API to v1 with no changes so all we really need is the apiVersion bump. Anyhow, with this commit, I'm doubling down on the approach from 4ee7b07 (Add apiextensions.k8s.io/v1 support for CRDs, 2019-10-22, #259) and collapsing the readers into two helpers that support all of our types and return runtime.Object. From 0a255ab (cvo: Use protobuf for sending events and other basic API commands, 2019-01-18, #90), protobuf is more efficient, so we should use it where possible. And because all of this is very tedious to maintain by hand, there's now a Python generator to spit out all the boilerplate. [1]: kubernetes/kubernetes#90018 [2]: #420 (comment) [3]: kubernetes/kubernetes#49642
1 parent ef236c3 commit e8ffccb

36 files changed

+946
-1000
lines changed

hack/generate-lib-resources.py

Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
#!/usr/bin/env python
2+
3+
import os.path
4+
5+
6+
def generate_lib_resources(directory, types, clients, modifiers, health_checks):
7+
generate_resourceread(directory=os.path.join(directory, 'resourceread'), types=types)
8+
generate_resourcebuilder(directory=os.path.join(directory, 'resourcebuilder'), types=types, clients=clients, modifiers=modifiers, health_checks=health_checks)
9+
10+
11+
def generate_resourceread(directory, types):
12+
package_name = os.path.basename(directory)
13+
path = os.path.join(directory, package_name + '.go')
14+
lines = [
15+
'// auto-generated with generate-lib-resources.py',
16+
'',
17+
'// Package {} reads supported objects from bytes.'.format(package_name),
18+
'package {}'.format(package_name),
19+
'',
20+
'import (',
21+
]
22+
23+
imports = {}
24+
for import_name in [
25+
'k8s.io/apimachinery/pkg/runtime',
26+
'k8s.io/apimachinery/pkg/runtime/serializer',
27+
]:
28+
imports[import_name] = '\t"{}"'.format(import_name)
29+
30+
for package in sorted(types.keys()):
31+
base, version = os.path.split(package) # FIXME: should be using network path operations on package names
32+
short_name = os.path.basename(base)
33+
imports[package] = '\t{}{} "{}"'.format(short_name, version, package)
34+
35+
lines.extend([import_line for _, import_line in sorted(imports.items(), key=lambda package_line: package_line[0])])
36+
lines.extend([
37+
')',
38+
'',
39+
'var (',
40+
'\tscheme = runtime.NewScheme()',
41+
'\tcodecs = serializer.NewCodecFactory(scheme)',
42+
'\tdecoder runtime.Decoder',
43+
')',
44+
'',
45+
'func init() {',
46+
])
47+
48+
sgvs = scheme_group_versions(types=types)
49+
for _, data in sorted(sgvs.items()):
50+
lines.extend([
51+
'\tif err := {0}{1}.AddToScheme(scheme); err != nil {{'.format(data['short_name'], data['version']),
52+
'\t\tpanic(err)',
53+
'\t}',
54+
])
55+
56+
lines.append('\tdecoder = codecs.UniversalDecoder(')
57+
lines.extend(['\t\t{},'.format(sgv) for sgv in sorted(sgvs.keys())])
58+
lines.extend([
59+
'\t)',
60+
'}',
61+
'',
62+
'// Read reads an object from bytes.',
63+
'func Read(objBytes []byte) (runtime.Object, error) {',
64+
'\treturn runtime.Decode(decoder, objBytes)',
65+
'}',
66+
'',
67+
'// ReadOrDie reads an object from bytes. Panics on error.',
68+
'func ReadOrDie(objBytes []byte) runtime.Object {',
69+
'\trequiredObj, err := runtime.Decode(decoder, objBytes)',
70+
'\tif err != nil {',
71+
'\t\tpanic(err)',
72+
'\t}',
73+
'\treturn requiredObj',
74+
'}',
75+
'', # trailing newline
76+
])
77+
with open(path, 'w') as f:
78+
f.write('\n'.join(lines))
79+
80+
81+
def generate_resourcebuilder(directory, types, clients, modifiers, health_checks):
82+
package_name = os.path.basename(directory)
83+
path = os.path.join(directory, package_name + '.go')
84+
lines = [
85+
'// auto-generated with generate-lib-resources.py',
86+
'',
87+
'// Package {} reads supported objects from bytes.'.format(package_name),
88+
'package {}'.format(package_name),
89+
'',
90+
'import (',
91+
'\t"context"',
92+
'\t"fmt"',
93+
'',
94+
]
95+
96+
imports = {}
97+
for import_name in [
98+
'github.com/openshift/cluster-version-operator/lib',
99+
'github.com/openshift/cluster-version-operator/lib/resourceapply',
100+
'github.com/openshift/cluster-version-operator/lib/resourceread',
101+
'k8s.io/client-go/rest',
102+
]:
103+
imports[import_name] = '\t"{}"'.format(import_name)
104+
105+
ignored_packages = set()
106+
107+
for package in types.keys():
108+
if not clients.get(package):
109+
ignored_packages.add(package)
110+
continue
111+
base, version = os.path.split(package)
112+
short_name = os.path.basename(base)
113+
imports[package] = '\t{}{} "{}"'.format(short_name, version, package)
114+
115+
client_properties = {}
116+
for package, client in clients.items():
117+
if package in ignored_packages:
118+
continue
119+
base, version = os.path.split(client['package'])
120+
short_name = os.path.basename(base)
121+
client_short_name = '{}client{}'.format(short_name, version)
122+
imports[client['package']] = '\t{} "{}"'.format(client_short_name, client['package'])
123+
client_properties['{}Client{}'.format(short_name, version)] = {
124+
'package': package,
125+
'client_short_name': client_short_name,
126+
'type': '*{}.{}'.format(client_short_name, client['type']),
127+
'protobuf': client['package'].startswith('k8s.io/') and 'kube-aggregator' not in client['package'],
128+
}
129+
130+
lines.extend([import_line for _, import_line in sorted(imports.items(), key=lambda package_line: package_line[0])])
131+
132+
longest_property = max(len(prop_name) for prop_name in client_properties.keys())
133+
134+
lines.extend([
135+
')',
136+
'',
137+
'// builder manages single-manifest cluster reconciliation and monitoring.',
138+
'type builder struct {',
139+
'\traw []byte',
140+
'\tmode Mode',
141+
'\tmodifier MetaV1ObjectModifierFunc',
142+
'',
143+
])
144+
lines.extend([
145+
'\t{:{width}} {}'.format(prop_name, data['type'], width=longest_property)
146+
for prop_name, data in sorted(client_properties.items())
147+
])
148+
149+
lines.extend([
150+
'}',
151+
'',
152+
'func newBuilder(config *rest.Config, m lib.Manifest) Interface {',
153+
'\treturn &builder{',
154+
'\t\traw: m.Raw,',
155+
'',
156+
])
157+
for prop_name, data in sorted(client_properties.items()):
158+
new_client_arg = 'config'
159+
if data.get('protobuf'):
160+
new_client_arg = 'withProtobuf({})'.format(new_client_arg)
161+
lines.append('\t\t{:{width}} {}.NewForConfigOrDie({}),'.format(prop_name + ':', data['client_short_name'], new_client_arg, width=longest_property+1))
162+
163+
lines.extend([
164+
'\t}',
165+
'}',
166+
'',
167+
'func (b *builder) WithMode(m Mode) Interface {',
168+
'\tb.mode = m',
169+
'\treturn b',
170+
'}',
171+
'',
172+
'func (b *builder) WithModifier(f MetaV1ObjectModifierFunc) Interface {',
173+
'\tb.modifier = f',
174+
'\treturn b',
175+
'}',
176+
'',
177+
'func (b *builder) Do(ctx context.Context) error {',
178+
'\tobj := resourceread.ReadOrDie(b.raw)',
179+
'',
180+
'\tswitch typedObject := obj.(type) {'
181+
])
182+
183+
for package, type_names in sorted(types.items()):
184+
if package in ignored_packages:
185+
continue
186+
base, version = os.path.split(package)
187+
short_name = os.path.basename(base)
188+
try:
189+
client_prop_name = [key for key, data in client_properties.items() if data['package'] == package][0]
190+
except IndexError as error:
191+
raise ValueError('no client property found for {}'.format(package))
192+
for type_name in sorted(type_names):
193+
lines.extend([
194+
'\tcase *{}{}.{}:'.format(short_name, version, type_name),
195+
'\t\tif b.modifier != nil {',
196+
'\t\t\tb.modifier(typedObject)',
197+
'\t\t}',
198+
])
199+
type_key = (package, type_name)
200+
modifier = modifiers.get(type_key)
201+
if modifier:
202+
lines.extend([
203+
'\t\tif err := {}(ctx, typedObject); err != nil {{'.format(modifier),
204+
'\t\t\treturn err',
205+
'\t\t}',
206+
])
207+
lines.extend([
208+
'\t\tif _, _, err := resourceapply.Apply{}{}(ctx, b.{}, typedObject); err != nil {{'.format(type_name, version, client_prop_name),
209+
'\t\t\treturn err',
210+
'\t\t}',
211+
])
212+
health_check = health_checks.get(type_key)
213+
if health_check:
214+
lines.append('\t\treturn {}(ctx, typedObject)'.format(health_check))
215+
216+
lines.extend([
217+
'\tdefault:',
218+
'\t\treturn fmt.Errorf("unrecognized manifest type: %T", obj)',
219+
'\t}',
220+
'',
221+
'\treturn nil',
222+
'}',
223+
'',
224+
'func init() {',
225+
'\trm := NewResourceMapper()',
226+
])
227+
228+
for sgv, data in sorted(scheme_group_versions(types=types).items()):
229+
if data['package'] in ignored_packages:
230+
continue
231+
for type_name in sorted(data['types']):
232+
lines.append('\trm.RegisterGVK({}.WithKind("{}"), newBuilder)'.format(sgv, type_name))
233+
234+
lines.extend([
235+
'\trm.AddToMap(Mapper)',
236+
'}',
237+
'', # trailing newline
238+
])
239+
240+
with open(path, 'w') as f:
241+
f.write('\n'.join(lines))
242+
243+
244+
def scheme_group_versions(types):
245+
sgvs = {}
246+
for package, type_names in types.items():
247+
base, version = os.path.split(package)
248+
short_name = os.path.basename(base)
249+
sgv = '{}{}.SchemeGroupVersion'.format(short_name, version)
250+
sgvs[sgv] = {
251+
'package': package,
252+
'short_name': short_name,
253+
'types': type_names,
254+
'version': version,
255+
}
256+
return sgvs
257+
258+
259+
if __name__ == '__main__':
260+
types = {
261+
'github.com/openshift/api/image/v1': {'ImageStream'}, # for payload loading
262+
'github.com/openshift/api/security/v1': {'SecurityContextConstraints'},
263+
'k8s.io/api/apps/v1': {'DaemonSet', 'Deployment'},
264+
'k8s.io/api/batch/v1': {'Job'},
265+
'k8s.io/api/core/v1': {'ConfigMap', 'Namespace', 'Service', 'ServiceAccount'},
266+
'k8s.io/api/rbac/v1': {'ClusterRole', 'ClusterRoleBinding', 'Role', 'RoleBinding'},
267+
'k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1': {'CustomResourceDefinition'},
268+
'k8s.io/kube-aggregator/pkg/apis/apiregistration/v1': {'APIService'},
269+
}
270+
271+
types['k8s.io/api/rbac/v1beta1'] = types['k8s.io/api/rbac/v1']
272+
types['k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1'] = types['k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1']
273+
types['k8s.io/kube-aggregator/pkg/apis/apiregistration/v1beta1'] = types['k8s.io/kube-aggregator/pkg/apis/apiregistration/v1']
274+
275+
clients = {
276+
'github.com/openshift/api/security/v1': {'package': 'github.com/openshift/client-go/security/clientset/versioned/typed/security/v1', 'type': 'SecurityV1Client'},
277+
'github.com/openshift/api/config/v1': {'package': 'github.com/openshift/client-go/config/clientset/versioned/typed/config/v1', 'type': 'ConfigV1Client'},
278+
'k8s.io/api/apps/v1': {'package': 'k8s.io/client-go/kubernetes/typed/apps/v1', 'type': 'AppsV1Client'},
279+
'k8s.io/api/batch/v1': {'package': 'k8s.io/client-go/kubernetes/typed/batch/v1', 'type': 'BatchV1Client'},
280+
'k8s.io/api/core/v1': {'package': 'k8s.io/client-go/kubernetes/typed/core/v1', 'type': 'CoreV1Client'},
281+
'k8s.io/api/rbac/v1': {'package': 'k8s.io/client-go/kubernetes/typed/rbac/v1', 'type': 'RbacV1Client'},
282+
'k8s.io/api/rbac/v1beta1': {'package': 'k8s.io/client-go/kubernetes/typed/rbac/v1beta1', 'type': 'RbacV1beta1Client'},
283+
'k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1': {'package': 'k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1', 'type': 'ApiextensionsV1Client'},
284+
'k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1': {'package': 'k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1', 'type': 'ApiextensionsV1beta1Client'},
285+
'k8s.io/kube-aggregator/pkg/apis/apiregistration/v1': {'package': 'k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/typed/apiregistration/v1', 'type': 'ApiregistrationV1Client'},
286+
'k8s.io/kube-aggregator/pkg/apis/apiregistration/v1beta1': {'package': 'k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/typed/apiregistration/v1beta1', 'type': 'ApiregistrationV1beta1Client'},
287+
}
288+
289+
modifiers = {
290+
('k8s.io/api/apps/v1', 'Deployment'): 'b.modifyDeployment',
291+
('k8s.io/api/apps/v1', 'DaemonSet'): 'b.modifyDaemonSet',
292+
}
293+
294+
health_checks = {
295+
('k8s.io/api/apps/v1', 'Deployment'): 'b.checkDeploymentHealth',
296+
('k8s.io/api/apps/v1', 'DaemonSet'): 'b.checkDaemonSetHealth',
297+
('k8s.io/api/batch/v1', 'Job'): 'b.checkJobHealth',
298+
}
299+
300+
generate_lib_resources(directory='lib', types=types, clients=clients, modifiers=modifiers, health_checks=health_checks)

lib/doc.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// Package lib defines a Manifest type.
2+
//
3+
// It also contains subpackages for reconciling in-cluster resources
4+
// with local state. The entrypoint is resourcebuilder, which consumes
5+
// resourceread and resourceapply. resourceapply in turn consumer
6+
// resourcemerge.
7+
package lib

lib/resourceapply/apiext.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import (
1414
"k8s.io/utils/pointer"
1515
)
1616

17-
func ApplyCustomResourceDefinitionV1beta1(ctx context.Context, client apiextclientv1beta1.CustomResourceDefinitionsGetter, required *apiextv1beta1.CustomResourceDefinition) (*apiextv1beta1.CustomResourceDefinition, bool, error) {
17+
func ApplyCustomResourceDefinitionv1beta1(ctx context.Context, client apiextclientv1beta1.CustomResourceDefinitionsGetter, required *apiextv1beta1.CustomResourceDefinition) (*apiextv1beta1.CustomResourceDefinition, bool, error) {
1818
existing, err := client.CustomResourceDefinitions().Get(ctx, required.Name, metav1.GetOptions{})
1919
if apierrors.IsNotFound(err) {
2020
actual, err := client.CustomResourceDefinitions().Create(ctx, required, metav1.CreateOptions{})
@@ -40,7 +40,7 @@ func ApplyCustomResourceDefinitionV1beta1(ctx context.Context, client apiextclie
4040
return actual, true, err
4141
}
4242

43-
func ApplyCustomResourceDefinitionV1(ctx context.Context, client apiextclientv1.CustomResourceDefinitionsGetter, required *apiextv1.CustomResourceDefinition) (*apiextv1.CustomResourceDefinition, bool, error) {
43+
func ApplyCustomResourceDefinitionv1(ctx context.Context, client apiextclientv1.CustomResourceDefinitionsGetter, required *apiextv1.CustomResourceDefinition) (*apiextv1.CustomResourceDefinition, bool, error) {
4444
existing, err := client.CustomResourceDefinitions().Get(ctx, required.Name, metav1.GetOptions{})
4545
if apierrors.IsNotFound(err) {
4646
actual, err := client.CustomResourceDefinitions().Create(ctx, required, metav1.CreateOptions{})

lib/resourceapply/apireg.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111
"k8s.io/utils/pointer"
1212
)
1313

14-
func ApplyAPIService(ctx context.Context, client apiregclientv1.APIServicesGetter, required *apiregv1.APIService) (*apiregv1.APIService, bool, error) {
14+
func ApplyAPIServicev1(ctx context.Context, client apiregclientv1.APIServicesGetter, required *apiregv1.APIService) (*apiregv1.APIService, bool, error) {
1515
existing, err := client.APIServices().Get(ctx, required.Name, metav1.GetOptions{})
1616
if apierrors.IsNotFound(err) {
1717
actual, err := client.APIServices().Create(ctx, required, metav1.CreateOptions{})

lib/resourceapply/apiregv1beta1.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package resourceapply
2+
3+
import (
4+
"context"
5+
6+
"github.com/openshift/cluster-version-operator/lib/resourcemerge"
7+
apierrors "k8s.io/apimachinery/pkg/api/errors"
8+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
9+
apiregv1beta1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1beta1"
10+
apiregclientv1beta1 "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/typed/apiregistration/v1beta1"
11+
"k8s.io/utils/pointer"
12+
)
13+
14+
func ApplyAPIServicev1beta1(ctx context.Context, client apiregclientv1beta1.APIServicesGetter, required *apiregv1beta1.APIService) (*apiregv1beta1.APIService, bool, error) {
15+
existing, err := client.APIServices().Get(ctx, required.Name, metav1.GetOptions{})
16+
if apierrors.IsNotFound(err) {
17+
actual, err := client.APIServices().Create(ctx, required, metav1.CreateOptions{})
18+
return actual, true, err
19+
}
20+
if err != nil {
21+
return nil, false, err
22+
}
23+
// if we only create this resource, we have no need to continue further
24+
if IsCreateOnly(required) {
25+
return nil, false, nil
26+
}
27+
28+
modified := pointer.BoolPtr(false)
29+
resourcemerge.EnsureAPIServicev1beta1(modified, existing, *required)
30+
if !*modified {
31+
return existing, false, nil
32+
}
33+
34+
actual, err := client.APIServices().Update(ctx, existing, metav1.UpdateOptions{})
35+
return actual, true, err
36+
}

lib/resourceapply/apps.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ import (
1212
"k8s.io/utils/pointer"
1313
)
1414

15-
// ApplyDeployment applies the required deployment to the cluster.
16-
func ApplyDeployment(ctx context.Context, client appsclientv1.DeploymentsGetter, required *appsv1.Deployment) (*appsv1.Deployment, bool, error) {
15+
// ApplyDeploymentv1 applies the required deployment to the cluster.
16+
func ApplyDeploymentv1(ctx context.Context, client appsclientv1.DeploymentsGetter, required *appsv1.Deployment) (*appsv1.Deployment, bool, error) {
1717
existing, err := client.Deployments(required.Namespace).Get(ctx, required.Name, metav1.GetOptions{})
1818
if apierrors.IsNotFound(err) {
1919
actual, err := client.Deployments(required.Namespace).Create(ctx, required, metav1.CreateOptions{})
@@ -63,8 +63,8 @@ func ApplyDeploymentFromCache(ctx context.Context, lister appslisterv1.Deploymen
6363
return actual, true, err
6464
}
6565

66-
// ApplyDaemonSet applies the required daemonset to the cluster.
67-
func ApplyDaemonSet(ctx context.Context, client appsclientv1.DaemonSetsGetter, required *appsv1.DaemonSet) (*appsv1.DaemonSet, bool, error) {
66+
// ApplyDaemonSetv1 applies the required daemonset to the cluster.
67+
func ApplyDaemonSetv1(ctx context.Context, client appsclientv1.DaemonSetsGetter, required *appsv1.DaemonSet) (*appsv1.DaemonSet, bool, error) {
6868
existing, err := client.DaemonSets(required.Namespace).Get(ctx, required.Name, metav1.GetOptions{})
6969
if apierrors.IsNotFound(err) {
7070
actual, err := client.DaemonSets(required.Namespace).Create(ctx, required, metav1.CreateOptions{})

0 commit comments

Comments
 (0)