Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 20 additions & 18 deletions pkg/analyze/kube_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,26 @@ import (
)

var Filemap = map[string]string{
"deployment": constants.CLUSTER_RESOURCES_DEPLOYMENTS,
"daemonset": constants.CLUSTER_RESOURCES_DAEMONSETS,
"statefulset": constants.CLUSTER_RESOURCES_STATEFULSETS,
"networkpolicy": constants.CLUSTER_RESOURCES_NETWORK_POLICY,
"pod": constants.CLUSTER_RESOURCES_PODS,
"ingress": constants.CLUSTER_RESOURCES_INGRESS,
"service": constants.CLUSTER_RESOURCES_SERVICES,
"resourcequota": constants.CLUSTER_RESOURCES_RESOURCE_QUOTA,
"job": constants.CLUSTER_RESOURCES_JOBS,
"persistentvolumeclaim": constants.CLUSTER_RESOURCES_PVCS,
"pvc": constants.CLUSTER_RESOURCES_PVCS,
"replicaset": constants.CLUSTER_RESOURCES_REPLICASETS,
"configmap": constants.CLUSTER_RESOURCES_CONFIGMAPS,
"namespace": fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_NAMESPACES),
"persistentvolume": fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_PVS),
"pv": fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_PVS),
"node": fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_NODES),
"storageclass": fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_STORAGE_CLASS),
"deployment": constants.CLUSTER_RESOURCES_DEPLOYMENTS,
"daemonset": constants.CLUSTER_RESOURCES_DAEMONSETS,
"statefulset": constants.CLUSTER_RESOURCES_STATEFULSETS,
"networkpolicy": constants.CLUSTER_RESOURCES_NETWORK_POLICY,
"pod": constants.CLUSTER_RESOURCES_PODS,
"ingress": constants.CLUSTER_RESOURCES_INGRESS,
"service": constants.CLUSTER_RESOURCES_SERVICES,
"resourcequota": constants.CLUSTER_RESOURCES_RESOURCE_QUOTA,
"job": constants.CLUSTER_RESOURCES_JOBS,
"persistentvolumeclaim": constants.CLUSTER_RESOURCES_PVCS,
"pvc": constants.CLUSTER_RESOURCES_PVCS,
"replicaset": constants.CLUSTER_RESOURCES_REPLICASETS,
"configmap": constants.CLUSTER_RESOURCES_CONFIGMAPS,
"validatingwebhookconfiguration": fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_VALIDATING_WEBHOOK_CONFIGURATIONS),
"mutatingwebhookconfiguration": fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_MUTATING_WEBHOOK_CONFIGURATIONS),
"namespace": fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_NAMESPACES),
"persistentvolume": fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_PVS),
"pv": fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_PVS),
"node": fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_NODES),
"storageclass": fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_STORAGE_CLASS),
}

type AnalyzeClusterResource struct {
Expand Down
60 changes: 60 additions & 0 deletions pkg/collect/cluster_resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,16 @@ func (c *CollectClusterResources) Collect(progressChan chan<- interface{}) (Coll

output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_CONFIGMAPS)), marshalErrors(configMapsErrors))

// Validating Webhook Configurations
validatingWebhookConfigurations, validatingWebhookConfigurationsErrors := validatingWebhookConfigurations(ctx, client)
output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_VALIDATING_WEBHOOK_CONFIGURATIONS)), bytes.NewBuffer(validatingWebhookConfigurations))
output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_VALIDATING_WEBHOOK_CONFIGURATIONS)), marshalErrors(validatingWebhookConfigurationsErrors))

// Mutating Webhook Configurations
mutatingWebhookConfigurations, mutatingWebhookConfigurationsErrors := mutatingWebhookConfigurations(ctx, client)
output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s.json", constants.CLUSTER_RESOURCES_MUTATING_WEBHOOK_CONFIGURATIONS)), bytes.NewBuffer(mutatingWebhookConfigurations))
output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_MUTATING_WEBHOOK_CONFIGURATIONS)), marshalErrors(mutatingWebhookConfigurationsErrors))

