diff --git a/api/v1beta1/provider_types.go b/api/v1beta1/provider_types.go index 63ac893ff..71d027e0c 100644 --- a/api/v1beta1/provider_types.go +++ b/api/v1beta1/provider_types.go @@ -28,7 +28,7 @@ const ( // ProviderSpec defines the desired state of Provider type ProviderSpec struct { // Type of provider - // +kubebuilder:validation:Enum=slack;discord;msteams;rocket;generic;github;gitlab;bitbucket;azuredevops;googlechat + // +kubebuilder:validation:Enum=slack;discord;msteams;rocket;generic;github;gitlab;bitbucket;azuredevops;googlechat;webex // +required Type string `json:"type"` @@ -69,6 +69,7 @@ const ( BitbucketProvider string = "bitbucket" AzureDevOpsProvider string = "azuredevops" GoogleChatProvider string = "googlechat" + WebexProvider string = "webex" ) // ProviderStatus defines the observed state of Provider diff --git a/config/crd/bases/notification.toolkit.fluxcd.io_providers.yaml b/config/crd/bases/notification.toolkit.fluxcd.io_providers.yaml index 511361fdd..0a64b85a7 100644 --- a/config/crd/bases/notification.toolkit.fluxcd.io_providers.yaml +++ b/config/crd/bases/notification.toolkit.fluxcd.io_providers.yaml @@ -80,6 +80,7 @@ spec: - bitbucket - azuredevops - googlechat + - webex type: string username: description: Bot username for this provider diff --git a/docs/spec/v1beta1/provider.md b/docs/spec/v1beta1/provider.md index 597adc25a..52ca94ca0 100644 --- a/docs/spec/v1beta1/provider.md +++ b/docs/spec/v1beta1/provider.md @@ -9,7 +9,7 @@ Spec: ```go type ProviderSpec struct { // Type of provider - // +kubebuilder:validation:Enum=slack;discord;msteams;rocket;generic;github;gitlab;bitbucket;azuredevops;googlechat + // +kubebuilder:validation:Enum=slack;discord;msteams;rocket;generic;github;gitlab;bitbucket;azuredevops;googlechat;webex // +required Type string `json:"type"` @@ -44,6 +44,7 @@ Notification providers: * Microsoft Teams * Rocket * Google Chat +* Webex * Generic webhook Git commit status providers: @@ -104,7 +105,7 @@ kubectl create secret generic webhook-url \ Note that the secret must contain an `address` field. -The provider type can be: `slack`, `msteams`, `rocket`, `discord`, `googlechat`, `github`, `gitlab`, `bitbucket`, `azuredevops` or `generic`. +The provider type can be: `slack`, `msteams`, `rocket`, `discord`, `googlechat`, `webex`, `github`, `gitlab`, `bitbucket`, `azuredevops` or `generic`. When type `generic` is specified, the notification controller will post the incoming [event](event.md) in JSON format to the webhook address. diff --git a/internal/notifier/factory.go b/internal/notifier/factory.go index 3fdd7c651..a9f3b3fce 100644 --- a/internal/notifier/factory.go +++ b/internal/notifier/factory.go @@ -68,6 +68,8 @@ func (f Factory) Notifier(provider string) (Interface, error) { n, err = NewAzureDevOps(f.URL, f.Token) case v1beta1.GoogleChatProvider: n, err = NewGoogleChat(f.URL, f.ProxyURL) + case v1beta1.WebexProvider: + n, err = NewWebex(f.URL, f.ProxyURL) default: err = fmt.Errorf("provider %s not supported", provider) } diff --git a/internal/notifier/webex.go b/internal/notifier/webex.go new file mode 100644 index 000000000..969cf97f1 --- /dev/null +++ b/internal/notifier/webex.go @@ -0,0 +1,78 @@ +/* +Copyright 2020 The Flux 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 notifier + +import ( + "fmt" + "net/url" + "strings" + + "github.com/fluxcd/pkg/runtime/events" +) + +// Webex holds the hook URL +type Webex struct { + URL string + ProxyURL string +} + +// WebexPayload holds the message text +type WebexPayload struct { + Text string `json:"text,omitempty"` + Markdown string `json:"markdown,omitempty"` +} + +// NewWebex validates the Webex URL and returns a Webex object +func NewWebex(hookURL, proxyURL string) (*Webex, error) { + _, err := url.ParseRequestURI(hookURL) + if err != nil { + return nil, fmt.Errorf("invalid Webex hook URL %s", hookURL) + } + + return &Webex{ + URL: hookURL, + ProxyURL: proxyURL, + }, nil +} + +// Post Webex message +func (s *Webex) Post(event events.Event) error { + // Skip any update events + if isCommitStatus(event.Metadata, "update") { + return nil + } + + objName := fmt.Sprintf("%s/%s.%s", strings.ToLower(event.InvolvedObject.Kind), event.InvolvedObject.Name, event.InvolvedObject.Namespace) + markdown := fmt.Sprintf("> **NAME** = %s | **MESSAGE** = %s", objName, event.Message) + + if len(event.Metadata) > 0 { + markdown += " | **METADATA** =" + for k, v := range event.Metadata { + markdown += fmt.Sprintf(" **%s**: %s", k, v) + } + } + + payload := WebexPayload{ + Text: "", + Markdown: markdown, + } + + if err := postMessage(s.URL, s.ProxyURL, payload); err != nil { + return fmt.Errorf("postMessage failed: %w", err) + } + return nil +} diff --git a/internal/notifier/webex_test.go b/internal/notifier/webex_test.go new file mode 100644 index 000000000..14ba0cfe0 --- /dev/null +++ b/internal/notifier/webex_test.go @@ -0,0 +1,57 @@ +/* +Copyright 2020 The Flux 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 notifier + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestWebex_Post(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + b, err := ioutil.ReadAll(r.Body) + require.NoError(t, err) + + var payload = WebexPayload{} + err = json.Unmarshal(b, &payload) + require.NoError(t, err) + require.Empty(t, payload.Text) + require.Equal(t, "> **NAME** = gitrepository/webapp.gitops-system | **MESSAGE** = message | **METADATA** = **test**: metadata", payload.Markdown) + })) + defer ts.Close() + + webex, err := NewWebex(ts.URL, "") + require.NoError(t, err) + + err = webex.Post(testEvent()) + require.NoError(t, err) +} + +func TestWebex_PostUpdate(t *testing.T) { + webex, err := NewWebex("http://localhost", "") + require.NoError(t, err) + + event := testEvent() + event.Metadata["commit_status"] = "update" + err = webex.Post(event) + require.NoError(t, err) +}