Skip to content
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

Configmap removals not being recognized in WATCH mode #25

Closed
gburton1 opened this issue Jul 9, 2021 · 16 comments
Closed

Configmap removals not being recognized in WATCH mode #25

gburton1 opened this issue Jul 9, 2021 · 16 comments

Comments

@gburton1
Copy link

gburton1 commented Jul 9, 2021

I've just switched over to kopf-k8s-sidecar due to this issue. It's working fine as a drop-in replacement, but it does not recognize when configmaps are removed. I'm using it for the standard Grafana dashboard sidecar use case, and I've shown the service account and role below. Here is the warning in the logs that looks relevant:

[2021-07-09 18:16:16,990] sidecar_settings     [INFO    ] Looking for resources with LABEL 'grafana_dashboard'
[2021-07-09 18:16:16,990] sidecar_settings     [INFO    ] The default FOLDER to write files to is /tmp/dashboards
[2021-07-09 18:16:16,990] sidecar_settings     [INFO    ] FOLDER_ANNOTATION for the destination folder is 'k8s-sidecar-target-directory'
[2021-07-09 18:16:16,990] sidecar_settings     [INFO    ] Looking for resources ONLY in the ['cortex-dashboards'] namespaces
[2021-07-09 18:16:16,990] sidecar_settings     [INFO    ] Monitoring configmap and secret resources for changes
[2021-07-09 18:16:16,990] sidecar_settings     [INFO    ] Using the WATCH METHOD
[2021-07-09 18:16:16,990] sidecar_settings     [INFO    ] DEFAULT_FILE_MODE is None
[2021-07-09 18:16:16,991] sidecar_settings     [INFO    ] Client watching requests using a timeout of 660 seconds
[2021-07-09 18:16:16,991] sidecar_settings     [INFO    ] Server watching requests using a timeout of 600 seconds
[2021-07-09 18:16:17,000] kopf._core.reactor.r [WARNING ] OS signals are ignored: running not in the main thread.
[2021-07-09 18:16:17,000] kopf.activities.star [INFO    ] Activity 'startup_tasks' succeeded.
[2021-07-09 18:16:17,001] kopf._core.engines.a [INFO    ] Initial authentication has been initiated.
[2021-07-09 18:16:17,003] kopf.activities.auth [INFO    ] Activity 'login_via_pykube' succeeded.
[2021-07-09 18:16:17,003] kopf._core.engines.a [INFO    ] Initial authentication has finished.
[2021-07-09 18:16:17,077] kopf._core.reactor.o [WARNING ] Not enough permissions to watch for resources: changes (creation/deletion/updates) will not be noticed; the resources are only refreshed on operator restarts.
...
  serviceAccount: prometheus-legacy-infra-grafana
  serviceAccountName: prometheus-legacy-infra-grafana

    Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from prometheus-legacy-infra-grafana-token-r2wh4 (ro)
docker-desktop# kubectl get clusterrolebinding prometheus-legacy-infra-grafana-clusterrolebinding -n cortex -o yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  labels:
    app.kubernetes.io/instance: prometheus-legacy-infra
    app.kubernetes.io/managed-by: Helm
    app.kubernetes.io/name: grafana
    app.kubernetes.io/version: 8.0.3
    argocd.argoproj.io/instance: prometheus-legacy-infra
    helm.sh/chart: grafana-6.13.2
  name: prometheus-legacy-infra-grafana-clusterrolebinding
  resourceVersion: "37673562"
  uid: 5ab6eed0-dc0d-439b-a0c5-256e5efc4811
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: prometheus-legacy-infra-grafana-clusterrole
subjects:
- kind: ServiceAccount
  name: prometheus-legacy-infra-grafana
  namespace: cortex
docker-desktop# kubectl get clusterrole prometheus-legacy-infra-grafana-clusterrole -n cortex -o yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/instance: prometheus-legacy-infra
    app.kubernetes.io/managed-by: Helm
    app.kubernetes.io/name: grafana
    app.kubernetes.io/version: 8.0.3
    argocd.argoproj.io/instance: prometheus-legacy-infra
    helm.sh/chart: grafana-6.13.2
  name: prometheus-legacy-infra-grafana-clusterrole
  resourceVersion: "37673555"
  uid: 3d1216f5-8110-44d5-a4f6-c1947f8cb6cd
