/
checkinject.go
293 lines (276 loc) · 9.28 KB
/
checkinject.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
// Copyright Istio Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package checkinject
import (
"context"
"fmt"
"io"
"reflect"
"sort"
"strings"
"github.com/fatih/color"
"github.com/spf13/cobra"
admitv1 "k8s.io/api/admissionregistration/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"istio.io/api/label"
"istio.io/istio/istioctl/pkg/cli"
"istio.io/istio/istioctl/pkg/completion"
"istio.io/istio/istioctl/pkg/util"
"istio.io/istio/istioctl/pkg/writer/table"
analyzer_util "istio.io/istio/pkg/config/analysis/analyzers/util"
)
var labelPairs string
func Cmd(ctx cli.Context) *cobra.Command {
cmd := &cobra.Command{
Use: "check-inject [<type>/]<name>[.<namespace>]",
Short: "Check the injection status or inject-ability of a given resource, explains why it is (or will be) injected or not",
Long: `
Checks associated resources of the given resource, and running webhooks to examine whether the pod can be or will be injected or not.`,
Example: ` # Check the injection status of a pod
istioctl experimental check-inject details-v1-fcff6c49c-kqnfk.test
# Check the injection status of a pod under a deployment
istioctl x check-inject deployment/details-v1
# Check the injection status of a pod under a deployment in namespace test
istioctl x check-inject deployment/details-v1 -n test
# Check the injection status of label pairs in a specific namespace before actual injection
istioctl x check-inject -n test -l app=helloworld,version=v1
`,
Args: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 && labelPairs == "" || len(args) > 1 {
cmd.Println(cmd.UsageString())
return fmt.Errorf("check-inject requires only [<resource-type>/]<resource-name>[.<namespace>], or specify labels flag")
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
kubeClient, err := ctx.CLIClient()
if err != nil {
return err
}
var podName, podNs string
var podLabels, nsLabels map[string]string
if len(args) == 1 {
podName, podNs, err = ctx.InferPodInfoFromTypedResource(args[0], ctx.Namespace())
if err != nil {
return err
}
pod, err := kubeClient.Kube().CoreV1().Pods(podNs).Get(context.TODO(), podName, metav1.GetOptions{})
if err != nil {
return err
}
ns, err := kubeClient.Kube().CoreV1().Namespaces().Get(context.TODO(), podNs, metav1.GetOptions{})
if err != nil {
return err
}
podLabels = pod.GetLabels()
nsLabels = ns.GetLabels()
} else {
namespace := ctx.NamespaceOrDefault(ctx.Namespace())
ns, err := kubeClient.Kube().CoreV1().Namespaces().Get(context.TODO(), namespace, metav1.GetOptions{})
if err != nil {
return err
}
ls, err := metav1.ParseToLabelSelector(labelPairs)
if err != nil {
return err
}
podLabels = ls.MatchLabels
nsLabels = ns.GetLabels()
}
whs, err := kubeClient.Kube().AdmissionregistrationV1().MutatingWebhookConfigurations().List(context.TODO(), metav1.ListOptions{})
if err != nil {
return err
}
checkResults := analyzeRunningWebhooks(whs.Items, podLabels, nsLabels)
return printCheckInjectorResults(cmd.OutOrStdout(), checkResults)
},
ValidArgsFunction: completion.ValidPodsNameArgs(ctx),
}
cmd.PersistentFlags().StringVarP(&labelPairs, "labels", "l", "",
"Check namespace and label pairs injection status, split multiple labels by commas")
return cmd
}
func printCheckInjectorResults(writer io.Writer, was []webhookAnalysis) error {
if len(was) == 0 {
fmt.Fprintf(writer, "ERROR: no Istio injection hooks present.\n")
return nil
}
w := table.NewStyleWriter(writer)
w.SetAddRowFunc(func(obj interface{}) table.Row {
wa := obj.(webhookAnalysis)
row := table.Row{
Cells: make([]table.Cell, 0),
}
row.Cells = append(row.Cells, table.NewCell(wa.Name), table.NewCell(wa.Revision))
if wa.Injected {
row.Cells = append(row.Cells, table.NewCell("✔", color.FgGreen))
} else {
row.Cells = append(row.Cells, table.NewCell("✘", color.FgRed))
}
row.Cells = append(row.Cells, table.NewCell(wa.Reason))
return row
})
w.AddHeader("WEBHOOK", "REVISION", "INJECTED", "REASON")
injectedTotal := 0
for _, ws := range was {
if ws.Injected {
injectedTotal++
}
w.AddRow(ws)
}
w.Flush()
if injectedTotal > 1 {
fmt.Fprintf(writer, "ERROR: multiple webhooks will inject, which can lead to errors")
}
return nil
}
type webhookAnalysis struct {
Name string
Revision string
Injected bool
Reason string
}
func analyzeRunningWebhooks(whs []admitv1.MutatingWebhookConfiguration, podLabels, nsLabels map[string]string) []webhookAnalysis {
results := make([]webhookAnalysis, 0)
for _, mwc := range whs {
if !isIstioWebhook(&mwc) {
continue
}
rev := extractRevision(&mwc)
reason, injected := analyzeWebhooksMatchStatus(mwc.Webhooks, podLabels, nsLabels)
results = append(results, webhookAnalysis{
Name: mwc.Name,
Revision: rev,
Injected: injected,
Reason: reason,
})
}
sort.Slice(results, func(i, j int) bool {
return results[i].Name < results[j].Name
})
return results
}
func analyzeWebhooksMatchStatus(whs []admitv1.MutatingWebhook, podLabels, nsLabels map[string]string) (reason string, injected bool) {
for _, wh := range whs {
nsMatched, nsLabel := extractMatchedSelectorInfo(wh.NamespaceSelector, nsLabels)
podMatched, podLabel := extractMatchedSelectorInfo(wh.ObjectSelector, podLabels)
if nsMatched && podMatched {
if nsLabel != "" && podLabel != "" {
return fmt.Sprintf("Namespace label %s matches, and pod label %s matches", nsLabel, podLabel), true
} else if nsLabel != "" {
outMsg := fmt.Sprintf("Namespace label %s matches", nsLabel)
if strings.Contains(nsLabel, "kubernetes.io/metadata.name") {
outMsg += " (Automatic injection is enabled in all namespaces)."
}
return outMsg, true
} else if podLabel != "" {
return fmt.Sprintf("Pod label %s matches", podLabel), true
}
} else if nsMatched {
for _, me := range wh.ObjectSelector.MatchExpressions {
switch me.Operator {
case metav1.LabelSelectorOpDoesNotExist:
v, ok := podLabels[me.Key]
if ok {
return fmt.Sprintf("Pod has %s=%s label, preventing injection", me.Key, v), false
}
case metav1.LabelSelectorOpNotIn:
v, ok := podLabels[me.Key]
if !ok {
continue
}
for _, nv := range me.Values {
if nv == v {
return fmt.Sprintf("Pod has %s=%s label, preventing injection", me.Key, v), false
}
}
}
}
} else if podMatched {
if v, ok := nsLabels[analyzer_util.InjectionLabelName]; ok {
if v != "enabled" {
return fmt.Sprintf("Namespace has %s=%s label, preventing injection",
analyzer_util.InjectionLabelName, v), false
}
}
}
}
noMatchingReason := func(whs []admitv1.MutatingWebhook) string {
nsMatchedLabels := make([]string, 0)
podMatchedLabels := make([]string, 0)
extractMatchLabels := func(selector *metav1.LabelSelector) []string {
if selector == nil {
return nil
}
labels := make([]string, 0)
for _, me := range selector.MatchExpressions {
if me.Operator != metav1.LabelSelectorOpIn {
continue
}
for _, v := range me.Values {
labels = append(labels, fmt.Sprintf("%s=%s", me.Key, v))
}
}
return labels
}
var isDeactivated bool
for _, wh := range whs {
if reflect.DeepEqual(wh.NamespaceSelector, util.NeverMatch) && reflect.DeepEqual(wh.ObjectSelector, util.NeverMatch) {
isDeactivated = true
}
nsMatchedLabels = append(nsMatchedLabels, extractMatchLabels(wh.NamespaceSelector)...)
podMatchedLabels = append(podMatchedLabels, extractMatchLabels(wh.ObjectSelector)...)
}
if isDeactivated {
return "The injection webhook is deactivated, and will never match labels."
}
return fmt.Sprintf("No matching namespace labels (%s) "+
"or pod labels (%s)", strings.Join(nsMatchedLabels, ", "), strings.Join(podMatchedLabels, ", "))
}
return noMatchingReason(whs), false
}
func extractMatchedSelectorInfo(ls *metav1.LabelSelector, objLabels map[string]string) (matched bool, injLabel string) {
if ls == nil {
return true, ""
}
selector, err := metav1.LabelSelectorAsSelector(ls)
if err != nil {
return false, ""
}
matched = selector.Matches(labels.Set(objLabels))
if !matched {
return matched, ""
}
for _, me := range ls.MatchExpressions {
switch me.Operator {
case metav1.LabelSelectorOpIn, metav1.LabelSelectorOpNotIn:
if v, exist := objLabels[me.Key]; exist {
return matched, fmt.Sprintf("%s=%s", me.Key, v)
}
}
}
return matched, ""
}
func extractRevision(wh *admitv1.MutatingWebhookConfiguration) string {
return wh.GetLabels()[label.IoIstioRev.Name]
}
func isIstioWebhook(wh *admitv1.MutatingWebhookConfiguration) bool {
for _, w := range wh.Webhooks {
if strings.HasSuffix(w.Name, "istio.io") {
return true
}
}
return false
}