// Replicated License
licenseData, licenseErr := replicatedLicense(ctx, client, namespaceNames)
if licenseErr == nil {
Expand Down Expand Up @@ -2195,6 +2205,56 @@ func configMaps(ctx context.Context, client kubernetes.Interface, namespaces []s
return configmapByNamespace, errorsByNamespace
}

func validatingWebhookConfigurations(ctx context.Context, client kubernetes.Interface) ([]byte, []string) {
validatingWebhookConfigurations, err := client.AdmissionregistrationV1().ValidatingWebhookConfigurations().List(ctx, metav1.ListOptions{})
if err != nil {
return nil, []string{err.Error()}
}

gvk, err := apiutil.GVKForObject(validatingWebhookConfigurations, scheme.Scheme)
if err == nil {
validatingWebhookConfigurations.GetObjectKind().SetGroupVersionKind(gvk)
}

for i, o := range validatingWebhookConfigurations.Items {
gvk, err := apiutil.GVKForObject(&o, scheme.Scheme)
if err == nil {
validatingWebhookConfigurations.Items[i].GetObjectKind().SetGroupVersionKind(gvk)
}
}

b, err := json.MarshalIndent(validatingWebhookConfigurations, "", " ")
if err != nil {
return nil, []string{err.Error()}
}
return b, nil
}

func mutatingWebhookConfigurations(ctx context.Context, client kubernetes.Interface) ([]byte, []string) {
mutatingWebhookConfigurations, err := client.AdmissionregistrationV1().MutatingWebhookConfigurations().List(ctx, metav1.ListOptions{})
if err != nil {
return nil, []string{err.Error()}
}

gvk, err := apiutil.GVKForObject(mutatingWebhookConfigurations, scheme.Scheme)
if err == nil {
mutatingWebhookConfigurations.GetObjectKind().SetGroupVersionKind(gvk)
}

for i, o := range mutatingWebhookConfigurations.Items {
gvk, err := apiutil.GVKForObject(&o, scheme.Scheme)
if err == nil {
mutatingWebhookConfigurations.Items[i].GetObjectKind().SetGroupVersionKind(gvk)
}
}

b, err := json.MarshalIndent(mutatingWebhookConfigurations, "", " ")
if err != nil {
return nil, []string{err.Error()}
}
return b, nil
}

// storeCustomResource stores a custom resource as JSON and YAML
// We use both formats for backwards compatibility. This way we
// avoid breaking existing tools and analysers that already rely on
Expand Down
165 changes: 165 additions & 0 deletions pkg/collect/cluster_resources_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/replicatedhq/troubleshoot/pkg/client/troubleshootclientset/scheme"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
certificatesv1 "k8s.io/api/certificates/v1"
v1 "k8s.io/api/coordination/v1"
corev1 "k8s.io/api/core/v1"
Expand Down Expand Up @@ -776,3 +777,167 @@ func createTestCertificateSigningRequests(client kubernetes.Interface, csrNames
}
return nil
}

func Test_ValidatingWebhookConfigurations(t *testing.T) {
tests := []struct {
name string
vwcNames []string
}{
{
name: "single validating webhook configuration",
vwcNames: []string{"test-vwc"},
},
{
name: "multiple validating webhook configurations",
vwcNames: []string{"vwc-1", "vwc-2", "vwc-3"},
},
{
name: "empty list",
vwcNames: []string{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := testclient.NewSimpleClientset()
ctx := context.Background()
err := createTestValidatingWebhookConfigurations(client, tt.vwcNames)
require.NoError(t, err)

data, errs := validatingWebhookConfigurations(ctx, client)
assert.Empty(t, errs)
assert.NotNil(t, data)

var list admissionregistrationv1.ValidatingWebhookConfigurationList
err = json.Unmarshal(data, &list)
require.NoError(t, err)
assert.Len(t, list.Items, len(tt.vwcNames))
for _, item := range list.Items {
assert.Contains(t, tt.vwcNames, item.Name)
}
})
}
}