rules:
- apiGroups:
  - ""
  resources:
  - configmaps
  - secrets
  verbs:
  - get
  - watch
  - list
@OmegaVVeapon
Copy link
Owner

Hi @gburton1 , that warning is fine.
That's just the operator saying that it won't be able to monitor everything.

You have provided RBAC to configmaps and secrets, which is enough.

However, I'm not able to reproduce the behaviour you're seeing...

This is what I see from a fresh pod after creating some configmaps

❯ k apply -f configmaps.yaml
configmap/fake-dashboard created
configmap/fake-dashboard-2 created
configmap/fake-dashboard-folder-annotation created
configmap/fake-datasource-1 created
configmap/fake-datasource-2 created

and then deleting them:

❯ k delete -f configmaps.yaml
configmap "fake-dashboard" deleted
configmap "fake-dashboard-2" deleted
configmap "fake-dashboard-folder-annotation" deleted
configmap "fake-datasource-1" deleted
configmap "fake-datasource-2" deleted

Setting USER environment variable to 65534
[2021-07-09 21:38:22,708[] sidecar_settings     [INFO    [] Looking for resources with LABEL 'fake-dashboard'
[2021-07-09 21:38:22,709[] sidecar_settings     [INFO    [] The default FOLDER to write files to is /tmp/
[2021-07-09 21:38:22,709[] sidecar_settings     [INFO    [] FOLDER_ANNOTATION for the destination folder is 'k8s-sidecar-target-directory'
[2021-07-09 21:38:22,709[] sidecar_settings     [INFO    [] Looking for resources in the entire cluster
[2021-07-09 21:38:22,709[] sidecar_settings     [INFO    [] Monitoring configmap and secret resources for changes
[2021-07-09 21:38:22,709[] sidecar_settings     [INFO    [] Using the WATCH METHOD
[2021-07-09 21:38:22,709[] sidecar_settings     [INFO    [] DEFAULT_FILE_MODE is None
[2021-07-09 21:38:22,709[] sidecar_settings     [INFO    [] Client watching requests using a timeout of 660 seconds
[2021-07-09 21:38:22,709[] sidecar_settings     [INFO    [] Server watching requests using a timeout of 600 seconds
[2021-07-09 21:38:22,709[] sidecar_settings     [INFO    [] Unique filenames will be enforced.
[2021-07-09 21:38:22,720[] kopf._core.reactor.r [WARNING [] OS signals are ignored: running not in the main thread.
[2021-07-09 21:38:22,721[] kopf.activities.star [INFO    [] Activity 'startup_tasks' succeeded.
[2021-07-09 21:38:22,722[] kopf._core.engines.a [INFO    [] Initial authentication has been initiated.
[2021-07-09 21:38:22,724[] kopf.activities.auth [INFO    [] Activity 'login_via_pykube' succeeded.
[2021-07-09 21:38:22,725[] kopf._core.engines.a [INFO    [] Initial authentication has finished.
[2021-07-09 21:38:22,788[] kopf._core.reactor.o [WARNING [] Not enough permissions to watch for resources: changes (creation/deletion/updates) will not be noticed; the resources are only refreshed on operator restarts.
[2021-07-09 21:39:26,931[] kopf.objects         [INFO    [] [ADDED:ConfigMap[] Writing content to file /tmp/test-namespace.configmap_fake-dashboard.test-first-dashboard
[2021-07-09 21:39:26,932[] kopf.objects         [INFO    [] [ADDED:ConfigMap[] Writing content to file /tmp/test-namespace.configmap_fake-dashboard.test-second-dashboard
[2021-07-09 21:39:26,932[] kopf.objects         [INFO    [] Handler 'cru_fn' succeeded.
[2021-07-09 21:39:27,014[] kopf.objects         [INFO    [] [ADDED:ConfigMap[] Writing content to file /tmp/default.configmap_fake-dashboard-2.default-dashboard
[2021-07-09 21:39:27,015[] kopf.objects         [INFO    [] Handler 'cru_fn' succeeded.
[2021-07-09 21:39:27,091[] kopf.objects         [INFO    [] [ADDED:ConfigMap[] Writing content to file /tmp/default.configmap_fake-dashboard-folder-annotation.default-annotation
[2021-07-09 21:39:27,092[] kopf.objects         [INFO    [] Handler 'cru_fn' succeeded.
[2021-07-09 21:39:38,080[] kopf.objects         [INFO    [] [DELETED:ConfigMap[] Deleting file /tmp/test-namespace.configmap_fake-dashboard.test-first-dashboard.
[2021-07-09 21:39:38,081[] kopf.objects         [INFO    [] [DELETED:ConfigMap[] Deleting file /tmp/test-namespace.configmap_fake-dashboard.test-second-dashboard.
[2021-07-09 21:39:38,081[] kopf.objects         [INFO    [] Handler 'delete_fn' succeeded.
[2021-07-09 21:39:38,123[] kopf.objects         [INFO    [] [DELETED:ConfigMap[] Deleting file /tmp/default.configmap_fake-dashboard-2.default-dashboard.
[2021-07-09 21:39:38,123[] kopf.objects         [INFO    [] Handler 'delete_fn' succeeded.
[2021-07-09 21:39:38,169[] kopf.objects         [INFO    [] [DELETED:ConfigMap[] Deleting file /tmp/default.configmap_fake-dashboard-folder-annotation.default-annotation.
[2021-07-09 21:39:38,170[] kopf.objects         [INFO    [] Handler 'delete_fn' succeeded.

As you can see, the logs show everything being added/deleted as expected.

In your case, I see that you have:

Looking for resources ONLY in the ['cortex-dashboards'] namespaces

Are the configmaps you're creating/deleting in that namespace?

@gburton1
Copy link
Author

gburton1 commented Jul 9, 2021

Ah, the situation is a little different than I thought. The configmap is not actually being deleted and recreated. The k8s-sidecar-target-directory is simply being changed in the existing configmap. What behavior do you expect here? Should the dashboard previously mounted as a file at /tmp/dashboards/rms be removed after the new one is created at /tmp/dashboards/rms-test? Current behavior is that I end up with the old and new copies of the dashboard in Grafana.

Before:

kind: ConfigMap
metadata:
  annotations:
    k8s-sidecar-target-directory: /tmp/dashboards/rms

After:

kind: ConfigMap
metadata:
  annotations:
    k8s-sidecar-target-directory: /tmp/dashboards/rms-test

@OmegaVVeapon
Copy link
Owner

Ah, I see... that really is a different situation
I would say that it's working as expected.

The operator is fully stateless, it will create/delete/update files from where you tell it to.
In this case, you initially told it to create file X in /tmp/dashboards/rms, which it did.
Then, you updated the configmap to tell it to create file X in /tmp/dashboards/rms-test.
File X didn't already exist in that location, so it created a new one.

At that point, it has no memory that the configmap had a different k8s-sidecar-target-directory before...

@gburton1
Copy link
Author

gburton1 commented Jul 9, 2021

Yeah, thanks for the clarity here! I appreciate the response, and for your work on this side project to overcome the issue on the original!

@gburton1 gburton1 closed this as completed Jul 9, 2021
@gburton1
Copy link
Author

gburton1 commented Oct 8, 2021

Hi @OmegaVVeapon,

What behavior do you expect in this case? Let's say a config map defines two files. When the configmap is created, the sidecar mounts the two files into the desired location. If you delete the configmap, then the sidecar removes the two files from that location.

data:
  some-file-1.yaml: |
    fileContent: blah
    
  some-file-2.yaml: |
    fileContent: otherBlah
kind: ConfigMap
metadata:
  annotations:
    k8s-sidecar-target-directory: /tmp/rules/fake
  creationTimestamp: "2021-10-08T04:12:43Z"
  labels:
    argocd.argoproj.io/instance: grafana-telemetry-rules
    cortex_rules: "1"
  name: rules-auth-g5bff9d982
  namespace: cortex-rules
  resourceVersion: "83723913"
  uid: 6796a423-4be4-4539-9e31-41b12bc21e9c

But if you update the configmap content to remove one of the two files, we see that the sidecar does not remove that file from where it was mounted. Is that expected behavior?

data:
  some-file-1.yaml: |
    fileContent: blah
   
kind: ConfigMap
metadata:
  annotations:
    k8s-sidecar-target-directory: /tmp/rules/fake
  creationTimestamp: "2021-10-08T04:12:43Z"
  labels:
    argocd.argoproj.io/instance: grafana-telemetry-rules
    cortex_rules: "1"
  name: rules-auth-g5bff9d982
  namespace: cortex-rules
  resourceVersion: "83723913"
  uid: 6796a423-4be4-4539-9e31-41b12bc21e9c

@OmegaVVeapon
Copy link
Owner

OmegaVVeapon commented Oct 9, 2021

Unfortunately, I think this would fall under the statelessness problem...
When you update the file, the operator gets an UPDATE event and checks the configmap to see one file inside of it. Then creates it if it doesn't exist or updates/leaves it alone if it does.

However, it has no way to know that there were previously two files in the configmap.

I'll have to think if there's a way we can re-add some kind of "memory" for cases like these without falling into the issues that persistence generated before.

Just to give you some history, in previous iterations, we relied in the kopf persistence via the kopf.zalando.org/last-handled-configuration annotation.

With that annotation, you would be able to "diff" the previous run against the current one, and know that things changed.
However, that comes with the cost of storing the entire Configmap or Secret inside the annotation.
So you're basically doubling their sizes.
If you have small files, that's no issue, however, at my workplace, we use the operator for Grafana dashboards and some of those YAMLs are enormous.
When adding this annotation's x2 size multiplier, it would cause them to exceed the allowed etcd size limit for the ConfigMap which would print some scary warnings in the operator logs and cause the annotation addition to fail. Leaving you in a stateless scenario anyways (nothing to diff against).

It also created some issues with some k8s validation tools that didn't expect this magical annotation to appear in the Configmap after kubectl applying it when it didn't exist there initially, but I digress...

I'll have to see if there's something we can do without adding instability...

Is this a blocker for you?

@gburton1
Copy link
Author

gburton1 commented Oct 9, 2021

We are actually using the sidecar for Grafana dashboards too, and we also use it for Cortex alert rules (basically the same thing as Prometheus alert rules). Our users like the pattern of grouping similar alert rules and dashboards into the same config map, so the pattern is everywhere. We have a PR check on size, so that people know when they have hit the ConfigMap size limit.

We have also faced the annotation size issue when our CD tool (ArgoCD) was decorating these configmaps with a "last-configuration" annotation, so we have disabled those. Making the sidecar add an annotation comes with the big downsides you described. Our ArgoCD tool, a strict infra-as-code enforcer, will fight to revert any change like that to objects it manages. So I see why you removed state from the current iteration of sidecar.

So yes, this is a bit of a blocker. I need to find a good plan soon because users are noticing that rules/dashboards are not getting deleted.

What about adding a boolean configuration to this sidecar that defaults to FALSE, but when TRUE would tell the sidecar that during handling of UPDATE events, it should delete everything in k8s-sidecar-target-directory before doing the file creations specified in that configmap? This also seems like it could be tricky because more than one config map can have the same k8s-sidecar-target-directory.

@gburton1
Copy link
Author

We have a pretty decent workaround that we were already getting for another reason in our dashboards setup; in order to avoid the bulky last-applied annotation that you get with kubectl apply, we had instructed our Kustomize layer that manages configmaps to perform changes with a replace rather than an apply. The side effect of that is that you get a delete/recreate cycle which actually comes in handy to make the sidecar delete everything before recreating the new stuff.

We hadn't needed to worry about the bulky annotations with rules because they are smaller, but now we are applying the same configuration as dashboards anyway just to get this other benefit.

Now the only problem we are seeing is that there seems to be an occasional flakiness where 1 instance out of many identical ones that are all watching the same k8s resources will just stop getting events from a resource. So while the most instances have a flurry of delete and create activity, one instance can just sit there with no activity. We'll try upgrading to 1.3.5, but I'm not sure that's the issue.

@OmegaVVeapon
Copy link
Owner

Sounds good, glad you found that workaround!
Was thinking about this over the weekend and I think I may have a solution but I'll need to test it... will do so as soon as I can.

Regarding the occasional flakiness, I'm curious about your setup. You mention you have several instances monitoring the same resources, do you have multiple grafana instances (one per team, I'd assume) and segregate them with the LABEL_VALUE or something?

Do let me know if 1.3.5 solves your issues, There's a new release of kopf 1.35.0 that includes one more robustness check so I was planning of bumping to that anyways.

Since we haven't seen issues on our end for a fairly long time, my urgency on this was low but I'll gladly tag a release if you continue to experience issues.

@gburton1
Copy link
Author

gburton1 commented Oct 13, 2021

Have you ever heard of Cortex? It's basically a horizontally scalable version of Prometheus that implements all Prometheus APIs. One component of it is the Ruler, which evaluates alert rules. And it can scale across multiple instances and shard rules across them.
https://cortexmetrics.io/docs/guides/ruler-sharding/

So the sidecar on each instance watches all rules (in configmaps) and mounts all of them into a staging directory of each Ruler instance, and then the Ruler instances negotiate who will take which ones, and the runtime config has no duplication. It works great. And when the sidecar deletes one from the staging directory, the instances respond by removing it from the runtime config. In this bug case, I observed 1 instance out of three in which the sidecar never removed a file, even though the sidecar on the other two instances that were watching the same config removed it fine. And the logs reflect that. On the instance that did not remove it, there is no activity at all in the logs at the time that the other instances were responding to the configmap change events.

Now I'm fighting that new healthcheck that was added in this release. It has a port conflict with something else in my pod 😧

@gburton1
Copy link
Author

The new health check port is hard-coded in kopf-k8s-sidecar to 8080. Unfortunately, that port is deeply entrenched in all my stuff it looks like. It will take some time to move ports around without breaking stuff.

@OmegaVVeapon
Copy link
Owner

Huh! No, I hadn't heard of Cortex and now that I'm going over the docs, it sounds incredibly useful!
Also, apologies over that port conflict.
On hindsight, I should have offered a setting to make it configurable.
Should be a trivial patch release... I'll spin that up right now.

@OmegaVVeapon
Copy link
Owner

Opened #27

@OmegaVVeapon
Copy link
Owner

Github actions completed.
Hopefully 1.3.6 helps with the flakiness.

@OmegaVVeapon
Copy link
Owner

@gburton1 Hey, just checking up.
Did you continue to experience that behaviour where one or more operators didn't respond to changes in state?

@gburton1
Copy link
Author

gburton1 commented Nov 1, 2021

Everything has been better since we changed to an immutable model for config maps, so they are always guaranteed to have a delete/create cycle on every change. So I think the sidecar is working great!

We have one more little issue that I'm thinking about...the tool we use (Kustomize) to generate our configmaps that contain dashboards and alert rules has an annoying trait--when a config map changes content, the old config map is deleted and a new one is created, each with a unique suffix in the config map name. But unfortunately, the order is not guaranteed, so you get a delete/create operation within milliseconds of each other, and sometimes the create happens first, which makes the sidecar react in that order, mounting the new config map and then deleting the file that it just mounted a few milliseconds later! I need to ask in that project if I can get a guaranteed order somehow. My main workaround for now will be to make the sidecar reset more often via WATCH_SERVER_TIMEOUT, which would reduce the downtime from the default 10 minutes to something better.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants