Skip to content

Commit 7324ae5

Browse files
authored
Merge pull request #2217 from mickeyzzc/pods-shards-support-no-node-scraping
fix: Support scraping pods metrics that is still in scheduling status and has no assigned node
2 parents 5041137 + 37ceaf4 commit 7324ae5

File tree

6 files changed

+205
-22
lines changed

6 files changed

+205
-22
lines changed

README.md

+17-1
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ The downside of using an auto-sharded setup comes from the rollout strategy supp
251251

252252
For pod metrics, they can be sharded per node with the following flag:
253253

254-
* `--node`
254+
* `--node=$(NODE_NAME)`
255255

256256
Each kube-state-metrics pod uses FieldSelector (spec.nodeName) to watch/list pod metrics only on the same node.
257257

@@ -277,6 +277,22 @@ spec:
277277
fieldPath: spec.nodeName
278278
```
279279

280+
To track metrics for unassigned pods, you need to add an additional deployment and set `--node=""`, as shown in the following example:
281+
282+
```
283+
apiVersion: apps/v1
284+
kind: Deployment
285+
spec:
286+
template:
287+
spec:
288+
containers:
289+
- image: registry.k8s.io/kube-state-metrics/kube-state-metrics:IMAGE_TAG
290+
name: kube-state-metrics
291+
args:
292+
- --resources=pods
293+
- --node=""
294+
```
295+
280296
Other metrics can be sharded via [Horizontal sharding](#horizontal-sharding).
281297

282298
### Setup
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
apiVersion: apps/v1
2+
kind: Deployment
3+
metadata:
4+
labels:
5+
app.kubernetes.io/component: exporter
6+
app.kubernetes.io/name: kube-state-metrics-pods
7+
app.kubernetes.io/version: 2.10.0
8+
name: kube-state-metrics-pods
9+
namespace: kube-system
10+
spec:
11+
replicas: 1
12+
selector:
13+
matchLabels:
14+
app.kubernetes.io/name: kube-state-metrics
15+
template:
16+
metadata:
17+
labels:
18+
app.kubernetes.io/component: exporter
19+
app.kubernetes.io/name: kube-state-metrics
20+
app.kubernetes.io/version: 2.10.0
21+
spec:
22+
automountServiceAccountToken: true
23+
containers:
24+
- args:
25+
- --resources=pods
26+
- --node=""
27+
image: registry.k8s.io/kube-state-metrics/kube-state-metrics:v2.10.0
28+
livenessProbe:
29+
httpGet:
30+
path: /healthz
31+
port: 8080
32+
initialDelaySeconds: 5
33+
timeoutSeconds: 5
34+
name: kube-state-metrics
35+
ports:
36+
- containerPort: 8080
37+
name: http-metrics
38+
- containerPort: 8081
39+
name: telemetry
40+
readinessProbe:
41+
httpGet:
42+
path: /
43+
port: 8081
44+
initialDelaySeconds: 5
45+
timeoutSeconds: 5
46+
securityContext:
47+
allowPrivilegeEscalation: false
48+
capabilities:
49+
drop:
50+
- ALL
51+
readOnlyRootFilesystem: true
52+
runAsNonRoot: true
53+
runAsUser: 65534
54+
seccompProfile:
55+
type: RuntimeDefault
56+
nodeSelector:
57+
kubernetes.io/os: linux
58+
serviceAccountName: kube-state-metrics

jsonnet/kube-state-metrics/kube-state-metrics.libsonnet

+24
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,30 @@
373373
},
374374
),
375375

376+
deploymentNoNodePods:
377+
local c = ksm.deployment.spec.template.spec.containers[0] {
378+
args: [
379+
'--resources=pods',
380+
'--node=""',
381+
],
382+
};
383+
local shardksmname = ksm.name + "-pods";
384+
std.mergePatch(ksm.deployment,
385+
{
386+
metadata: {
387+
name: shardksmname,
388+
labels: {'app.kubernetes.io/name': shardksmname}
389+
},
390+
spec: {
391+
template: {
392+
spec: {
393+
containers: [c],
394+
},
395+
},
396+
},
397+
},
398+
),
399+
376400
daemonset:
377401
// extending the default container from above
378402
local c0 = ksm.deployment.spec.template.spec.containers[0] {

pkg/options/options.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ func NewOptions() *Options {
7373
MetricAllowlist: MetricSet{},
7474
MetricDenylist: MetricSet{},
7575
MetricOptInList: MetricSet{},
76+
Node: NodeType{},
7677
AnnotationsAllowList: LabelsAllowList{},
7778
LabelsAllowList: LabelsAllowList{},
7879
}
@@ -136,7 +137,7 @@ func (o *Options) AddFlags(cmd *cobra.Command) {
136137
o.cmd.Flags().StringVar(&o.TLSConfig, "tls-config", "", "Path to the TLS configuration file")
137138
o.cmd.Flags().StringVar(&o.TelemetryHost, "telemetry-host", "::", `Host to expose kube-state-metrics self metrics on.`)
138139
o.cmd.Flags().StringVar(&o.Config, "config", "", "Path to the kube-state-metrics options config file")
139-
o.cmd.Flags().StringVar((*string)(&o.Node), "node", "", "Name of the node that contains the kube-state-metrics pod. Most likely it should be passed via the downward API. This is used for daemonset sharding. Only available for resources (pod metrics) that support spec.nodeName fieldSelector. This is experimental.")
140+
o.cmd.Flags().Var(&o.Node, "node", "Name of the node that contains the kube-state-metrics pod. Most likely it should be passed via the downward API. This is used for daemonset sharding. Only available for resources (pod metrics) that support spec.nodeName fieldSelector. This is experimental.")
140141
o.cmd.Flags().Var(&o.AnnotationsAllowList, "metric-annotations-allowlist", "Comma-separated list of Kubernetes annotations keys that will be used in the resource' labels metric. By default the annotations metrics are not exposed. To include them, provide a list of resource names in their plural form and Kubernetes annotation keys you would like to allow for them (Example: '=namespaces=[kubernetes.io/team,...],pods=[kubernetes.io/team],...)'. A single '*' can be provided per resource instead to allow any annotations, but that has severe performance implications (Example: '=pods=[*]').")
141142
o.cmd.Flags().Var(&o.LabelsAllowList, "metric-labels-allowlist", "Comma-separated list of additional Kubernetes label keys that will be used in the resource' labels metric. By default the labels metrics are not exposed. To include them, provide a list of resource names in their plural form and Kubernetes label keys you would like to allow for them (Example: '=namespaces=[k8s-label-1,k8s-label-n,...],pods=[app],...)'. A single '*' can be provided per resource instead to allow any labels, but that has severe performance implications (Example: '=pods=[*]'). Additionally, an asterisk (*) can be provided as a key, which will resolve to all resources, i.e., assuming '--resources=deployments,pods', '=*=[*]' will resolve to '=deployments=[*],pods=[*]'.")
142143
o.cmd.Flags().Var(&o.MetricAllowlist, "metric-allowlist", "Comma-separated list of metrics to be exposed. This list comprises of exact metric names and/or regex patterns. The allowlist and denylist are mutually exclusive.")
@@ -161,7 +162,7 @@ func (o *Options) Usage() {
161162
// Validate validates arguments
162163
func (o *Options) Validate() error {
163164
shardableResource := "pods"
164-
if o.Node == "" {
165+
if o.Node.String() == "" {
165166
return nil
166167
}
167168
for _, x := range o.Resources.AsSlice() {

pkg/options/types.go

+47-4
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package options
1818

1919
import (
2020
"errors"
21+
"regexp"
2122
"sort"
2223
"strings"
2324

@@ -105,14 +106,56 @@ func (r *ResourceSet) Type() string {
105106
}
106107

107108
// NodeType represents a nodeName to query from.
108-
type NodeType string
109+
type NodeType map[string]struct{}
110+
111+
// Set converts a comma-separated string of nodename into a slice and appends it to the NodeList
112+
func (n *NodeType) Set(value string) error {
113+
s := *n
114+
cols := strings.Split(value, ",")
115+
for _, col := range cols {
116+
col = strings.TrimSpace(col)
117+
if len(col) != 0 {
118+
s[col] = struct{}{}
119+
}
120+
}
121+
return nil
122+
}
123+
124+
// AsSlice returns the LabelsAllowList in the form of plain string slice.
125+
func (n NodeType) AsSlice() []string {
126+
cols := make([]string, 0, len(n))
127+
for col := range n {
128+
cols = append(cols, col)
129+
}
130+
return cols
131+
}
132+
133+
func (n NodeType) String() string {
134+
return strings.Join(n.AsSlice(), ",")
135+
}
136+
137+
// Type returns a descriptive string about the NodeList type.
138+
func (n *NodeType) Type() string {
139+
return "string"
140+
}
109141

110142
// GetNodeFieldSelector returns a nodename field selector.
111143
func (n *NodeType) GetNodeFieldSelector() string {
112-
if string(*n) != "" {
113-
return fields.OneTermEqualSelector("spec.nodeName", string(*n)).String()
144+
if nil == n || len(*n) == 0 {
145+
klog.InfoS("Using node type is nil")
146+
return EmptyFieldSelector()
114147
}
115-
return EmptyFieldSelector()
148+
pattern := "[^a-zA-Z0-9_,-]+"
149+
re := regexp.MustCompile(pattern)
150+
result := re.ReplaceAllString(n.String(), "")
151+
klog.InfoS("Using node type", "node", result)
152+
return fields.OneTermEqualSelector("spec.nodeName", result).String()
153+
154+
}
155+
156+
// NodeValue represents a nodeName to query from.
157+
type NodeValue interface {
158+
GetNodeFieldSelector() string
116159
}
117160

118161
// EmptyFieldSelector returns an empty field selector.

pkg/options/types_test.go

+56-15
Original file line numberDiff line numberDiff line change
@@ -162,13 +162,30 @@ func TestNodeFieldSelector(t *testing.T) {
162162
Wanted string
163163
}{
164164
{
165-
Desc: "empty node name",
166-
Node: "",
165+
Desc: "with node name",
167166
Wanted: "",
168167
},
169168
{
170169
Desc: "with node name",
171-
Node: "k8s-node-1",
170+
Node: nil,
171+
Wanted: "",
172+
},
173+
{
174+
Desc: "empty node name",
175+
Node: NodeType(
176+
map[string]struct{}{
177+
"": {},
178+
},
179+
),
180+
Wanted: "spec.nodeName=",
181+
},
182+
{
183+
Desc: "with node name",
184+
Node: NodeType(
185+
map[string]struct{}{
186+
"k8s-node-1": {},
187+
},
188+
),
172189
Wanted: "spec.nodeName=k8s-node-1",
173190
},
174191
}
@@ -194,43 +211,67 @@ func TestMergeFieldSelectors(t *testing.T) {
194211
Desc: "empty DeniedNamespaces",
195212
Namespaces: NamespaceList{"default", "kube-system"},
196213
DeniedNamespaces: NamespaceList{},
197-
Node: "",
198-
Wanted: "",
214+
Node: NodeType(
215+
map[string]struct{}{
216+
"": {},
217+
},
218+
),
219+
Wanted: "spec.nodeName=",
199220
},
200221
{
201222
Desc: "all DeniedNamespaces",
202223
Namespaces: DefaultNamespaces,
203224
DeniedNamespaces: NamespaceList{"some-system"},
204-
Node: "",
205-
Wanted: "metadata.namespace!=some-system",
225+
Node: NodeType(
226+
map[string]struct{}{
227+
"": {},
228+
},
229+
),
230+
Wanted: "metadata.namespace!=some-system,spec.nodeName=",
206231
},
207232
{
208233
Desc: "general case",
209234
Namespaces: DefaultNamespaces,
210235
DeniedNamespaces: NamespaceList{"case1-system", "case2-system"},
211-
Node: "",
212-
Wanted: "metadata.namespace!=case1-system,metadata.namespace!=case2-system",
236+
Node: NodeType(
237+
map[string]struct{}{
238+
"": {},
239+
},
240+
),
241+
Wanted: "metadata.namespace!=case1-system,metadata.namespace!=case2-system,spec.nodeName=",
213242
},
214243
{
215244
Desc: "empty DeniedNamespaces",
216245
Namespaces: NamespaceList{"default", "kube-system"},
217246
DeniedNamespaces: NamespaceList{},
218-
Node: "k8s-node-1",
219-
Wanted: "spec.nodeName=k8s-node-1",
247+
Node: NodeType(
248+
map[string]struct{}{
249+
"k8s-node-1": {},
250+
},
251+
),
252+
Wanted: "spec.nodeName=k8s-node-1",
220253
},
221254
{
222255
Desc: "all DeniedNamespaces",
223256
Namespaces: DefaultNamespaces,
224257
DeniedNamespaces: NamespaceList{"some-system"},
225-
Node: "k8s-node-1",
226-
Wanted: "metadata.namespace!=some-system,spec.nodeName=k8s-node-1",
258+
Node: NodeType(
259+
map[string]struct{}{
260+
"k8s-node-1": {},
261+
},
262+
),
263+
Wanted: "metadata.namespace!=some-system,spec.nodeName=k8s-node-1",
227264
},
228265
{
229266
Desc: "general case",
230267
Namespaces: DefaultNamespaces,
231268
DeniedNamespaces: NamespaceList{"case1-system", "case2-system"},
232-
Node: "k8s-node-1",
233-
Wanted: "metadata.namespace!=case1-system,metadata.namespace!=case2-system,spec.nodeName=k8s-node-1",
269+
Node: NodeType(
270+
map[string]struct{}{
271+
"k8s-node-1": {},
272+
},
273+
),
274+
Wanted: "metadata.namespace!=case1-system,metadata.namespace!=case2-system,spec.nodeName=k8s-node-1",
234275
},
235276
}
236277

0 commit comments

Comments
 (0)