Merge branch 'master' into feature/helm_v3_overall_upgrade
commit
80c241991a
|
@ -13,7 +13,7 @@ echo "Packaging charts from source code"
|
|||
mkdir -p temp
|
||||
for d in chart/*
|
||||
do
|
||||
# shellcheck disable=SC2039
|
||||
# shellcheck disable=SC3010
|
||||
if [[ -d $d ]]
|
||||
then
|
||||
# Will generate a helm package per chart in a folder
|
||||
|
|
|
@ -81,6 +81,8 @@ The following table lists has the main configurable parameters (polling, trigger
|
|||
| ------------------------------------------- | -------------------------------------- | --------------------------------------------------------- |
|
||||
| `polling.enabled` | Docker registries polling | `true` |
|
||||
| `helmProvider.enabled` | Enable/disable Helm provider | `true` |
|
||||
| `helmProvider.helmDriver` | Set driver for Helm3 | `` |
|
||||
| `helmProvider.helmDriverSqlConnectionString`| Set SQL connection string for Helm3 | `` |
|
||||
| `gcr.enabled` | Enable/disable GCR Registry | `false` |
|
||||
| `gcr.projectId` | GCP Project ID GCR belongs to | |
|
||||
| `gcr.pubsub.enabled` | Enable/disable GCP Pub/Sub trigger | `false` |
|
||||
|
@ -97,6 +99,8 @@ The following table lists has the main configurable parameters (polling, trigger
|
|||
| `slack.token` | Slack token | |
|
||||
| `slack.channel` | Slack channel | |
|
||||
| `slack.approvalsChannel` | Slack channel for approvals | |
|
||||
| `teams.enabled` | Enable/disable MS Teams Notification | `false` |
|
||||
| `teams.webhookUrl` | MS Teams Connector's webhook url | |
|
||||
| `service.enabled` | Enable/disable Keel service | `false` |
|
||||
| `service.type` | Keel service type | `LoadBalancer` |
|
||||
| `service.externalIP` | Keel static IP | |
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
schemaVersion: v1
|
||||
summary: Security mitigation information for this application is tracked by the security-mitigation.yaml file that's part of this helm chart.
|
||||
mitigations: []
|
|
@ -55,10 +55,9 @@ spec:
|
|||
- name: HELM_PROVIDER
|
||||
value: "true"
|
||||
- name: HELM_DRIVER
|
||||
value: "{{ .Values.helmProvider.driver }}"
|
||||
value: "{{ .Values.helmProvider.helmDriver }}"
|
||||
- name: HELM_DRIVER_SQL_CONNECTION_STRING
|
||||
value: "{{ .Values.helmProvider.driverSqlConnectionString }}"
|
||||
{{- end }}
|
||||
value: "{{ .Values.helmProvider.helmDriverSqlConnectionString }}"
|
||||
{{- end }}
|
||||
{{- if .Values.gcr.enabled }}
|
||||
# Enable GCR with pub/sub support
|
||||
|
|
|
@ -24,6 +24,9 @@ data:
|
|||
HIPCHAT_TOKEN: {{ .Values.hipchat.token | b64enc}}
|
||||
HIPCHAT_APPROVALS_PASSWORT: {{ .Values.hipchat.password | b64enc }}
|
||||
{{- end }}
|
||||
{{- if .Values.teams.enabled }}
|
||||
TEAMS_WEBHOOK_URL: {{ .Values.teams.webhookUrl | b64enc }}
|
||||
{{- end }}
|
||||
{{- if and .Values.mail.enabled .Values.mail.smtp.pass }}
|
||||
MAIL_SMTP_PASS: {{ .Values.mail.smtp.pass | b64enc }}
|
||||
{{- end }}
|
||||
|
|
|
@ -16,10 +16,10 @@ helmProvider:
|
|||
# Additional Helm configuration, more info here:
|
||||
# https://helm.sh/docs/helm/helm/
|
||||
#
|
||||
# driver sets HELM_DRIVER
|
||||
driver: ""
|
||||
# driverSqlConnectionString sets HELM_DRIVER_SQL_CONNECTION_STRING
|
||||
driverSqlConnectionString: ""
|
||||
# helmDriver sets HELM_DRIVER
|
||||
helmDriver: ""
|
||||
# helmDriverSqlConnectionString sets HELM_DRIVER_SQL_CONNECTION_STRING
|
||||
helmDriverSqlConnectionString: ""
|
||||
|
||||
# Google Container Registry
|
||||
# GCP Project ID
|
||||
|
@ -67,10 +67,16 @@ hipchat:
|
|||
userName: ""
|
||||
password: ""
|
||||
|
||||
# Mattermost notifications
|
||||
mattermost:
|
||||
enabled: false
|
||||
endpoint: ""
|
||||
|
||||
# MS Teams notifications
|
||||
teams:
|
||||
enabled: false
|
||||
webhookUrl: ""
|
||||
|
||||
# Mail notifications
|
||||
mail:
|
||||
enabled: false
|
||||
|
|
|
@ -42,6 +42,7 @@ import (
|
|||
_ "github.com/keel-hq/keel/extension/notification/mail"
|
||||
_ "github.com/keel-hq/keel/extension/notification/mattermost"
|
||||
_ "github.com/keel-hq/keel/extension/notification/slack"
|
||||
_ "github.com/keel-hq/keel/extension/notification/teams"
|
||||
_ "github.com/keel-hq/keel/extension/notification/webhook"
|
||||
|
||||
// credentials helpers
|
||||
|
|
|
@ -31,6 +31,9 @@ const (
|
|||
EnvMattermostEndpoint = "MATTERMOST_ENDPOINT"
|
||||
EnvMattermostName = "MATTERMOST_USERNAME"
|
||||
|
||||
// MS Teams webhook url, see https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/connectors-using#setting-up-a-custom-incoming-webhook
|
||||
EnvTeamsWebhookUrl = "TEAMS_WEBHOOK_URL"
|
||||
|
||||
// Mail notification settings
|
||||
EnvMailTo = "MAIL_TO"
|
||||
EnvMailFrom = "MAIL_FROM"
|
||||
|
|
|
@ -12,4 +12,5 @@ The manifests in this are generated from the Helm chart automatically.
|
|||
The `values.yaml` files used to configure `keel` can be found in
|
||||
[`values`](./values/).
|
||||
|
||||
<!-- Deprecated -->
|
||||
They are automatically generated by running `./deployment/scripts/gen-deploy.sh`.
|
||||
|
|
|
@ -182,6 +182,9 @@ spec:
|
|||
# Enable mattermost endpoint
|
||||
- name: MATTERMOST_ENDPOINT
|
||||
value: ""
|
||||
# Enable MS Teams webhook endpoint
|
||||
- name: TEAMS_WEBHOOK_URL
|
||||
value: "{{ .teams_webhook_url }}"
|
||||
- name: SLACK_TOKEN
|
||||
value: "{{ .slack_token }}"
|
||||
- name: SLACK_CHANNELS
|
||||
|
|
|
@ -0,0 +1,142 @@
|
|||
package teams
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/keel-hq/keel/constants"
|
||||
"github.com/keel-hq/keel/extension/notification"
|
||||
"github.com/keel-hq/keel/types"
|
||||
"github.com/keel-hq/keel/version"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const timeout = 5 * time.Second
|
||||
|
||||
type sender struct {
|
||||
endpoint string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// Config represents the configuration of a Teams Webhook Sender.
|
||||
type Config struct {
|
||||
Endpoint string
|
||||
}
|
||||
|
||||
func init() {
|
||||
notification.RegisterSender("teams", &sender{})
|
||||
}
|
||||
|
||||
func (s *sender) Configure(config *notification.Config) (bool, error) {
|
||||
// Get configuration
|
||||
var httpConfig Config
|
||||
|
||||
if os.Getenv(constants.EnvTeamsWebhookUrl) != "" {
|
||||
httpConfig.Endpoint = os.Getenv(constants.EnvTeamsWebhookUrl)
|
||||
} else {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Validate endpoint URL.
|
||||
if httpConfig.Endpoint == "" {
|
||||
return false, nil
|
||||
}
|
||||
if _, err := url.ParseRequestURI(httpConfig.Endpoint); err != nil {
|
||||
return false, fmt.Errorf("could not parse endpoint URL: %s\n", err)
|
||||
}
|
||||
s.endpoint = httpConfig.Endpoint
|
||||
|
||||
// Setup HTTP client.
|
||||
s.client = &http.Client{
|
||||
Transport: http.DefaultTransport,
|
||||
Timeout: timeout,
|
||||
}
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"name": "teams",
|
||||
"webhook": s.endpoint,
|
||||
}).Info("extension.notification.teams: sender configured")
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
type SimpleTeamsMessageCard struct {
|
||||
AtContext string `json:"@context"`
|
||||
AtType string `json:"@type"`
|
||||
Sections []TeamsMessageSection `json:"sections"`
|
||||
Summary string `json:"summary"`
|
||||
ThemeColor string `json:"themeColor"`
|
||||
}
|
||||
|
||||
type TeamsMessageSection struct {
|
||||
ActivityImage string `json:"activityImage"`
|
||||
ActivitySubtitle string `json:"activitySubtitle"`
|
||||
ActivityText string `json:"activityText"`
|
||||
ActivityTitle string `json:"activityTitle"`
|
||||
Facts []TeamsFact `json:"facts"`
|
||||
Markdown bool `json:"markdown"`
|
||||
}
|
||||
|
||||
type TeamsFact struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// Microsoft Teams expects the hexidecimal formatted color to not have a "#" at the front
|
||||
// Source: https://stackoverflow.com/a/48798875/2199949
|
||||
func TrimFirstChar(s string) string {
|
||||
for i := range s {
|
||||
if i > 0 {
|
||||
// The value i is the index in s of the second
|
||||
// character. Slice to remove the first character.
|
||||
return s[i:]
|
||||
}
|
||||
}
|
||||
// There are 0 or 1 characters in the string.
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *sender) Send(event types.EventNotification) error {
|
||||
// Marshal notification.
|
||||
jsonNotification, err := json.Marshal(SimpleTeamsMessageCard{
|
||||
AtType: "MessageCard",
|
||||
AtContext: "http://schema.org/extensions",
|
||||
ThemeColor: TrimFirstChar(event.Level.Color()),
|
||||
Summary: event.Type.String(),
|
||||
Sections: []TeamsMessageSection{
|
||||
{
|
||||
ActivityImage: constants.KeelLogoURL,
|
||||
ActivityText: fmt.Sprintf("*%s*: %s", event.Name, event.Message),
|
||||
ActivityTitle: fmt.Sprintf("**%s**",event.Type.String()),
|
||||
Facts: []TeamsFact{
|
||||
{
|
||||
Name: "Version",
|
||||
Value: fmt.Sprintf("[https://keel.sh](https://keel.sh) %s", version.GetKeelVersion().Version),
|
||||
},
|
||||
},
|
||||
Markdown: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not marshal: %s", err)
|
||||
}
|
||||
|
||||
// Send notification via HTTP POST.
|
||||
resp, err := s.client.Post(s.endpoint, "application/json", bytes.NewBuffer(jsonNotification))
|
||||
if err != nil || resp == nil || (resp.StatusCode != 200 && resp.StatusCode != 201) {
|
||||
if resp != nil {
|
||||
return fmt.Errorf("got status %d, expected 200/201", resp.StatusCode)
|
||||
}
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
package teams
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"fmt"
|
||||
|
||||
"github.com/keel-hq/keel/constants"
|
||||
"github.com/keel-hq/keel/types"
|
||||
"github.com/keel-hq/keel/version"
|
||||
)
|
||||
|
||||
func TestTrimLeftChar(t *testing.T) {
|
||||
fmt.Printf("%q\n", "Hello, 世界")
|
||||
fmt.Printf("%q\n", TrimFirstChar(""))
|
||||
fmt.Printf("%q\n", TrimFirstChar("H"))
|
||||
fmt.Printf("%q\n", TrimFirstChar("世"))
|
||||
fmt.Printf("%q\n", TrimFirstChar("Hello"))
|
||||
fmt.Printf("%q\n", TrimFirstChar("世界"))
|
||||
}
|
||||
|
||||
func TestTeamsRequest(t *testing.T) {
|
||||
handler := func(resp http.ResponseWriter, req *http.Request) {
|
||||
body, err := ioutil.ReadAll(req.Body)
|
||||
if err != nil {
|
||||
t.Errorf("failed to parse body: %s", err)
|
||||
}
|
||||
|
||||
bodyStr := string(body)
|
||||
|
||||
if !strings.Contains(bodyStr, "MessageCard") {
|
||||
t.Errorf("missing MessageCard indicator")
|
||||
}
|
||||
|
||||
if !strings.Contains(bodyStr, "themeColor") {
|
||||
t.Errorf("missing themeColor")
|
||||
}
|
||||
|
||||
if !strings.Contains(bodyStr, constants.KeelLogoURL) {
|
||||
t.Errorf("missing logo url")
|
||||
}
|
||||
|
||||
if !strings.Contains(bodyStr, "**" + types.NotificationPreDeploymentUpdate.String() + "**") {
|
||||
t.Errorf("missing deployment type")
|
||||
}
|
||||
|
||||
if !strings.Contains(bodyStr, version.GetKeelVersion().Version) {
|
||||
t.Errorf("missing version")
|
||||
}
|
||||
|
||||
if !strings.Contains(bodyStr, "update deployment") {
|
||||
t.Errorf("missing name")
|
||||
}
|
||||
if !strings.Contains(bodyStr, "message here") {
|
||||
t.Errorf("missing message")
|
||||
}
|
||||
|
||||
t.Log(bodyStr)
|
||||
|
||||
}
|
||||
|
||||
// create test server with handler
|
||||
ts := httptest.NewServer(http.HandlerFunc(handler))
|
||||
defer ts.Close()
|
||||
|
||||
s := &sender{
|
||||
endpoint: ts.URL,
|
||||
client: &http.Client{},
|
||||
}
|
||||
|
||||
s.Send(types.EventNotification{
|
||||
Name: "update deployment",
|
||||
Message: "message here",
|
||||
Type: types.NotificationPreDeploymentUpdate,
|
||||
})
|
||||
}
|
1
go.sum
1
go.sum
|
@ -775,6 +775,7 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
|
|||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
|
|
|
@ -81,7 +81,7 @@ func (s *TriggerServer) harborHandler(resp http.ResponseWriter, req *http.Reques
|
|||
"event": hn,
|
||||
}).Debug("harborHandler: received event, looking for a pushImage tag")
|
||||
|
||||
if hn.Type == "pushImage" {
|
||||
if hn.Type == "pushImage" || hn.Type == "PUSH_ARTIFACT" {
|
||||
// go trough all the ressource items
|
||||
for _, e := range hn.EventData.Resources {
|
||||
imageRepo, err := image.Parse(e.ResourceURL)
|
||||
|
|
|
@ -31,6 +31,30 @@ var fakeHarborWebhook = ` {
|
|||
}
|
||||
`
|
||||
|
||||
var fakeHarborWebhook2 = `
|
||||
{
|
||||
"type": "PUSH_ARTIFACT",
|
||||
"occur_at": 1582640688,
|
||||
"operator": "user",
|
||||
"event_data": {
|
||||
"resources": [
|
||||
{
|
||||
"digest": "sha256:b4758aaed11c155a476b9857e1178f157759c99cb04c907a04993f5481eff848",
|
||||
"tag": "latest",
|
||||
"resource_url": "quay.io/mynamespace/repository:1.2.3"
|
||||
}
|
||||
],
|
||||
"repository": {
|
||||
"date_created": 1582634337,
|
||||
"name": "repository",
|
||||
"namespace": "mynamespace",
|
||||
"repo_full_name": "mynamespace/repository",
|
||||
"repo_type": "private"
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
func TestHarborWebhookHandler(t *testing.T) {
|
||||
|
||||
fp := &fakeProvider{}
|
||||
|
@ -65,6 +89,40 @@ func TestHarborWebhookHandler(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestHarborWebhookHandler2(t *testing.T) {
|
||||
|
||||
fp := &fakeProvider{}
|
||||
srv, teardown := NewTestingServer(fp)
|
||||
defer teardown()
|
||||
|
||||
req, err := http.NewRequest("POST", "/v1/webhooks/harbor", bytes.NewBuffer([]byte(fakeHarborWebhook2)))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create req: %s", err)
|
||||
}
|
||||
|
||||
//The response recorder used to record HTTP responses
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
srv.router.ServeHTTP(rec, req)
|
||||
if rec.Code != 200 {
|
||||
t.Errorf("unexpected status code: %d", rec.Code)
|
||||
|
||||
t.Log(rec.Body.String())
|
||||
}
|
||||
|
||||
if len(fp.submitted) != 1 {
|
||||
t.Fatalf("unexpected number of events submitted: %d", len(fp.submitted))
|
||||
}
|
||||
|
||||
if fp.submitted[0].Repository.Name != "quay.io/mynamespace/repository" {
|
||||
t.Errorf("expected quay.io/mynamespace/repository but got %s", fp.submitted[0].Repository.Name)
|
||||
}
|
||||
|
||||
if fp.submitted[0].Repository.Tag != "1.2.3" {
|
||||
t.Errorf("expected 1.2.3 but got %s", fp.submitted[0].Repository.Tag)
|
||||
}
|
||||
}
|
||||
|
||||
var fakeHarborWebhookMalformed = ` {
|
||||
"type": "pushImage",
|
||||
"occur_at": 1582640688,
|
||||
|
|
Loading…
Reference in New Issue