Update Apply{DaemonSet,Deployment} to rely on a hash of the spec#773
Update Apply{DaemonSet,Deployment} to rely on a hash of the spec#773openshift-merge-robot merged 3 commits intoopenshift:masterfrom marun:apply-deployment
Conversation
bertinatto
left a comment
There was a problem hiding this comment.
@marun also, we probably want the behaviour of ApplyDaemonSet() to match ApplyDeployment().
Instead of doing a breaking change, what about introducing a new function adds the annotation and calls Apply[Deployment|DaemonSet]()?
For sure. Want to make sure the approach is sound first.
As per the PR description, the breakage is intentional to force callers to choose between keeping the old behavior (for now) or adapting to the new. I don't think this is too much to ask - a quick survey of the code of existing operators shows that majority have only a single call to |
|
While I am in favor of this change, it breaks unit tests - a new annotations appears and needs to be be added on operator input (when the tests checks that operator does nothing when nothing is expected) and ignored when comparing expected / actual objects on output. Exposing the hash computation as a public function would help to fix the unit tests. |
Right, I guess I'm not a big fan of a breaking change here, but we'll use whatever approach you decide. |
| if err != nil { | ||
| return nil, false, err | ||
| } | ||
| specHash := fmt.Sprintf("%x", sha256.Sum256(jsonBytes)) |
There was a problem hiding this comment.
The deployment controller creates the replicaSet with a hash in its name. This hash is calculated from the deployment's pod template [0]. Same for the daemonSet controller.
It's not the same as we're doing here, as we're hashing the whole deployment's spec, but it'd be nice to use a similar approach. For that, we could either:
-
Hash the
deployment.Spec.Templateas well, and use the public available utility function [1] that does that. The downside is thatApplyDeployment()wouldn't update the deployment if we change the number of replicas, for instance. -
Create our own hashing function based on the approach of[1] that calculates the
deployment.Spechash (as opposed todeployment.Spec.Template.
Does this make sense?
[0] https://github.com/kubernetes/kubernetes/blob/98e65951dccfd40d3b4f31949c2ab8df5912d93e/pkg/controller/deployment/sync.go#L189
[1] https://github.com/kubernetes/kubernetes/blob/98e65951dccfd40d3b4f31949c2ab8df5912d93e/pkg/controller/controller_utils.go#L1129
There was a problem hiding this comment.
deployment controller: hash to come up with a unique replicaset name
operator: hash to ensure that a change in intent results in a rollout
Given that the use cases are different, why would it be desirable to match the implementation of the deployment controller?
I think a unit test that cares about an internal detail of ApplyDeployment is a brittle test. Why not just strip the annotation for the purposes of comparison? |
|
The unit test needs to add the annotation to fake API server object, so when ApplyDeployment gets it (as Stripping the annotation after the test, when comparing result is a good idea, but it's orthogonal to ^. |
| // ApplyDeployment merges ObjectMeta of the provided deployment with an existing one | ||
| // if it exists and updates the API if the deployment spec and metadata differ. To | ||
| // ensure an update in response to state external to the deployment spec, the caller | ||
| // should set an annotation representing that external state. |
There was a problem hiding this comment.
give an example of such an annotation
| const specHashAnnotation = "operator.openshift.io/spec-hash" | ||
|
|
||
| // ApplyDeployment merges ObjectMeta of the provided deployment with an existing one | ||
| // if it exists and updates the API if the deployment spec and metadata differ. To |
There was a problem hiding this comment.
if the deployment spec or metadata differ from the previously required spec and metadata. To be reliable, the input of the required spec from the operator should be stable. It does not need to set all fields, since some fields are defaulted server-side.
Detection of spec drift from intent by other actors is determined by generation, not by spec comparison.
| } | ||
| required.Annotations[specHashAnnotation] = specHash | ||
|
|
||
| return ApplyDeploymentWithForce(client, recorder, required, 0, false) |
There was a problem hiding this comment.
I think you need an expected generation in order to know if another actor mutated the spec. The hash of the input spec lets you know if the operator decided a new value was needed (--loglevel for instance) and the generation lets you know if an external actor changed the deployment spec.
Because defaults can change on the server (new field appearing in a new level of kube for instance), you cannot reliably read and compare a previous hash.
@bertinatto we're sensitive to the fact that this causes some pain, but working with @jsafrane I think we've found a better pattern for handling updates in a reliable way that works how our callers really expect them to run. To make the change "easy" to adopt, we'll first make the change obvious and @marun should point to the renamed legacy method for a release before we remove or hide it. Given that the old method defied expectations, I think it's better in this case to break compilation and make callers look at the doc briefly to decide about updating right then or switching to the older method. |
Understood. Updated to expose |
| func ApplyDeployment(client appsclientv1.DeploymentsGetter, recorder events.Recorder, | ||
| required *appsv1.Deployment, expectedGeneration int64) (*appsv1.Deployment, bool, error) { | ||
|
|
||
| err := SetSpecHashAnnotation(&required.ObjectMeta, required.Spec) |
There was a problem hiding this comment.
need to make a copy of required so you don't mutate your input to ApplyDeployment which would be unexpected.
There was a problem hiding this comment.
Done. Also added a commit to ensure ApplyDeploymentWithForce and ApplyDaemonset copy before mutating.
This removes the need for the caller to be able to force a rollout. Any state that is not present in the spec should instead be added as an annotation so that a rollout will occur when the external state changes. This is an intentional breaking change to encourage switching to the revised function. Callers who want to continue using the previous (deprecated) implementation will need to change the name of the function they are calling to ApplyDeploymentWithForce.
This removes the need for the caller to be able to force a rollout. Any state that is not present in the spec should instead be added as an annotation so that a rollout will occur when the external state changes. This is an intentional breaking change to encourage switching to the revised function. Callers who want to continue using the previous (deprecated) implementation will need to change the name of the function they are calling to ApplyDaemonSetWithForce.
|
Added a new commit to perform the same change to ApplyDaemonSet. |
|
/lgtm |
|
[APPROVALNOTIFIER] This PR is APPROVED This pull-request has been approved by: deads2k, marun The full list of commands accepted by this bot can be found here. The pull request process is described here DetailsNeeds approval from an approver in each of these files:
Approvers can indicate their approval by writing |
|
/cherry-pick release-4.4 |
|
@marun: #773 failed to apply on top of branch "release-4.4": DetailsIn response to this:
Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes/test-infra repository. |
Update Apply{DaemonSet,Deployment} to rely on a hash of the spec
This removes the need for the caller to supply an expected generation and be able to force a rollout. Any state that is not present in the spec should instead be added as an annotation so that a rollout will be triggered when the annotation changes.
This is intentionally a breaking change to encourage switching to the revised function. Callers who want to continue using the previous (deprecated) implementation will need to change the name of the function they are calling to
ApplyDeploymentWithForce.Canaries: