-
Notifications
You must be signed in to change notification settings - Fork 461
Send alert when MCO can't safely apply updated Kubelet CA on nodes in paused pool #2802
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
Changes from all commits
2c44c12
35b4e81
89599be
dc42079
f023c19
f371cf0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,41 @@ | ||
| apiVersion: monitoring.coreos.com/v1 | ||
| kind: PrometheusRule | ||
| metadata: | ||
| name: machine-config-controller | ||
| namespace: openshift-machine-config-operator | ||
| labels: | ||
| k8s-app: machine-config-controller | ||
| annotations: | ||
| include.release.openshift.io/ibm-cloud-managed: "true" | ||
| include.release.openshift.io/self-managed-high-availability: "true" | ||
| include.release.openshift.io/single-node-developer: "true" | ||
| spec: | ||
| groups: | ||
| - name: mcc-paused-pool-kubelet-ca | ||
| rules: | ||
| - alert: MachineConfigControllerPausedPoolKubeletCA | ||
| expr: | | ||
| max by (namespace,pool) (last_over_time(machine_config_controller_paused_pool_kubelet_ca[5m])) > 0 | ||
| for: 60m | ||
| labels: | ||
| severity: warning | ||
| annotations: | ||
| summary: "Paused machine configuration pool '{{$labels.pool}}' is blocking a necessary certificate rotation and must be unpaused before the current kube-apiserver-to-kubelet-signer certificate expires on {{ $value | humanizeTimestamp }}." | ||
| description: "Machine config pools have a 'pause' feature, which allows config to be rendered, but prevents it from being rolled out to the nodes. This alert indicates that a certificate rotation has taken place, and the new kubelet-ca certificate bundle has been rendered into a machine config, but because the pool '{{$labels.pool}}' is paused, the config cannot be rolled out to the nodes in that pool. You will notice almost immediately that for nodes in pool '{{$labels.pool}}', pod logs will not be visible in the console and interactive commands (oc log, oc exec, oc debug, oc attach) will not work. You must unpause machine config pool '{{$labels.pool}}' to let the certificates through before the kube-apiserver-to-kubelet-signer certificate expires on {{ $value | humanizeTimestamp }} or this pool's nodes will cease to function properly." | ||
| runbook_url: https://github.com/openshift/blob/master/alerts/machine-config-operator/MachineConfigControllerPausedPoolKubeletCA.md | ||
| - alert: MachineConfigControllerPausedPoolKubeletCA | ||
| expr: | | ||
| max by (namespace,pool) (last_over_time(machine_config_controller_paused_pool_kubelet_ca[5m]) - time()) < (86400 * 14) AND max by (namespace,pool) (last_over_time(machine_config_controller_paused_pool_kubelet_ca[5m])) > 0 | ||
| for: 60m | ||
|
||
| labels: | ||
| severity: critical | ||
|
||
| annotations: | ||
| summary: "Paused machine configuration pool '{{$labels.pool}}' is blocking a necessary certificate rotation and must be unpaused before the current kube-apiserver-to-kubelet-signer certificate expires in {{ $value | humanizeDuration }}." | ||
| description: "Machine config pools have a 'pause' feature, which allows config to be rendered, but prevents it from being rolled out to the nodes. This alert indicates that a certificate rotation has taken place, and the new kubelet-ca certificate bundle has been rendered into a machine config, but because the pool '{{$labels.pool}}' is paused, the config cannot be rolled out to the nodes in that pool. You will notice almost immediately that for nodes in pool '{{$labels.pool}}', pod logs will not be visible in the console and interactive commands (oc log, oc exec, oc debug, oc attach) will not work. You must unpause machine config pool '{{$labels.pool}}' to let the certificates through before the kube-apiserver-to-kubelet-signer certificate expires. You have approximately {{ $value | humanizeDuration }} remaining before this happens and nodes in '{{$labels.pool}}' cease to function properly." | ||
| runbook_url: https://github.com/openshift/blob/master/alerts/machine-config-operator/MachineConfigControllerPausedPoolKubeletCA.md | ||
| --- | ||
| apiVersion: monitoring.coreos.com/v1 | ||
| kind: PrometheusRule | ||
| metadata: | ||
| name: machine-config-daemon | ||
| namespace: openshift-machine-config-operator | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,10 +27,42 @@ spec: | |
| cpu: 20m | ||
| memory: 50Mi | ||
| terminationMessagePolicy: FallbackToLogsOnError | ||
| - name: oauth-proxy | ||
|
||
| image: {{.Images.OauthProxy}} | ||
| ports: | ||
| - containerPort: 9001 | ||
| name: metrics | ||
| protocol: TCP | ||
| args: | ||
| - --https-address=:9001 | ||
| - --provider=openshift | ||
| - --openshift-service-account=machine-config-controller | ||
| - --upstream=http://127.0.0.1:8797 | ||
| - --tls-cert=/etc/tls/private/tls.crt | ||
| - --tls-key=/etc/tls/private/tls.key | ||
| - --cookie-secret-file=/etc/tls/cookie-secret/cookie-secret | ||
| - '--openshift-sar={"resource": "namespaces", "verb": "get"}' | ||
| - '--openshift-delegate-urls={"/": {"resource": "namespaces", "verb": "get"}}' | ||
| resources: | ||
| requests: | ||
| cpu: 20m | ||
| memory: 50Mi | ||
| volumeMounts: | ||
| - mountPath: /etc/tls/private | ||
| name: proxy-tls | ||
| - mountPath: /etc/tls/cookie-secret | ||
| name: cookie-secret | ||
| serviceAccountName: machine-config-controller | ||
| nodeSelector: | ||
| node-role.kubernetes.io/master: "" | ||
| priorityClassName: "system-cluster-critical" | ||
| volumes: | ||
| - name: proxy-tls | ||
| secret: | ||
| secretName: mcc-proxy-tls | ||
| - name: cookie-secret | ||
| secret: | ||
| secretName: cookie-secret | ||
| restartPolicy: Always | ||
| tolerations: | ||
| - key: node-role.kubernetes.io/master | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,12 +2,15 @@ package common | |
|
|
||
| import ( | ||
| "context" | ||
| "crypto/x509" | ||
| "encoding/pem" | ||
| "fmt" | ||
| "io/ioutil" | ||
| "net/url" | ||
| "os" | ||
| "reflect" | ||
| "sort" | ||
| "strings" | ||
|
|
||
| "github.com/clarketm/json" | ||
| fcctbase "github.com/coreos/fcct/base/v0_1" | ||
|
|
@@ -740,3 +743,50 @@ func GetIgnitionFileDataByPath(config *ign3types.Config, path string) ([]byte, e | |
| } | ||
| return nil, nil | ||
| } | ||
|
|
||
| // GetNewestCertificatesFromPEMBundle breaks a pem-encoded bundle out into its component certificates | ||
| func GetCertificatesFromPEMBundle(pemBytes []byte) ([]*x509.Certificate, error) { | ||
| var certs []*x509.Certificate | ||
| // There can be multiple certificates in the file | ||
| for { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. wait sorry - do we really want an infinite loop here? could we make this a little more explicit?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Future John will come back to this later to add clarifying comments which is fine with me. |
||
| // Decode a block to parse | ||
| block, rest := pem.Decode(pemBytes) | ||
| // Once we get no more blocks, we've read all the certs | ||
| if block == nil { | ||
| break | ||
| } | ||
| // Right now we just care about certificates, not keys | ||
| if block.Type == "CERTIFICATE" { | ||
| cert, err := x509.ParseCertificate(block.Bytes) | ||
| if err != nil { | ||
| // This isn't fatal, *this* cert could just be junk, next one could be okay | ||
| glog.Warningf("Failed to parse certificate: %v", err.Error()) | ||
| } else { | ||
| certs = append(certs, cert) | ||
| } | ||
| } | ||
| // Keep reading from where we left off | ||
| pemBytes = rest | ||
| } | ||
| return certs, nil | ||
| } | ||
|
|
||
| // GetLongestValidCertificate returns the latest-expiring certificate from a given list of certificates | ||
| // whose Subject.CommonName also matches any of the given common-name prefixes | ||
| func GetLongestValidCertificate(certificateList []*x509.Certificate, subjectPrefixes []string) *x509.Certificate { | ||
| // Sort is smallest-to-largest, so we're putting the cert with the latest expiry date at the top | ||
| sort.Slice(certificateList, func(i, j int) bool { | ||
| return certificateList[i].NotAfter.After(certificateList[j].NotAfter) | ||
| }) | ||
| // For each certificate in our list | ||
| for _, certificate := range certificateList { | ||
| // Check it against our prefixes | ||
| for _, prefix := range subjectPrefixes { | ||
| // If it matches, this is the latest-expiring one since it's closest to the "top" | ||
| if strings.HasPrefix(certificate.Subject.CommonName, prefix) { | ||
| return certificate | ||
| } | ||
| } | ||
| } | ||
| return nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| package common | ||
|
|
||
| import ( | ||
| "context" | ||
| "net/http" | ||
|
|
||
| "github.com/golang/glog" | ||
| "github.com/prometheus/client_golang/prometheus" | ||
| "github.com/prometheus/client_golang/prometheus/promhttp" | ||
| ) | ||
|
|
||
| const ( | ||
| // DefaultBindAddress is the port for the metrics listener | ||
| DefaultBindAddress = ":8797" | ||
|
||
| ) | ||
|
|
||
| var ( | ||
| // MachineConfigControllerPausedPoolKubeletCA logs when a certificate rotation is being held up by pause | ||
| MachineConfigControllerPausedPoolKubeletCA = prometheus.NewGaugeVec( | ||
| prometheus.GaugeOpts{ | ||
| Name: "machine_config_controller_paused_pool_kubelet_ca", | ||
| Help: "Set to the unix timestamp in utc of the current certificate expiry date if a certificate rotation is pending in specified paused pool", | ||
| }, []string{"pool"}) | ||
|
|
||
| metricsList = []prometheus.Collector{ | ||
| MachineConfigControllerPausedPoolKubeletCA, | ||
| } | ||
| ) | ||
|
|
||
| func RegisterMCCMetrics() error { | ||
| for _, metric := range metricsList { | ||
| err := prometheus.Register(metric) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| // StartMetricsListener is metrics listener via http on localhost | ||
| func StartMetricsListener(addr string, stopCh <-chan struct{}) { | ||
|
||
| if addr == "" { | ||
| addr = DefaultBindAddress | ||
| } | ||
|
|
||
| glog.Info("Registering Prometheus metrics") | ||
| if err := RegisterMCCMetrics(); err != nil { | ||
| glog.Errorf("unable to register metrics: %v", err) | ||
|
||
| // No sense in continuing starting the listener if this fails | ||
| return | ||
| } | ||
|
|
||
| glog.Infof("Starting metrics listener on %s", addr) | ||
| mux := http.NewServeMux() | ||
| mux.Handle("/metrics", promhttp.Handler()) | ||
| s := http.Server{Addr: addr, Handler: mux} | ||
|
|
||
| go func() { | ||
| if err := s.ListenAndServe(); err != nil && err != http.ErrServerClosed { | ||
| glog.Errorf("metrics listener exited with error: %v", err) | ||
| } | ||
| }() | ||
| <-stopCh | ||
| if err := s.Shutdown(context.Background()); err != http.ErrServerClosed { | ||
| glog.Errorf("error stopping metrics listener: %v", err) | ||
| } | ||
| } | ||
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.
Firing
warningalerts immediately seems like it's wound a bit too tightly. We can go a while before the lack of certificate rotation becomes a problem (significant fractions of a year?). So setting a large-ishforhere seems useful to avoid desensitizing folks by alerting every time they briefly pause. Of course, you could have someone leaving pools paused for the bulk of that time, and then very briefly unpause, have Prom happen to scrape, and then pause again, before the pool had time to rotate out many certs. Which is one reason "are we paused now, and have we been paused for a bit?" only a rough proxy for "are we at risk of certs expiring?". And that makes picking an appropriatefordifficult. But if we stick with the paused-pool angle on this, I'd expect at leastfor: 60mon this alert, based on our consistency guidelines.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.
Also, I think if alerting config allows to change severity then it should change to
criticalafter certain number of warning alert has been fired. This is to emphasize that unpausing pool is necessary to keep cluster healthy.