Dennis-Mircea Ciupitu created FLINK-41050:
---------------------------------------------

             Summary: Canary health checks revert canary metadata changes and 
restart a healthy operator 
                 Key: FLINK-41050
                 URL: https://issues.apache.org/jira/browse/FLINK-41050
             Project: Flink
          Issue Type: Bug
          Components: Kubernetes Operator
            Reporter: Dennis-Mircea Ciupitu


h1. Problem

Canary resources let the operator health probe verify that spec changes are 
still picked up and reconciled. Every 
{{kubernetes.operator.health.canary.resource.timeout}} cycle (60s by default), 
the operator bumps the canary {{spec.restartNonce}} and checks that the 
previous bump was reconciled in time. If not, the probe reports the operator 
unhealthy and Kubernetes restarts it.

The way the canary spec is updated has several problems. Users can lose their 
changes to the canary, the operator can modify resources that are no longer 
canaries, and the operator can be restarted although it works fine:
 # *Metadata changes on the canary are reverted* - Labels or annotations added 
to a canary (by a user, Argo CD, Kustomize, ...) are removed again on the next 
canary check. GitOps tools then keep fighting with the operator.
 # *Removing the canary label does not stop the canary checks* - The label is 
added back on the next check, so a resource cannot stop being a canary without 
being deleted.
 # *Any failed spec update makes the operator unhealthy* - A single failed 
update, for example a transient API server error (or a not found error = 404) 
or a concurrent write, makes the next check fail. The unhealthy state lasts 
until the check after that, longer than the default liveness probe budget, so 
the operator is restarted although it reconciles correctly.
 # *A canary that is gone or no longer watched restarts the operator* - If the 
canary is deleted without the operator cleanup (e.g. its finalizer was removed 
manually), or its namespace is removed from 
{{kubernetes.operator.watched.namespaces}} at runtime 
({{{}kubernetes.operator.dynamic.namespaces.enabled: true{}}}), every following 
update fails and the operator is restarted.

h1. Root cause
h2. The canary is updated from a stale copy

The spec update uses {{replace()}} on the canary copy cached at the last 
reconcile. Metadata-only changes do not change the generation, so they are not 
reconciled and the cached copy (labels, annotations, resource version) goes 
stale. The update then gets a {{{}409 Conflict{}}}, and the fabric8 client 
retries {{replace()}} by refreshing only the resource version and sending the 
same stale copy again. The update succeeds and overwrites every metadata change 
made since the last reconcile, including the removal of the canary label.
h2. The health check does not depend on the spec update

A failed update is only logged ("Could not bump canary deployment, it may have 
been deleted"), and the health check is scheduled anyway. The check then 
compares the reconciled generation with a generation that was never written, so 
it always fails, whatever the cause of the failure:
 * a transient error fails one check, which is enough to restart the operator.
 * a permanent error ({{{}404{}}} for a canary deleted without cleanup, {{403}} 
for a namespace that is no longer watched and whose Role was removed) fails 
every check.

h2. The canary is not dropped when the operator stops owning it

The tracked canaries are only dropped by the canary deletion cleanup. Nothing 
drops a canary when the operator stops watching its namespace, or when the 
canary label is removed, so the operator keeps updating resources it no longer 
reconciles.
h1. How to reproduce
h2. Metadata changes reverted
 # Operator watching the {{default}} namespace, apply the example canary:
{code:bash}
kubectl apply -f examples/canary.yaml
{code}

 # After a canary check (e.g. {{{}restartNonce: 2{}}}), add a label and an 
annotation:
{code:bash}
kubectl label flinkdeployment canary repro=409
kubectl annotate flinkdeployment canary repro/note=keep-me
{code}

 # After the next canary check ({{{}restartNonce: 3{}}}), the label {{repro}} 
and the annotation {{repro/note}} are gone:
{code:bash}
kubectl get flinkdeployment canary -o 
jsonpath='{.metadata.labels}{"\n"}{.metadata.annotations}{"\n"}{.spec}{"\n"}'
{code}

Removing the {{flink.apache.org/canary}} label instead of adding one behaves 
the same way, the label comes back on the next check.
h2. Namespace removed from the watch list
 # Operator installed with Helm, namespace-scoped RBAC, {{watchNamespaces: 
[default, canary]}} and {{{}kubernetes.operator.dynamic.namespaces.enabled: 
true{}}}. Apply the canary in the {{canary}} namespace and wait until it is 
reconciled:
{code:bash}
kubectl create namespace canary
kubectl apply -f examples/canary.yaml -n canary
{code}

 # Run {{helm upgrade}} with {{{}watchNamespaces: [default]{}}}, without 
deleting the namespace. The operator stops watching {{canary}} and its Role 
there is removed.
 # The next canary update fails, the check is evaluated anyway, and the probe 
reports unhealthy until the operator is restarted:
{code:java}
WARN  o.a.f.k.o.h.CanaryResourceManager Could not bump canary deployment, it 
may have been deleted
io.fabric8.kubernetes.client.KubernetesClientException: Failure executing: PUT 
at: .../namespaces/canary/flinkdeployments/canary. Message: 
flinkdeployments.flink.apache.org "canary" is forbidden: User 
"system:serviceaccount:default:flink-operator" cannot update resource 
"flinkdeployments" in API group "flink.apache.org" in the namespace "canary". 
Received status: Status(apiVersion=v1, code=403, ...)
...
ERROR o.a.f.k.o.h.CanaryResourceManager Canary deployment 
ResourceID{name='canary', namespace='canary'} latest spec not reconciled. 
Expected generation larger than 2, received 2
ERROR o.a.f.k.operator.health.HealthProbe Unhealthy canary resources
ERROR o.a.f.k.operator.health.HealthProbe Unhealthy canary resources
ERROR o.a.f.k.operator.health.HealthProbe Unhealthy canary resources
INFO  i.javaoperatorsdk.operator.Operator Operator SDK 5.5.0 is shutting down...
{code}
{code:bash}
kubectl get pods
NAME                                         READY   STATUS    RESTARTS      AGE
flink-kubernetes-operator-5bdbb9d45f-pvbsl   2/2     Running   1 (43s ago)   
3m24s
{code}

h2. Canary deleted without cleanup
 # Apply the example canary and wait for the first canary check.
 # Remove its finalizer and delete it:
{code:bash}
kubectl patch flinkdeployment canary --type=json 
-p='[{"op":"remove","path":"/metadata/finalizers"}]'
kubectl delete flinkdeployment canary
{code}

 # Every following check fails and the operator is restarted.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to