chore: make spec loaders internal APIs (#1313)

* chore: make specs an internal package

* Some minor improvements

* Use LoadClusterSpecs in support bundle implementation

* Remove change accidentally committed

* Use LoadFromCLIArgs in preflight CLI implementation

* Update comment

* Fix edge case where the label selector is an empty string

* Fix failing test
This commit is contained in:
Evans Mungai
2023-08-30 14:02:30 +01:00
committed by GitHub
parent b7d5a9876c
commit ff03bfa9cd
17 changed files with 715 additions and 456 deletions

View File

@@ -0,0 +1,51 @@
package specs
import (
"context"
"github.com/pkg/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/klog/v2"
)
func LoadFromConfigMap(ctx context.Context, client kubernetes.Interface, ns string, name string, key string) ([]byte, error) {
foundConfigMap, err := client.CoreV1().ConfigMaps(ns).Get(ctx, name, metav1.GetOptions{})
if err != nil {
return nil, errors.Wrap(err, "failed to get configmap")
}
spec, ok := foundConfigMap.Data[key]
if !ok {
return nil, errors.Errorf("spec not found in configmap %s", name)
}
klog.V(1).InfoS("Loaded spec from config map", "name",
foundConfigMap.Name, "namespace", foundConfigMap.Namespace, "data key", key,
)
return []byte(spec), nil
}
func LoadFromConfigMapMatchingLabel(ctx context.Context, client kubernetes.Interface, label string, ns string, key string) ([]string, error) {
var configMapMatchingKey []string
configMaps, err := client.CoreV1().ConfigMaps(ns).List(ctx, metav1.ListOptions{LabelSelector: label})
if err != nil {
return nil, errors.Wrap(err, "failed to search for configmaps in the cluster")
}
for _, configMap := range configMaps.Items {
spec, ok := configMap.Data[key]
if !ok {
continue
}
klog.V(1).InfoS("Loaded spec from config map", "name", configMap.Name,
"namespace", configMap.Namespace, "data key", key, "label selector", label,
)
configMapMatchingKey = append(configMapMatchingKey, string(spec))
}
return configMapMatchingKey, nil
}

View File

@@ -0,0 +1,561 @@
package specs
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
testclient "k8s.io/client-go/kubernetes/fake"
)
func Test_LoadFromConfigMapMatchingLabel(t *testing.T) {
tests := []struct {
name string
supportBundleConfigMaps []corev1.ConfigMap
want []string
wantErr bool
}{
{
name: "support bundle configmap with matching label and key",
supportBundleConfigMaps: []corev1.ConfigMap{
{
ObjectMeta: metav1.ObjectMeta{
Name: "configmap",
Namespace: "default",
Labels: map[string]string{
"troubleshoot.io/kind": "support-bundle",
},
},
Data: map[string]string{
"support-bundle-spec": `apiVersion: troubleshoot.sh/v1beta2
kind: SupportBundle
metadata:
name: test
spec:
collectors:
- runPod:
name: "run-ping"
namespace: default
podSpec:
containers:
- name: run-ping
image: busybox:1
command: ["ping"]
args: ["-w", "5", "www.google.com"]`,
},
},
},
want: []string{
`apiVersion: troubleshoot.sh/v1beta2
kind: SupportBundle
metadata:
name: test
spec:
collectors:
- runPod:
name: "run-ping"
namespace: default
podSpec:
containers:
- name: run-ping
image: busybox:1
command: ["ping"]
args: ["-w", "5", "www.google.com"]`,
},
},
{
name: "mutlidoc support bundle secret with matching label and key",
supportBundleConfigMaps: []corev1.ConfigMap{
{
ObjectMeta: metav1.ObjectMeta{
Name: "configmap",
Namespace: "default",
Labels: map[string]string{
"troubleshoot.io/kind": "support-bundle",
},
},
Data: map[string]string{
"support-bundle-spec": `apiVersion: troubleshoot.sh/v1beta2
kind: SupportBundle
metadata:
name: test
spec:
collectors:
- runPod:
name: "run-ping"
namespace: default
podSpec:
containers:
- name: run-ping
image: busybox:1
command: ["ping"]
args: ["-w", "5", "www.google.com"]
---
apiVersion: troubleshoot.sh/v1beta2
kind: Redactor
metadata:
name: Usernames
spec:
redactors:
- name: Redact usernames in multiline JSON
removals:
regex:
- selector: '(?i)"name": *".*user[^\"]*"'
redactor: '(?i)("value": *")(?P<mask>.*[^\"]*)(")'`,
},
},
},
want: []string{
`apiVersion: troubleshoot.sh/v1beta2
kind: SupportBundle
metadata:
name: test
spec:
collectors:
- runPod:
name: "run-ping"
namespace: default
podSpec:
containers:
- name: run-ping
image: busybox:1
command: ["ping"]
args: ["-w", "5", "www.google.com"]
---
apiVersion: troubleshoot.sh/v1beta2
kind: Redactor
metadata:
name: Usernames
spec:
redactors:
- name: Redact usernames in multiline JSON
removals:
regex:
- selector: '(?i)"name": *".*user[^\"]*"'
redactor: '(?i)("value": *")(?P<mask>.*[^\"]*)(")'`,
},
},
{
name: "support bundle configmap with missing label",
supportBundleConfigMaps: []corev1.ConfigMap{
{
ObjectMeta: metav1.ObjectMeta{
Name: "configap",
Namespace: "default",
},
Data: map[string]string{
"support-bundle-spec": `apiVersion: troubleshoot.sh/v1beta2
kind: SupportBundle
metadata:
name: test
spec:
collectors:
- data:
name: static/data.txt
data: |
static data`,
},
},
},
want: []string(nil),
},
{
name: "support bundle configmap with matching label but wrong key",
supportBundleConfigMaps: []corev1.ConfigMap{
{
ObjectMeta: metav1.ObjectMeta{
Name: "configmap",
Namespace: "default",
},
Data: map[string]string{
"support-bundle-specc": `apiVersion: troubleshoot.sh/v1beta2
kind: SupportBundle
metadata:
name: test
spec:
collectors:
- data:
name: static/data.txt
data: |
static data`,
},
},
},
want: []string(nil),
},
{
name: "multiple support bundle configmaps in the same namespace with matching label and key",
supportBundleConfigMaps: []corev1.ConfigMap{
{
ObjectMeta: metav1.ObjectMeta{
Name: "configmap",
Namespace: "default",
Labels: map[string]string{
"troubleshoot.io/kind": "support-bundle",
},
},
Data: map[string]string{
"support-bundle-spec": `apiVersion: troubleshoot.sh/v1beta2
kind: SupportBundle
metadata:
name: cluster-info
spec:
collectors:
- clusterInfo: {}`,
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "configmap-2",
Namespace: "default",
Labels: map[string]string{
"troubleshoot.io/kind": "support-bundle",
},
},
Data: map[string]string{
"support-bundle-spec": `apiVersion: troubleshoot.sh/v1beta2
kind: SupportBundle
metadata:
name: cluster-resources
spec:
collectors:
- clusterResources: {}`,
},
},
},
want: []string{
`apiVersion: troubleshoot.sh/v1beta2
kind: SupportBundle
metadata:
name: cluster-info
spec:
collectors:
- clusterInfo: {}`,
`apiVersion: troubleshoot.sh/v1beta2
kind: SupportBundle
metadata:
name: cluster-resources
spec:
collectors:
- clusterResources: {}`,
},
},
{
name: "multiple support bundle configmaps in different namespaces with matching label and key",
supportBundleConfigMaps: []corev1.ConfigMap{
{
ObjectMeta: metav1.ObjectMeta{
Name: "configmap",
Namespace: "some-namespace",
Labels: map[string]string{
"troubleshoot.io/kind": "support-bundle",
},
},
Data: map[string]string{
"support-bundle-spec": `apiVersion: troubleshoot.sh/v1beta2
kind: SupportBundle
metadata:
name: cluster-info
spec:
collectors:
- clusterInfo: {}`,
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "configmap-2",
Namespace: "some-namespace-2",
Labels: map[string]string{
"troubleshoot.io/kind": "support-bundle",
},
},
Data: map[string]string{
"support-bundle-spec": `apiVersion: troubleshoot.sh/v1beta2
kind: SupportBundle
metadata:
name: cluster-resources
spec:
collectors:
- clusterResources: {}`,
},
},
},
want: []string{
`apiVersion: troubleshoot.sh/v1beta2
kind: SupportBundle
metadata:
name: cluster-info
spec:
collectors:
- clusterInfo: {}`,
`apiVersion: troubleshoot.sh/v1beta2
kind: SupportBundle
metadata:
name: cluster-resources
spec:
collectors:
- clusterResources: {}`,
},
},
{
name: "multiple support bundle configmaps in different namespaces but only one with correct label and key",
supportBundleConfigMaps: []corev1.ConfigMap{
{
ObjectMeta: metav1.ObjectMeta{
Name: "configmap",
Namespace: "some-namespace",
Labels: map[string]string{
"troubleshoot.io/kind": "support-bundle-wrong",
},
},
Data: map[string]string{
"support-bundle-spec-wrong": `apiVersion: troubleshoot.sh/v1beta2
kind: SupportBundle
metadata:
name: cluster-info
spec:
collectors:
- clusterInfo: {}`,
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "configmap-2",
Namespace: "some-namespace-2",
Labels: map[string]string{
"troubleshoot.io/kind": "support-bundle",
},
},
Data: map[string]string{
"support-bundle-spec": `apiVersion: troubleshoot.sh/v1beta2
kind: SupportBundle
metadata:
name: cluster-resources
spec:
collectors:
- clusterResources: {}`,
},
},
},
want: []string{
`apiVersion: troubleshoot.sh/v1beta2
kind: SupportBundle
metadata:
name: cluster-resources
spec:
collectors:
- clusterResources: {}`,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
client := testclient.NewSimpleClientset()
for _, configmap := range tt.supportBundleConfigMaps {
_, err := client.CoreV1().ConfigMaps(configmap.Namespace).Create(ctx, &configmap, metav1.CreateOptions{})
require.NoError(t, err)
}
got, err := LoadFromConfigMapMatchingLabel(ctx, client, "troubleshoot.io/kind=support-bundle", "", "support-bundle-spec")
if tt.wantErr {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, tt.want, got)
}
})
}
}
func TestUserProvidedNamespace_LoadFromConfigMapMatchingLabel(t *testing.T) {
tests := []struct {
name string
supportBundleConfigMaps []corev1.ConfigMap
want []string
wantErr bool
}{
{
name: "support bundle configmap with matching label and key in user provided namespace",
supportBundleConfigMaps: []corev1.ConfigMap{
{
ObjectMeta: metav1.ObjectMeta{
Name: "configmap",
Namespace: "some-namespace",
Labels: map[string]string{
"troubleshoot.io/kind": "support-bundle",
},
},
Data: map[string]string{
"support-bundle-spec": `apiVersion: troubleshoot.sh/v1beta2
kind: SupportBundle
metadata:
name: test
spec:
collectors:
- data:
name: static/data.txt
data: |
static data`,
},
},
},
want: []string{
`apiVersion: troubleshoot.sh/v1beta2
kind: SupportBundle
metadata:
name: test
spec:
collectors:
- data:
name: static/data.txt
data: |
static data`,
},
},
{
name: "support bundle configmap with matching label and key outside of user provided namespace",
supportBundleConfigMaps: []corev1.ConfigMap{
{
ObjectMeta: metav1.ObjectMeta{
Name: "configmap",
Namespace: "not-your-namespace",
Labels: map[string]string{
"troubleshoot.io/kind": "support-bundle",
},
},
Data: map[string]string{
"support-bundle-spec": `apiVersion: troubleshoot.sh/v1beta2
kind: SupportBundle
metadata:
name: test
spec:
collectors:
- data:
name: static/data.txt
data: |
static data`,
},
},
},
want: []string(nil),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
client := testclient.NewSimpleClientset()
for _, configmap := range tt.supportBundleConfigMaps {
_, err := client.CoreV1().ConfigMaps(configmap.Namespace).Create(ctx, &configmap, metav1.CreateOptions{})
require.NoError(t, err)
}
got, err := LoadFromConfigMapMatchingLabel(ctx, client, "troubleshoot.io/kind=support-bundle", "some-namespace", "support-bundle-spec")
if tt.wantErr {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, tt.want, got)
}
})
}
}
func TestRedactors_LoadFromConfigMapMatchingLabel(t *testing.T) {
tests := []struct {
name string
supportBundleConfigMaps []corev1.ConfigMap
want []string
wantErr bool
}{
{
name: "redactor configmap with matching label and key",
supportBundleConfigMaps: []corev1.ConfigMap{
{
ObjectMeta: metav1.ObjectMeta{
Name: "configmap",
Namespace: "default",
Labels: map[string]string{
"troubleshoot.io/kind": "support-bundle",
},
},
Data: map[string]string{
"redactor-spec": `apiVersion: troubleshoot.sh/v1beta2
kind: Redactor
metadata:
name: redact-some-content
spec:
redactors:
- name: replace some-content
fileSelector:
file: result.json
removals:
values:
- some-content`,
},
},
},
want: []string{
`apiVersion: troubleshoot.sh/v1beta2
kind: Redactor
metadata:
name: redact-some-content
spec:
redactors:
- name: replace some-content
fileSelector:
file: result.json
removals:
values:
- some-content`,
},
},
{
name: "redactor configmap with matching label but wrong key",
supportBundleConfigMaps: []corev1.ConfigMap{
{
ObjectMeta: metav1.ObjectMeta{
Name: "configmap",
Namespace: "default",
Labels: map[string]string{
"troubleshoot.io/kind": "support-bundle",
},
},
Data: map[string]string{
"redactor-spec-wrong": `apiVersion: troubleshoot.sh/v1beta2
kind: Redactor
metadata:
name: redact-some-content
spec:
redactors:
- name: replace some-content
fileSelector:
file: result.json
removals:
values:
- some-content`,
},
},
},
want: []string(nil),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
client := testclient.NewSimpleClientset()
for _, configmap := range tt.supportBundleConfigMaps {
_, err := client.CoreV1().ConfigMaps(configmap.Namespace).Create(ctx, &configmap, metav1.CreateOptions{})
require.NoError(t, err)
}
got, err := LoadFromConfigMapMatchingLabel(ctx, client, "troubleshoot.io/kind=support-bundle", "", "redactor-spec")
if tt.wantErr {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, tt.want, got)
}
})
}
}

