Skip to content

Commit

Permalink
lib: Add autogeneration for some resource* functionality
Browse files Browse the repository at this point in the history
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
wking committed Jul 31, 2020
1 parent ef236c3 commit 8a020a7
Show file tree
Hide file tree
Showing 36 changed files with 927 additions and 1,000 deletions.
286 changes: 286 additions & 0 deletions hack/generate-lib-resources.py
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)
7 changes: 7 additions & 0 deletions lib/doc.go
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
4 changes: 2 additions & 2 deletions lib/resourceapply/apiext.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
"k8s.io/utils/pointer"
)

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

func ApplyCustomResourceDefinitionV1(ctx context.Context, client apiextclientv1.CustomResourceDefinitionsGetter, required *apiextv1.CustomResourceDefinition) (*apiextv1.CustomResourceDefinition, bool, error) {
func ApplyCustomResourceDefinitionv1(ctx context.Context, client apiextclientv1.CustomResourceDefinitionsGetter, required *apiextv1.CustomResourceDefinition) (*apiextv1.CustomResourceDefinition, bool, error) {
existing, err := client.CustomResourceDefinitions().Get(ctx, required.Name, metav1.GetOptions{})
if apierrors.IsNotFound(err) {
actual, err := client.CustomResourceDefinitions().Create(ctx, required, metav1.CreateOptions{})
Expand Down
2 changes: 1 addition & 1 deletion lib/resourceapply/apireg.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
"k8s.io/utils/pointer"
)

func ApplyAPIService(ctx context.Context, client apiregclientv1.APIServicesGetter, required *apiregv1.APIService) (*apiregv1.APIService, bool, error) {
func ApplyAPIServicev1(ctx context.Context, client apiregclientv1.APIServicesGetter, required *apiregv1.APIService) (*apiregv1.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{})
Expand Down
36 changes: 36 additions & 0 deletions lib/resourceapply/apiregv1beta1.go
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
}
8 changes: 4 additions & 4 deletions lib/resourceapply/apps.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import (
"k8s.io/utils/pointer"
)

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

// ApplyDaemonSet applies the required daemonset to the cluster.
func ApplyDaemonSet(ctx context.Context, client appsclientv1.DaemonSetsGetter, required *appsv1.DaemonSet) (*appsv1.DaemonSet, bool, error) {
// ApplyDaemonSetv1 applies the required daemonset to the cluster.
func ApplyDaemonSetv1(ctx context.Context, client appsclientv1.DaemonSetsGetter, required *appsv1.DaemonSet) (*appsv1.DaemonSet, bool, error) {
existing, err := client.DaemonSets(required.Namespace).Get(ctx, required.Name, metav1.GetOptions{})
if apierrors.IsNotFound(err) {
actual, err := client.DaemonSets(required.Namespace).Create(ctx, required, metav1.CreateOptions{})
Expand Down
4 changes: 2 additions & 2 deletions lib/resourceapply/batch.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import (
"k8s.io/utils/pointer"
)

// ApplyJob applies the required Job to the cluster.
func ApplyJob(ctx context.Context, client batchclientv1.JobsGetter, required *batchv1.Job) (*batchv1.Job, bool, error) {
// ApplyJobv1 applies the required Job to the cluster.
func ApplyJobv1(ctx context.Context, client batchclientv1.JobsGetter, required *batchv1.Job) (*batchv1.Job, bool, error) {
existing, err := client.Jobs(required.Namespace).Get(ctx, required.Name, metav1.GetOptions{})
if apierrors.IsNotFound(err) {
actual, err := client.Jobs(required.Namespace).Create(ctx, required, metav1.CreateOptions{})
Expand Down
Loading

0 comments on commit 8a020a7

Please sign in to comment.