func Test_ValidatingWebhookConfigurations_PermissionDenied(t *testing.T) {
client := testclient.NewSimpleClientset()
ctx := context.Background()

client.PrependReactor("list", "validatingwebhookconfigurations", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {
return true, nil, fmt.Errorf("validatingwebhookconfigurations.admissionregistration.k8s.io is forbidden: User \"system:serviceaccount:default:default\" cannot list resource \"validatingwebhookconfigurations\" in API group \"admissionregistration.k8s.io\" at the cluster scope")
})

data, errs := validatingWebhookConfigurations(ctx, client)

assert.Nil(t, data)
require.NotEmpty(t, errs)
assert.Len(t, errs, 1)
assert.Contains(t, errs[0], "forbidden")
}

func createTestValidatingWebhookConfigurations(client kubernetes.Interface, names []string) error {
for _, name := range names {
_, err := client.AdmissionregistrationV1().ValidatingWebhookConfigurations().Create(context.Background(), &admissionregistrationv1.ValidatingWebhookConfiguration{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Webhooks: []admissionregistrationv1.ValidatingWebhook{
{
Name: "test-webhook.example.com",
ClientConfig: admissionregistrationv1.WebhookClientConfig{
Service: &admissionregistrationv1.ServiceReference{
Namespace: "default",
Name: "webhook-service",
},
},
AdmissionReviewVersions: []string{"v1"},
},
},
}, metav1.CreateOptions{})
if err != nil {
return err
}
}
return nil
}

func Test_MutatingWebhookConfigurations(t *testing.T) {
tests := []struct {
name string
mwcNames []string
}{
{
name: "single mutating webhook configuration",
mwcNames: []string{"test-mwc"},
},
{
name: "multiple mutating webhook configurations",
mwcNames: []string{"mwc-1", "mwc-2"},
},
{
name: "empty list",
mwcNames: []string{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := testclient.NewSimpleClientset()
ctx := context.Background()
err := createTestMutatingWebhookConfigurations(client, tt.mwcNames)
require.NoError(t, err)

data, errs := mutatingWebhookConfigurations(ctx, client)
assert.Empty(t, errs)
assert.NotNil(t, data)

var list admissionregistrationv1.MutatingWebhookConfigurationList
err = json.Unmarshal(data, &list)
require.NoError(t, err)
assert.Len(t, list.Items, len(tt.mwcNames))
for _, item := range list.Items {
assert.Contains(t, tt.mwcNames, item.Name)
}
})
}
}

func Test_MutatingWebhookConfigurations_PermissionDenied(t *testing.T) {
client := testclient.NewSimpleClientset()
ctx := context.Background()

client.PrependReactor("list", "mutatingwebhookconfigurations", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {
return true, nil, fmt.Errorf("mutatingwebhookconfigurations.admissionregistration.k8s.io is forbidden: User \"system:serviceaccount:default:default\" cannot list resource \"mutatingwebhookconfigurations\" in API group \"admissionregistration.k8s.io\" at the cluster scope")
})

data, errs := mutatingWebhookConfigurations(ctx, client)

assert.Nil(t, data)
require.NotEmpty(t, errs)
assert.Len(t, errs, 1)
assert.Contains(t, errs[0], "forbidden")
}

