diff --git a/cmd/analyze.go b/cmd/analyze.go index 7f850ca..feabbbc 100644 --- a/cmd/analyze.go +++ b/cmd/analyze.go @@ -31,6 +31,7 @@ const ( ServiceMonitor AnalyzeKind = "servicemonitor" Operator AnalyzeKind = "operator" Prometheus AnalyzeKind = "prometheus" + Alertmanager AnalyzeKind = "alertmanager" ) type AnalyzeFlags struct { @@ -81,6 +82,8 @@ func run(cmd *cobra.Command, _ []string) error { return analyzers.RunOperatorAnalyzer(cmd.Context(), clientSets, analyzerFlags.Name, analyzerFlags.Namespace) case Prometheus: return analyzers.RunPrometheusAnalyzer(cmd.Context(), clientSets, analyzerFlags.Name, analyzerFlags.Namespace) + case Alertmanager: + return analyzers.RunAlertmanagerAnalyzer(cmd.Context(), clientSets, analyzerFlags.Name, analyzerFlags.Namespace) default: return fmt.Errorf("kind %s not supported", analyzerFlags.Kind) } diff --git a/internal/analyzers/alertmanager.go b/internal/analyzers/alertmanager.go new file mode 100644 index 0000000..4f73289 --- /dev/null +++ b/internal/analyzers/alertmanager.go @@ -0,0 +1,124 @@ +// Copyright 2024 The prometheus-operator 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 analyzers + +import ( + "context" + "fmt" + "log/slog" + + "github.com/prometheus-operator/poctl/internal/k8sutil" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" +) + +func RunAlertmanagerAnalyzer(ctx context.Context, clientSets *k8sutil.ClientSets, name, namespace string) error { + alertmanager, err := clientSets.MClient.MonitoringV1().Alertmanagers(namespace).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + if errors.IsNotFound(err) { + return fmt.Errorf("alertmanager %s not found in namespace %s", name, namespace) + } + return fmt.Errorf("error while getting Alertmanager: %v", err) + } + + _, err = clientSets.KClient.CoreV1().ServiceAccounts(namespace).Get(ctx, alertmanager.Spec.ServiceAccountName, metav1.GetOptions{}) + if err != nil { + if errors.IsNotFound(err) { + return fmt.Errorf("alertmanager serviceaccount not found in namespace %s", namespace) + } + return fmt.Errorf("error while getting ServiceAcounts: %w", err) + } + + if alertmanager.Spec.AlertmanagerConfigSelector == nil && alertmanager.Spec.AlertmanagerConfiguration == nil { + if alertmanager.Spec.ConfigSecret != "" { + // use provided config secret + if err := checkAlertmanagerSecret(ctx, clientSets, alertmanager.Spec.ConfigSecret, namespace, "alertmanager.yaml"); err != nil { + return fmt.Errorf("error checking Alertmanager secret: %w", err) + } + } + if alertmanager.Spec.ConfigSecret == "" { + // use the default generated secret from pkg/alertmanager/statefulset.go + amConfigSecretName := fmt.Sprintf("alertmanager-%s-generated", alertmanager.Name) + if err := checkAlertmanagerSecret(ctx, clientSets, amConfigSecretName, namespace, "alertmanager.yaml.gz"); err != nil { + return fmt.Errorf("error checking Alertmanager secret: %w", err) + } + } + } + // If 'AlertmanagerConfigNamespaceSelector' is nil, only check own namespace. + if alertmanager.Spec.AlertmanagerConfigNamespaceSelector != nil { + if err := k8sutil.CheckResourceNamespaceSelectors(ctx, *clientSets, alertmanager.Spec.AlertmanagerConfigNamespaceSelector); err != nil { + return fmt.Errorf("alertmanagerConfigNamespaceSelector is not properly defined: %s", err) + } + } + + if alertmanager.Spec.AlertmanagerConfigSelector != nil { + if err := checkAlertmanagerConfigs(ctx, clientSets, alertmanager.Spec.AlertmanagerConfigSelector, namespace); err != nil { + return fmt.Errorf("alertmanagerConfigSelectors is not properly defined: %s", err) + } + } + + if alertmanager.Spec.AlertmanagerConfiguration != nil { + _, err := clientSets.MClient.MonitoringV1alpha1().AlertmanagerConfigs(namespace).Get(ctx, alertmanager.Spec.AlertmanagerConfiguration.Name, metav1.GetOptions{}) + if err != nil { + if errors.IsNotFound(err) { + return fmt.Errorf("alertmanagerConfigs not found in namespace %s", namespace) + } + return fmt.Errorf("error while getting AlertmanagerConfig: %w", err) + } + } + + slog.Info("Alertmanager is compliant, no issues found", "name", name, "namespace", namespace) + return nil +} + +func checkAlertmanagerSecret(ctx context.Context, clientSets *k8sutil.ClientSets, secretName, namespace string, secretData string) error { + alertmanagerSecret, err := clientSets.KClient.CoreV1().Secrets(namespace).Get(ctx, secretName, metav1.GetOptions{}) + if err != nil { + if errors.IsNotFound(err) { + return fmt.Errorf("failed to get alertmanager secret %s not found in namespace %s", secretName, namespace) + } + return fmt.Errorf("error while getting alertmanager secret%s %v", secretName, err) + } + if len(alertmanagerSecret.Data) == 0 { + return fmt.Errorf("alertmanager Secret %s is empty", secretName) + } + _, found := alertmanagerSecret.Data[secretData] + if !found { + return fmt.Errorf("the %s key not found in Secret %s", secretData, secretName) + } + return nil +} + +func checkAlertmanagerConfigs(ctx context.Context, clientSets *k8sutil.ClientSets, labelSelector *metav1.LabelSelector, namespace string) error { + if len(labelSelector.MatchLabels) == 0 && len(labelSelector.MatchExpressions) == 0 { + return nil + } + + labelMap, err := metav1.LabelSelectorAsMap(labelSelector) + if err != nil { + return fmt.Errorf("invalid label selector format in %v", err) + } + + alertmamagerConfigs, err := clientSets.MClient.MonitoringV1alpha1().AlertmanagerConfigs(namespace).List(ctx, metav1.ListOptions{LabelSelector: labels.SelectorFromSet(labelMap).String()}) + if err != nil { + return fmt.Errorf("failed to list AlertmanagerConfigs in %s: %v", namespace, err) + } + if len(alertmamagerConfigs.Items) == 0 { + return fmt.Errorf("no AlertmanagerConfigs match the provided selector in %s", namespace) + } + + return nil +} diff --git a/internal/analyzers/alertmanager_test.go b/internal/analyzers/alertmanager_test.go new file mode 100644 index 0000000..dd15f9c --- /dev/null +++ b/internal/analyzers/alertmanager_test.go @@ -0,0 +1,305 @@ +// Copyright 2024 The prometheus-operator 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 analyzers + +import ( + "context" + "testing" + + "github.com/prometheus-operator/poctl/internal/k8sutil" + monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" + monitoringv1alpha1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1alpha1" + monitoringclient "github.com/prometheus-operator/prometheus-operator/pkg/client/versioned/fake" + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/fake" + clienttesting "k8s.io/client-go/testing" +) + +func TestAlertmanagerAnalyzer(t *testing.T) { + type testCase struct { + name string + namespace string + getMockedClientSets func(tc testCase) k8sutil.ClientSets + shouldFail bool + } + + tests := []testCase{ + { + name: "AlertmanagerNotFound", + namespace: "test", + shouldFail: true, + getMockedClientSets: func(tc testCase) k8sutil.ClientSets { + mClient := monitoringclient.NewSimpleClientset(&monitoringv1.AlertmanagerList{}) + mClient.PrependReactor("get", "alertmanagers", func(_ clienttesting.Action) (bool, runtime.Object, error) { + return true, nil, errors.NewNotFound(monitoringv1.Resource("alertmanagers"), tc.name) + }) + + kClient := fake.NewSimpleClientset() + return k8sutil.ClientSets{ + MClient: mClient, + KClient: kClient, + } + }, + }, + { + name: "AlertmanagerMissingServiceAccount", + namespace: "test", + shouldFail: true, + getMockedClientSets: func(tc testCase) k8sutil.ClientSets { + mClient := monitoringclient.NewSimpleClientset(&monitoringv1.AlertmanagerList{}) + mClient.PrependReactor("get", "alertmanagers", func(_ clienttesting.Action) (bool, runtime.Object, error) { + return true, &monitoringv1.Alertmanager{ + ObjectMeta: metav1.ObjectMeta{ + Name: tc.name, + Namespace: tc.namespace, + }, + Spec: monitoringv1.AlertmanagerSpec{ + ServiceAccountName: "test-sa", + }, + }, nil + }) + + kClient := fake.NewSimpleClientset(&corev1.ServiceAccount{}) + kClient.PrependReactor("get", "serviceaccount", func(_ clienttesting.Action) (bool, runtime.Object, error) { + return true, nil, errors.NewInternalError(nil) + }) + return k8sutil.ClientSets{ + MClient: mClient, + KClient: kClient, + } + }, + }, + { + name: "AlertmanagerFailToGetConfigSecret", + namespace: "test", + shouldFail: true, + getMockedClientSets: func(tc testCase) k8sutil.ClientSets { + mClient := monitoringclient.NewSimpleClientset(&monitoringv1.AlertmanagerList{}) + mClient.PrependReactor("get", "alertmanagers", func(_ clienttesting.Action) (bool, runtime.Object, error) { + return true, &monitoringv1.Alertmanager{ + ObjectMeta: metav1.ObjectMeta{ + Name: tc.name, + Namespace: tc.namespace, + }, + Spec: monitoringv1.AlertmanagerSpec{ + ConfigSecret: "test-secret", + }, + }, nil + }) + + kClient := fake.NewSimpleClientset(&corev1.Secret{}) + kClient.PrependReactor("get", "secret", func(_ clienttesting.Action) (bool, runtime.Object, error) { + return true, nil, errors.NewInternalError(nil) + }) + return k8sutil.ClientSets{ + MClient: mClient, + KClient: kClient, + } + }, + }, + { + name: "AlertmanagerSecretEmptyData", + namespace: "test", + shouldFail: true, + getMockedClientSets: func(tc testCase) k8sutil.ClientSets { + mClient := monitoringclient.NewSimpleClientset(&monitoringv1.AlertmanagerList{}) + mClient.PrependReactor("get", "alertmanagers", func(_ clienttesting.Action) (bool, runtime.Object, error) { + return true, &monitoringv1.Alertmanager{ + ObjectMeta: metav1.ObjectMeta{ + Name: tc.name, + Namespace: tc.namespace, + }, + Spec: monitoringv1.AlertmanagerSpec{ + ConfigSecret: "test-secret", + }, + }, nil + }) + + kClient := fake.NewSimpleClientset(&corev1.Secret{}) + kClient.PrependReactor("get", "secrets", func(_ clienttesting.Action) (bool, runtime.Object, error) { + return true, &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-secret", + Namespace: tc.namespace, + }, + Data: map[string][]byte{}, + }, nil + }) + + return k8sutil.ClientSets{ + MClient: mClient, + KClient: kClient, + } + }, + }, + { + name: "AlertmanagerSecretKeyNotFound", + namespace: "test", + shouldFail: true, + getMockedClientSets: func(tc testCase) k8sutil.ClientSets { + mClient := monitoringclient.NewSimpleClientset(&monitoringv1.AlertmanagerList{}) + mClient.PrependReactor("get", "alertmanagers", func(_ clienttesting.Action) (bool, runtime.Object, error) { + return true, &monitoringv1.Alertmanager{ + ObjectMeta: metav1.ObjectMeta{ + Name: tc.name, + Namespace: tc.namespace, + }, + Spec: monitoringv1.AlertmanagerSpec{ + ConfigSecret: "test-secret", + }, + }, nil + }) + + kClient := fake.NewSimpleClientset(&corev1.Secret{}) + kClient.PrependReactor("get", "secrets", func(_ clienttesting.Action) (bool, runtime.Object, error) { + return true, &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-secret", + Namespace: tc.namespace, + }, + Data: map[string][]byte{ + "some-other-key": []byte("value"), + }, + }, nil + }) + + return k8sutil.ClientSets{ + MClient: mClient, + KClient: kClient, + } + }, + }, + { + name: "AlertmanagerNamespaceSelectorWithoutMatchLabels", + namespace: "test", + shouldFail: true, + getMockedClientSets: func(tc testCase) k8sutil.ClientSets { + mClient := monitoringclient.NewSimpleClientset(&monitoringv1.AlertmanagerList{}) + mClient.PrependReactor("get", "alertmanagers", func(_ clienttesting.Action) (bool, runtime.Object, error) { + return true, &monitoringv1.Alertmanager{ + ObjectMeta: metav1.ObjectMeta{ + Name: tc.name, + Namespace: tc.namespace, + }, + Spec: monitoringv1.AlertmanagerSpec{ + AlertmanagerConfigNamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"environment": "test"}, + }, + }, + }, nil + }) + + kClient := fake.NewSimpleClientset(&corev1.Namespace{}) + kClient.PrependReactor("get", "namespace", func(_ clienttesting.Action) (bool, runtime.Object, error) { + return true, &corev1.NamespaceList{ + Items: []corev1.Namespace{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-namespace", + Labels: map[string]string{"another": "label"}, + }, + }, + }, + }, nil + }) + return k8sutil.ClientSets{ + MClient: mClient, + KClient: kClient, + } + }, + }, + { + name: "AlertmanagerSelectorWithoutMatchLabels", + namespace: "test", + shouldFail: true, + getMockedClientSets: func(tc testCase) k8sutil.ClientSets { + mClient := monitoringclient.NewSimpleClientset(&monitoringv1.AlertmanagerList{}) + mClient.PrependReactor("get", "alertmanagers", func(_ clienttesting.Action) (bool, runtime.Object, error) { + return true, &monitoringv1.Alertmanager{ + ObjectMeta: metav1.ObjectMeta{ + Name: tc.name, + Namespace: tc.namespace, + }, + Spec: monitoringv1.AlertmanagerSpec{ + AlertmanagerConfigNamespaceSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"amconfig": "test"}, + }, + }, + }, nil + }) + + mClient.PrependReactor("get", "alertmanagerconfigs", func(_ clienttesting.Action) (bool, runtime.Object, error) { + return true, &monitoringv1alpha1.AlertmanagerConfigList{ + Items: []*monitoringv1alpha1.AlertmanagerConfig{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "alertmanagerconfigs-crd", + Namespace: tc.namespace, + Labels: map[string]string{"label": "another-value"}, + }, + }, + }, + }, nil + }) + return k8sutil.ClientSets{ + MClient: mClient, + } + }, + }, + { + name: "AlertmanagerFailedGetAMConfigs", + namespace: "test", + shouldFail: true, + getMockedClientSets: func(tc testCase) k8sutil.ClientSets { + mClient := monitoringclient.NewSimpleClientset(&monitoringv1.AlertmanagerList{}) + mClient.PrependReactor("get", "alertmanagers", func(_ clienttesting.Action) (bool, runtime.Object, error) { + return true, &monitoringv1.Alertmanager{ + ObjectMeta: metav1.ObjectMeta{ + Name: tc.name, + Namespace: tc.namespace, + }, + Spec: monitoringv1.AlertmanagerSpec{ + AlertmanagerConfiguration: &monitoringv1.AlertmanagerConfiguration{ + Name: "test-amconfig", + }, + }, + }, nil + }) + + mClient.PrependReactor("get", "alertmanagerconfigs", func(_ clienttesting.Action) (bool, runtime.Object, error) { + return true, nil, errors.NewNotFound(monitoringv1alpha1.Resource("alertmanagerconfigs"), tc.name) + }) + return k8sutil.ClientSets{ + MClient: mClient, + } + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + clientSets := tc.getMockedClientSets(tc) + err := RunAlertmanagerAnalyzer(context.Background(), &clientSets, tc.name, tc.namespace) + if tc.shouldFail { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/internal/analyzers/prometheus.go b/internal/analyzers/prometheus.go index 9919a2a..c7cbc93 100644 --- a/internal/analyzers/prometheus.go +++ b/internal/analyzers/prometheus.go @@ -67,23 +67,23 @@ func RunPrometheusAnalyzer(ctx context.Context, clientSets *k8sutil.ClientSets, } } - if err := checkResourceNamespaceSelectors(ctx, clientSets, prometheus.Spec.PodMonitorNamespaceSelector); err != nil { + if err := k8sutil.CheckResourceNamespaceSelectors(ctx, *clientSets, prometheus.Spec.PodMonitorNamespaceSelector); err != nil { return fmt.Errorf("podMonitorNamespaceSelector is not properly defined: %s", err) } - if err := checkResourceNamespaceSelectors(ctx, clientSets, prometheus.Spec.ProbeNamespaceSelector); err != nil { + if err := k8sutil.CheckResourceNamespaceSelectors(ctx, *clientSets, prometheus.Spec.ProbeNamespaceSelector); err != nil { return fmt.Errorf("probeNamespaceSelector is not properly defined: %s", err) } - if err := checkResourceNamespaceSelectors(ctx, clientSets, prometheus.Spec.ServiceMonitorNamespaceSelector); err != nil { + if err := k8sutil.CheckResourceNamespaceSelectors(ctx, *clientSets, prometheus.Spec.ServiceMonitorNamespaceSelector); err != nil { return fmt.Errorf("serviceMonitorNamespaceSelector is not properly defined: %s", err) } - if err := checkResourceNamespaceSelectors(ctx, clientSets, prometheus.Spec.ScrapeConfigNamespaceSelector); err != nil { + if err := k8sutil.CheckResourceNamespaceSelectors(ctx, *clientSets, prometheus.Spec.ScrapeConfigNamespaceSelector); err != nil { return fmt.Errorf("scrapeConfigNamespaceSelector is not properly defined: %s", err) } - if err := checkResourceNamespaceSelectors(ctx, clientSets, prometheus.Spec.RuleNamespaceSelector); err != nil { + if err := k8sutil.CheckResourceNamespaceSelectors(ctx, *clientSets, prometheus.Spec.RuleNamespaceSelector); err != nil { return fmt.Errorf("ruleNamespaceSelector is not properly defined: %s", err) } @@ -171,33 +171,6 @@ func checkClusterRoleRules(crb v1.ClusterRoleBinding, cr *v1.ClusterRole) error return nil } -func checkResourceNamespaceSelectors(ctx context.Context, clientSets *k8sutil.ClientSets, labelSelector *metav1.LabelSelector) error { - if labelSelector == nil { - return nil - } - - if len(labelSelector.MatchLabels) == 0 && len(labelSelector.MatchExpressions) == 0 { - return nil - } - - labelMap, err := metav1.LabelSelectorAsMap(labelSelector) - if err != nil { - return fmt.Errorf("invalid label selector format in %s: %v", labelSelector, err) - } - - namespaces, err := clientSets.KClient.CoreV1().Namespaces().List(ctx, metav1.ListOptions{LabelSelector: labels.SelectorFromSet(labelMap).String()}) - - if err != nil { - return fmt.Errorf("failed to list Namespaces in %s: %v", labelSelector, err) - } - - if len(namespaces.Items) == 0 { - return fmt.Errorf("no namespaces match the selector %s", labelSelector) - } - - return nil -} - func checkResourceLabelSelectors(ctx context.Context, clientSets *k8sutil.ClientSets, labelSelector *metav1.LabelSelector, resourceName, namespace string) error { if labelSelector == nil { return fmt.Errorf("%s selector is not defined", resourceName) diff --git a/internal/k8sutil/k8sutil.go b/internal/k8sutil/k8sutil.go index 3dd6a56..59b3625 100644 --- a/internal/k8sutil/k8sutil.go +++ b/internal/k8sutil/k8sutil.go @@ -16,6 +16,7 @@ package k8sutil import ( "bytes" + "context" "fmt" "io" "log/slog" @@ -26,10 +27,12 @@ import ( monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" monitoringv1alpha1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1alpha1" monitoringclient "github.com/prometheus-operator/prometheus-operator/pkg/client/versioned" + v1 "k8s.io/api/rbac/v1" apiv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" apiExtensions "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/client-go/dynamic" @@ -37,7 +40,6 @@ import ( clientgoscheme "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" - v1 "k8s.io/api/rbac/v1" ) var ApplyOption = metav1.ApplyOptions{ @@ -158,3 +160,30 @@ func IsServiceAccountBoundToRoleBindingList(clusterRoleBindings *v1.ClusterRoleB } return false } + +func CheckResourceNamespaceSelectors(ctx context.Context, clientSets ClientSets, labelSelector *metav1.LabelSelector) error { + if labelSelector == nil { + return nil + } + + if len(labelSelector.MatchLabels) == 0 && len(labelSelector.MatchExpressions) == 0 { + return nil + } + + labelMap, err := metav1.LabelSelectorAsMap(labelSelector) + if err != nil { + return fmt.Errorf("invalid label selector format in %s: %v", labelSelector, err) + } + + namespaces, err := clientSets.KClient.CoreV1().Namespaces().List(ctx, metav1.ListOptions{LabelSelector: labels.SelectorFromSet(labelMap).String()}) + + if err != nil { + return fmt.Errorf("failed to list Namespaces in %s: %v", labelSelector, err) + } + + if len(namespaces.Items) == 0 { + return fmt.Errorf("no namespaces match the selector %s", labelSelector) + } + + return nil +}