50
internal/specs/secrets.go Normal file
View File

@@ -0,0 +1,50 @@
package specs
import (
"context"
"github.com/pkg/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/klog/v2"
)
func LoadFromSecret(ctx context.Context, client kubernetes.Interface, ns string, name string, key string) ([]byte, error) {
foundSecret, err := client.CoreV1().Secrets(ns).Get(ctx, name, metav1.GetOptions{})
if err != nil {
return nil, errors.Wrap(err, "failed to get secret")
}
spec, ok := foundSecret.Data[key]
if !ok {
return nil, errors.Errorf("spec not found in secret %s", name)
}
klog.V(1).InfoS("Loaded spec from secret", "name",
foundSecret.Name, "namespace", foundSecret.Namespace, "data key", key,
)
return spec, nil
}
func LoadFromSecretMatchingLabel(ctx context.Context, client kubernetes.Interface, label string, ns string, key string) ([]string, error) {
var secretsMatchingKey []string
secrets, err := client.CoreV1().Secrets(ns).List(ctx, metav1.ListOptions{LabelSelector: label})
if err != nil {
return nil, errors.Wrap(err, "failed to search for secrets in the cluster")
}
for _, secret := range secrets.Items {
spec, ok := secret.Data[key]
if !ok {
continue
}
klog.V(1).InfoS("Loaded spec from secret", "name", secret.Name,
"namespace", secret.Namespace, "data key", key, "label selector", label,
)
secretsMatchingKey = append(secretsMatchingKey, string(spec))
}
return secretsMatchingKey, nil
}

