forked from openshift/cluster-version-operator
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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, openshift#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, openshift#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]: openshift#420 (comment) [3]: kubernetes/kubernetes#49642
- Loading branch information
Showing
36 changed files
with
927 additions
and
1,000 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,286 @@ | ||
#!/usr/bin/env python | ||
|
||
import os.path | ||
|
||
|
||
def generate_lib_resources(directory, types, clients, health_checks): | ||
generate_resourceread(directory=os.path.join(directory, 'resourceread'), types=types) | ||
generate_resourcebuilder(directory=os.path.join(directory, 'resourcebuilder'), types=types, clients=clients, health_checks=health_checks) | ||
|
||
|
||
def generate_resourceread(directory, types): | ||
package_name = os.path.basename(directory) | ||
path = os.path.join(directory, package_name + '.go') | ||
lines = [ | ||
'// auto-generated with generate-lib-resources.py', | ||
'', | ||
'// Package {} reads supported objects from bytes.'.format(package_name), | ||
'package {}'.format(package_name), | ||
'', | ||
'import (', | ||
] | ||
|
||
imports = {} | ||
for import_name in [ | ||
'k8s.io/apimachinery/pkg/runtime', | ||
'k8s.io/apimachinery/pkg/runtime/serializer', | ||
]: | ||
imports[import_name] = '\t"{}"'.format(import_name) | ||
|
||
for package in sorted(types.keys()): | ||
base, version = os.path.split(package) # FIXME: should be using network path operations on package names | ||
short_name = os.path.basename(base) | ||
imports[package] = '\t{}{} "{}"'.format(short_name, version, package) | ||
|
||
lines.extend([import_line for _, import_line in sorted(imports.items(), key=lambda package_line: package_line[0])]) | ||
lines.extend([ | ||
')', | ||
'', | ||
'var (', | ||
'\tscheme = runtime.NewScheme()', | ||
'\tcodecs = serializer.NewCodecFactory(scheme)', | ||
'\tdecoder runtime.Decoder', | ||
')', | ||
'', | ||
'func init() {', | ||
]) | ||
|
||
sgvs = scheme_group_versions(types=types) | ||
for _, data in sorted(sgvs.items()): | ||
lines.extend([ | ||
'\tif err := {0}{1}.AddToScheme(scheme); err != nil {{'.format(data['short_name'], data['version']), | ||
'\t\tpanic(err)', | ||
'\t}', | ||
]) | ||
|
||
lines.append('\tdecoder = codecs.UniversalDecoder(') | ||
lines.extend(['\t\t{},'.format(sgv) for sgv in sorted(sgvs.keys())]) | ||
lines.extend([ | ||
'\t)', | ||
'}', | ||
'', | ||
'// Read reads an object from bytes.', | ||
'func Read(objBytes []byte) (runtime.Object, error) {', | ||
'\treturn runtime.Decode(decoder, objBytes)', | ||
'}', | ||
'', | ||
'// ReadOrDie reads an object from bytes. Panics on error.', | ||
'func ReadOrDie(objBytes []byte) runtime.Object {', | ||
'\trequiredObj, err := runtime.Decode(decoder, objBytes)', | ||
'\tif err != nil {', | ||
'\t\tpanic(err)', | ||
'\t}', | ||
'\treturn requiredObj', | ||
'}', | ||
'', # trailing newline | ||
]) | ||
with open(path, 'w') as f: | ||
f.write('\n'.join(lines)) | ||
|
||
|
||
def generate_resourcebuilder(directory, types, clients, health_checks): | ||
package_name = os.path.basename(directory) | ||
path = os.path.join(directory, package_name + '.go') | ||
lines = [ | ||
'// auto-generated with generate-lib-resources.py', | ||
'', | ||
'// Package {} reads supported objects from bytes.'.format(package_name), | ||
'package {}'.format(package_name), | ||
'', | ||
'import (', | ||
'\t"context"', | ||
'\t"fmt"', | ||
'', | ||
] | ||
|
||
imports = {} | ||
for import_name in [ | ||
'github.com/openshift/cluster-version-operator/lib', | ||
'github.com/openshift/cluster-version-operator/lib/resourceapply', | ||
'github.com/openshift/cluster-version-operator/lib/resourceread', | ||
'k8s.io/client-go/rest', | ||
]: | ||
imports[import_name] = '\t"{}"'.format(import_name) | ||
|
||
ignored_packages = set() | ||
|
||
for package in types.keys(): | ||
if not clients.get(package): | ||
ignored_packages.add(package) | ||
continue | ||
base, version = os.path.split(package) | ||
short_name = os.path.basename(base) | ||
imports[package] = '\t{}{} "{}"'.format(short_name, version, package) | ||
|
||
client_properties = {} | ||
for package, client in clients.items(): | ||
if package in ignored_packages: | ||
continue | ||
base, version = os.path.split(client['package']) | ||
short_name = os.path.basename(base) | ||
client_short_name = '{}client{}'.format(short_name, version) | ||
imports[client['package']] = '\t{} "{}"'.format(client_short_name, client['package']) | ||
client_properties['{}Client{}'.format(short_name, version)] = { | ||
'package': package, | ||
'client_short_name': client_short_name, | ||
'type': '*{}.{}'.format(client_short_name, client['type']), | ||
'protobuf': client['package'].startswith('k8s.io/') and 'kube-aggregator' not in client['package'], | ||
} | ||
|
||
lines.extend([import_line for _, import_line in sorted(imports.items(), key=lambda package_line: package_line[0])]) | ||
|
||
longest_property = max(len(prop_name) for prop_name in client_properties.keys()) | ||
|
||
lines.extend([ | ||
')', | ||
'', | ||
'// builder manages single-manifest cluster reconciliation and monitoring.', | ||
'type builder struct {', | ||
'\traw []byte', | ||
'\tmode Mode', | ||
'\tmodifier MetaV1ObjectModifierFunc', | ||
'', | ||
]) | ||
lines.extend([ | ||
'\t{:{width}} {}'.format(prop_name, data['type'], width=longest_property) | ||
for prop_name, data in sorted(client_properties.items()) | ||
]) | ||
|
||
lines.extend([ | ||
'}', | ||
'', | ||
'func newBuilder(config *rest.Config, m lib.Manifest) Interface {', | ||
'\treturn &builder{', | ||
'\t\traw: m.Raw,', | ||
'', | ||
]) | ||
for prop_name, data in sorted(client_properties.items()): | ||
new_client_arg = 'config' | ||
if data.get('protobuf'): | ||
new_client_arg = 'withProtobuf({})'.format(new_client_arg) | ||
lines.append('\t\t{:{width}} {}.NewForConfigOrDie({}),'.format(prop_name + ':', data['client_short_name'], new_client_arg, width=longest_property+1)) | ||
|
||
lines.extend([ | ||
'\t}', | ||
'}', | ||
'', | ||
'func (b *builder) WithMode(m Mode) Interface {', | ||
'\tb.mode = m', | ||
'\treturn b', | ||
'}', | ||
'', | ||
'func (b *builder) WithModifier(f MetaV1ObjectModifierFunc) Interface {', | ||
'\tb.modifier = f', | ||
'\treturn b', | ||
'}', | ||
'', | ||
'func (b *builder) Do(ctx context.Context) error {', | ||
'\tobj := resourceread.ReadOrDie(b.raw)', | ||
'', | ||
'\tswitch typedObject := obj.(type) {' | ||
]) | ||
|
||
for package, type_names in sorted(types.items()): | ||
if package in ignored_packages: | ||
continue | ||
base, version = os.path.split(package) | ||
short_name = os.path.basename(base) | ||
try: | ||
client_prop_name = [key for key, data in client_properties.items() if data['package'] == package][0] | ||
except IndexError as error: | ||
raise ValueError('no client property found for {}'.format(package)) | ||
for type_name in sorted(type_names): | ||
lines.extend([ | ||
'\tcase *{}{}.{}:'.format(short_name, version, type_name), | ||
'\t\tif b.modifier != nil {', | ||
'\t\t\tb.modifier(typedObject)', | ||
'\t\t}', | ||
'\t\tif _, _, err := resourceapply.Apply{}{}(ctx, b.{}, typedObject); err != nil {{'.format(type_name, version, client_prop_name), | ||
'\t\t\treturn err', | ||
'\t\t}', | ||
]) | ||
type_key = (package, type_name) | ||
health_check = health_checks.get(type_key) | ||
if health_check: | ||
lines.append('\t\treturn {}(ctx, typedObject)'.format(health_check)) | ||
|
||
lines.extend([ | ||
'\tdefault:', | ||
'\t\treturn fmt.Errorf("unrecognized manifest type: %T", obj)', | ||
'\t}', | ||
'', | ||
'\treturn nil', | ||
'}', | ||
'', | ||
'func init() {', | ||
'\trm := NewResourceMapper()', | ||
]) | ||
|
||
for sgv, data in sorted(scheme_group_versions(types=types).items()): | ||
if data['package'] in ignored_packages: | ||
continue | ||
for type_name in sorted(data['types']): | ||
lines.append('\trm.RegisterGVK({}.WithKind("{}"), newBuilder)'.format(sgv, type_name)) | ||
|
||
lines.extend([ | ||
'\trm.AddToMap(Mapper)', | ||
'}', | ||
'', # trailing newline | ||
]) | ||
|
||
with open(path, 'w') as f: | ||
f.write('\n'.join(lines)) | ||
|
||
|
||
def scheme_group_versions(types): | ||
sgvs = {} | ||
for package, type_names in types.items(): | ||
base, version = os.path.split(package) | ||
short_name = os.path.basename(base) | ||
sgv = '{}{}.SchemeGroupVersion'.format(short_name, version) | ||
sgvs[sgv] = { | ||
'package': package, | ||
'short_name': short_name, | ||
'types': type_names, | ||
'version': version, | ||
} | ||
return sgvs | ||
|
||
|
||
if __name__ == '__main__': | ||
types = { | ||
'github.com/openshift/api/image/v1': {'ImageStream'}, # for payload loading | ||
'github.com/openshift/api/security/v1': {'SecurityContextConstraints'}, | ||
'k8s.io/api/apps/v1': {'DaemonSet', 'Deployment'}, | ||
'k8s.io/api/batch/v1': {'Job'}, | ||
'k8s.io/api/core/v1': {'ConfigMap', 'Namespace', 'Service', 'ServiceAccount'}, | ||
'k8s.io/api/rbac/v1': {'ClusterRole', 'ClusterRoleBinding', 'Role', 'RoleBinding'}, | ||
'k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1': {'CustomResourceDefinition'}, | ||
'k8s.io/kube-aggregator/pkg/apis/apiregistration/v1': {'APIService'}, | ||
} | ||
|
||
types['k8s.io/api/rbac/v1beta1'] = types['k8s.io/api/rbac/v1'] | ||
types['k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1'] = types['k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1'] | ||
types['k8s.io/kube-aggregator/pkg/apis/apiregistration/v1beta1'] = types['k8s.io/kube-aggregator/pkg/apis/apiregistration/v1'] | ||
|
||
clients = { | ||
'github.com/openshift/api/security/v1': {'package': 'github.com/openshift/client-go/security/clientset/versioned/typed/security/v1', 'type': 'SecurityV1Client'}, | ||
'github.com/openshift/api/config/v1': {'package': 'github.com/openshift/client-go/config/clientset/versioned/typed/config/v1', 'type': 'ConfigV1Client'}, | ||
'k8s.io/api/apps/v1': {'package': 'k8s.io/client-go/kubernetes/typed/apps/v1', 'type': 'AppsV1Client'}, | ||
'k8s.io/api/batch/v1': {'package': 'k8s.io/client-go/kubernetes/typed/batch/v1', 'type': 'BatchV1Client'}, | ||
'k8s.io/api/core/v1': {'package': 'k8s.io/client-go/kubernetes/typed/core/v1', 'type': 'CoreV1Client'}, | ||
'k8s.io/api/rbac/v1': {'package': 'k8s.io/client-go/kubernetes/typed/rbac/v1', 'type': 'RbacV1Client'}, | ||
'k8s.io/api/rbac/v1beta1': {'package': 'k8s.io/client-go/kubernetes/typed/rbac/v1beta1', 'type': 'RbacV1beta1Client'}, | ||
'k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1': {'package': 'k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1', 'type': 'ApiextensionsV1Client'}, | ||
'k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1': {'package': 'k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1', 'type': 'ApiextensionsV1beta1Client'}, | ||
'k8s.io/kube-aggregator/pkg/apis/apiregistration/v1': {'package': 'k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/typed/apiregistration/v1', 'type': 'ApiregistrationV1Client'}, | ||
'k8s.io/kube-aggregator/pkg/apis/apiregistration/v1beta1': {'package': 'k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/typed/apiregistration/v1beta1', 'type': 'ApiregistrationV1beta1Client'}, | ||
} | ||
|
||
health_checks = { | ||
('k8s.io/api/apps/v1', 'Deployment'): 'b.checkDeploymentHealth', | ||
('k8s.io/api/apps/v1', 'DaemonSet'): 'b.checkDaemonSetHealth', | ||
('k8s.io/api/batch/v1', 'Job'): 'b.checkJobHealth', | ||
} | ||
|
||
generate_lib_resources(directory='lib', types=types, clients=clients, health_checks=health_checks) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
// Package lib defines a Manifest type. | ||
// | ||
// It also contains subpackages for reconciling in-cluster resources | ||
// with local state. The entrypoint is resourcebuilder, which consumes | ||
// resourceread and resourceapply. resourceapply in turn consumer | ||
// resourcemerge. | ||
package lib |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
package resourceapply | ||
|
||
import ( | ||
"context" | ||
|
||
"github.com/openshift/cluster-version-operator/lib/resourcemerge" | ||
apierrors "k8s.io/apimachinery/pkg/api/errors" | ||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
apiregv1beta1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1beta1" | ||
apiregclientv1beta1 "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/typed/apiregistration/v1beta1" | ||
"k8s.io/utils/pointer" | ||
) | ||
|
||
func ApplyAPIServicev1beta1(ctx context.Context, client apiregclientv1beta1.APIServicesGetter, required *apiregv1beta1.APIService) (*apiregv1beta1.APIService, bool, error) { | ||
existing, err := client.APIServices().Get(ctx, required.Name, metav1.GetOptions{}) | ||
if apierrors.IsNotFound(err) { | ||
actual, err := client.APIServices().Create(ctx, required, metav1.CreateOptions{}) | ||
return actual, true, err | ||
} | ||
if err != nil { | ||
return nil, false, err | ||
} | ||
// if we only create this resource, we have no need to continue further | ||
if IsCreateOnly(required) { | ||
return nil, false, nil | ||
} | ||
|
||
modified := pointer.BoolPtr(false) | ||
resourcemerge.EnsureAPIServicev1beta1(modified, existing, *required) | ||
if !*modified { | ||
return existing, false, nil | ||
} | ||
|
||
actual, err := client.APIServices().Update(ctx, existing, metav1.UpdateOptions{}) | ||
return actual, true, err | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.