diff --git a/config/config.go b/config/config.go index eb8bc7b868..7c2a5bdcc8 100644 --- a/config/config.go +++ b/config/config.go @@ -38,6 +38,137 @@ import ( "github.com/prometheus/alertmanager/tracing" ) +<<<<<<< HEAD +======= +const secretToken = "" + +var secretTokenJSON string + +func init() { + b, err := json.Marshal(secretToken) + if err != nil { + panic(err) + } + secretTokenJSON = string(b) +} + +// URL is a custom type that represents an HTTP or HTTPS URL and allows validation at configuration load time. +type URL struct { + *url.URL +} + +// Copy makes a deep-copy of the struct. +func (u *URL) Copy() *URL { + v := *u.URL + return &URL{&v} +} + +// MarshalYAML implements the yaml.Marshaler interface for URL. +func (u URL) MarshalYAML() (any, error) { + if u.URL != nil { + return u.String(), nil + } + return nil, nil +} + +// UnmarshalYAML implements the yaml.Unmarshaler interface for URL. +func (u *URL) UnmarshalYAML(unmarshal func(any) error) error { + var s string + if err := unmarshal(&s); err != nil { + return err + } + urlp, err := parseURL(s) + if err != nil { + return err + } + u.URL = urlp.URL + return nil +} + +// MarshalJSON implements the json.Marshaler interface for URL. +func (u URL) MarshalJSON() ([]byte, error) { + if u.URL != nil { + return json.Marshal(u.String()) + } + return []byte("null"), nil +} + +// UnmarshalJSON implements the json.Marshaler interface for URL. +func (u *URL) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + urlp, err := parseURL(s) + if err != nil { + return err + } + u.URL = urlp.URL + return nil +} + +// SecretURL is a URL that must not be revealed on marshaling. +type SecretURL URL + +// MarshalYAML implements the yaml.Marshaler interface for SecretURL. +func (s SecretURL) MarshalYAML() (any, error) { + if s.URL != nil { + if commoncfg.MarshalSecretValue { + return s.String(), nil + } + return secretToken, nil + } + return nil, nil +} + +// UnmarshalYAML implements the yaml.Unmarshaler interface for SecretURL. +func (s *SecretURL) UnmarshalYAML(unmarshal func(any) error) error { + var str string + if err := unmarshal(&str); err != nil { + return err + } + // In order to deserialize a previously serialized configuration (eg from + // the Alertmanager API with amtool), `` needs to be treated + // specially, as it isn't a valid URL. + if str == secretToken { + s.URL = &url.URL{} + return nil + } + return unmarshal((*URL)(s)) +} + +// MarshalJSON implements the json.Marshaler interface for SecretURL. +func (s SecretURL) MarshalJSON() ([]byte, error) { + if s.URL == nil { + return json.Marshal("") + } + if commoncfg.MarshalSecretValue { + return json.Marshal(s.String()) + } + return json.Marshal(secretToken) +} + +// UnmarshalJSON implements the json.Marshaler interface for SecretURL. +func (s *SecretURL) UnmarshalJSON(data []byte) error { + // In order to deserialize a previously serialized configuration (eg from + // the Alertmanager API with amtool), `` needs to be treated + // specially, as it isn't a valid URL. + if string(data) == secretToken || string(data) == secretTokenJSON { + s.URL = &url.URL{} + return nil + } + // Redact the secret URL in case of errors + if err := json.Unmarshal(data, (*URL)(s)); err != nil { + if commoncfg.MarshalSecretValue { + return err + } + return errors.New(strings.ReplaceAll(err.Error(), string(data), "[REDACTED]")) + } + + return nil +} + +>>>>>>> 87c1d1c3 (refactor: replace config.Secret with commoncfg.Secret) // containsTemplating checks if the string contains template syntax. func containsTemplating(s string) (bool, error) { if !strings.Contains(s, "{{") { @@ -801,6 +932,7 @@ type GlobalConfig struct { HTTPConfig *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"` +<<<<<<< HEAD JiraAPIURL *amcommoncfg.URL `yaml:"jira_api_url,omitempty" json:"jira_api_url,omitempty"` SMTPFrom string `yaml:"smtp_from,omitempty" json:"smtp_from,omitempty"` SMTPHello string `yaml:"smtp_hello,omitempty" json:"smtp_hello,omitempty"` @@ -841,6 +973,46 @@ type GlobalConfig struct { RocketchatTokenIDFile string `yaml:"rocketchat_token_id_file,omitempty" json:"rocketchat_token_id_file,omitempty"` MattermostWebhookURL *amcommoncfg.SecretURL `yaml:"mattermost_webhook_url,omitempty" json:"mattermost_webhook_url,omitempty"` MattermostWebhookURLFile string `yaml:"mattermost_webhook_url_file,omitempty" json:"mattermost_webhook_url_file,omitempty"` +======= + JiraAPIURL *URL `yaml:"jira_api_url,omitempty" json:"jira_api_url,omitempty"` + SMTPFrom string `yaml:"smtp_from,omitempty" json:"smtp_from,omitempty"` + SMTPHello string `yaml:"smtp_hello,omitempty" json:"smtp_hello,omitempty"` + SMTPSmarthost HostPort `yaml:"smtp_smarthost,omitempty" json:"smtp_smarthost,omitempty"` + SMTPAuthUsername string `yaml:"smtp_auth_username,omitempty" json:"smtp_auth_username,omitempty"` + SMTPAuthPassword commoncfg.Secret `yaml:"smtp_auth_password,omitempty" json:"smtp_auth_password,omitempty"` + SMTPAuthPasswordFile string `yaml:"smtp_auth_password_file,omitempty" json:"smtp_auth_password_file,omitempty"` + SMTPAuthSecret commoncfg.Secret `yaml:"smtp_auth_secret,omitempty" json:"smtp_auth_secret,omitempty"` + SMTPAuthSecretFile string `yaml:"smtp_auth_secret_file,omitempty" json:"smtp_auth_secret_file,omitempty"` + SMTPAuthIdentity string `yaml:"smtp_auth_identity,omitempty" json:"smtp_auth_identity,omitempty"` + SMTPRequireTLS bool `yaml:"smtp_require_tls" json:"smtp_require_tls,omitempty"` + SMTPTLSConfig *commoncfg.TLSConfig `yaml:"smtp_tls_config,omitempty" json:"smtp_tls_config,omitempty"` + SMTPForceImplicitTLS *bool `yaml:"smtp_force_implicit_tls,omitempty" json:"smtp_force_implicit_tls,omitempty"` + SlackAPIURL *SecretURL `yaml:"slack_api_url,omitempty" json:"slack_api_url,omitempty"` + SlackAPIURLFile string `yaml:"slack_api_url_file,omitempty" json:"slack_api_url_file,omitempty"` + SlackAppToken commoncfg.Secret `yaml:"slack_app_token,omitempty" json:"slack_app_token,omitempty"` + SlackAppTokenFile string `yaml:"slack_app_token_file,omitempty" json:"slack_app_token_file,omitempty"` + SlackAppURL *URL `yaml:"slack_app_url,omitempty" json:"slack_app_url,omitempty"` + PagerdutyURL *URL `yaml:"pagerduty_url,omitempty" json:"pagerduty_url,omitempty"` + OpsGenieAPIURL *URL `yaml:"opsgenie_api_url,omitempty" json:"opsgenie_api_url,omitempty"` + OpsGenieAPIKey commoncfg.Secret `yaml:"opsgenie_api_key,omitempty" json:"opsgenie_api_key,omitempty"` + OpsGenieAPIKeyFile string `yaml:"opsgenie_api_key_file,omitempty" json:"opsgenie_api_key_file,omitempty"` + WeChatAPIURL *URL `yaml:"wechat_api_url,omitempty" json:"wechat_api_url,omitempty"` + WeChatAPISecret commoncfg.Secret `yaml:"wechat_api_secret,omitempty" json:"wechat_api_secret,omitempty"` + WeChatAPISecretFile string `yaml:"wechat_api_secret_file,omitempty" json:"wechat_api_secret_file,omitempty"` + WeChatAPICorpID string `yaml:"wechat_api_corp_id,omitempty" json:"wechat_api_corp_id,omitempty"` + VictorOpsAPIURL *URL `yaml:"victorops_api_url,omitempty" json:"victorops_api_url,omitempty"` + VictorOpsAPIKey commoncfg.Secret `yaml:"victorops_api_key,omitempty" json:"victorops_api_key,omitempty"` + VictorOpsAPIKeyFile string `yaml:"victorops_api_key_file,omitempty" json:"victorops_api_key_file,omitempty"` + TelegramAPIUrl *URL `yaml:"telegram_api_url,omitempty" json:"telegram_api_url,omitempty"` + TelegramBotToken commoncfg.Secret `yaml:"telegram_bot_token,omitempty" json:"telegram_bot_token,omitempty"` + TelegramBotTokenFile string `yaml:"telegram_bot_token_file,omitempty" json:"telegram_bot_token_file,omitempty"` + WebexAPIURL *URL `yaml:"webex_api_url,omitempty" json:"webex_api_url,omitempty"` + RocketchatAPIURL *URL `yaml:"rocketchat_api_url,omitempty" json:"rocketchat_api_url,omitempty"` + RocketchatToken *commoncfg.Secret `yaml:"rocketchat_token,omitempty" json:"rocketchat_token,omitempty"` + RocketchatTokenFile string `yaml:"rocketchat_token_file,omitempty" json:"rocketchat_token_file,omitempty"` + RocketchatTokenID *commoncfg.Secret `yaml:"rocketchat_token_id,omitempty" json:"rocketchat_token_id,omitempty"` + RocketchatTokenIDFile string `yaml:"rocketchat_token_id_file,omitempty" json:"rocketchat_token_id_file,omitempty"` +>>>>>>> 87c1d1c3 (refactor: replace config.Secret with commoncfg.Secret) } // UnmarshalYAML implements the yaml.Unmarshaler interface for GlobalConfig. diff --git a/config/config_test.go b/config/config_test.go index 7686c6128d..2297a0e87d 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -538,6 +538,205 @@ func TestJSONMarshal(t *testing.T) { } } +<<<<<<< HEAD +======= +func TestJSONMarshalHideSecretURL(t *testing.T) { + urlp, err := url.Parse("http://example.com/") + if err != nil { + t.Fatal(err) + } + u := &SecretURL{urlp} + + c, err := json.Marshal(u) + if err != nil { + t.Fatal(err) + } + // u003c -> "<" + // u003e -> ">" + require.Equal(t, "\"\\u003csecret\\u003e\"", string(c), "SecretURL not properly elided in JSON.") + // Check that the marshaled data can be unmarshaled again. + out := &SecretURL{} + err = json.Unmarshal(c, out) + if err != nil { + t.Fatal(err) + } + + c, err = yaml.Marshal(u) + if err != nil { + t.Fatal(err) + } + require.Equal(t, "\n", string(c), "SecretURL not properly elided in YAML.") + // Check that the marshaled data can be unmarshaled again. + out = &SecretURL{} + err = yaml.Unmarshal(c, &out) + if err != nil { + t.Fatal(err) + } +} + +func TestUnmarshalSecretURL(t *testing.T) { + b := []byte(`"http://example.com/se cret"`) + var u SecretURL + + err := json.Unmarshal(b, &u) + if err != nil { + t.Fatal(err) + } + require.Equal(t, "http://example.com/se%20cret", u.String(), "SecretURL not properly unmarshaled in JSON.") + + err = yaml.Unmarshal(b, &u) + if err != nil { + t.Fatal(err) + } + + require.Equal(t, "http://example.com/se%20cret", u.String(), "SecretURL not properly unmarshaled in YAML.") +} + +func TestHideSecretURL(t *testing.T) { + b := []byte(`"://wrongurl/"`) + var u SecretURL + + err := json.Unmarshal(b, &u) + require.Error(t, err) + require.NotContains(t, err.Error(), "wrongurl") +} + +func TestShowMarshalSecretURL(t *testing.T) { + commoncfg.MarshalSecretValue = true + defer func() { commoncfg.MarshalSecretValue = false }() + + b := []byte(`"://wrongurl/"`) + var u SecretURL + + err := json.Unmarshal(b, &u) + require.Error(t, err) + require.Contains(t, err.Error(), "wrongurl") +} + +func TestMarshalURL(t *testing.T) { + for name, tc := range map[string]struct { + input *URL + expectedJSON string + expectedYAML string + }{ + "url": { + input: mustParseURL("http://example.com/"), + expectedJSON: "\"http://example.com/\"", + expectedYAML: "http://example.com/\n", + }, + + "wrapped nil value": { + input: &URL{}, + expectedJSON: "null", + expectedYAML: "null\n", + }, + + "wrapped empty URL": { + input: &URL{&url.URL{}}, + expectedJSON: "\"\"", + expectedYAML: "\"\"\n", + }, + } { + t.Run(name, func(t *testing.T) { + j, err := json.Marshal(tc.input) + require.NoError(t, err) + require.Equal(t, tc.expectedJSON, string(j), "URL not properly marshaled into JSON.") + + y, err := yaml.Marshal(tc.input) + require.NoError(t, err) + require.Equal(t, tc.expectedYAML, string(y), "URL not properly marshaled into YAML.") + }) + } +} + +func TestUnmarshalNilURL(t *testing.T) { + b := []byte(`null`) + + { + var u URL + err := json.Unmarshal(b, &u) + require.Error(t, err, "unsupported scheme \"\" for URL") + } + + { + var u URL + err := yaml.Unmarshal(b, &u) + require.NoError(t, err) + } +} + +func TestUnmarshalEmptyURL(t *testing.T) { + b := []byte(`""`) + + { + var u URL + err := json.Unmarshal(b, &u) + require.Error(t, err, "unsupported scheme \"\" for URL") + require.Equal(t, (*url.URL)(nil), u.URL) + } + + { + var u URL + err := yaml.Unmarshal(b, &u) + require.Error(t, err, "unsupported scheme \"\" for URL") + require.Equal(t, (*url.URL)(nil), u.URL) + } +} + +func TestUnmarshalURL(t *testing.T) { + b := []byte(`"http://example.com/a b"`) + var u URL + + err := json.Unmarshal(b, &u) + if err != nil { + t.Fatal(err) + } + require.Equal(t, "http://example.com/a%20b", u.String(), "URL not properly unmarshaled in JSON.") + + err = yaml.Unmarshal(b, &u) + if err != nil { + t.Fatal(err) + } + require.Equal(t, "http://example.com/a%20b", u.String(), "URL not properly unmarshaled in YAML.") +} + +func TestUnmarshalInvalidURL(t *testing.T) { + for _, b := range [][]byte{ + []byte(`"://example.com"`), + []byte(`"http:example.com"`), + []byte(`"telnet://example.com"`), + } { + var u URL + + err := json.Unmarshal(b, &u) + if err == nil { + t.Errorf("Expected an error unmarshaling %q from JSON", string(b)) + } + + err = yaml.Unmarshal(b, &u) + if err == nil { + t.Errorf("Expected an error unmarshaling %q from YAML", string(b)) + } + t.Logf("%s", err) + } +} + +func TestUnmarshalRelativeURL(t *testing.T) { + b := []byte(`"/home"`) + var u URL + + err := json.Unmarshal(b, &u) + if err == nil { + t.Errorf("Expected an error parsing URL") + } + + err = yaml.Unmarshal(b, &u) + if err == nil { + t.Errorf("Expected an error parsing URL") + } +} + +>>>>>>> 87c1d1c3 (refactor: replace config.Secret with commoncfg.Secret) func TestMarshalRegexpWithNilValue(t *testing.T) { r := &Regexp{} diff --git a/config/notifiers.go b/config/notifiers.go index 3da2e9cf69..0cd1843d67 100644 --- a/config/notifiers.go +++ b/config/notifiers.go @@ -530,11 +530,19 @@ type SlackConfig struct { HTTPConfig *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"` +<<<<<<< HEAD APIURL *amcommoncfg.SecretURL `yaml:"api_url,omitempty" json:"api_url,omitempty"` APIURLFile string `yaml:"api_url_file,omitempty" json:"api_url_file,omitempty"` AppToken commoncfg.Secret `yaml:"app_token,omitempty" json:"app_token,omitempty"` AppTokenFile string `yaml:"app_token_file,omitempty" json:"app_token_file,omitempty"` AppURL *amcommoncfg.URL `yaml:"app_url,omitempty" json:"app_url,omitempty"` +======= + APIURL *SecretURL `yaml:"api_url,omitempty" json:"api_url,omitempty"` + APIURLFile string `yaml:"api_url_file,omitempty" json:"api_url_file,omitempty"` + AppToken commoncfg.Secret `yaml:"app_token,omitempty" json:"app_token,omitempty"` + AppTokenFile string `yaml:"app_token_file,omitempty" json:"app_token_file,omitempty"` + AppURL *URL `yaml:"app_url,omitempty" json:"app_url,omitempty"` +>>>>>>> 87c1d1c3 (refactor: replace config.Secret with commoncfg.Secret) // Slack channel override, (like #other-channel or @username). Channel string `yaml:"channel,omitempty" json:"channel,omitempty"` @@ -682,7 +690,11 @@ type WechatConfig struct { APISecretFile string `yaml:"api_secret_file,omitempty" json:"api_secret_file,omitempty"` CorpID string `yaml:"corp_id,omitempty" json:"corp_id,omitempty"` Message string `yaml:"message,omitempty" json:"message,omitempty"` +<<<<<<< HEAD APIURL *amcommoncfg.URL `yaml:"api_url,omitempty" json:"api_url,omitempty"` +======= + APIURL *URL `yaml:"api_url,omitempty" json:"api_url,omitempty"` +>>>>>>> 87c1d1c3 (refactor: replace config.Secret with commoncfg.Secret) ToUser string `yaml:"to_user,omitempty" json:"to_user,omitempty"` ToParty string `yaml:"to_party,omitempty" json:"to_party,omitempty"` ToTag string `yaml:"to_tag,omitempty" json:"to_tag,omitempty"` @@ -923,7 +935,11 @@ type TelegramConfig struct { HTTPConfig *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"` +<<<<<<< HEAD APIUrl *amcommoncfg.URL `yaml:"api_url" json:"api_url,omitempty"` +======= + APIUrl *URL `yaml:"api_url" json:"api_url,omitempty"` +>>>>>>> 87c1d1c3 (refactor: replace config.Secret with commoncfg.Secret) BotToken commoncfg.Secret `yaml:"bot_token,omitempty" json:"token,omitempty"` BotTokenFile string `yaml:"bot_token_file,omitempty" json:"token_file,omitempty"` ChatID int64 `yaml:"chat_id,omitempty" json:"chat,omitempty"` @@ -1117,7 +1133,11 @@ type RocketchatConfig struct { HTTPConfig *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"` +<<<<<<< HEAD APIURL *amcommoncfg.URL `yaml:"api_url,omitempty" json:"api_url,omitempty"` +======= + APIURL *URL `yaml:"api_url,omitempty" json:"api_url,omitempty"` +>>>>>>> 87c1d1c3 (refactor: replace config.Secret with commoncfg.Secret) TokenID *commoncfg.Secret `yaml:"token_id,omitempty" json:"token_id,omitempty"` TokenIDFile string `yaml:"token_id_file,omitempty" json:"token_id_file,omitempty"` Token *commoncfg.Secret `yaml:"token,omitempty" json:"token,omitempty"` diff --git a/docs/high_availability.md b/docs/high_availability.md index 55b8d0cdb6..451f970c3a 100644 --- a/docs/high_availability.md +++ b/docs/high_availability.md @@ -4,8 +4,6 @@ sort_rank: 4 nav_icon: network --- -# High Availability - Alertmanager supports configuration to create a cluster for high availability. This document describes how the HA mechanism works, its design goals, and operational considerations. ## Design Goals diff --git a/notify/opsgenie/opsgenie_test.go b/notify/opsgenie/opsgenie_test.go index ad7ad04044..06542e5f8c 100644 --- a/notify/opsgenie/opsgenie_test.go +++ b/notify/opsgenie/opsgenie_test.go @@ -60,7 +60,11 @@ func TestOpsGenieRedactedURL(t *testing.T) { key := "key" notifier, err := New( &config.OpsGenieConfig{ +<<<<<<< HEAD APIURL: &amcommoncfg.URL{URL: u}, +======= + APIURL: &config.URL{URL: u}, +>>>>>>> 87c1d1c3 (refactor: replace config.Secret with commoncfg.Secret) APIKey: commoncfg.Secret(key), HTTPConfig: &commoncfg.HTTPClientConfig{}, }, diff --git a/notify/pagerduty/pagerduty_test.go b/notify/pagerduty/pagerduty_test.go index 302b4c3d3b..7e03cf8320 100644 --- a/notify/pagerduty/pagerduty_test.go +++ b/notify/pagerduty/pagerduty_test.go @@ -102,7 +102,11 @@ func TestPagerDutyRedactedURLV2(t *testing.T) { key := "01234567890123456789012345678901" notifier, err := New( &config.PagerdutyConfig{ +<<<<<<< HEAD URL: &amcommoncfg.URL{URL: u}, +======= + URL: &config.URL{URL: u}, +>>>>>>> 87c1d1c3 (refactor: replace config.Secret with commoncfg.Secret) RoutingKey: commoncfg.Secret(key), HTTPConfig: &commoncfg.HTTPClientConfig{}, }, @@ -536,7 +540,11 @@ func TestPagerDutyEmptySrcHref(t *testing.T) { pagerDutyConfig := config.PagerdutyConfig{ HTTPConfig: &commoncfg.HTTPClientConfig{}, RoutingKey: commoncfg.Secret("01234567890123456789012345678901"), +<<<<<<< HEAD URL: &amcommoncfg.URL{URL: url}, +======= + URL: &config.URL{URL: url}, +>>>>>>> 87c1d1c3 (refactor: replace config.Secret with commoncfg.Secret) Images: images, Links: links, } @@ -604,7 +612,11 @@ func TestPagerDutyTimeout(t *testing.T) { cfg := config.PagerdutyConfig{ HTTPConfig: &commoncfg.HTTPClientConfig{}, RoutingKey: commoncfg.Secret("01234567890123456789012345678901"), +<<<<<<< HEAD URL: &amcommoncfg.URL{URL: u}, +======= + URL: &config.URL{URL: u}, +>>>>>>> 87c1d1c3 (refactor: replace config.Secret with commoncfg.Secret) Timeout: tt.timeout, } diff --git a/notify/victorops/victorops_test.go b/notify/victorops/victorops_test.go index 7766c6f9cb..71d9d33637 100644 --- a/notify/victorops/victorops_test.go +++ b/notify/victorops/victorops_test.go @@ -109,7 +109,11 @@ func TestVictorOpsRedactedURL(t *testing.T) { secret := "secret" notifier, err := New( &config.VictorOpsConfig{ +<<<<<<< HEAD APIURL: &amcommoncfg.URL{URL: u}, +======= + APIURL: &config.URL{URL: u}, +>>>>>>> 87c1d1c3 (refactor: replace config.Secret with commoncfg.Secret) APIKey: commoncfg.Secret(secret), HTTPConfig: &commoncfg.HTTPClientConfig{}, },