View File

@@ -0,0 +1,561 @@
package specs
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
testclient "k8s.io/client-go/kubernetes/fake"
)
func Test_LoadFromSecretMatchingLabel(t *testing.T) {
tests := []struct {
name string
supportBundleSecrets []corev1.Secret
want []string
wantErr bool
}{
{
name: "support bundle secret with matching label and key",
supportBundleSecrets: []corev1.Secret{
{
ObjectMeta: metav1.ObjectMeta{
Name: "secret",
Namespace: "default",
Labels: map[string]string{
"troubleshoot.io/kind": "support-bundle",
},
},
Data: map[string][]byte{
"support-bundle-spec": []byte(`apiVersion: troubleshoot.sh/v1beta2
kind: SupportBundle
metadata:
name: test
spec:
collectors:
- runPod:
name: "run-ping"
namespace: default
podSpec:
containers:
- name: run-ping
image: busybox:1
command: ["ping"]
args: ["-w", "5", "www.google.com"]`),
},
},
},
want: []string{
`apiVersion: troubleshoot.sh/v1beta2
kind: SupportBundle
metadata:
name: test
spec:
collectors:
- runPod:
name: "run-ping"
namespace: default
podSpec:
containers:
- name: run-ping
image: busybox:1
command: ["ping"]
args: ["-w", "5", "www.google.com"]`,
},
},
{
name: "mutlidoc support bundle secret with matching label and key",
supportBundleSecrets: []corev1.Secret{
{
ObjectMeta: metav1.ObjectMeta{
Name: "secret",
Namespace: "default",
Labels: map[string]string{
"troubleshoot.io/kind": "support-bundle",
},
},
Data: map[string][]byte{
"support-bundle-spec": []byte(`apiVersion: troubleshoot.sh/v1beta2
kind: SupportBundle
metadata:
name: test
spec:
collectors:
- runPod:
name: "run-ping"
namespace: default
podSpec:
containers:
- name: run-ping
image: busybox:1
command: ["ping"]
args: ["-w", "5", "www.google.com"]
---
apiVersion: troubleshoot.sh/v1beta2
kind: Redactor
metadata:
name: Usernames
spec:
redactors:
- name: Redact usernames in multiline JSON
removals:
regex:
- selector: '(?i)"name": *".*user[^\"]*"'
redactor: '(?i)("value": *")(?P<mask>.*[^\"]*)(")'`),
},
},
},
want: []string{
`apiVersion: troubleshoot.sh/v1beta2
kind: SupportBundle
metadata:
name: test
spec:
collectors:
- runPod:
name: "run-ping"
namespace: default
podSpec:
containers:
- name: run-ping
image: busybox:1
command: ["ping"]
args: ["-w", "5", "www.google.com"]
---
apiVersion: troubleshoot.sh/v1beta2
kind: Redactor
metadata:
name: Usernames
spec:
redactors:
- name: Redact usernames in multiline JSON
removals:
regex:
- selector: '(?i)"name": *".*user[^\"]*"'
redactor: '(?i)("value": *")(?P<mask>.*[^\"]*)(")'`,
},
},
{
name: "support bundle secret with missing label",
supportBundleSecrets: []corev1.Secret{
{
ObjectMeta: metav1.ObjectMeta{
Name: "secret",
Namespace: "default",
},
Data: map[string][]byte{
"support-bundle-spec": []byte(`apiVersion: troubleshoot.sh/v1beta2
kind: SupportBundle
metadata:
name: test
spec:
collectors:
- data:
name: static/data.txt
data: |
static data`),
},
},
},
want: []string(nil),
},
{
name: "support bundle secret with matching label but wrong key",
supportBundleSecrets: []corev1.Secret{
{
ObjectMeta: metav1.ObjectMeta{
Name: "secret",
Namespace: "default",
},
Data: map[string][]byte{
"support-bundle-specc": []byte(`apiVersion: troubleshoot.sh/v1beta2
kind: SupportBundle
metadata:
name: test
spec:
collectors:
- data:
name: static/data.txt
data: |
static data`),
},
},
},
want: []string(nil),
},
{
name: "multiple support bundle secrets in the same namespace with matching label and key",
supportBundleSecrets: []corev1.Secret{
{
ObjectMeta: metav1.ObjectMeta{
Name: "secret",
Namespace: "default",
Labels: map[string]string{
"troubleshoot.io/kind": "support-bundle",
},
},
Data: map[string][]byte{
"support-bundle-spec": []byte(`apiVersion: troubleshoot.sh/v1beta2
kind: SupportBundle
metadata:
name: cluster-info
spec:
collectors:
- clusterInfo: {}`),
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "secret-2",
Namespace: "default",
Labels: map[string]string{
"troubleshoot.io/kind": "support-bundle",
},
},
Data: map[string][]byte{
"support-bundle-spec": []byte(`apiVersion: troubleshoot.sh/v1beta2
kind: SupportBundle
metadata:
name: cluster-resources
spec:
collectors:
- clusterResources: {}`),
},
},
},
want: []string{
`apiVersion: troubleshoot.sh/v1beta2
kind: SupportBundle
metadata:
name: cluster-info
spec:
collectors:
- clusterInfo: {}`,
`apiVersion: troubleshoot.sh/v1beta2
kind: SupportBundle
metadata:
name: cluster-resources
spec:
collectors:
- clusterResources: {}`,
},
},
{
name: "multiple support bundle secrets in different namespaces with matching label and key",
supportBundleSecrets: []corev1.Secret{
{
ObjectMeta: metav1.ObjectMeta{
Name: "secret",
Namespace: "some-namespace",
Labels: map[string]string{
"troubleshoot.io/kind": "support-bundle",
},
},
Data: map[string][]byte{
"support-bundle-spec": []byte(`apiVersion: troubleshoot.sh/v1beta2
kind: SupportBundle
metadata:
name: cluster-info
spec:
collectors:
- clusterInfo: {}`),
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "secret-2",
Namespace: "some-namespace-2",
Labels: map[string]string{
"troubleshoot.io/kind": "support-bundle",
},
},
Data: map[string][]byte{
"support-bundle-spec": []byte(`apiVersion: troubleshoot.sh/v1beta2
kind: SupportBundle
metadata:
name: cluster-resources
spec:
collectors:
- clusterResources: {}`),
},
},
},
want: []string{
`apiVersion: troubleshoot.sh/v1beta2
kind: SupportBundle
metadata:
name: cluster-info
spec:
collectors:
- clusterInfo: {}`,
`apiVersion: troubleshoot.sh/v1beta2
kind: SupportBundle
metadata:
name: cluster-resources
spec:
collectors:
- clusterResources: {}`,
},
},
{
name: "multiple support bundle secrets in different namespaces but only one with correct label and key",
supportBundleSecrets: []corev1.Secret{
{
ObjectMeta: metav1.ObjectMeta{
Name: "secret",
Namespace: "some-namespace",
Labels: map[string]string{
"troubleshoot.io/kind": "support-bundle-wrong",
},
},
Data: map[string][]byte{
"support-bundle-spec-wrong": []byte(`apiVersion: troubleshoot.sh/v1beta2
kind: SupportBundle
metadata:
name: cluster-info
spec:
collectors:
- clusterInfo: {}`),
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "secret-2",
Namespace: "some-namespace-2",
Labels: map[string]string{
"troubleshoot.io/kind": "support-bundle",
},
},
Data: map[string][]byte{
"support-bundle-spec": []byte(`apiVersion: troubleshoot.sh/v1beta2
kind: SupportBundle
metadata:
name: cluster-resources
spec:
collectors:
- clusterResources: {}`),
},
},
},
want: []string{
`apiVersion: troubleshoot.sh/v1beta2
kind: SupportBundle
metadata:
name: cluster-resources
spec:
collectors:
- clusterResources: {}`,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
client := testclient.NewSimpleClientset()
for _, secret := range tt.supportBundleSecrets {
_, err := client.CoreV1().Secrets(secret.Namespace).Create(ctx, &secret, metav1.CreateOptions{})
require.NoError(t, err)
}
got, err := LoadFromSecretMatchingLabel(ctx, client, "troubleshoot.io/kind=support-bundle", "", "support-bundle-spec")
if tt.wantErr {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, tt.want, got)
}
})
}
}
func TestUserProvidedNamespace_LoadFromSecretMatchingLabel(t *testing.T) {
tests := []struct {
name string
supportBundleSecrets []corev1.Secret
want []string
wantErr bool
}{
{
name: "support bundle secret with matching label and key in user provided namespace",
supportBundleSecrets: []corev1.Secret{
{
ObjectMeta: metav1.ObjectMeta{
Name: "secret",
Namespace: "some-namespace",
Labels: map[string]string{
"troubleshoot.io/kind": "support-bundle",
},
},
Data: map[string][]byte{
"support-bundle-spec": []byte(`apiVersion: troubleshoot.sh/v1beta2
kind: SupportBundle
metadata:
name: test
spec:
collectors:
- data:
name: static/data.txt
data: |
static data`),
},
},
},
want: []string{
`apiVersion: troubleshoot.sh/v1beta2
kind: SupportBundle
metadata:
name: test
spec:
collectors:
- data:
name: static/data.txt
data: |
static data`,
},
},
{
name: "support bundle secret with matching label and key outside of user provided namespace",
supportBundleSecrets: []corev1.Secret{
{
ObjectMeta: metav1.ObjectMeta{
Name: "secret",
Namespace: "not-your-namespace",
Labels: map[string]string{
"troubleshoot.io/kind": "support-bundle",
},
},
Data: map[string][]byte{
"support-bundle-spec": []byte(`apiVersion: troubleshoot.sh/v1beta2
kind: SupportBundle
metadata:
name: test
spec:
collectors:
- data:
name: static/data.txt
data: |
static data`),
},
},
},
want: []string(nil),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
client := testclient.NewSimpleClientset()
for _, secret := range tt.supportBundleSecrets {
_, err := client.CoreV1().Secrets(secret.Namespace).Create(ctx, &secret, metav1.CreateOptions{})
require.NoError(t, err)
}
got, err := LoadFromSecretMatchingLabel(ctx, client, "troubleshoot.io/kind=support-bundle", "some-namespace", "support-bundle-spec")
if tt.wantErr {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, tt.want, got)
}
})
}
}
func TestRedactors_LoadFromSecretMatchingLabel(t *testing.T) {
tests := []struct {
name string
supportBundleSecrets []corev1.Secret
want []string
wantErr bool
}{
{
name: "redactor secret with matching label and key",
supportBundleSecrets: []corev1.Secret{
{
ObjectMeta: metav1.ObjectMeta{
Name: "secret",
Namespace: "default",
Labels: map[string]string{
"troubleshoot.io/kind": "support-bundle",
},
},
Data: map[string][]byte{
"redactor-spec": []byte(`apiVersion: troubleshoot.sh/v1beta2
kind: Redactor
metadata:
name: redact-some-content
spec:
redactors:
- name: replace some-content
fileSelector:
file: result.json
removals:
values:
- some-content`),
},
},
},
want: []string{
`apiVersion: troubleshoot.sh/v1beta2
kind: Redactor
metadata:
name: redact-some-content
spec:
redactors:
- name: replace some-content
fileSelector:
file: result.json
removals:
values:
- some-content`,
},
},
{
name: "redactor secret with matching label but wrong key",
supportBundleSecrets: []corev1.Secret{
{
ObjectMeta: metav1.ObjectMeta{
Name: "secret",
Namespace: "default",
Labels: map[string]string{
"troubleshoot.io/kind": "support-bundle",
},
},
Data: map[string][]byte{
"redactor-spec-wrong": []byte(`apiVersion: troubleshoot.sh/v1beta2
kind: Redactor
metadata:
name: redact-some-content
spec:
redactors:
- name: replace some-content
fileSelector:
file: result.json
removals:
values:
- some-content`),
},
},
},
want: []string(nil),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
client := testclient.NewSimpleClientset()
for _, secret := range tt.supportBundleSecrets {
_, err := client.CoreV1().Secrets(secret.Namespace).Create(ctx, &secret, metav1.CreateOptions{})
require.NoError(t, err)
}
got, err := LoadFromSecretMatchingLabel(ctx, client, "troubleshoot.io/kind=support-bundle", "", "redactor-spec")
if tt.wantErr {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, tt.want, got)
}
})
}
}

