Skip to content

Commit dc1687a

Browse files
authored
fix: Discover specs from namespaces user is allowed (#1098)
* fix: Discover specs from namespaces user is allowed If a user has limited access to read secrets and config maps from certain namespaces in a cluster, we'd need to gracefully fail when forbidden errors are caught. We'll log them and continue searching for specs in other namespaces.
1 parent 0f3f827 commit dc1687a

File tree

3 files changed

+217
-130
lines changed

3 files changed

+217
-130
lines changed

cmd/troubleshoot/cli/run.go

Lines changed: 165 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cli
22

33
import (
4+
"context"
45
"crypto/tls"
56
"encoding/json"
67
"fmt"
@@ -27,14 +28,16 @@ import (
2728
"github.com/replicatedhq/troubleshoot/pkg/supportbundle"
2829
"github.com/spf13/viper"
2930
spin "github.com/tj/go-spin"
31+
k8serrors "k8s.io/apimachinery/pkg/api/errors"
32+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3033
"k8s.io/apimachinery/pkg/labels"
3134
"k8s.io/client-go/kubernetes"
3235
"k8s.io/client-go/rest"
3336
"k8s.io/klog/v2"
3437
)
3538

3639
func runTroubleshoot(v *viper.Viper, arg []string) error {
37-
if v.GetBool("load-cluster-specs") == false && len(arg) < 1 {
40+
if !v.GetBool("load-cluster-specs") && len(arg) < 1 {
3841
return errors.New("flag load-cluster-specs must be set if no specs are provided on the command line")
3942
}
4043

@@ -107,105 +110,15 @@ func runTroubleshoot(v *viper.Viper, arg []string) error {
107110
}
108111

109112
if v.GetBool("load-cluster-specs") {
110-
labelSelector := strings.Join(v.GetStringSlice("selector"), ",")
111-
112-
parsedSelector, err := labels.Parse(labelSelector)
113-
if err != nil {
114-
return errors.Wrap(err, "unable to parse selector")
115-
}
116-
117-
config, err := k8sutil.GetRESTConfig()
118-
if err != nil {
119-
return errors.Wrap(err, "failed to convert kube flags to rest config")
120-
}
121-
122-
client, err := kubernetes.NewForConfig(config)
123-
if err != nil {
124-
return errors.Wrap(err, "failed to convert create k8s client")
125-
}
126-
127-
namespace := ""
128-
129-
if v.GetString("namespace") != "" {
130-
namespace = v.GetString("namespace")
131-
} else {
132-
IsNamespacedScopeRBAC, err := k8sutil.IsNamespacedScopeRBAC(client)
133-
if err != nil {
134-
return errors.Wrap(err, "failed to check if cluster is namespaced")
135-
}
136-
137-
if !IsNamespacedScopeRBAC {
138-
kubeconfig := k8sutil.GetKubeconfig()
139-
namespace, _, _ = kubeconfig.Namespace()
140-
}
141-
}
142-
143-
var bundlesFromCluster []string
144-
145-
// Search cluster for Troubleshoot objects in cluster
146-
bundlesFromSecrets, err := specs.LoadFromSecretMatchingLabel(client, parsedSelector.String(), namespace, specs.SupportBundleKey)
147-
if err != nil {
148-
klog.Errorf("failed to load support bundle spec from secrets: %s", err)
149-
}
150-
bundlesFromCluster = append(bundlesFromCluster, bundlesFromSecrets...)
151-
152-
bundlesFromConfigMaps, err := specs.LoadFromConfigMapMatchingLabel(client, parsedSelector.String(), namespace, specs.SupportBundleKey)
153-
if err != nil {
154-
klog.Errorf("failed to load support bundle spec from secrets: %s", err)
155-
}
156-
bundlesFromCluster = append(bundlesFromCluster, bundlesFromConfigMaps...)
157-
158-
for _, bundle := range bundlesFromCluster {
159-
multidocs := strings.Split(string(bundle), "\n---\n")
160-
parsedBundleFromSecret, err := supportbundle.ParseSupportBundleFromDoc([]byte(multidocs[0]))
161-
if err != nil {
162-
klog.Errorf("failed to parse support bundle spec: %s", err)
163-
continue
164-
}
165-
166-
if mainBundle == nil {
167-
mainBundle = parsedBundleFromSecret
168-
} else {
169-
mainBundle = supportbundle.ConcatSpec(mainBundle, parsedBundleFromSecret)
170-
}
171-
172-
parsedRedactors, err := supportbundle.ParseRedactorsFromDocs(multidocs)
173-
if err != nil {
174-
klog.Errorf("failed to parse redactors from doc: %s", err)
175-
continue
176-
}
177-
178-
additionalRedactors.Spec.Redactors = append(additionalRedactors.Spec.Redactors, parsedRedactors...)
179-
}
180-
181-
var redactorsFromCluster []string
182-
183-
// Search cluster for Troubleshoot objects in ConfigMaps
184-
redactorsFromSecrets, err := specs.LoadFromSecretMatchingLabel(client, parsedSelector.String(), namespace, specs.RedactorKey)
113+
sbFromCluster, redactorsFromCluster, err := loadClusterSpecs()
185114
if err != nil {
186-
klog.Errorf("failed to load redactor specs from config maps: %s", err)
115+
return err
187116
}
188-
redactorsFromCluster = append(redactorsFromCluster, redactorsFromSecrets...)
189-
190-
redactorsFromConfigMaps, err := specs.LoadFromConfigMapMatchingLabel(client, parsedSelector.String(), namespace, specs.RedactorKey)
191-
if err != nil {
192-
klog.Errorf("failed to load redactor specs from config maps: %s", err)
193-
}
194-
redactorsFromCluster = append(redactorsFromCluster, redactorsFromConfigMaps...)
195-
196-
for _, redactor := range redactorsFromCluster {
197-
multidocs := strings.Split(string(redactor), "\n---\n")
198-
parsedRedactors, err := supportbundle.ParseRedactorsFromDocs(multidocs)
199-
if err != nil {
200-
klog.Errorf("failed to parse redactors from doc: %s", err)
201-
}
202-
203-
additionalRedactors.Spec.Redactors = append(additionalRedactors.Spec.Redactors, parsedRedactors...)
204-
}
205-
206-
if mainBundle == nil {
117+
if sbFromCluster == nil {
207118
return errors.New("no specs found in cluster")
208119
}
120+
mainBundle = supportbundle.ConcatSpec(mainBundle, sbFromCluster)
121+
additionalRedactors.Spec.Redactors = append(additionalRedactors.Spec.Redactors, redactorsFromCluster.Spec.Redactors...)
209122
}
210123

211124
if mainBundle == nil {
@@ -346,6 +259,162 @@ the %s Admin Console to begin analysis.`
346259
return nil
347260
}
348261

262+
// loadClusterSpecs loads the support bundle and redactor specs from the cluster
263+
// based on troubleshoot.io/kind=support-bundle label selector. We search for secrets
264+
// and configmaps with the label selector and parse the data as a support bundle. If the
265+
// user does not have sufficient permissions to list & read secrets and configmaps from
266+
// all namespaces, we will fallback to trying each namespace individually, and eventually
267+
// default to the configured kubeconfig namespace.
268+
func loadClusterSpecs() (*troubleshootv1beta2.SupportBundle, *troubleshootv1beta2.Redactor, error) {
269+
var parsedBundle *troubleshootv1beta2.SupportBundle
270+
redactors := &troubleshootv1beta2.Redactor{}
271+
272+
v := viper.GetViper() // It's singleton, so we can use it anywhere
273+
274+
klog.Info("Discover troubleshoot specs from cluster")
275+
276+
labelSelector := strings.Join(v.GetStringSlice("selector"), ",")
277+
278+
parsedSelector, err := labels.Parse(labelSelector)
279+
if err != nil {
280+
return nil, nil, errors.Wrap(err, "unable to parse selector")
281+
}
282+
283+
config, err := k8sutil.GetRESTConfig()
284+
if err != nil {
285+
return nil, nil, errors.Wrap(err, "failed to convert kube flags to rest config")
286+
}
287+
288+
client, err := kubernetes.NewForConfig(config)
289+
if err != nil {
290+
return nil, nil, errors.Wrap(err, "failed to convert create k8s client")
291+
}
292+
293+
// List of namespaces we want to search for secrets and configmaps with support bundle specs
294+
namespaces := []string{}
295+
ctx := context.Background()
296+
297+
if v.GetString("namespace") != "" {
298+
// Just progress with the namespace provided
299+
namespaces = []string{v.GetString("namespace")}
300+
} else {
301+
// Check if I can read secrets and configmaps in all namespaces
302+
ican, err := k8sutil.CanIListAndGetAllSecretsAndConfigMaps(ctx, client)
303+
if err != nil {
304+
return nil, nil, errors.Wrap(err, "failed to check if I can read secrets and configmaps")
305+
}
306+
klog.V(1).Infof("Can I read any secrets and configmaps: %v", ican)
307+
308+
if ican {
309+
// I can read secrets and configmaps in all namespaces
310+
// No need to iterate over all namespaces
311+
namespaces = []string{""}
312+
} else {
313+
// Get list of all namespaces and try to find specs from each namespace
314+
nsList, err := client.CoreV1().Namespaces().List(ctx, metav1.ListOptions{})
315+
if err != nil {
316+
if k8serrors.IsForbidden(err) {
317+
kubeconfig := k8sutil.GetKubeconfig()
318+
ns, _, err := kubeconfig.Namespace()
319+
if err != nil {
320+
return nil, nil, errors.Wrap(err, "failed to get namespace from kubeconfig")
321+
}
322+
// If we are not allowed to list namespaces, just use the default namespace
323+
// configured in the kubeconfig
324+
namespaces = []string{ns}
325+
} else {
326+
return nil, nil, errors.Wrap(err, "failed to list namespaces")
327+
}
328+
}
329+
330+
for _, ns := range nsList.Items {
331+
namespaces = append(namespaces, ns.Name)
332+
}
333+
}
334+
}
335+
336+
var bundlesFromCluster []string
337+
338+
// Search cluster for support bundle specs
339+
klog.V(1).Infof("Search support bundle specs from [%q] namespaces using %q selector", strings.Join(namespaces, ", "), parsedSelector.String())
340+
for _, ns := range namespaces {
341+
bundlesFromSecrets, err := specs.LoadFromSecretMatchingLabel(client, parsedSelector.String(), ns, specs.SupportBundleKey)
342+
if err != nil {
343+
if !k8serrors.IsForbidden(err) {
344+
klog.Errorf("failed to load support bundle spec from secrets: %s", err)
345+
} else {
346+
klog.Warningf("Reading secrets from %q namespace forbidden", ns)
347+
}
348+
}
349+
bundlesFromCluster = append(bundlesFromCluster, bundlesFromSecrets...)
350+
351+
bundlesFromConfigMaps, err := specs.LoadFromConfigMapMatchingLabel(client, parsedSelector.String(), ns, specs.SupportBundleKey)
352+
if err != nil {
353+
if !k8serrors.IsForbidden(err) {
354+
klog.Errorf("failed to load support bundle spec from configmap: %s", err)
355+
} else {
356+
klog.Warningf("Reading configmaps from %q namespace forbidden", ns)
357+
}
358+
}
359+
bundlesFromCluster = append(bundlesFromCluster, bundlesFromConfigMaps...)
360+
}
361+
362+
for _, bundle := range bundlesFromCluster {
363+
multidocs := strings.Split(string(bundle), "\n---\n")
364+
parsedBundle, err = supportbundle.ParseSupportBundleFromDoc([]byte(multidocs[0]))
365+
if err != nil {
366+
klog.Errorf("failed to parse support bundle spec: %s", err)
367+
continue
368+
}
369+
370+
parsedRedactors, err := supportbundle.ParseRedactorsFromDocs(multidocs)
371+
if err != nil {
372+
klog.Errorf("failed to parse redactors from doc: %s", err)
373+
continue
374+
}
375+
376+
redactors.Spec.Redactors = append(redactors.Spec.Redactors, parsedRedactors...)
377+
}
378+
379+
var redactorsFromCluster []string
380+
381+
// Search cluster for redactor specs
382+
klog.V(1).Infof("Search redactor specs from [%q] namespaces using %q selector", strings.Join(namespaces, ", "), parsedSelector.String())
383+
for _, ns := range namespaces {
384+
redactorsFromSecrets, err := specs.LoadFromSecretMatchingLabel(client, parsedSelector.String(), ns, specs.RedactorKey)
385+
if err != nil {
386+
if !k8serrors.IsForbidden(err) {
387+
klog.Errorf("failed to load support bundle spec from secrets: %s", err)
388+
} else {
389+
klog.Warningf("Reading secrets from %q namespace forbidden", ns)
390+
}
391+
}
392+
redactorsFromCluster = append(redactorsFromCluster, redactorsFromSecrets...)
393+
394+
redactorsFromConfigMaps, err := specs.LoadFromConfigMapMatchingLabel(client, parsedSelector.String(), ns, specs.RedactorKey)
395+
if err != nil {
396+
if !k8serrors.IsForbidden(err) {
397+
klog.Errorf("failed to load support bundle spec from configmap: %s", err)
398+
} else {
399+
klog.Warningf("Reading configmaps from %q namespace forbidden", ns)
400+
}
401+
}
402+
redactorsFromCluster = append(redactorsFromCluster, redactorsFromConfigMaps...)
403+
}
404+
405+
for _, redactor := range redactorsFromCluster {
406+
multidocs := strings.Split(string(redactor), "\n---\n")
407+
parsedRedactors, err := supportbundle.ParseRedactorsFromDocs(multidocs)
408+
if err != nil {
409+
klog.Errorf("failed to parse redactors from doc: %s", err)
410+
}
411+
412+
redactors.Spec.Redactors = append(redactors.Spec.Redactors, parsedRedactors...)
413+
}
414+
415+
return parsedBundle, redactors, nil
416+
}
417+
349418
func parseTimeFlags(v *viper.Viper) (*time.Time, error) {
350419
var (
351420
sinceTime time.Time

pkg/k8sutil/auth.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package k8sutil
2+
3+
import (
4+
"context"
5+
6+
authorizationv1 "k8s.io/api/authorization/v1"
7+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
8+
"k8s.io/client-go/kubernetes"
9+
)
10+
11+
// CanIListAndGetAllSecretsAndConfigMaps checks if the current user can list and get secrets and configmaps
12+
// from all namespaces
13+
func CanIListAndGetAllSecretsAndConfigMaps(ctx context.Context, client kubernetes.Interface) (bool, error) {
14+
canis := []struct{ ns, verb, resource string }{
15+
{"", "get", "secrets"},
16+
{"", "get", "configmaps"},
17+
{"", "list", "secrets"},
18+
{"", "list", "configmaps"},
19+
}
20+
21+
for _, cani := range canis {
22+
ican, err := authCanI(ctx, client, cani.ns, cani.verb, cani.resource)
23+
if err != nil {
24+
return false, err
25+
}
26+
27+
if !ican {
28+
return false, nil
29+
}
30+
}
31+
32+
return true, nil
33+
}
34+
35+
func authCanI(ctx context.Context, client kubernetes.Interface, ns, verb, resource string) (bool, error) {
36+
sar := &authorizationv1.SelfSubjectAccessReview{
37+
Spec: authorizationv1.SelfSubjectAccessReviewSpec{
38+
ResourceAttributes: &authorizationv1.ResourceAttributes{
39+
Namespace: ns,
40+
Verb: verb,
41+
Resource: resource,
42+
},
43+
},
44+
}
45+
46+
resp, err := client.AuthorizationV1().SelfSubjectAccessReviews().Create(ctx, sar, metav1.CreateOptions{})
47+
if err != nil {
48+
return false, err
49+
}
50+
51+
return resp.Status.Allowed, nil
52+
}

pkg/k8sutil/namespace.go

Lines changed: 0 additions & 34 deletions
This file was deleted.

0 commit comments

Comments
 (0)