Skip to content

Commit

Permalink
add initial support for alertmanager external-url (#622)
Browse files Browse the repository at this point in the history
* add initial support for alertmanager external-url

* fixed test

* address review feedback - 1
  • Loading branch information
pintohutch authored Oct 25, 2023
1 parent 1d8832a commit 9fcee3d
Show file tree
Hide file tree
Showing 8 changed files with 96 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,9 @@ spec:
required:
- key
x-kubernetes-map-type: atomic
externalURL:
type: string
description: ExternalURL is the URL under which Alertmanager is externally reachable (for example, if Alertmanager is served via a reverse proxy). Used for generating relative and absolute links back to Alertmanager itself. If the URL has a path portion, it will be used to prefix all HTTP endpoints served by Alertmanager. If omitted, relevant URL components will be derived automatically.
rules:
type: object
description: Rules specifies how the operator configures and deployes rule-evaluator.
Expand Down
1 change: 1 addition & 0 deletions doc/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@ ManagedAlertmanagerSpec holds configuration information for the managed Alertman
| Field | Description | Scheme | Required |
| ----- | ----------- | ------ | -------- |
| configSecret | ConfigSecret refers to the name of a single-key Secret in the public namespace that holds the managed Alertmanager config file. | *corev1.SecretKeySelector | false |
| externalURL | ExternalURL is the URL under which Alertmanager is externally reachable (for example, if Alertmanager is served via a reverse proxy). Used for generating relative and absolute links back to Alertmanager itself. If the URL has a path portion, it will be used to prefix all HTTP endpoints served by Alertmanager. If omitted, relevant URL components will be derived automatically. | string | false |

[Back to TOC](#table-of-contents)

Expand Down
29 changes: 27 additions & 2 deletions e2e/alertmanager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"context"
"errors"
"fmt"
"strings"
"testing"
"time"

Expand Down Expand Up @@ -74,6 +75,7 @@ route:
},
Key: "my-secret-key",
},
ExternalURL: "https://alertmanager.mycompany.com/",
},
&corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Expand Down Expand Up @@ -108,15 +110,15 @@ func testAlertmanager(ctx context.Context, t *OperatorContext, spec *monitoringv
})
t.Run("deployed", t.subtest(func(ctx context.Context, t *OperatorContext) {
t.Parallel()
testAlertmanagerDeployed(ctx, t)
testAlertmanagerDeployed(ctx, t, spec)
}))
t.Run("config set", t.subtest(func(ctx context.Context, t *OperatorContext) {
t.Parallel()
testAlertmanagerConfig(ctx, t, secret, key)
}))
}