259
internal/specs/specs.go Normal file
View File

@@ -0,0 +1,259 @@
package specs
import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"os"
"reflect"
"strings"
"github.com/pkg/errors"
"github.com/replicatedhq/troubleshoot/internal/util"
"github.com/replicatedhq/troubleshoot/pkg/constants"
"github.com/replicatedhq/troubleshoot/pkg/k8sutil"
"github.com/replicatedhq/troubleshoot/pkg/loader"
"github.com/replicatedhq/troubleshoot/pkg/oci"
"github.com/replicatedhq/troubleshoot/pkg/types"
"github.com/spf13/viper"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/client-go/kubernetes"
"k8s.io/klog/v2"
)
// SplitTroubleshootSecretLabelSelector splits a label selector into two selectors, if applicable:
// 1. troubleshoot.io/kind=support-bundle and non-troubleshoot (if contains) labels selector.
// 2. troubleshoot.sh/kind=support-bundle and non-troubleshoot (if contains) labels selector.
func SplitTroubleshootSecretLabelSelector(ctx context.Context, labelSelector labels.Selector) ([]string, error) {
klog.V(1).Infof("Split %q selector into troubleshoot and non-troubleshoot labels selector separately, if applicable", labelSelector.String())
selectorRequirements, selectorSelectable := labelSelector.Requirements()
if !selectorSelectable {
return nil, errors.Errorf("Selector %q is not selectable", labelSelector.String())
}
var troubleshootReqs, otherReqs []labels.Requirement
for _, req := range selectorRequirements {
if req.Key() == constants.TroubleshootIOLabelKey || req.Key() == constants.TroubleshootSHLabelKey {
troubleshootReqs = append(troubleshootReqs, req)
} else {
otherReqs = append(otherReqs, req)
}
}
parsedSelectorStrings := make([]string, 0)
// Combine each troubleshoot requirement with other requirements to form new selectors
s := labelSelector.String()
if len(troubleshootReqs) == 0 && s != "" {
return []string{s}, nil
}
for _, tReq := range troubleshootReqs {
reqs := append(otherReqs, tReq)
newSelector := labels.NewSelector().Add(reqs...)
parsedSelectorStrings = append(parsedSelectorStrings, newSelector.String())
}
return parsedSelectorStrings, nil
}
func LoadFromCLIArgs(ctx context.Context, client kubernetes.Interface, args []string, vp *viper.Viper) (*loader.TroubleshootKinds, error) {
rawSpecs := []string{}
for _, v := range args {
if strings.HasPrefix(v, "secret/") {
// format secret/namespace-name/secret-name
pathParts := strings.Split(v, "/")
if len(pathParts) != 3 {
return nil, types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, errors.Errorf("path %s must have 3 components", v))
}
spec, err := LoadFromSecret(ctx, client, pathParts[1], pathParts[2], "preflight-spec")
if err != nil {
return nil, types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, errors.Wrap(err, "failed to get spec from secret"))
}
rawSpecs = append(rawSpecs, string(spec))
} else if _, err := os.Stat(v); err == nil {
b, err := os.ReadFile(v)
if err != nil {
return nil, types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, err)
}
rawSpecs = append(rawSpecs, string(b))
} else if v == "-" {
b, err := io.ReadAll(os.Stdin)
if err != nil {
return nil, types.NewExitCodeError(constants.EXIT_CODE_CATCH_ALL, err)
}
rawSpecs = append(rawSpecs, string(b))
} else {
u, err := url.Parse(v)
if err != nil {
return nil, types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, err)
}
if u.Scheme == "oci" {
content, err := oci.PullPreflightFromOCI(v)
if err != nil {
if err == oci.ErrNoRelease {
return nil, types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, errors.Errorf("no release found for %s.\nCheck the oci:// uri for errors or contact the application vendor for support.", v))
}
return nil, types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, err)
}
rawSpecs = append(rawSpecs, string(content))
} else {
if !util.IsURL(v) {
return nil, types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, fmt.Errorf("%s is not a URL and was not found (err %s)", v, err))
}
req, err := http.NewRequest("GET", v, nil)
if err != nil {
// exit code: should this be catch all or spec issues...?
return nil, types.NewExitCodeError(constants.EXIT_CODE_CATCH_ALL, err)
}
req.Header.Set("User-Agent", "Replicated_Preflight/v1beta2")
resp, err := http.DefaultClient.Do(req)
if err != nil {
// exit code: should this be catch all or spec issues...?
return nil, types.NewExitCodeError(constants.EXIT_CODE_CATCH_ALL, err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, err)
}
rawSpecs = append(rawSpecs, string(body))
}
}
}
kinds, err := loader.LoadSpecs(ctx, loader.LoadOptions{
RawSpecs: rawSpecs,
})
if err != nil {
return nil, err
}
if vp.GetBool("load-cluster-specs") {
clusterKinds, err := LoadFromCluster(ctx, client, vp.GetStringSlice("selector"), vp.GetString("namespace"))
if err != nil {
return nil, types.NewExitCodeError(constants.EXIT_CODE_SPEC_ISSUES, err)
}
kinds.Add(clusterKinds)
}
return kinds, nil
}
// LoadFromCluster loads troubleshoot specs from the cluster based on the provided labels.
// By default this will be troubleshoot.io/kind=support-bundle and troubleshoot.sh/kind=support-bundle
// labels. We search for secrets and configmaps with the label selector and extract the raw data.
// We then load the specs from the raw data. If the user does not have sufficient permissions
// to list & read secrets and configmaps from all namespaces, we will fallback to trying each
// namespace individually, and eventually default to the configured kubeconfig namespace.
func LoadFromCluster(ctx context.Context, client kubernetes.Interface, selectors []string, ns string) (*loader.TroubleshootKinds, error) {
if reflect.DeepEqual(selectors, []string{"troubleshoot.sh/kind=support-bundle"}) {
// Its the default selector so we append troubleshoot.io/kind=support-bundle to it due to backwards compatibility
selectors = append(selectors, "troubleshoot.io/kind=support-bundle")
}
labelSelector := strings.Join(selectors, ",")
parsedSelector, err := labels.Parse(labelSelector)
if err != nil {
return nil, errors.Wrap(err, "unable to parse selector")
}
// List of namespaces we want to search for secrets and configmaps with support bundle specs
namespaces := []string{}
if ns != "" {
// Just progress with the namespace provided
namespaces = []string{ns}
} else {
// Check if I can read secrets and configmaps in all namespaces
ican, err := k8sutil.CanIListAndGetAllSecretsAndConfigMaps(ctx, client)
if err != nil {
return nil, errors.Wrap(err, "failed to check if I can read secrets and configmaps")
}
klog.V(1).Infof("Can I read any secrets and configmaps: %v", ican)
if ican {
// I can read secrets and configmaps in all namespaces
// No need to iterate over all namespaces
namespaces = []string{""}
} else {
// Get list of all namespaces and try to find specs from each namespace
nsList, err := client.CoreV1().Namespaces().List(ctx, metav1.ListOptions{})
if err != nil {
if k8serrors.IsForbidden(err) {
kubeconfig := k8sutil.GetKubeconfig()
ns, _, err := kubeconfig.Namespace()
if err != nil {
return nil, errors.Wrap(err, "failed to get namespace from kubeconfig")
}
// If we are not allowed to list namespaces, just use the default namespace
// configured in the kubeconfig
namespaces = []string{ns}
} else {
return nil, errors.Wrap(err, "failed to list namespaces")
}
}
for _, ns := range nsList.Items {
namespaces = append(namespaces, ns.Name)
}
}
}
var rawSpecs []string
parsedSelectorStrings, err := SplitTroubleshootSecretLabelSelector(ctx, parsedSelector)
if err != nil {
klog.Errorf("failed to parse troubleshoot labels selector %s", err)
}
// Iteratively search for troubleshoot specs in all namespaces using the given selectors
for _, parsedSelectorString := range parsedSelectorStrings {
klog.V(1).Infof("Search specs from [%q] namespace using %q selector", strings.Join(namespaces, ", "), parsedSelectorString)
for _, ns := range namespaces {
for _, key := range []string{constants.SupportBundleKey, constants.PreflightKey, constants.RedactorKey} {
specs, err := LoadFromSecretMatchingLabel(ctx, client, parsedSelectorString, ns, key)
if err != nil {
if !k8serrors.IsForbidden(err) {
klog.Errorf("failed to load support bundle spec from secrets: %s", err)
} else {
klog.Warningf("Reading secrets from %q namespace forbidden", ns)
}
}
rawSpecs = append(rawSpecs, specs...)
specs, err = LoadFromConfigMapMatchingLabel(ctx, client, parsedSelectorString, ns, key)
if err != nil {
if !k8serrors.IsForbidden(err) {
klog.Errorf("failed to load support bundle spec from configmap: %s", err)
} else {
klog.Warningf("Reading configmaps from %q namespace forbidden", ns)
}
}
rawSpecs = append(rawSpecs, specs...)
}
}
}
// Load troubleshoot specs from the raw specs
return loader.LoadSpecs(ctx, loader.LoadOptions{
RawSpecs: rawSpecs,
})
}

