diff --git a/artifacts/flagger/crd.yaml b/artifacts/flagger/crd.yaml index d2fb67dda..3bfd3ed5d 100644 --- a/artifacts/flagger/crd.yaml +++ b/artifacts/flagger/crd.yaml @@ -1178,6 +1178,9 @@ spec: address: description: Hook URL address of this provider type: string + proxy: + description: Http/s proxy of this provider + type: string secretRef: description: Kubernetes secret reference containing the provider address type: object diff --git a/charts/flagger/README.md b/charts/flagger/README.md index ed4353e69..51ac698a1 100644 --- a/charts/flagger/README.md +++ b/charts/flagger/README.md @@ -125,9 +125,11 @@ Parameter | Description | Default `configTracking.enabled` | If `true`, flagger will track changes in Secrets and ConfigMaps referenced in the target deployment | `true` `eventWebhook` | If set, Flagger will publish events to the given webhook | None `slack.url` | Slack incoming webhook | None +`slack.proxyUrl` | Slack proxy url | None `slack.channel` | Slack channel | None `slack.user` | Slack username | `flagger` `msteams.url` | Microsoft Teams incoming webhook | None +`msteams.proxyUrl` | Microsoft Teams proxy url | None `podMonitor.enabled` | If `true`, create a PodMonitor for [monitoring the metrics](https://docs.flagger.app/usage/monitoring#metrics) | `false` `podMonitor.namespace` | Namespace where the PodMonitor is created | the same namespace `podMonitor.interval` | Interval at which metrics should be scraped | `15s` diff --git a/charts/flagger/crds/crd.yaml b/charts/flagger/crds/crd.yaml index d2fb67dda..3bfd3ed5d 100644 --- a/charts/flagger/crds/crd.yaml +++ b/charts/flagger/crds/crd.yaml @@ -1178,6 +1178,9 @@ spec: address: description: Hook URL address of this provider type: string + proxy: + description: Http/s proxy of this provider + type: string secretRef: description: Kubernetes secret reference containing the provider address type: object diff --git a/charts/flagger/templates/deployment.yaml b/charts/flagger/templates/deployment.yaml index 3d8b246c2..ad0ed2591 100644 --- a/charts/flagger/templates/deployment.yaml +++ b/charts/flagger/templates/deployment.yaml @@ -90,6 +90,9 @@ spec: {{- if .Values.slack.url }} - -slack-url={{ .Values.slack.url }} {{- end }} + {{- if .Values.slack.proxyUrl }} + - -slack-proxy-url={{ .Values.slack.proxyUrl }} + {{- end }} {{- if .Values.slack.user }} - -slack-user={{ .Values.slack.user }} {{- end }} @@ -99,6 +102,9 @@ spec: {{- if .Values.msteams.url }} - -msteams-url={{ .Values.msteams.url }} {{- end }} + {{- if .Values.msteams.proxyUrl }} + - -msteams-proxy-url={{ .Values.msteams.proxyUrl }} + {{- end }} {{- if .Values.leaderElection.enabled }} - -enable-leader-election=true - -leader-election-namespace={{ .Release.Namespace }} diff --git a/charts/flagger/values.yaml b/charts/flagger/values.yaml index c4d5ec26f..8d7d09780 100644 --- a/charts/flagger/values.yaml +++ b/charts/flagger/values.yaml @@ -55,6 +55,7 @@ slack: channel: # incoming webhook https://api.slack.com/incoming-webhooks url: + proxy: msteams: # MS Teams incoming webhook URL @@ -72,11 +73,21 @@ podMonitor: # secretKeyRef: # name: slack # key: url +#- name: SLACK_PROXY_URL +# valueFrom: +# secretKeyRef: +# name: slack +# key: proxy-url #- name: MSTEAMS_URL # valueFrom: # secretKeyRef: # name: msteams # key: url +#- name: MSTEAMS_PROXY_URL +# valueFrom: +# secretKeyRef: +# name: msteams +# key: proxy-url #- name: EVENT_WEBHOOK_URL # valueFrom: # secretKeyRef: diff --git a/cmd/flagger/main.go b/cmd/flagger/main.go index 6a9411071..1bab4ec2e 100644 --- a/cmd/flagger/main.go +++ b/cmd/flagger/main.go @@ -63,8 +63,10 @@ var ( logLevel string port string msteamsURL string + msteamsProxyURL string includeLabelPrefix string slackURL string + slackProxyURL string slackUser string slackChannel string eventWebhook string @@ -93,10 +95,12 @@ func init() { flag.StringVar(&logLevel, "log-level", "debug", "Log level can be: debug, info, warning, error.") flag.StringVar(&port, "port", "8080", "Port to listen on.") flag.StringVar(&slackURL, "slack-url", "", "Slack hook URL.") + flag.StringVar(&slackProxyURL, "slack-proxy-url", "", "Slack proxy URL.") flag.StringVar(&slackUser, "slack-user", "flagger", "Slack user name.") flag.StringVar(&slackChannel, "slack-channel", "", "Slack channel.") flag.StringVar(&eventWebhook, "event-webhook", "", "Webhook for publishing flagger events") flag.StringVar(&msteamsURL, "msteams-url", "", "MS Teams incoming webhook URL.") + flag.StringVar(&msteamsProxyURL, "msteams-proxy-url", "", "MS Teams proxy URL.") flag.StringVar(&includeLabelPrefix, "include-label-prefix", "", "List of prefixes of labels that are copied when creating primary deployments or daemonsets. Use * to include all.") flag.IntVar(&threadiness, "threadiness", 2, "Worker concurrency.") flag.BoolVar(&zapReplaceGlobals, "zap-replace-globals", false, "Whether to change the logging level of the global zap logger.") @@ -349,11 +353,13 @@ func startLeaderElection(ctx context.Context, run func(), ns string, kubeClient func initNotifier(logger *zap.SugaredLogger) (client notifier.Interface) { provider := "slack" notifierURL := fromEnv("SLACK_URL", slackURL) + notifierProxyURL := fromEnv("SLACK_PROXY_URL", slackProxyURL) if msteamsURL != "" || os.Getenv("MSTEAMS_URL") != "" { provider = "msteams" notifierURL = fromEnv("MSTEAMS_URL", msteamsURL) + notifierProxyURL = fromEnv("MSTEAMS_PROXY_URL", msteamsProxyURL) } - notifierFactory := notifier.NewFactory(notifierURL, slackUser, slackChannel) + notifierFactory := notifier.NewFactory(notifierURL, notifierProxyURL, slackUser, slackChannel) var err error client, err = notifierFactory.Notifier(provider) diff --git a/docs/gitbook/usage/alerting.md b/docs/gitbook/usage/alerting.md index a2636178d..7cee7a24a 100644 --- a/docs/gitbook/usage/alerting.md +++ b/docs/gitbook/usage/alerting.md @@ -20,6 +20,7 @@ Once the webhook has been generated. Flagger can be configured to send Slack not ```bash helm upgrade -i flagger flagger/flagger \ --set slack.url=https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK \ +--set slack.proxy-url=my-http-proxy.com \ # optional http/s proxy --set slack.channel=general \ --set slack.user=flagger ``` @@ -41,7 +42,8 @@ Flagger can be configured to send notifications to Microsoft Teams: ```bash helm upgrade -i flagger flagger/flagger \ ---set msteams.url=https://outlook.office.com/webhook/YOUR/TEAMS/WEBHOOK +--set msteams.url=https://outlook.office.com/webhook/YOUR/TEAMS/WEBHOOK \ +--set msteams.proxy-url=my-http-proxy.com # optional http/s proxy ``` Similar to Slack, Flagger alerts on canary analysis events: @@ -71,6 +73,8 @@ spec: username: flagger # webhook address (ignored if secretRef is specified) address: https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK + # optional http/s proxy + proxy: http://my-http-proxy.com # secret containing the webhook address (optional) secretRef: name: on-call-url diff --git a/kustomize/base/flagger/crd.yaml b/kustomize/base/flagger/crd.yaml index 8db3506f4..abd1f4a84 100644 --- a/kustomize/base/flagger/crd.yaml +++ b/kustomize/base/flagger/crd.yaml @@ -1177,6 +1177,9 @@ spec: address: description: Hook URL address of this provider type: string + proxy: + description: Http/s proxy of this provider + type: string secretRef: description: Kubernetes secret reference containing the provider address type: object diff --git a/pkg/apis/flagger/v1beta1/alert.go b/pkg/apis/flagger/v1beta1/alert.go index 90a5c923a..baabb8987 100644 --- a/pkg/apis/flagger/v1beta1/alert.go +++ b/pkg/apis/flagger/v1beta1/alert.go @@ -64,6 +64,10 @@ type AlertProviderSpec struct { // +optional Address string `json:"address,omitempty"` + // HTTP/S address of the proxy + // +optional + Proxy string `json:"proxy,omitempty"` + // Secret reference containing the provider webhook URL // +optional SecretRef *corev1.LocalObjectReference `json:"secretRef,omitempty"` diff --git a/pkg/controller/events.go b/pkg/controller/events.go index ee8e4c0d5..c70123e66 100644 --- a/pkg/controller/events.go +++ b/pkg/controller/events.go @@ -149,9 +149,13 @@ func (c *Controller) alert(canary *flaggerv1.Canary, message string, metadata bo if provider.Spec.Channel != "" { channel = provider.Spec.Channel } + proxy := "" + if provider.Spec.Proxy != "" { + proxy = provider.Spec.Proxy + } // create notifier based on provider type - f := notifier.NewFactory(url, username, channel) + f := notifier.NewFactory(url, proxy, username, channel) n, err := f.Notifier(provider.Spec.Type) if err != nil { c.logger.With("canary", fmt.Sprintf("%s.%s", canary.Name, canary.Namespace)). diff --git a/pkg/notifier/client.go b/pkg/notifier/client.go index 0e0f7495e..e1cee44e9 100644 --- a/pkg/notifier/client.go +++ b/pkg/notifier/client.go @@ -22,11 +22,35 @@ import ( "encoding/json" "fmt" "io/ioutil" + "net" "net/http" + "net/url" + "runtime" "time" ) -func postMessage(address string, payload interface{}) error { +func postMessage(address string, proxy string, payload interface{}) error { + httpClient := http.DefaultClient + + if proxy != "" { + proxyURL, err := url.Parse(proxy) + if err != nil { + return fmt.Errorf("unable to parse proxy URL '%s', error: %w", proxy, err) + } + httpClient.Transport = &http.Transport{ + Proxy: http.ProxyURL(proxyURL), + DialContext: (&net.Dialer{ + Timeout: 15 * time.Second, + KeepAlive: 30 * time.Second, + }).DialContext, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + MaxIdleConnsPerHost: runtime.GOMAXPROCS(0) + 1, + } + } + data, err := json.Marshal(payload) if err != nil { return fmt.Errorf("marshalling notification payload failed: %w", err) @@ -43,7 +67,7 @@ func postMessage(address string, payload interface{}) error { ctx, cancel := context.WithTimeout(req.Context(), 5*time.Second) defer cancel() - res, err := http.DefaultClient.Do(req.WithContext(ctx)) + res, err := httpClient.Do(req.WithContext(ctx)) if err != nil { return fmt.Errorf("sending notification failed: %w", err) } diff --git a/pkg/notifier/client_test.go b/pkg/notifier/client_test.go index 022dbfb3d..086cf83b8 100644 --- a/pkg/notifier/client_test.go +++ b/pkg/notifier/client_test.go @@ -39,6 +39,6 @@ func Test_postMessage(t *testing.T) { })) defer ts.Close() - err := postMessage(ts.URL, map[string]string{"status": "success"}) + err := postMessage(ts.URL, "", map[string]string{"status": "success"}) require.NoError(t, err) } diff --git a/pkg/notifier/discord.go b/pkg/notifier/discord.go index 0f6553df8..9a8be1a9a 100644 --- a/pkg/notifier/discord.go +++ b/pkg/notifier/discord.go @@ -27,12 +27,13 @@ import ( // Discord holds the hook URL type Discord struct { URL string + ProxyURL string Username string Channel string } // NewDiscord validates the URL and returns a Discord object -func NewDiscord(hookURL string, username string, channel string) (*Discord, error) { +func NewDiscord(hookURL string, proxyURL string, username string, channel string) (*Discord, error) { webhook, err := url.ParseRequestURI(hookURL) if err != nil { return nil, fmt.Errorf("invalid Discord hook URL %s", hookURL) @@ -56,6 +57,7 @@ func NewDiscord(hookURL string, username string, channel string) (*Discord, erro return &Discord{ Channel: channel, URL: hookURL, + ProxyURL: proxyURL, Username: username, }, nil } @@ -88,7 +90,7 @@ func (s *Discord) Post(workload string, namespace string, message string, fields payload.Attachments = []SlackAttachment{a} - err := postMessage(s.URL, payload) + err := postMessage(s.URL, s.ProxyURL, payload) if err != nil { return fmt.Errorf("postMessage failed: %w", err) } diff --git a/pkg/notifier/discord_test.go b/pkg/notifier/discord_test.go index bc33a5a10..95dd7ac68 100644 --- a/pkg/notifier/discord_test.go +++ b/pkg/notifier/discord_test.go @@ -45,7 +45,7 @@ func TestDiscord_Post(t *testing.T) { })) defer ts.Close() - discord, err := NewDiscord(ts.URL, "test", "test") + discord, err := NewDiscord(ts.URL, "", "test", "test") require.NoError(t, err) assert.True(t, strings.HasSuffix(discord.URL, "/slack")) diff --git a/pkg/notifier/factory.go b/pkg/notifier/factory.go index bc4ab46c7..16b833e04 100644 --- a/pkg/notifier/factory.go +++ b/pkg/notifier/factory.go @@ -22,13 +22,15 @@ import ( type Factory struct { URL string + ProxyURL string Username string Channel string } -func NewFactory(url string, username string, channel string) *Factory { +func NewFactory(url string, proxy string, username string, channel string) *Factory { return &Factory{ URL: url, + ProxyURL: proxy, Channel: channel, Username: username, } @@ -43,13 +45,13 @@ func (f Factory) Notifier(provider string) (Interface, error) { var err error switch provider { case "slack": - n, err = NewSlack(f.URL, f.Username, f.Channel) + n, err = NewSlack(f.URL, f.ProxyURL, f.Username, f.Channel) case "discord": - n, err = NewDiscord(f.URL, f.Username, f.Channel) + n, err = NewDiscord(f.URL, f.ProxyURL, f.Username, f.Channel) case "rocket": - n, err = NewRocket(f.URL, f.Username, f.Channel) + n, err = NewRocket(f.URL, f.ProxyURL, f.Username, f.Channel) case "msteams": - n, err = NewMSTeams(f.URL) + n, err = NewMSTeams(f.URL, f.ProxyURL) default: err = fmt.Errorf("provider %s not supported", provider) } diff --git a/pkg/notifier/rocket.go b/pkg/notifier/rocket.go index dff6f142f..f9d0a6c5a 100644 --- a/pkg/notifier/rocket.go +++ b/pkg/notifier/rocket.go @@ -25,12 +25,13 @@ import ( // Rocket holds the hook URL type Rocket struct { URL string + ProxyURL string Username string Channel string } // NewRocket validates the Rocket URL and returns a Rocket object -func NewRocket(hookURL string, username string, channel string) (*Rocket, error) { +func NewRocket(hookURL string, proxyUrl string, username string, channel string) (*Rocket, error) { _, err := url.ParseRequestURI(hookURL) if err != nil { return nil, fmt.Errorf("invalid Rocket hook URL %s", hookURL) @@ -47,6 +48,7 @@ func NewRocket(hookURL string, username string, channel string) (*Rocket, error) return &Rocket{ Channel: channel, URL: hookURL, + ProxyURL: proxyUrl, Username: username, }, nil } @@ -79,7 +81,7 @@ func (s *Rocket) Post(workload string, namespace string, message string, fields payload.Attachments = []SlackAttachment{a} - err := postMessage(s.URL, payload) + err := postMessage(s.URL, s.ProxyURL, payload) if err != nil { return fmt.Errorf("postMessage failed: %w", err) } diff --git a/pkg/notifier/rocket_test.go b/pkg/notifier/rocket_test.go index bd45e7ec2..84a7a0008 100644 --- a/pkg/notifier/rocket_test.go +++ b/pkg/notifier/rocket_test.go @@ -44,7 +44,7 @@ func TestRocket_Post(t *testing.T) { })) defer ts.Close() - rocket, err := NewRocket(ts.URL, "test", "test") + rocket, err := NewRocket(ts.URL, "", "test", "test") require.NoError(t, err) err = rocket.Post("podinfo", "test", "test", fields, "error") diff --git a/pkg/notifier/slack.go b/pkg/notifier/slack.go index 50688dadb..c5ca88782 100644 --- a/pkg/notifier/slack.go +++ b/pkg/notifier/slack.go @@ -25,6 +25,7 @@ import ( // Slack holds the hook URL type Slack struct { URL string + ProxyURL string Username string Channel string } @@ -55,7 +56,7 @@ type SlackField struct { } // NewSlack validates the Slack URL and returns a Slack object -func NewSlack(hookURL string, username string, channel string) (*Slack, error) { +func NewSlack(hookURL string, proxyURL string, username string, channel string) (*Slack, error) { _, err := url.ParseRequestURI(hookURL) if err != nil { return nil, fmt.Errorf("invalid Slack hook URL %s", hookURL) @@ -72,6 +73,7 @@ func NewSlack(hookURL string, username string, channel string) (*Slack, error) { return &Slack{ Channel: channel, URL: hookURL, + ProxyURL: proxyURL, Username: username, }, nil } @@ -104,7 +106,7 @@ func (s *Slack) Post(workload string, namespace string, message string, fields [ payload.Attachments = []SlackAttachment{a} - err := postMessage(s.URL, payload) + err := postMessage(s.URL, s.ProxyURL, payload) if err != nil { return fmt.Errorf("postMessage failed: %w", err) } diff --git a/pkg/notifier/slack_test.go b/pkg/notifier/slack_test.go index add5f57f8..ac0d9698f 100644 --- a/pkg/notifier/slack_test.go +++ b/pkg/notifier/slack_test.go @@ -44,7 +44,7 @@ func TestSlack_Post(t *testing.T) { })) defer ts.Close() - slack, err := NewSlack(ts.URL, "test", "test") + slack, err := NewSlack(ts.URL, "", "test", "test") require.NoError(t, err) err = slack.Post("podinfo", "test", "test", fields, "error") diff --git a/pkg/notifier/teams.go b/pkg/notifier/teams.go index c97833c57..77701b7f5 100644 --- a/pkg/notifier/teams.go +++ b/pkg/notifier/teams.go @@ -23,7 +23,8 @@ import ( // MS Teams holds the incoming webhook URL type MSTeams struct { - URL string + URL string + ProxyURL string } // MSTeamsPayload holds the message card data @@ -48,14 +49,15 @@ type MSTeamsField struct { } // NewMSTeams validates the MS Teams URL and returns a MSTeams object -func NewMSTeams(hookURL string) (*MSTeams, error) { +func NewMSTeams(hookURL string, proxyURL string) (*MSTeams, error) { _, err := url.ParseRequestURI(hookURL) if err != nil { return nil, fmt.Errorf("invalid MS Teams webhook URL %s", hookURL) } return &MSTeams{ - URL: hookURL, + URL: hookURL, + ProxyURL: proxyURL, }, nil } @@ -84,7 +86,7 @@ func (s *MSTeams) Post(workload string, namespace string, message string, fields payload.ThemeColor = "FF0000" } - err := postMessage(s.URL, payload) + err := postMessage(s.URL, s.ProxyURL, payload) if err != nil { return fmt.Errorf("postMessage failed: %w", err) } diff --git a/pkg/notifier/teams_test.go b/pkg/notifier/teams_test.go index 191c77992..ad439a238 100644 --- a/pkg/notifier/teams_test.go +++ b/pkg/notifier/teams_test.go @@ -45,7 +45,7 @@ func TestTeams_Post(t *testing.T) { })) defer ts.Close() - teams, err := NewMSTeams(ts.URL) + teams, err := NewMSTeams(ts.URL, "") require.NoError(t, err) err = teams.Post("podinfo", "test", "test", fields, "info")