func testAlertmanagerDeployed(ctx context.Context, t *OperatorContext) {
func testAlertmanagerDeployed(ctx context.Context, t *OperatorContext, config *monitoringv1.ManagedAlertmanagerSpec) {
err := wait.Poll(time.Second, 1*time.Minute, func() (bool, error) {
var ss appsv1.StatefulSet
if err := t.Client().Get(ctx, client.ObjectKey{Namespace: t.namespace, Name: operator.NameAlertmanager}, &ss); err != nil {
Expand All @@ -135,6 +137,29 @@ func testAlertmanagerDeployed(ctx context.Context, t *OperatorContext) {
return false, fmt.Errorf("unexpected annotations (-want, +got): %s", diff)
}

// If config spec is empty, no need to assert EXTRA_ARGS.
if config == nil {
return true, nil
}

// TODO(pintohutch): clean-up wantArgs init logic.
var wantArgs []string
for _, c := range ss.Spec.Template.Spec.Containers {
if c.Name != "alertmanager" {
continue
}
// We're mainly interested in the dynamic flags but checking the entire set including
// the static ones is ultimately simpler.
if externalURL := config.ExternalURL; externalURL != "" {
wantArgs = append(wantArgs, fmt.Sprintf("--web.external-url=%q", config.ExternalURL))
}

if diff := cmp.Diff(strings.Join(wantArgs, " "), getEnvVar(c.Env, "EXTRA_ARGS")); diff != "" {
return false, fmt.Errorf("unexpected flags (-want, +got): %s", diff)
}
return true, nil
}

return true, nil
})
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions e2e/collector_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ func testCollectorDeployed(ctx context.Context, t *OperatorContext) {
return false, fmt.Errorf("unexpected annotations (-want, +got): %s", diff)
}

// TODO(pintohutch): clean-up wantArgs init logic.
for _, c := range ds.Spec.Template.Spec.Containers {
if c.Name != operator.CollectorPrometheusContainerName {
continue
Expand Down
1 change: 1 addition & 0 deletions e2e/ruler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ func testRuleEvaluatorDeployment(ctx context.Context, t *OperatorContext) {
return false, fmt.Errorf("unexpected annotations (-want, +got): %s", diff)
}

// TODO(pintohutch): clean-up wantArgs init logic.
for _, c := range deploy.Spec.Template.Spec.Containers {
if c.Name != "evaluator" {
continue
Expand Down
3 changes: 3 additions & 0 deletions manifests/setup.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -986,6 +986,9 @@ spec:
required:
- key
x-kubernetes-map-type: atomic
externalURL:
type: string
description: ExternalURL is the URL under which Alertmanager is externally reachable (for example, if Alertmanager is served via a reverse proxy). Used for generating relative and absolute links back to Alertmanager itself. If the URL has a path portion, it will be used to prefix all HTTP endpoints served by Alertmanager. If omitted, relevant URL components will be derived automatically.
rules:
type: object
description: Rules specifies how the operator configures and deployes rule-evaluator.
Expand Down
7 changes: 7 additions & 0 deletions pkg/operator/apis/monitoring/v1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,13 @@ type ManagedAlertmanagerSpec struct {
// ConfigSecret refers to the name of a single-key Secret in the public namespace that
// holds the managed Alertmanager config file.
ConfigSecret *corev1.SecretKeySelector `json:"configSecret,omitempty"`
// ExternalURL is the URL under which Alertmanager is externally reachable
// (for example, if Alertmanager is served via a reverse proxy).
// Used for generating relative and absolute links back to Alertmanager
// itself. If the URL has a path portion, it will be used to prefix all HTTP
// endpoints served by Alertmanager.
// If omitted, relevant URL components will be derived automatically.
ExternalURL string `json:"externalURL,omitempty"`
}

// AlertmanagerEndpoints defines a selection of a single Endpoints object
Expand Down
53 changes: 53 additions & 0 deletions pkg/operator/operator_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,10 +220,15 @@ func (r *operatorConfigReconciler) Reconcile(ctx context.Context, req reconcile.
return reconcile.Result{}, fmt.Errorf("ensure rule-evaluator config: %w", err)
}

// Ensure the alertmanager configuration is pulled from the spec.
if err := r.ensureAlertmanagerConfigSecret(ctx, config.ManagedAlertmanager); err != nil {
return reconcile.Result{}, fmt.Errorf("ensure alertmanager config secret: %w", err)
}

if err := r.ensureAlertmanagerStatefulSet(ctx, config.ManagedAlertmanager); err != nil {
return reconcile.Result{}, fmt.Errorf("ensure alertmanager statefulset: %w", err)
}

// Mirror the fetched secret data to where the rule-evaluator can
// mount and access.
if err := r.ensureRuleEvaluatorSecrets(ctx, secretData); err != nil {
Expand Down Expand Up @@ -383,6 +388,54 @@ func (r *operatorConfigReconciler) ensureAlertmanagerConfigSecret(ctx context.Co
return nil
}

// ensureAlertmanagerStatefulSet configures the managed Alertmanager instance
// to reflect the provided spec.
func (r *operatorConfigReconciler) ensureAlertmanagerStatefulSet(ctx context.Context, spec *monitoringv1.ManagedAlertmanagerSpec) error {
if spec == nil {
return nil
}

logger, _ := logr.FromContext(ctx)

var sset appsv1.StatefulSet
err := r.client.Get(ctx, client.ObjectKey{Namespace: r.opts.OperatorNamespace, Name: NameAlertmanager}, &sset)
// Some users deliberately not want to run the alertmanager.
// Only emit a warning but don't cause retries
// as this logic gets re-triggered anyway if the StatefulSet is created later.
if apierrors.IsNotFound(err) {
logger.Error(err, "Alertmanager StatefulSet does not exist")
return nil
}
if err != nil {
return err
}

var flags []string
if externalURL := spec.ExternalURL; externalURL != "" {
flags = append(flags, fmt.Sprintf("--web.external-url=%q", externalURL))
}

// Set EXTRA_ARGS envvar in alertmanager container.
for i, c := range sset.Spec.Template.Spec.Containers {
if c.Name != "alertmanager" {
continue
}
var repl []corev1.EnvVar

for _, ev := range c.Env {
if ev.Name != "EXTRA_ARGS" {
repl = append(repl, ev)
}
}
repl = append(repl, corev1.EnvVar{Name: "EXTRA_ARGS", Value: strings.Join(flags, " ")})

sset.Spec.Template.Spec.Containers[i].Env = repl
}

// Upsert alertmanager StatefulSet.
return r.client.Update(ctx, &sset)
}

// ensureRuleEvaluatorDeployment reconciles the Deployment for rule-evaluator.
func (r *operatorConfigReconciler) ensureRuleEvaluatorDeployment(ctx context.Context, spec *monitoringv1.RuleEvaluatorSpec) error {
logger, _ := logr.FromContext(ctx)
Expand Down

0 comments on commit 9fcee3d

Please sign in to comment.