func createTestMutatingWebhookConfigurations(client kubernetes.Interface, names []string) error {
for _, name := range names {
_, err := client.AdmissionregistrationV1().MutatingWebhookConfigurations().Create(context.Background(), &admissionregistrationv1.MutatingWebhookConfiguration{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
Webhooks: []admissionregistrationv1.MutatingWebhook{
{
Name: "test-mutating-webhook.example.com",
ClientConfig: admissionregistrationv1.WebhookClientConfig{
Service: &admissionregistrationv1.ServiceReference{
Namespace: "default",
Name: "webhook-service",
},
},
AdmissionReviewVersions: []string{"v1"},
},
},
}, metav1.CreateOptions{})
if err != nil {
return err
}
}
return nil
}
2 changes: 1 addition & 1 deletion pkg/collect/collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,10 +223,10 @@ func getCollectorName(c interface{}) string {
collector = "dns"
case *CollectEtcd:
collector = "etcd"

default:
collector = "<none>"
}

if name != "" {
return fmt.Sprintf("%s/%s", collector, name)
}
Expand Down
82 changes: 42 additions & 40 deletions pkg/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,46 +23,48 @@ const (
ANALYSIS_FILENAME = "analysis.json"

// Cluster Resources Collector Directories
CLUSTER_RESOURCES_DIR = "cluster-resources"
CLUSTER_RESOURCES_NAMESPACES = "namespaces"
CLUSTER_RESOURCES_AUTH_CANI = "auth-cani-list"
CLUSTER_RESOURCES_PODS = "pods"
CLUSTER_RESOURCES_PODS_LOGS = "pods/logs"
CLUSTER_RESOURCES_POD_DISRUPTION_BUDGETS = "pod-disruption-budgets"
CLUSTER_RESOURCES_SERVICES = "services"
CLUSTER_RESOURCES_DEPLOYMENTS = "deployments"
CLUSTER_RESOURCES_REPLICASETS = "replicasets"
CLUSTER_RESOURCES_STATEFULSETS = "statefulsets"
CLUSTER_RESOURCES_DAEMONSETS = "daemonsets"
CLUSTER_RESOURCES_JOBS = "jobs"
CLUSTER_RESOURCES_CRONJOBS = "cronjobs"
CLUSTER_RESOURCES_INGRESS = "ingress"
CLUSTER_RESOURCES_NETWORK_POLICY = "network-policy"
CLUSTER_RESOURCES_RESOURCE_QUOTA = "resource-quota"
CLUSTER_RESOURCES_STORAGE_CLASS = "storage-classes"
CLUSTER_RESOURCES_CUSTOM_RESOURCE_DEFINITIONS = "custom-resource-definitions"
CLUSTER_RESOURCES_CUSTOM_RESOURCES = "custom-resources"
CLUSTER_RESOURCES_IMAGE_PULL_SECRETS = "image-pull-secrets" // nolint:gosec
CLUSTER_RESOURCES_NODES = "nodes"
CLUSTER_RESOURCES_GROUPS = "groups"
CLUSTER_RESOURCES_RESOURCES = "resources"
CLUSTER_RESOURCES_LIMITRANGES = "limitranges"
CLUSTER_RESOURCES_EVENTS = "events"
CLUSTER_RESOURCES_PVS = "pvs"
CLUSTER_RESOURCES_PVCS = "pvcs"
CLUSTER_RESOURCES_ROLES = "roles"
CLUSTER_RESOURCES_ROLE_BINDINGS = "rolebindings"
CLUSTER_RESOURCES_CLUSTER_ROLES = "clusterroles"
CLUSTER_RESOURCES_CLUSTER_ROLE_BINDINGS = "clusterrolebindings"
CLUSTER_RESOURCES_PRIORITY_CLASS = "priorityclasses"
CLUSTER_RESOURCES_ENDPOINTS = "endpoints"
CLUSTER_RESOURCES_ENDPOINTSLICES = "endpointslices"
CLUSTER_RESOURCES_SERVICE_ACCOUNTS = "serviceaccounts"
CLUSTER_RESOURCES_LEASES = "leases"
CLUSTER_RESOURCES_VOLUME_ATTACHMENTS = "volumeattachments"
CLUSTER_RESOURCES_CONFIGMAPS = "configmaps"
CLUSTER_RESOURCES_REPLICATED_LICENSE = "license.json"
CLUSTER_RESOURCES_CERTIFICATE_SIGNING_REQUESTS = "certificatesigningrequests"
CLUSTER_RESOURCES_DIR = "cluster-resources"
CLUSTER_RESOURCES_NAMESPACES = "namespaces"
CLUSTER_RESOURCES_AUTH_CANI = "auth-cani-list"
CLUSTER_RESOURCES_PODS = "pods"
CLUSTER_RESOURCES_PODS_LOGS = "pods/logs"
CLUSTER_RESOURCES_POD_DISRUPTION_BUDGETS = "pod-disruption-budgets"
CLUSTER_RESOURCES_SERVICES = "services"
CLUSTER_RESOURCES_DEPLOYMENTS = "deployments"
CLUSTER_RESOURCES_REPLICASETS = "replicasets"
CLUSTER_RESOURCES_STATEFULSETS = "statefulsets"
CLUSTER_RESOURCES_DAEMONSETS = "daemonsets"
CLUSTER_RESOURCES_JOBS = "jobs"
CLUSTER_RESOURCES_CRONJOBS = "cronjobs"
CLUSTER_RESOURCES_INGRESS = "ingress"
CLUSTER_RESOURCES_NETWORK_POLICY = "network-policy"
CLUSTER_RESOURCES_RESOURCE_QUOTA = "resource-quota"
CLUSTER_RESOURCES_STORAGE_CLASS = "storage-classes"
CLUSTER_RESOURCES_CUSTOM_RESOURCE_DEFINITIONS = "custom-resource-definitions"
CLUSTER_RESOURCES_CUSTOM_RESOURCES = "custom-resources"
CLUSTER_RESOURCES_IMAGE_PULL_SECRETS = "image-pull-secrets" // nolint:gosec
CLUSTER_RESOURCES_NODES = "nodes"
CLUSTER_RESOURCES_GROUPS = "groups"
CLUSTER_RESOURCES_RESOURCES = "resources"
CLUSTER_RESOURCES_LIMITRANGES = "limitranges"
CLUSTER_RESOURCES_EVENTS = "events"
CLUSTER_RESOURCES_PVS = "pvs"
CLUSTER_RESOURCES_PVCS = "pvcs"
CLUSTER_RESOURCES_ROLES = "roles"
CLUSTER_RESOURCES_ROLE_BINDINGS = "rolebindings"
CLUSTER_RESOURCES_CLUSTER_ROLES = "clusterroles"
CLUSTER_RESOURCES_CLUSTER_ROLE_BINDINGS = "clusterrolebindings"
CLUSTER_RESOURCES_PRIORITY_CLASS = "priorityclasses"
CLUSTER_RESOURCES_ENDPOINTS = "endpoints"
CLUSTER_RESOURCES_ENDPOINTSLICES = "endpointslices"
CLUSTER_RESOURCES_SERVICE_ACCOUNTS = "serviceaccounts"
CLUSTER_RESOURCES_LEASES = "leases"
CLUSTER_RESOURCES_VOLUME_ATTACHMENTS = "volumeattachments"
CLUSTER_RESOURCES_CONFIGMAPS = "configmaps"
CLUSTER_RESOURCES_REPLICATED_LICENSE = "license.json"
CLUSTER_RESOURCES_CERTIFICATE_SIGNING_REQUESTS = "certificatesigningrequests"
CLUSTER_RESOURCES_VALIDATING_WEBHOOK_CONFIGURATIONS = "validating-webhook-configurations"
CLUSTER_RESOURCES_MUTATING_WEBHOOK_CONFIGURATIONS = "mutating-webhook-configurations"

// SelfSubjectRulesReview evaluation responses
SELFSUBJECTRULESREVIEW_ERROR_AUTHORIZATION_WEBHOOK_UNSUPPORTED = "webhook authorizer does not support user rule resolution"
Expand Down