View File

@@ -0,0 +1,203 @@
package specs
import (
"context"
"fmt"
"reflect"
"testing"
"github.com/google/uuid"
"github.com/replicatedhq/troubleshoot/internal/testutils"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/replicatedhq/troubleshoot/pkg/loader"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
testclient "k8s.io/client-go/kubernetes/fake"
)
func Test_SplitTroubleshootSecretLabelSelector(t *testing.T) {
tests := []struct {
name string
selectorString string
expectedSelectors []string
expectedError bool
}{
{
name: "Split both troubleshoot and non-troubleshoot labels",
selectorString: "troubleshoot.io/kind=support-bundle,troubleshoot.sh/kind=support-bundle,a=b",
expectedSelectors: []string{
"a=b,troubleshoot.io/kind=support-bundle",
"a=b,troubleshoot.sh/kind=support-bundle",
},
expectedError: false,
},
{
name: "Split only troubleshoot.io label",
selectorString: "troubleshoot.io/kind=support-bundle",
expectedSelectors: []string{"troubleshoot.io/kind=support-bundle"},
expectedError: false,
},
{
name: "Split only troubleshoot.sh label",
selectorString: "troubleshoot.sh/kind=support-bundle",
expectedSelectors: []string{"troubleshoot.sh/kind=support-bundle"},
expectedError: false,
},
{
name: "Split only non-troubleshoot label",
selectorString: "a=b",
expectedSelectors: []string{"a=b"},
expectedError: false,
},
{
name: "No selector labels to split",
selectorString: "",
expectedSelectors: []string{},
expectedError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
selector, err := labels.Parse(tt.selectorString)
if err != nil {
t.Errorf("Error parsing selector string: %v", err)
return
}
gotSelectors, err := SplitTroubleshootSecretLabelSelector(context.TODO(), selector)
if (err != nil) != tt.expectedError {
t.Errorf("Expected error: %v, got: %v", tt.expectedError, err)
return
}
assert.ElementsMatch(t, tt.expectedSelectors, gotSelectors)
})
}
}
func TestLoadFromCluster(t *testing.T) {
theRedactor := troubleshootv1beta2.Redactor{
TypeMeta: metav1.TypeMeta{
APIVersion: "troubleshoot.sh/v1beta2",
Kind: "Redactor",
},
ObjectMeta: metav1.ObjectMeta{
Name: "redact-some-content",
},
Spec: troubleshootv1beta2.RedactorSpec{
Redactors: []*troubleshootv1beta2.Redact{
{
Name: "redact-text-1",
Removals: troubleshootv1beta2.Removals{
Values: []string{"TEXT"},
},
},
},
},
}
tests := []struct {
name string
selectors []string
namespace string
objects []runtime.Object
want *loader.TroubleshootKinds
}{
{
name: "no selectors",
want: loader.NewTroubleshootKinds(),
},
{
name: "spec in secret and default label selector",
namespace: "bigbank",
selectors: []string{
"troubleshoot.sh/kind=support-bundle",
},
objects: []runtime.Object{
secretObject("bigbank", map[string]string{
"troubleshoot.io/kind": "support-bundle",
}),
},
want: &loader.TroubleshootKinds{
RedactorsV1Beta2: []troubleshootv1beta2.Redactor{theRedactor},
},
},
{
name: "spec in secret and no selector argument passed",
namespace: "bigbank",
objects: []runtime.Object{
secretObject("bigbank", map[string]string{
"troubleshoot.io/kind": "support-bundle",
}),
},
want: loader.NewTroubleshootKinds(),
},
{
name: "multiple specs default selector",
namespace: "bigbank",
selectors: []string{
"troubleshoot.sh/kind=support-bundle",
},
objects: []runtime.Object{
secretObject("bigbank", map[string]string{
"troubleshoot.io/kind": "support-bundle",
}),
secretObject("bigbank", map[string]string{
"troubleshoot.io/kind": "support-bundle",
}),
},
want: &loader.TroubleshootKinds{
RedactorsV1Beta2: []troubleshootv1beta2.Redactor{theRedactor, theRedactor},
},
},
{
name: "spec in secret but different namespace",
namespace: "bigbank",
objects: []runtime.Object{
secretObject("anotherbank", map[string]string{
"troubleshoot.io/kind": "support-bundle",
}),
},
want: loader.NewTroubleshootKinds(),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
client := testclient.NewSimpleClientset(tt.objects...)
got, err := LoadFromCluster(ctx, client, tt.selectors, tt.namespace)
require.NoError(t, err)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("got = %v, want %v", testutils.AsJSON(t, got), testutils.AsJSON(t, tt.want))
}
})
}
}
func secretObject(ns string, selectors map[string]string) runtime.Object {
return &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("secret-name-%s", uuid.New().String()),
Namespace: ns,
Labels: selectors,
},
Data: map[string][]byte{
"redactor-spec": []byte(`apiVersion: troubleshoot.sh/v1beta2
kind: Redactor
metadata:
name: redact-some-content
spec:
redactors:
- name: redact-text-1
removals:
values:
- TEXT`),
},
}
}