{"id":20155276,"url":"https://github.com/redhat-cop/operator-utils","last_synced_at":"2025-04-04T19:14:07.300Z","repository":{"id":36748197,"uuid":"183929412","full_name":"redhat-cop/operator-utils","owner":"redhat-cop","description":"Utilities to support operators","archived":false,"fork":false,"pushed_at":"2023-12-18T23:14:13.000Z","size":759,"stargazers_count":153,"open_issues_count":6,"forks_count":39,"subscribers_count":20,"default_branch":"master","last_synced_at":"2025-03-28T18:13:46.235Z","etag":null,"topics":["container-cop"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/redhat-cop.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-04-28T15:59:51.000Z","updated_at":"2025-03-17T13:22:33.000Z","dependencies_parsed_at":"2024-06-18T13:33:01.994Z","dependency_job_id":"6f9585e8-4c9f-41d4-8b11-5a790b8c0a54","html_url":"https://github.com/redhat-cop/operator-utils","commit_stats":null,"previous_names":[],"tags_count":36,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/redhat-cop%2Foperator-utils","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/redhat-cop%2Foperator-utils/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/redhat-cop%2Foperator-utils/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/redhat-cop%2Foperator-utils/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/redhat-cop","download_url":"https://codeload.github.com/redhat-cop/operator-utils/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247234923,"owners_count":20905854,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["container-cop"],"created_at":"2024-11-13T23:31:14.944Z","updated_at":"2025-04-04T19:14:07.248Z","avatar_url":"https://github.com/redhat-cop.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Operator Utility Library\n\n![build status](https://github.com/redhat-cop/operator-utils/workflows/push/badge.svg)\n[![GoDoc reference](https://img.shields.io/badge/godoc-reference-blue.svg)](https://pkg.go.dev/github.com/redhat-cop/operator-utils)\n[![Go Report Card](https://goreportcard.com/badge/github.com/redhat-cop/operator-utils)](https://goreportcard.com/report/github.com/redhat-cop/operator-utils)\n![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/redhat-cop/operator-utils)\n\nThis library layers on top of the Operator SDK and with the objective of helping writing better and more consistent operators.\n\n*NOTICE* versions of this library up to `v0.3.7` are compatible with [operator-sdk](https://github.com/operator-framework/operator-sdk) `0.x`, starting from version v0.4.0 this library will be compatible only with [operator-sdk](https://github.com/operator-framework/operator-sdk) 1.x.\n\n## Scope of this library\n\nThis library covers three main areas:\n\n1. [Utility Methods](#Utility-Methods) Utility methods that are callable by any operator.\n2. [Idempotent methods](#Idempotent-Methods-to-Manipulate-Resources) to manipulate resources and arrays of resources\n3. [Basic operator lifecycle](#Basic-Operator-Lifecycle-Management) needs (validation, initialization, status and error management, finalization)\n4. [Enforcing resources operator support](#Enforcing-Resource-Operator-Support). For those operators which calculate a set of resources that need to exist and then enforce them, generalized support for the enforcing phase is provided.\n\n## Utility Methods\n\nPrior to version v1.3.x the general philosophy of this library was that new operator would inherit from `ReconcilerBase` and in doing so they would have access to a bunch of utility methods.\nWith release v1.3.0 a new approach is available. Utility methods are callable by any operator without having to inherit. This makes it easier to use this library and does not conflict with autogenerate code from `kube-builder` and `operator-sdk`.\nMost of the Utility methods receive a context.Context parameter. Normally this context must be initialized with a `logr.Logger` and a `rest.Config`. Some utility methods may require more, see each individual documentation.\n\nUtility methods are currently organized in the following folders:\n\n1. crud: idempotent create/update/delete functions.\n2. discoveryclient: methods related to the discovery client, typically used to load `apiResource` objects.\n3. dynamicclient: methods related to building client based on object whose type is not known at compile time.\n4. templates: utility methods for dealing with templates whose output is an object or a list of objects.\n\n## Idempotent Methods to Manipulate Resources\n\nThe following idempotent methods are provided (and their corresponding array version):\n\n1. createIfNotExists\n2. createOrUpdate\n3. deleteIfExists\n\nAlso there are utility methods to manage finalizers, test ownership and process templates of resources.\n\n## Basic Operator Lifecycle Management\n\n---\n\nNote\n\nThis part of the library is largely deprecated. For initialization and defaulting a MutatingWebHook should be used. For validation a Validating WebHook should be used.\nThe part regarding the finalization is still relevant.\n\n---\n\nTo get started with this library do the following:\n\nChange your reconciler initialization as exemplified below to add a set of utility methods to it\n\n```go\nimport \"github.com/redhat-cop/operator-utils/pkg/util\"\n\n...\ntype MyReconciler struct {\n  util.ReconcilerBase\n  Log logr.Logger\n  ... other optional fields ...\n}\n```\n\nin main.go change like this\n\n```go\n  if err = (\u0026controllers.MyReconciler{\n    ReconcilerBase: util.NewReconcilerBase(mgr.GetClient(), mgr.GetScheme(), mgr.GetConfig(), mgr.GetEventRecorderFor(\"My_controller\"), mgr.GetAPIReader()),\n    Log:            ctrl.Log.WithName(\"controllers\").WithName(\"My\"),\n  }).SetupWithManager(mgr); err != nil {\n    setupLog.Error(err, \"unable to create controller\", \"controller\", \"My\")\n    os.Exit(1)\n  }\n```\n\nAlso make sure to create the manager with `configmap` as the lease option for leader election:\n\n```go\n  mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{\n    Scheme:                     scheme,\n    MetricsBindAddress:         metricsAddr,\n    Port:                       9443,\n    LeaderElection:             enableLeaderElection,\n    LeaderElectionID:           \"dcb036b8.redhat.io\",\n    LeaderElectionResourceLock: \"configmaps\",\n  })\n```  \n\nIf you want status management, add this to your CRD:\n\n```go\n  // +patchMergeKey=type\n  // +patchStrategy=merge\n  // +listType=map\n  // +listMapKey=type\n  Conditions []metav1.Condition `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"`\n}\n\nfunc (m *MyCRD) GetConditions() []metav1.Condition {\n  return m.Status.Conditions\n}\n\nfunc (m *MyCRD) SetConditions(conditions []metav1.Condition) {\n  m.Status.Conditions = conditions\n}\n```\n\nAt this point your controller is able to leverage the utility methods of this library:\n\n1. [managing CR validation](#managing-cr-validation)\n2. [managing CR initialization](#managing-cr-initialization)\n3. [managing status and error conditions](#managing-status-and-error-conditions)\n4. [managing CR finalization](#managing-cr-finalization)\n5. high-level object manipulation functions such as:\n   - createOrUpdate, createIfNotExists, deleteIfExists\n   - same functions on an array of objects\n   - go template processing of objects\n\nA full example is provided [here](./controllers/mycrd_controller.go)\n\n### Managing CR validation\n\nTo enable CR validation add this to your controller:\n\n```go\nif ok, err := r.IsValid(instance); !ok {\n return r.ManageError(ctx, instance, err)\n}\n```\n\nThe implement the following function:\n\n```go\nfunc (r *ReconcileMyCRD) IsValid(obj metav1.Object) (bool, error) {\n mycrd, ok := obj.(*examplev1alpha1.MyCRD)\n ...\n}\n```\n\n### Managing CR Initialization\n\nTo enable CR initialization, add this to your controller:\n\n```go\nif ok := r.IsInitialized(instance); !ok {\n err := r.GetClient().Update(context.TODO(), instance)\n if err != nil {\n  log.Error(err, \"unable to update instance\", \"instance\", instance)\n  return r.ManageError(ctx, instance, err)\n }\n return reconcile.Result{}, nil\n}\n```\n\nThen implement the following function:\n\n```go\nfunc (r *ReconcileMyCRD) IsInitialized(obj metav1.Object) bool {\n mycrd, ok := obj.(*examplev1alpha1.MyCRD)\n}\n```\n\n### Managing Status and Error Conditions\n\nTo update the status with success and return from the reconciliation cycle, code the following:\n\n```go\nreturn r.ManageSuccess(ctx, instance)\n```\n\nTo update the status with failure, record an event and return from the reconciliation cycle, code the following:\n\n```go\nreturn r.ManageError(ctx, instance, err)\n```\n\nnotice that this function will reschedule a reconciliation cycle with increasingly longer wait time up to six hours.\n\nThere are also variants of these calls to allow for requeuing after a given delay.\nRequeuing is handy when reconciliation depends on a cluster-external state which is not observable from within the api-server.\n\n```go\nreturn r.ManageErrorWithRequeue(ctx, instance, err, 3*time.Second)\n```\n\n```go\nreturn r.ManageSuccessWithRequeue(ctx, instance, 3*time.Second)\n```\n\nor simply using the convenience function:\n\n```go\nreturn r.ManageOutcomeWithRequeue(ctx, instance, err, 3*time.Second)\n```\n\nwhich will delegate to the error or success variant depending on `err` being `nil` or not.\n\n### Managing CR Finalization\n\nto enable CR finalization add this to your controller:\n\n```go\nif util.IsBeingDeleted(instance) {\n if !util.HasFinalizer(instance, controllerName) {\n  return reconcile.Result{}, nil\n }\n err := r.manageCleanUpLogic(instance)\n if err != nil {\n  log.Error(err, \"unable to delete instance\", \"instance\", instance)\n  return r.ManageError(ctx, instance, err)\n }\n util.RemoveFinalizer(instance, controllerName)\n err = r.GetClient().Update(context.TODO(), instance)\n if err != nil {\n  log.Error(err, \"unable to update instance\", \"instance\", instance)\n  return r.ManageError(ctx, instance, err)\n }\n return reconcile.Result{}, nil\n}\n```\n\nThen implement this method:\n\n```go\nfunc (r *ReconcileMyCRD) manageCleanUpLogic(mycrd *examplev1alpha1.MyCRD) error {\n  ...\n}\n```\n\n## Support for operators that need to enforce a set of resources to a defined state\n\nMany operators have the following logic:\n\n1. Phase 1: based on the CR and potentially additional status, a set of resources that need to exist is calculated.\n2. Phase 2: These resources are then created or updated against the master API.\n3. Phase 3: A well written operator also ensures that these resources stay in place and are not accidentally or maliciously changed by third parties.\n\nThese phases are of increasing difficulty to implement. It's also true that phase 2 and 3 can be generalized.\n\nOperator-utils offers some scaffolding to assist in writing these kinds of operators.\n\nSimilarly to the `BaseReconciler` class, we have a base type to extend called: `EnforcingReconciler`. This class extends from `BaseReconciler`, so you have all the same facilities as above.\n\nWhen initializing the EnforcingReconciler, one must chose whether watchers will be created at the cluster level or at the namespace level.\n\n- if cluster level is chosen a watch per CR and type defined in it will be created. This will require the operator to have cluster level access.\n\n- if namespace level watchers is chosen a watch per CR, type and namespace will be created. This will minimize the needed permissions, but depending on what the operator needs to do may open a very high number of connections to the API server.\n\nThe body of the reconciler function will look something like this:\n\n```golang\nvalidation...\ninitialization...\n(optional) finalization...\nPhase1 ... calculate a set of resources to be enforced -\u003e LockedResources\n\n  err = r.UpdateLockedResources(context,instance, lockedResources, ...)\n  if err != nil {\n    log.Error(err, \"unable to update locked resources\")\n    return r.ManageError(ctx, instance, err)\n }\n\n  return r.ManageSuccess(ctx, instance)\n```\n\nthis is all you have to do for basic functionality. For more details see the [example](pkg/controller/apis/enforcingcrd/enforcingcrd_controller.go)\nthe EnforcingReconciler will do the following:\n\n1. restore the resources to the desired stated if the are changed. Notice that you can exclude paths from being considered when deciding whether to restore a resource. As set of JSON Path can be passed together with the LockedResource. It is recommended to set these paths:\n    1. `.metadata`\n    2. `.status`\n\n2. restore resources when they are deleted.\n\nThe `UpdateLockedResources` will validate the input as follows:\n\n1. the passed resource must be defined in the current apiserver\n2. the passed resource must be syntactically compliant with the OpenAPI definition of the resource defined in the server.\n3. if the passed resource is namespaced, the namespace field must be initialized.\n\nThe finalization method will look like this:\n\n```golang\nfunc (r *ReconcileEnforcingCRD) manageCleanUpLogic(instance *examplev1alpha1.EnforcingCRD) error {\n  err := r.Terminate(instance, true)\n  if err != nil {\n    log.Error(err, \"unable to terminate enforcing reconciler for\", \"instance\", instance)\n    return err\n  }\n  ... additional finalization logic ...\n  return nil\n}\n```\n\nConvenience methods are also available for when resources are templated. See the [templatedenforcingcrd](./pkgcontroller/templatedenforcingcrd/templatedenforcingcrd_controller.go) controller as an example.\n\n## Support for operators that need to enforce a set of patches\n\nFor similar reasons stated in the previous paragraphs, operators might need to enforce patches.\nA patch modifies an object created by another entity. Because in this case the CR does not own the to-be-modified object a patch must be enforced against changes made on it.\nOne must be careful not to create circular situations where an operator deletes the patch and this operator recreates the patch.\nIn some situations, a patch must be parametric on some state of the cluster. For this reason, it's possible to monitor source objects that will be used as parameters to calculate the patch.\n\nA patch is defined as follows:\n\n```golang\ntype LockedPatch struct { \n  Name             string                           `json:\"name,omitempty\"`\n  SourceObjectRefs []utilsapi.SourceObjectReference `json:\"sourceObjectRefs,omitempty\"`\n  TargetObjectRef  utilsapi.TargetObjectReference   `json:\"targetObjectRef,omitempty\"`\n  PatchType        types.PatchType                  `json:\"patchType,omitempty\"`\n  PatchTemplate    string                           `json:\"patchTemplate,omitempty\"`\n  Template         template.Template                `json:\"-\"`\n}\n```\n\nthe targetObjectRef and sourceObjectRefs are watched for changes by the reconciler.\n\ntargetObjectRef can select multiple objects, this is the logic\n\n| Namespaced Type | Namespace | Name | Selection type |\n| --- | --- | --- | --- |\n| yes | null | null | multiple selection across namespaces |\n| yes | null | not null | multiple selection across namespaces where the name corresponds to the passed name |\n| yes | not null | null | multiple selection within a namespace |\n| yes | not null | not nul | single selection |\n| no | N/A | null | multiple selection  |\n| no | N/A | not null | single selection |\n\nSelection can be further narrowed down by filtering by labels and/or annotations. The patch will be applied to all of the selected instances.\n\nName and Namespace of sourceRefObjects are interpreted as golang templates with the current target instance and the only parameter. This allows to select different source object for each target object.\n\nThe relevant part of the operator code would look like this:\n\n```golang\nvalidation...\ninitialization...\nPhase1 ... calculate a set of patches to be enforced -\u003e LockedPatches\n\n  err = r.UpdateLockedResources(context, instance, ..., lockedPatches...)\n  if err != nil {\n    log.Error(err, \"unable to update locked resources\")\n    return r.ManageError(ctx, instance, err)\n }\n\n  return r.ManageSuccess(ctx, instance)\n```\n\nThe `UpdateLockedResources` will validate the input as follows:\n\n1. the passed patch target/source `ObjectRef` resource must be defined in the current apiserver\n2. if the passed patch target/source `ObjectRef` resources are namespaced the corresponding namespace field must be initialized.\n3. the ID must have a not null and unique value in the array of the passed patches.\n\nPatches cannot be undone so there is no need to manage a finalizer.\n\n[Here](./pkg/controller/enforcingpatch/enforcingpatch_controller.go) you can find an example of how to implement an operator with this the ability to enforce patches.\n\n## Support for operators that need dynamic creation of locked resources using templates\n\nOperators may also need to leverage locked resources created dynamically through templates. This can be done using [go templates](https://golang.org/pkg/text/template/) and leveraging the `GetLockedResourcesFromTemplates` function.\n\n```golang\nlockedResources, err := r.GetLockedResourcesFromTemplates(templates..., params...)\nif err != nil {\n  log.Error(err, \"unable to process templates with param\")\n  return err\n}\n```\n\nThe `GetLockedResourcesFromTemplates` will validate the input as follows:\n\n1. check that the passed template is valid\n2. format the template using the properties of the passed object in the params parameter\n3. create an array of `LockedResource` objects based on parsed template\n\nThe example below shows how templating can be used to reference the name of the resource passed as the parameter and use it as a property in the creation of the `LockedResource`.\n\n```golang\nobjectTemplate: |\n  apiVersion: v1\n  kind: Namespace\n  metadata:\n    name: {{ .Name }}\n```\n\nThis functionality can leverage advanced features of go templating, such as loops, to generate more than one object following a set pattern. The below example will create an array of namespace `LockedResources` using the title of any key where the associated value matches the text *devteam* in the key/value pair of the `Labels` property of the resource passed in the params parameter.\n\n```golang\nobjectTemplate: |\n  {{range $key, $value := $.Labels}}\n    {{if eq $value \"devteam\"}}\n      - apiVersion: v1\n        kind: Namespace\n        metadata:\n          name: {{ $key }}\n    {{end}}\n  {{end}}\n```\n\n## Support for operators that need advanced templating functionality\n\nOperators may need to utilize advanced templating functions not found in the base go templating library. This advanced template functionality matches the same available in the popular k8s management tool [Helm](https://helm.sh/). `LockedPatch` templates uses this functionality by default. To utilize these features when using `LockedResources` the following function is required,\n\n```golang\nlockedResources, err := r.GetLockedResourcesFromTemplatesWithRestConfig(templates..., rest.Config..., params...)\nif err != nil {\n  log.Error(err, \"unable to process templates with param\")\n  return err\n}\n```  \n\n## Deployment\n\n### Deploying with Helm\n\nHere are the instructions to install the latest release with Helm.\n\n```shell\noc new-project operator-utils\nhelm repo add operator-utils https://redhat-cop.github.io/operator-utils\nhelm repo update\nhelm install operator-utils operator-utils/operator-utils\n```\n\nThis can later be updated with the following commands:\n\n```shell\nhelm repo update\nhelm upgrade operator-utils operator-utils/operator-utils\n```\n\n## Development\n\n## Running the operator locally\n\n```shell\nmake install\noc new-project operator-utils-operator-local\nkustomize build ./config/local-development | oc apply -f - -n operator-utils-operator-local\nexport token=$(oc serviceaccounts get-token 'operator-utils-operator-controller-manager' -n operator-utils-operator-local)\noc login --token ${token}\nmake run ENABLE_WEBHOOKS=false\n```\n\n### testing\n\nPatches\n\n```shell\noc new-project patch-test\noc create sa test -n patch-test\noc adm policy add-cluster-role-to-user cluster-admin -z default -n patch-test\noc apply -f ./test/enforcing-patch.yaml -n patch-test\noc apply -f ./test/enforcing-patch-multiple.yaml -n patch-test\noc apply -f ./test/enforcing-patch-multiple-cluster-level.yaml -n patch-test\n```\n\n## Building/Pushing the operator image\n\n```shell\nexport repo=raffaelespazzoli #replace with yours\ndocker login quay.io/$repo\nmake docker-build IMG=quay.io/$repo/operator-utils:latest\nmake docker-push IMG=quay.io/$repo/operator-utils:latest\n```\n\n## Deploy to OLM via bundle\n\n```shell\nmake manifests\nmake bundle IMG=quay.io/$repo/operator-utils:latest\noperator-sdk bundle validate ./bundle --select-optional name=operatorhub\nmake bundle-build BUNDLE_IMG=quay.io/$repo/operator-utils-bundle:latest\ndocker push quay.io/$repo/operator-utils-bundle:latest\noperator-sdk bundle validate quay.io/$repo/operator-utils-bundle:latest --select-optional name=operatorhub\noc new-project operator-utils\noc label namespace operator-utils openshift.io/cluster-monitoring=\"true\"\noperator-sdk cleanup operator-utils -n operator-utils\noperator-sdk run bundle --install-mode AllNamespaces -n operator-utils quay.io/$repo/operator-utils-bundle:latest\n```\n\n## Releasing\n\n```shell\ngit tag -a \"\u003ctagname\u003e\" -m \"\u003ccommit message\u003e\"\ngit push upstream \u003ctagname\u003e\n```\n\nIf you need to remove a release:\n\n```shell\ngit tag -d \u003ctagname\u003e\ngit push upstream --delete \u003ctagname\u003e\n```\n\nIf you need to \"move\" a release to the current main\n\n```shell\ngit tag -f \u003ctagname\u003e\ngit push upstream -f \u003ctagname\u003e\n```\n\n### Cleaning up\n\n```shell\noperator-sdk cleanup operator-utils -n operator-utils\noc delete operatorgroup operator-sdk-og\noc delete catalogsource operator-utils-catalog\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fredhat-cop%2Foperator-utils","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fredhat-cop%2Foperator-utils","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fredhat-cop%2Foperator-utils/lists"}