-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Add CreateOrUpdate utility method #98
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add CreateOrUpdate utility method #98
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
comments inline, also needs tests
} | ||
|
||
// TransformFn is a function which take in a kubernetes object and returns the | ||
// desired state of that object |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note whether or not it's allowed/expected to mutate the object. It can be helpful when refactoring later.
OperationUpdated = "updated" | ||
) | ||
|
||
// CreateOrUpdate creates or updates a kuberenes resource. It takes in a key and |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo: "kubernetes"
|
||
// CreateOrUpdate creates or updates a kuberenes resource. It takes in a key and | ||
// a placeholder for the existing object and returns the modified object | ||
func CreateOrUpdate(ctx context.Context, c client.Client, key client.ObjectKey, existing runtime.Object, t TransformFn) (runtime.Object, OperationType, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
our client methods tend not to return objects, and instead operate "in-place" on them. This should do so as well
Hi @DirectXMan12 I've implemented your feedback and also wrote tests |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, so, I think in general you can drop the key
from this, and extract it from the object in question. The signature should be reminiscent of the Create
and Update
methods. Sorry for not catching that last review.
|
||
// CreateOrUpdate creates or updates a kubernetes resource. It takes in a key and | ||
// a placeholder for the existing object and returns the operation executed | ||
func CreateOrUpdate(ctx context.Context, c client.Client, key client.ObjectKey, existing runtime.Object, t TransformFn) (OperationType, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is the purpose of OperationType
just logging?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup, but we can also pass it to the transform function, so that you know that is it going to create or update.
// Create a new zero value object so that the in parameter of | ||
// TransformFn is always a "clean" object, with only Name and Namespace | ||
// set | ||
zero := reflect.New(reflect.TypeOf(existing).Elem()).Interface() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is what existing
is for. It should never be nil, so you shouldn't have to construct a new zero object.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason for creating a new zero value struct is that you can pass in existing an initialized one, eg. with .spec
filled. I'm not sure that client.Get
zeroes the object if it doesn't find one.
What I want to enforce here, is that if this function is going to create a new object, your transform callback always gets an empty object with only Name and Namespace filled in.
By("returning OperationCreated") | ||
Expect(op).Should(BeEquivalentTo(controllerutil.OperationCreated)) | ||
|
||
By("returning returning no error") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo: duplicate word
|
||
var scale int32 = 2 | ||
|
||
Expect(c.Create(context.TODO(), depl)).Should(Succeed()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style is general Expect(X).To(Y)
(Should
is reserved for the omega character, which we don't use)
@DirectXMan12 what do you think about changing the signature to CreateOrUpdate (ctx context.Context, c client.Client, obj runtime.Object, t ...TransformFn) That way you can just pass the desired object and if you need to apply some changes you can define a set of transform functions. The reason for keeping the transform function is that for example in case of Service another controller fills in the The type TransformFn func(existing, desired runtime.Object, op OperationType) (runtime.Object, error) Edit: I've added to the PR 164020f which does that, so it can be reverted easily. |
Also, do you thing that I should move this to a |
I think it's probably fine where it is.
I'm fine with the multiple transform functions. I'm not entirely certain as to the purpose of passing the operation type to the transform function, though. Same with passing existing and desired pointers -- ideally, the transformation should be idempotent, and should take an object, and turn it into the desired state. I'd expect a signature of type TransformFunc func(obj runtime.Object) error
func CreateOrUpdate(ctx context.Context, c client.Client, obj runtime.Object, transforms... TransformFunc) error but it's possible I'm missing something here. |
5dc6b34
to
1431a57
Compare
@DirectXMan12 I think I've made some logic mistakes in the previous commit so I rewrote it in 2fe301a. Let me try to better illustrate the use cases for transform functions which are actually reconcile functions (I have renamed the type to better illustrate this to Probably 90% of times you would use For example if you pass in a So the type ReconcileFn func(desired, existing runtime.Object) error As for the operation I've dropped it from the |
1431a57
to
2fe301a
Compare
aaaah, I see what you're saying with the reconcile func. That makes sense, we just need to make sure that's blindingly obvious. |
@DirectXMan12 after using it in real code, I'm having second thoughts about the current state of type ReconcileFn func(desired, existing runtime.Object) error because I allways end up with code like: existing.DeepCopyInto(desired)
desired.replicas = 2 Ideally you want your reconcile function to just do: existing.replicas = 2 And that is because you always want to mutate the existing state towards a desired state. So the proposal is to change the signatures to: // CreateOrUpdate a resrouce with the same type, name and namespace as obj
// and recocile it's state using the passed in ReconcileFn.
// It returns the executed operation and an error.
func CreateOrUpdate(ctx context.Context, c client.Client, obj runtime.Object, r ReconcileFn) (OperationType, error) {}
// ReconcileFn is a function which mutates the existing object into it's desired state.
// If there is no existing object, a zero value object is passed in.
type ReconcileFn func(existing runtime.Object) error Here is an example which always creates/updates a deployment with the scale of 2: func scale(existing runtime.Object) error {
var replicas int32 = 2
out := existing.(*appsv1.Deployment)
existing.Sepc.Replicas = &replicas
}
CreateOrUpdate(context.TODO(), c, &appsv1.Deployment{Name: "foo", Namespace: "bar"}, scale) |
So, we just need to be clear on the intended usage in the comments -- the |
Yep, you are correct, the |
2fe301a
to
74a702e
Compare
@DirectXMan12, I've pushed the changes. I've also added an example. It is ready for review. |
5110ac3
to
0b3a3fd
Compare
I've rebased the changes and also found an issue when the @DirectXMan12 PTAL |
0b3a3fd
to
2f12419
Compare
@DirectXMan12 what's the status of this? |
hey, sorry, busy week. let me try and take a look today. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sorry for taking so long to get back to you. I think the internals are close, but there's a couple of rough spots. comments inline.
// It returns the executed operation and an error. | ||
func CreateOrUpdate(ctx context.Context, c client.Client, obj runtime.Object, r ReconcileFn) (OperationType, error) { | ||
// op is the operation we are going to attempt | ||
var op OperationType = OperationNoop |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we need to have the operation type here? I thought you were going to remove it...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think needs to be a way to surface the operation. For example, I'm using it to emit events based of that, eg. Successfully created Deployment default/foo
. Is there a better way of doing it?
var op OperationType = OperationNoop | ||
|
||
// get the existing object | ||
mo, ok := obj.(v1.Object) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what does mo
stand for here? when you can, use more descriptive names.
) | ||
|
||
// CreateOrUpdate a resource with the same type, name and namespace as obj | ||
// and reconcile it's state using the passed in ReconcileFn. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this needs to document the use of obj
, since it's a bit tricky
Name: mo.GetName(), | ||
Namespace: mo.GetNamespace(), | ||
} | ||
existing := reflect.New(reflect.TypeOf(obj).Elem()).Interface().(runtime.Object) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shouldn't we be using obj
here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If used, obj
might get mutated by c.Get
, that's why the new existing
object.
If we agree that obj
can be mutated (eg. we return the existing object with it), than yes, we can use obj
directly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
after more code reading I see that client.Create
and client.Update
mutate the obj
. I'll update the PR accordingly.
} | ||
|
||
if errors.IsNotFound(err) { | ||
err = c.Create(ctx, newobj) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
generally, the passed-in object should be updated with the changes.
2f12419
to
12b7536
Compare
@DirectXMan12 PTAL. I've implemented the feedback regarding For example I'm using the |
I think this looks good. Let's have a quick peek from @droot and then we'll merge. |
@DirectXMan12 @calind looking at it. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I am late to the party. Have a few comments.
if reflect.DeepEqual(existing, obj) { | ||
return OperationNoop, nil | ||
} | ||
err = c.Update(ctx, obj) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Trying to analyze the semantics of this wrt status sub-resource. If an API implements status
as sub-resource (which is the recommended approach), then Update
is updating just the Spec
part of API (Then the DeepEqual
should be of the spec's field only ?) @DirectXMan12
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@droot obj
being a runtime.Object
, is it possible to get the spec of that? If so what should happen with types not having a Spec
field?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thinking more about it.. Is it something the updateFn
should tell us ? because it has more knowledge about the logic whether update is needed or not. This also takes away reflect.DeepEqual
which could be costly from runtime performance perspective as well.
possible signature.
updateFn(obj runtime.Object) (needsUpdate bool, error)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The updateFn
is applied regardless of Create
or Update
. Its role is to change the existing state towards the desired state. For creation, it starts with an empty obj
and for update with the existing object.
Update
need to be called if updateFn
changed something about obj
, that's why the DeepEqual
.
I think the better name is mutateFn
to make it clear that it does the heavy lifting regardless of Create or Update.
@droot PTAL |
@calind looks good to me. Can you pl. squash the commits ? |
9b41e73
to
d47ce17
Compare
@droot done |
/lgtm |
/approve |
[APPROVALNOTIFIER] This PR is APPROVED This pull-request has been approved by: calind, DirectXMan12 The full list of commands accepted by this bot can be found here. The pull request process is described here
Needs approval from an approver in each of these files:
Approvers can indicate their approval by writing |
@calind Is it correct to assume that I would pass on the desired state object as // Fetch the OdooCluster instance
instance := &clusterv1beta1.OdooCluster{}
err := r.Get(context.TODO(), request.NamespacedName, instance)
if err != nil {
if errors.IsNotFound(err) {
// Object not found, return. Created objects are automatically garbage collected.
// For additional cleanup logic use finalizers.
return reconcile.Result{}, nil
}
// Error reading the object - requeue the request.
return reconcile.Result{}, err
}
// Define the desired DBNamespace object
objDBNamespace := &clusterv1beta1.DBNamespace{
ObjectMeta: metav1.ObjectMeta{
Name: instance.Name + "-dbnamespace",
Namespace: instance.Namespace,
},
Spec: instance.Spec.DBSpec, // Desired state
}
if err := controllerutil.SetControllerReference(instance, objDBNamespace, r.scheme); err != nil {
return reconcile.Result{}, err
}
_, err := controllerutil.CreateOrUpdate(context.TODO(), r, objDBNamespace, scale)
// Still don't know how to properly handle _ but doesn't matter, here. |
The object you pass in is the receiver object, aka the result of // Define the object name/namespace
objDBNamespace := &clusterv1beta1.DBNamespace{
ObjectMeta: metav1.ObjectMeta{
Name: instance.Name + "-dbnamespace",
Namespace: instance.Namespace,
},
}
result, err := controllerutil.CreateOrUpdate(context.TODO(), r.Client, objDBNamespace, func (existing runtime.Object) error{
// mutate here the state of existing object to the desired state
// note that at this point _objDBNamespace_ and _existing_ point to the same struct
out := existing.(*clusterv1beta1.DBNamespace)
out.Spec = instance.Spec.DBSpec // Desired state
if err := controllerutil.SetControllerReference(instance, out, r.scheme); err != nil {
return err
}
return nil
})
if err != nil {
return reconcile.Result{}, err
}
// Your objDBNamespace now contains the object from the culster after Create/Update
// see: https://godoc.org/github.com/kubernetes-sigs/controller-runtime/pkg/client#Writer
// Result contains the applied operation: Create/Update/NoOp
log.Info("object reconciled", "operation", result)
return reconcile.Result{}, nil |
Thanks, got it! The inline function idiom is a nice idea to keep logic linear to read! |
@calind This commit enabled a beautiful pattern https://github.com/xoe-labs/odoo-operator/blob/2a0248030b53ad82b99c2c229fdd1db3f3ae38aa/pkg/controller/odoocluster/odoocluster_controller.go#L212-L459 |
Cool, glad to hear that! You can verify in the in the mutate function that |
Add CreateOrUpdate utility method
Add travis and reportcard images
Closes #85.