From dd13a6dcebe4a4593b182bf1cbde81e9addef9e2 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Tue, 26 Oct 2021 21:02:03 +1100 Subject: [PATCH 01/17] feat: rewrite logs2x services --- services/logs2notifications/Dockerfile | 38 +++ services/logs2notifications/go.mod | 20 ++ services/logs2notifications/go.sum | 187 +++++++++++++ .../internal/handler/main.go | 225 ++++++++++++++++ .../internal/handler/main_test.go | 18 ++ .../internal/handler/rocketchat_events.go | 227 ++++++++++++++++ .../internal/handler/slack_events.go | 64 +++++ .../_lgraphql/projectNotifications.graphql | 41 +++ .../internal/lagoon/client/client.go | 93 +++++++ .../internal/lagoon/client/client_test.go | 81 ++++++ .../internal/lagoon/client/helper_test.go | 6 + .../lagoon/client/lgraphql/lgraphql.go | 247 ++++++++++++++++++ .../internal/lagoon/client/query.go | 25 ++ .../internal/lagoon/jwt/jwt.go | 30 +++ .../internal/lagoon/project.go | 20 ++ .../internal/schema/notifications.go | 107 ++++++++ .../internal/schema/project.go | 8 + services/logs2notifications/main.go | 185 +++++++++++++ 18 files changed, 1622 insertions(+) create mode 100644 services/logs2notifications/Dockerfile create mode 100644 services/logs2notifications/go.mod create mode 100644 services/logs2notifications/go.sum create mode 100644 services/logs2notifications/internal/handler/main.go create mode 100644 services/logs2notifications/internal/handler/main_test.go create mode 100644 services/logs2notifications/internal/handler/rocketchat_events.go create mode 100644 services/logs2notifications/internal/handler/slack_events.go create mode 100644 services/logs2notifications/internal/lagoon/client/_lgraphql/projectNotifications.graphql create mode 100644 services/logs2notifications/internal/lagoon/client/client.go create mode 100644 services/logs2notifications/internal/lagoon/client/client_test.go create mode 100644 services/logs2notifications/internal/lagoon/client/helper_test.go create mode 100644 services/logs2notifications/internal/lagoon/client/lgraphql/lgraphql.go create mode 100644 services/logs2notifications/internal/lagoon/client/query.go create mode 100644 services/logs2notifications/internal/lagoon/jwt/jwt.go create mode 100644 services/logs2notifications/internal/lagoon/project.go create mode 100644 services/logs2notifications/internal/schema/notifications.go create mode 100644 services/logs2notifications/internal/schema/project.go create mode 100644 services/logs2notifications/main.go diff --git a/services/logs2notifications/Dockerfile b/services/logs2notifications/Dockerfile new file mode 100644 index 0000000000..34ef5e9920 --- /dev/null +++ b/services/logs2notifications/Dockerfile @@ -0,0 +1,38 @@ +# build the binary +ARG GO_VERSION +ARG UPSTREAM_REPO +ARG UPSTREAM_TAG +FROM golang:${GO_VERSION:-1.13.8} AS builder +# bring in all the packages +COPY . /go/src/github.com/uselagoon/lagoon/services/logs2notifications/ +WORKDIR /go/src/github.com/uselagoon/lagoon/services/logs2notifications/ + +# tests currently don't work because mocking rabbit is interesting +RUN GO111MODULE=on go test ./... +# compile +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o logs2notifications . + +# put the binary into container +# use the commons image to get entrypoints +FROM ${UPSTREAM_REPO:-uselagoon}/commons:${UPSTREAM_TAG:-latest} + +ARG LAGOON_VERSION +ENV LAGOON_VERSION=$LAGOON_VERSION + +WORKDIR /app/ + +# bring the auto-idler binary from the builder +COPY --from=builder /go/src/github.com/uselagoon/lagoon/services/logs2notifications/logs2notifications . + +ENV LAGOON=logs2notifications +# set defaults +ENV JWT_SECRET=super-secret-string \ + JWT_AUDIENCE=api.dev \ + GRAPHQL_ENDPOINT="http://api:3000/graphql" \ + RABBITMQ_ADDRESS=broker \ + RABBITMQ_PORT=5672 \ + RABBITMQ_USERNAME=guest \ + RABBITMQ_PASSWORD=guest + +ENTRYPOINT ["/sbin/tini", "--", "/lagoon/entrypoints.sh"] +CMD ["/app/logs2notifications"] \ No newline at end of file diff --git a/services/logs2notifications/go.mod b/services/logs2notifications/go.mod new file mode 100644 index 0000000000..06e5305c9a --- /dev/null +++ b/services/logs2notifications/go.mod @@ -0,0 +1,20 @@ +module github.com/uselagoon/lagoon/services/logs2notifications + +go 1.14 + +require ( + github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927 // indirect + github.com/cheshir/go-mq v1.0.2 + github.com/dgrijalva/jwt-go v3.2.0+incompatible + github.com/fsouza/go-dockerclient v1.7.3 // indirect + github.com/machinebox/graphql v0.2.3-0.20181106130121-3a9253180225 + github.com/matryer/is v1.4.0 // indirect + github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2 + github.com/tiago4orion/conjure v0.0.0-20150908101743-93cb30b9d218 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect +) + +// Fixes for AppID +replace github.com/cheshir/go-mq v1.0.2 => github.com/shreddedbacon/go-mq v0.0.0-20200419104937-b8e9af912ead + +replace github.com/NeowayLabs/wabbit v0.0.0-20200409220312-12e68ab5b0c6 => github.com/shreddedbacon/wabbit v0.0.0-20200419104837-5b7b769d7204 \ No newline at end of file diff --git a/services/logs2notifications/go.sum b/services/logs2notifications/go.sum new file mode 100644 index 0000000000..bcac1aac42 --- /dev/null +++ b/services/logs2notifications/go.sum @@ -0,0 +1,187 @@ +bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Microsoft/go-winio v0.4.16-0.20201130162521-d1ffc52c7331/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= +github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= +github.com/Microsoft/hcsshim v0.8.14/go.mod h1:NtVKoYxQuTLx6gEq0L96c9Ju4JbRJ4nY2ow3VK6a9Lg= +github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927/go.mod h1:h/aW8ynjgkuj+NQRlZcDbAbM1ORAbXjXX77sX7T289U= +github.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59/go.mod h1:pA0z1pT8KYB3TCXK/ocprsh7MAkoW8bZVzPdih9snmM= +github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= +github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.4.3/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= +github.com/containerd/continuity v0.0.0-20210208174643-50096c924a4e/go.mod h1:EXlVlkqNba9rJe3j7w3Xa924itAMLgZH4UD/Q4PExuQ= +github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= +github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= +github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= +github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc= +github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/docker/docker v20.10.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/fsouza/go-dockerclient v1.7.3/go.mod h1:8xfZB8o9SptLNJ13VoV5pMiRbZGWkU/Omu5VOu/KC9Y= +github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/machinebox/graphql v0.2.3-0.20181106130121-3a9253180225 h1:guHWmqIKr4G+gQ4uYU5vcZjsUhhklRA2uOcGVfcfqis= +github.com/machinebox/graphql v0.2.3-0.20181106130121-3a9253180225/go.mod h1:F+kbVMHuwrQ5tYgU9JXlnskM8nOaFxCAEolaQybkjWA= +github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2 h1:JAEbJn3j/FrhdWA9jW8B5ajsLIjeuEHLi8xE4fk997o= +github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2/go.mod h1:0KeJpeMD6o+O4hW7qJOT7vyQPKrWmj26uf5wMc/IiIs= +github.com/moby/sys/mount v0.2.0/go.mod h1:aAivFE2LB3W4bACsUXChRHQ0qKWsetY4Y9V7sxOougM= +github.com/moby/sys/mountinfo v0.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= +github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shreddedbacon/go-mq v0.0.0-20200419104937-b8e9af912ead h1:brBqfI3SWHkBhydQ4zdYbeQj/4Rq68GHO+Me8W7Fji8= +github.com/shreddedbacon/go-mq v0.0.0-20200419104937-b8e9af912ead/go.mod h1:lAwW/xhfO27t6WSVHFtLdgYioymwJvQxMSH19z00/BY= +github.com/shreddedbacon/wabbit v0.0.0-20200419104837-5b7b769d7204 h1:jXml7E4X/d9v6LATMXFPCF1yW6TKrs+o5wMtYTaBdTw= +github.com/shreddedbacon/wabbit v0.0.0-20200419104837-5b7b769d7204/go.mod h1:l7t6U3j3uZUYroWctp1FvWEktRMuGqx2zCcb5cd8cS8= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/streadway/amqp v0.0.0-20200108173154-1c71cc93ed71 h1:2MR0pKUzlP3SGgj5NYJe/zRYDwOu9ku6YHy+Iw7l5DM= +github.com/streadway/amqp v0.0.0-20200108173154-1c71cc93ed71/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/tiago4orion/conjure v0.0.0-20150908101743-93cb30b9d218/go.mod h1:GQei++1WClbEC7AN1B9ipY1jCjzllM/7UNg0okAh/Z4= +github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/uselagoon/lagoon v1.15.1 h1:/EUJJrWmmNrFVIh4vYm71/+fk2yXlCk+M5g8L+2ll0c= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200922070232-aee5d888a860/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210216224549-f992740a1bac/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201113234701-d7a72108b828/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= +gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/services/logs2notifications/internal/handler/main.go b/services/logs2notifications/internal/handler/main.go new file mode 100644 index 0000000000..9ccbd7cd1a --- /dev/null +++ b/services/logs2notifications/internal/handler/main.go @@ -0,0 +1,225 @@ +package handler + +import ( + "context" + "encoding/json" + "fmt" + "log" + "time" + + "github.com/cheshir/go-mq" + "github.com/matryer/try" + "github.com/uselagoon/lagoon/services/logs2notifications/internal/lagoon" + lclient "github.com/uselagoon/lagoon/services/logs2notifications/internal/lagoon/client" + "github.com/uselagoon/lagoon/services/logs2notifications/internal/lagoon/jwt" + // "github.com/uselagoon/lagoon/services/logs2notifications/internal/schema" +) + +// RabbitBroker . +type RabbitBroker struct { + Hostname string `json:"hostname"` + Port string `json:"port"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + QueueName string `json:"queueName"` + ExchangeName string `json:"exchangeName"` +} + +// LagoonAPI . +type LagoonAPI struct { + Endpoint string `json:"endpoint"` + JWTAudience string `json:"audience"` + TokenSigningKey string `json:"tokenSigningKey` + JWTSubject string `json:"subject"` + JWTIssuer string `json:"issuer"` +} + +// Action is the structure of an action that is received via the message queue. +type Action struct { + Type string `json:"type"` // defines the action type + EventType string `json:"eventType"` // defines the eventtype field in the event notification + Data map[string]interface{} `json:"data"` // contains the payload for the action, this could be any json so using a map +} + +type messaging interface { + Consumer() + Publish(string, []byte) +} + +// Messaging is used for the config and client information for the messaging queue. +type Messaging struct { + Config mq.Config + LagoonAPI LagoonAPI + ConnectionAttempts int + ConnectionRetryInterval int + EnableDebug bool + LagoonAppID string +} + +// Notification . +type Notification struct { + Severity string `json:"severity"` + Project string `json:"project"` + UUID string `json:"uuid"` + Event string `json:"event"` + Meta struct { + User struct { + ID string `json:"id"` + PreferredUsername string `json:"preferred_username"` + Email string `json:"email"` + } `json:"user"` + Headers struct { + UserAgent string `json:"user-agent"` + ContentType string `json:"content-type"` + ContentLength string `json:"content-length"` + Host string `json:"host"` + IPAddress string `json:"ipAddress"` + } `json:"headers"` + Project string `json:"project"` + ProjectName string `json:"projectName"` + BranchName string `json:"branchName` + Event string `json:"event"` + Level string `json:"level"` + Message string `json:"message"` + Timestamp string `json:"timestamp"` + ShortSha string `json:"shortSha"` + BuildName string `json:"buildName"` + CommitURL string `json:"commitUrl"` + Environment string `json:"environment"` + EnvironmentID string `json:"environmentId"` + EnvironmentName string `json:"environmentName"` + Error string `json:"error"` + JobName string `json:"jobName"` + LogLink string `json:"logLink"` + Name string `json:"name"` + OpenshiftProject string `json:"openshiftProject"` + PromoteSourceEnvironment string `json:"promoteSourceEnvironment"` + PullrequestNumber string `json:"pullrequestNumber"` + PullrequestTitle string `json:"pullrequestTitle"` + PullrequestURL string `json:"pullrequestUrl"` + RemoteID string `json:"remoteId"` + RepoFullName string `json:"repoFullName"` + RepoName string `json:"repoName"` + RepoURL string `json:"repoUrl"` + Route string `json:"route"` + Routes string `json:"routes"` + Task string `json:"task"` + } `json:"meta"` + Message string `json:"message"` +} + +// NewMessaging returns a messaging with config +func NewMessaging(config mq.Config, lagoonAPI LagoonAPI, startupAttempts int, startupInterval int, enableDebug bool, appID string) *Messaging { + return &Messaging{ + Config: config, + LagoonAPI: lagoonAPI, + ConnectionAttempts: startupAttempts, + ConnectionRetryInterval: startupInterval, + EnableDebug: enableDebug, + LagoonAppID: appID, + } +} + +// Consumer handles consuming messages sent to the queue that this action handler is connected to and processes them accordingly +func (h *Messaging) Consumer() { + ctx := context.TODO() + + var messageQueue mq.MQ + // if no mq is found when the goroutine starts, retry a few times before exiting + // default is 10 retry with 30 second delay = 5 minutes + err := try.Do(func(attempt int) (bool, error) { + var err error + messageQueue, err = mq.New(h.Config) + if err != nil { + log.Println(err, + fmt.Sprintf( + "Failed to initialize message queue manager, retrying in %d seconds, attempt %d/%d", + h.ConnectionRetryInterval, + attempt, + h.ConnectionAttempts, + ), + ) + time.Sleep(time.Duration(h.ConnectionRetryInterval) * time.Second) + } + return attempt < h.ConnectionAttempts, err + }) + if err != nil { + log.Fatalf("Finally failed to initialize message queue manager: %v", err) + } + defer messageQueue.Close() + + go func() { + for err := range messageQueue.Error() { + log.Println(fmt.Sprintf("Caught error from message queue: %v", err)) + } + }() + + forever := make(chan bool) + + // Handle any tasks that go to the queue + log.Println("Listening for messages in queue lagoon-logs:notifications") + err = messageQueue.SetConsumerHandler("notifications-queue", func(message mq.Message) { + notification := &Notification{} + json.Unmarshal(message.Body(), notification) + switch notification.Event { + // check if this a `deployEnvironmentLatest` type of action + // and perform the steps to run the mutation against the lagoon api + case "api:unknownEvent": + default: + // marshal unmarshal the data into the input we need to use when talking to the lagoon api + token, err := jwt.OneMinuteAdminToken(h.LagoonAPI.TokenSigningKey, h.LagoonAPI.JWTAudience, h.LagoonAPI.JWTSubject, h.LagoonAPI.JWTIssuer) + if err != nil { + // the token wasn't generated + if h.EnableDebug { + log.Println(err) + } + break + } + // get all notifications for said project + l := lclient.New(h.LagoonAPI.Endpoint, token, "logs2notifications", false) + projectNotifications, err := lagoon.NotificationsForProject(ctx, notification.Project, l) + if err != nil { + log.Println(err) + break + } + // if len(projectNotifications.Notifications.Slack) > 0 { + // fmt.Println(projectNotifications.Notifications.Slack) + // } + if len(projectNotifications.Notifications.RocketChat) > 0 { + for _, rc := range projectNotifications.Notifications.RocketChat { + SendToRocketChat(notification, rc.Channel, rc.Webhook, h.LagoonAppID) + } + } + // if len(projectNotifications.Notifications.Email) > 0 { + // fmt.Println(projectNotifications.Notifications.Email) + // } + // if len(projectNotifications.Notifications.MicrosoftTeams) > 0 { + // fmt.Println(projectNotifications.Notifications.MicrosoftTeams) + // } + // if len(projectNotifications.Notifications.Webhook) > 0 { + // fmt.Println(projectNotifications.Notifications.Webhook) + // } + } + message.Ack(false) // ack to remove from queue + }) + if err != nil { + log.Println(fmt.Sprintf("Failed to set handler to consumer `%s`: %v", "items-queue", err)) + } + <-forever +} + +// toLagoonLogs sends logs to the lagoon-logs message queue +func (h *Messaging) toLagoonLogs(messageQueue mq.MQ, message map[string]interface{}) { + msgBytes, err := json.Marshal(message) + if err != nil { + if h.EnableDebug { + log.Println(err, "Unable to encode message as JSON") + } + } + producer, err := messageQueue.AsyncProducer("lagoon-logs") + if err != nil { + log.Println(fmt.Sprintf("Failed to get async producer: %v", err)) + return + } + producer.Produce(msgBytes) +} diff --git a/services/logs2notifications/internal/handler/main_test.go b/services/logs2notifications/internal/handler/main_test.go new file mode 100644 index 0000000000..0adc367c3f --- /dev/null +++ b/services/logs2notifications/internal/handler/main_test.go @@ -0,0 +1,18 @@ +package handler + +import ( + "bytes" + "reflect" + "testing" +) + +func checkEqual(t *testing.T, got, want interface{}, msgs ...interface{}) { + if !reflect.DeepEqual(got, want) { + buf := bytes.Buffer{} + buf.WriteString("got:\n[%v]\nwant:\n[%v]\n") + for _, v := range msgs { + buf.WriteString(v.(string)) + } + t.Errorf(buf.String(), got, want) + } +} \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/rocketchat_events.go b/services/logs2notifications/internal/handler/rocketchat_events.go new file mode 100644 index 0000000000..b8f0b8ce8f --- /dev/null +++ b/services/logs2notifications/internal/handler/rocketchat_events.go @@ -0,0 +1,227 @@ +package handler + +import ( + "bytes" + "encoding/json" + "fmt" + "log" + "net/http" +) + +// RocketChatData . +type RocketChatData struct { + Channel string `json:"channel"` + Attachments []RocketChatAttachment `json:"attachments"` +} + +// RocketChatAttachment . +type RocketChatAttachment struct { + Text string `json:"text"` + Color string `json:"color"` + Fields []RocketChatAttachmentField `json:"fields"` +} + +// RocketChatAttachmentField . +type RocketChatAttachmentField struct { + Short bool `json:"short"` + Title string `json:"title"` + Value string `json:"value"` +} + +// SendToRocketChat . +func SendToRocketChat(notification *Notification, channel, webhook, appID string) { + + emoji, color, template, err := getRocketChatEvent(notification.Event) + + var text string + switch template { + case "mergeRequestOpened": + text = fmt.Sprintf("*[%s]* PR [#%s (%s)](%s) opened in [%s](%s)", + notification.Meta.ProjectName, + notification.Meta.PullrequestNumber, + notification.Meta.PullrequestTitle, + notification.Meta.PullrequestURL, + notification.Meta.RepoName, + notification.Meta.RepoURL, + ) + case "mergeRequestUpdated": + text = fmt.Sprintf("*[%s]* PR [#%s (%s)](%s) updated in [%s](%s)", + notification.Meta.ProjectName, + notification.Meta.PullrequestNumber, + notification.Meta.PullrequestTitle, + notification.Meta.PullrequestURL, + notification.Meta.RepoName, + notification.Meta.RepoURL, + ) + case "mergeRequestClosed": + text = fmt.Sprintf("*[%s]* PR [#%s (%s)](%s) closed in [%s](%s)", + notification.Meta.ProjectName, + notification.Meta.PullrequestNumber, + notification.Meta.PullrequestTitle, + notification.Meta.PullrequestURL, + notification.Meta.RepoName, + notification.Meta.RepoURL, + ) + case "deleteEnvironment": + text = fmt.Sprintf("*[%s]* delete trigger `%s`", + notification.Meta.ProjectName, + notification.Meta.EnvironmentName, + ) + case "repoPushHandled": + text = fmt.Sprintf("*[%s]* [%s](%s/tree/%s)", + notification.Meta.ProjectName, + notification.Meta.BranchName, + notification.Meta.RepoURL, + notification.Meta.BranchName, + ) + if notification.Meta.ShortSha != "" { + text = fmt.Sprintf("%s ([%s](%s))", + text, + notification.Meta.ShortSha, + notification.Meta.CommitURL, + ) + } + text = fmt.Sprintf("%s pushed in [%s](%s)", + text, + notification.Meta.RepoFullName, + notification.Meta.RepoURL, + ) + case "repoPushSkipped": + text = fmt.Sprintf("*[%s]* [%s](%s/tree/%s)", + notification.Meta.ProjectName, + notification.Meta.BranchName, + notification.Meta.RepoURL, + notification.Meta.BranchName, + ) + if notification.Meta.ShortSha != "" { + text = fmt.Sprintf("%s ([%s](%s))", + text, + notification.Meta.ShortSha, + notification.Meta.CommitURL, + ) + } + text = fmt.Sprintf("%s pushed in [%s](%s) *deployment skipped*", + text, + notification.Meta.RepoFullName, + notification.Meta.RepoURL, + ) + case "deployEnvironment": + text = fmt.Sprintf("*[%s]* API deploy trigger `%s`", + notification.Meta.ProjectName, + notification.Meta.BranchName, + ) + if notification.Meta.ShortSha != "" { + text = fmt.Sprintf("%s (%s)", + text, + notification.Meta.ShortSha, + ) + } + default: + text = fmt.Sprintf("*[%s]* Event received for `%s`", + notification.Meta.ProjectName, + notification.Meta.BranchName, + ) + } + + data := RocketChatData{ + Channel: channel, + Attachments: []RocketChatAttachment{{ + Text: fmt.Sprintf("%s %s", emoji, text), + Color: color, + Fields: []RocketChatAttachmentField{{ + Short: true, + Title: "Source", + Value: appID, + }}, + }}, + } + + jsonBytes, _ := json.Marshal(data) + req, err := http.NewRequest("POST", webhook, bytes.NewBuffer(jsonBytes)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Content-Length", fmt.Sprintf("%d", len(jsonBytes))) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + panic(err) + } + defer resp.Body.Close() + log.Println(fmt.Sprintf("Sent %s message to rocketchat", notification.Event)) +} + +func getRocketChatEvent(msgEvent string) (string, string, string, error) { + if val, ok := rocketChatEventTypeMap[msgEvent]; ok { + return val.Emoji, val.Color, val.Template, nil + } + return "", "", "", fmt.Errorf("no matching event source") +} + +type rocketChatEvent struct { + Emoji string `json:"emoji"` + Color string `json:"color"` + Template string `json:"template"` +} + +var rocketChatEventTypeMap = map[string]rocketChatEvent{ + "github:pull_request:opened:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestOpened"}, + "gitlab:merge_request:opened:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestOpened"}, + "bitbucket:pullrequest:created:opened:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestOpened"}, + "bitbucket:pullrequest:created:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestOpened"}, + + "github:pull_request:synchronize:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, + "gitlab:merge_request:updated:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, + "bitbucket:pullrequest:updated:opened:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, + "bitbucket:pullrequest:updated:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, + + "github:pull_request:closed:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestClosed"}, + "bitbucket:pullrequest:fulfilled:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestClosed"}, + "bitbucket:pullrequest:rejected:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestClosed"}, + "gitlab:merge_request:closed:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestClosed"}, + + "github:delete:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "deleteEnvironment"}, + "gitlab:remove:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "deleteEnvironment"}, + "bitbucket:delete:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "deleteEnvironment"}, + "api:deleteEnvironment": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "deleteEnvironment"}, + + "github:push:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "repoPushHandled"}, + "bitbucket:repo:push:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "repoPushHandled"}, + "gitlab:push:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "repoPushHandled"}, + + "github:push:skipped": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "repoPushSkipped"}, + "gitlab:push:skipped": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "repoPushSkipped"}, + "bitbucket:push:skipped": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "repoPushSkipped"}, + + "api:deployEnvironmentLatest": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "deployEnvironment"}, + "api:deployEnvironmentBranch": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "deployEnvironment"}, + + "task:deploy-openshift:finished": {Emoji: ":white_check_mark:", Color: "lawngreen", Template: "deployFinished"}, + "task:remove-openshift-resources:finished": {Emoji: ":white_check_mark:", Color: "lawngreen", Template: "deployFinished"}, + "task:builddeploy-openshift:complete": {Emoji: ":white_check_mark:", Color: "lawngreen", Template: "deployFinished"}, + "task:builddeploy-kubernetes:complete": {Emoji: ":white_check_mark:", Color: "lawngreen", Template: "deployFinished"}, + + "task:remove-openshift:finished": {Emoji: ":white_check_mark:", Color: "lawngreen", Template: "removeFinished"}, + "task:remove-kubernetes:finished": {Emoji: ":white_check_mark:", Color: "lawngreen", Template: "removeFinished"}, + + "task:deploy-openshift:retry": {Emoji: ":warning:", Color: "gold", Template: "removeRetry"}, + "task:remove-openshift:retry": {Emoji: ":warning:", Color: "gold", Template: "removeRetry"}, + "task:remove-kubernetes:retry": {Emoji: ":warning:", Color: "gold", Template: "removeRetry"}, + "task:remove-openshift-resources:retry": {Emoji: ":warning:", Color: "gold", Template: "removeRetry"}, + + "task:deploy-openshift:error": {Emoji: ":bangbang:", Color: "red", Template: "deployError"}, + "task:builddeploy-openshift:failed": {Emoji: ":bangbang:", Color: "red", Template: "deployError"}, + "task:builddeploy-kubernetes:failed": {Emoji: ":bangbang:", Color: "red", Template: "deployError"}, + "task:remove-openshift:error": {Emoji: ":bangbang:", Color: "red", Template: "deployError"}, + "task:remove-kubernetes:error": {Emoji: ":bangbang:", Color: "red", Template: "deployError"}, + "task:remove-openshift-resources:error": {Emoji: ":bangbang:", Color: "red", Template: "deployError"}, + + "github:pull_request:closed:CannotDeleteProductionEnvironment": {Emoji: ":warning:", Color: "gold", Template: "notDeleted"}, + "github:push:CannotDeleteProductionEnvironment": {Emoji: ":warning:", Color: "gold", Template: "notDeleted"}, + "bitbucket:repo:push:CannotDeleteProductionEnvironment": {Emoji: ":warning:", Color: "gold", Template: "notDeleted"}, + "gitlab:push:CannotDeleteProductionEnvironment": {Emoji: ":warning:", Color: "gold", Template: "notDeleted"}, + "rest:remove:CannotDeleteProductionEnvironment": {Emoji: ":warning:", Color: "gold", Template: "notDeleted"}, + + // "rest:deploy:receive": {Emoji: ":information_source:", Color: "#E8E8E8"}, + // "rest:remove:receive": {Emoji: ":information_source:", Color: "#E8E8E8"}, + // "rest:promote:receive": {Emoji: ":information_source:", Color: "#E8E8E8"}, +} diff --git a/services/logs2notifications/internal/handler/slack_events.go b/services/logs2notifications/internal/handler/slack_events.go new file mode 100644 index 0000000000..12b02c3c71 --- /dev/null +++ b/services/logs2notifications/internal/handler/slack_events.go @@ -0,0 +1,64 @@ +package handler + +import ( + "fmt" +) + +func getSlackEvent(msgEvent string) (string, string, error) { + if val, ok := slackEventTypeMap[msgEvent]; ok { + return val.Emoji, val.Color, nil + } + return "", "", fmt.Errorf("no matching event source") +} + +type slackEvent struct { + Emoji string `json:"emoji"` + Color string `json:"color"` +} + +var slackEventTypeMap = map[string]slackEvent{ + "github:pull_request:closed:handled": {Emoji: ":information_source:", Color: "#E8E8E8"}, + "github:pull_request:opened:handled": {Emoji: ":information_source:", Color: "#E8E8E8"}, + "github:pull_request:synchronize:handled": {Emoji: ":information_source:", Color: "#E8E8E8"}, + "github:delete:handled": {Emoji: ":information_source:", Color: "#E8E8E8"}, + "github:push:handled": {Emoji: ":information_source:", Color: "#E8E8E8"}, + "bitbucket:repo:push:handled": {Emoji: ":information_source:", Color: "#E8E8E8"}, + "bitbucket:pullrequest:created:handled": {Emoji: ":information_source:", Color: "#E8E8E8"}, + "bitbucket:pullrequest:updated:handled": {Emoji: ":information_source:", Color: "#E8E8E8"}, + "bitbucket:pullrequest:fulfilled:handled": {Emoji: ":information_source:", Color: "#E8E8E8"}, + "bitbucket:pullrequest:rejected:handled": {Emoji: ":information_source:", Color: "#E8E8E8"}, + "gitlab:push:handled": {Emoji: ":information_source:", Color: "#E8E8E8"}, + "gitlab:merge_request:opened:handled": {Emoji: ":information_source:", Color: "#E8E8E8"}, + "gitlab:merge_request:updated:handled": {Emoji: ":information_source:", Color: "#E8E8E8"}, + "gitlab:merge_request:closed:handled": {Emoji: ":information_source:", Color: "#E8E8E8"}, + "rest:deploy:receive": {Emoji: ":information_source:", Color: "#E8E8E8"}, + "rest:remove:receive": {Emoji: ":information_source:", Color: "#E8E8E8"}, + "rest:promote:receive": {Emoji: ":information_source:", Color: "#E8E8E8"}, + "api:deployEnvironmentLatest": {Emoji: ":information_source:", Color: "#E8E8E8"}, + "api:deployEnvironmentBranch": {Emoji: ":information_source:", Color: "#E8E8E8"}, + "api:deleteEnvironment": {Emoji: ":information_source:", Color: "#E8E8E8"}, + "github:push:skipped": {Emoji: ":information_source:", Color: "#E8E8E8"}, + "gitlab:push:skipped": {Emoji: ":information_source:", Color: "#E8E8E8"}, + "bitbucket:push:skipped": {Emoji: ":information_source:", Color: "#E8E8E8"}, + "task:deploy-openshift:finished": {Emoji: ":white_check_mark:", Color: "good"}, + "task:remove-openshift:finished": {Emoji: ":white_check_mark:", Color: "good"}, + "task:remove-kubernetes:finished": {Emoji: ":white_check_mark:", Color: "good"}, + "task:remove-openshift-resources:finished": {Emoji: ":white_check_mark:", Color: "good"}, + "task:builddeploy-openshift:complete": {Emoji: ":white_check_mark:", Color: "good"}, + "task:builddeploy-kubernetes:complete": {Emoji: ":white_check_mark:", Color: "good"}, + "task:deploy-openshift:retry": {Emoji: ":warning:", Color: "warning"}, + "task:remove-openshift:retry": {Emoji: ":warning:", Color: "warning"}, + "task:remove-kubernetes:retry": {Emoji: ":warning:", Color: "warning"}, + "task:remove-openshift-resources:retry": {Emoji: ":warning:", Color: "warning"}, + "github:pull_request:closed:CannotDeleteProductionEnvironment": {Emoji: ":warning:", Color: "warning"}, + "github:push:CannotDeleteProductionEnvironment": {Emoji: ":warning:", Color: "warning"}, + "bitbucket:repo:push:CannotDeleteProductionEnvironment": {Emoji: ":warning:", Color: "warning"}, + "gitlab:push:CannotDeleteProductionEnvironment": {Emoji: ":warning:", Color: "warning"}, + "rest:remove:CannotDeleteProductionEnvironment": {Emoji: ":warning:", Color: "warning"}, + "task:deploy-openshift:error": {Emoji: ":bangbang:", Color: "danger"}, + "task:remove-openshift:error": {Emoji: ":bangbang:", Color: "danger"}, + "task:remove-kubernetes:error": {Emoji: ":bangbang:", Color: "danger"}, + "task:remove-openshift-resources:error": {Emoji: ":bangbang:", Color: "danger"}, + "task:builddeploy-openshift:failed": {Emoji: ":bangbang:", Color: "danger"}, + "task:builddeploy-kubernetes:failed": {Emoji: ":bangbang:", Color: "danger"}, +} diff --git a/services/logs2notifications/internal/lagoon/client/_lgraphql/projectNotifications.graphql b/services/logs2notifications/internal/lagoon/client/_lgraphql/projectNotifications.graphql new file mode 100644 index 0000000000..a1f4eca381 --- /dev/null +++ b/services/logs2notifications/internal/lagoon/client/_lgraphql/projectNotifications.graphql @@ -0,0 +1,41 @@ +query ( + $name: String! +) { + projectByName( + name: $name + ) { + id + name + notifications { + ... on NotificationSlack { + __typename + webhook + name + channel + } + ... on NotificationRocketChat { + __typename + webhook + name + channel + } + ... on NotificationEmail { + __typename + emailAddress + name + } + ... on NotificationMicrosoftTeams { + __typename + webhook + name + } + ... on NotificationWebhook { + __typename + webhook + name + contentType + notificationSeverityThreshold + } + } + } +} \ No newline at end of file diff --git a/services/logs2notifications/internal/lagoon/client/client.go b/services/logs2notifications/internal/lagoon/client/client.go new file mode 100644 index 0000000000..9ca3bb2624 --- /dev/null +++ b/services/logs2notifications/internal/lagoon/client/client.go @@ -0,0 +1,93 @@ +//go:generate go-bindata -pkg lgraphql -o lgraphql/lgraphql.go -nometadata _lgraphql/ + +// Package client implements the interfaces required by the parent lagoon +// package. +package client + +import ( + "encoding/json" + "fmt" + "log" + "os" + + "github.com/machinebox/graphql" + "github.com/uselagoon/lagoon/services/logs2notifications/internal/lagoon/client/lgraphql" +) + +// Client implements the lagoon package interfaces for the Lagoon GraphQL API. +type Client struct { + userAgent string + token string + client *graphql.Client +} + +// New creates a new Client for the given endpoint. +func New(endpoint, token, userAgent string, debug bool) *Client { + if debug { + return &Client{ + userAgent: userAgent, + token: token, + client: graphql.NewClient(endpoint, + // enable debug logging to stderr + func(c *graphql.Client) { + l := log.New(os.Stderr, "graphql", 0) + c.Log = func(s string) { + l.Println(s) + } + }), + } + } + return &Client{ + userAgent: userAgent, + token: token, + client: graphql.NewClient(endpoint), + } +} + +// newRequest constructs a graphql request. +// assetName is the name of the graphql query template in _graphql/. +// varStruct is converted to a map of variables for the template. +func (c *Client) newRequest( + assetName string, varStruct interface{}) (*graphql.Request, error) { + + q, err := lgraphql.Asset(assetName) + if err != nil { + return nil, fmt.Errorf("couldn't get asset: %w", err) + } + + return c.doRequest(string(q), varStruct) +} + +func (c *Client) doRequest(query string, varStruct interface{}) (*graphql.Request, error) { + vars, err := structToVarMap(varStruct) + if err != nil { + return nil, fmt.Errorf("couldn't convert struct to map: %w", err) + } + + req := graphql.NewRequest(query) + for key, value := range vars { + req.Var(key, value) + } + + headers := map[string]string{ + "User-Agent": c.userAgent, + "Authorization": fmt.Sprintf("Bearer %s", c.token), + } + for key, value := range headers { + req.Header.Set(key, value) + } + + return req, nil +} + +// structToVarMap encodes the given struct to a map. The idea is that by +// round-tripping through Marshal/Unmarshal, omitempty is applied to the +// zero-valued fields. +func structToVarMap( + varStruct interface{}) (vars map[string]interface{}, err error) { + data, err := json.Marshal(varStruct) + if err != nil { + return vars, err + } + return vars, json.Unmarshal(data, &vars) +} diff --git a/services/logs2notifications/internal/lagoon/client/client_test.go b/services/logs2notifications/internal/lagoon/client/client_test.go new file mode 100644 index 0000000000..c0e17ec55b --- /dev/null +++ b/services/logs2notifications/internal/lagoon/client/client_test.go @@ -0,0 +1,81 @@ +package client_test + +import ( + "reflect" + "testing" + + "github.com/uselagoon/lagoon/services/logs2notifications/internal/lagoon/client" +) + +type testStruct0 struct { + Foo string `json:"foo"` + Bar uint `json:"bar"` + Baz string `json:"baz,omitempty"` + Quux uint `json:"quux,omitempty"` +} + +func TestStructToVarMap(t *testing.T) { + var testCases = map[string]struct { + input testStruct0 + expect map[string]interface{} + }{ + "simple struct": { + input: testStruct0{ + Foo: "abc", + Bar: 8, + }, + expect: map[string]interface{}{ + "foo": "abc", + "bar": float64(8), + }, + }, + "keep zero values": { + input: testStruct0{ + Foo: "abc", + Bar: 0, + }, + expect: map[string]interface{}{ + "foo": "abc", + "bar": float64(0), + }, + }, + "omit zero values": { + input: testStruct0{ + Foo: "abc", + Bar: 0, + Baz: "", + Quux: 0, + }, + expect: map[string]interface{}{ + "foo": "abc", + "bar": float64(0), + }, + }, + "keep non-zero values": { + input: testStruct0{ + Foo: "abc", + Bar: 0, + Baz: "hi", + Quux: 9, + }, + expect: map[string]interface{}{ + "foo": "abc", + "bar": float64(0), + "baz": "hi", + "quux": float64(9), + }, + }, + } + for name, tc := range testCases { + t.Run(name, func(tt *testing.T) { + vars, err := client.StructToVarMap(&tc.input) + if err != nil { + tt.Error(err) + } + if !reflect.DeepEqual(vars, tc.expect) { + tt.Logf("result:\n%s\nexpected:\n%s", vars, tc.expect) + tt.Errorf("result does not match expected") + } + }) + } +} \ No newline at end of file diff --git a/services/logs2notifications/internal/lagoon/client/helper_test.go b/services/logs2notifications/internal/lagoon/client/helper_test.go new file mode 100644 index 0000000000..6561a9155d --- /dev/null +++ b/services/logs2notifications/internal/lagoon/client/helper_test.go @@ -0,0 +1,6 @@ +package client + +// StructToVarMap exposes the private client.structToVarMap for tests. +func StructToVarMap(varStruct interface{}) (map[string]interface{}, error) { + return structToVarMap(varStruct) +} \ No newline at end of file diff --git a/services/logs2notifications/internal/lagoon/client/lgraphql/lgraphql.go b/services/logs2notifications/internal/lagoon/client/lgraphql/lgraphql.go new file mode 100644 index 0000000000..3e7a131df5 --- /dev/null +++ b/services/logs2notifications/internal/lagoon/client/lgraphql/lgraphql.go @@ -0,0 +1,247 @@ +// Code generated by go-bindata. (@generated) DO NOT EDIT. + + //Package lgraphql generated by go-bindata.// sources: +// _lgraphql/projectNotifications.graphql +package lgraphql + +import ( + "bytes" + "compress/gzip" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" + "time" +) + +func bindataRead(data []byte, name string) ([]byte, error) { + gz, err := gzip.NewReader(bytes.NewBuffer(data)) + if err != nil { + return nil, fmt.Errorf("read %q: %v", name, err) + } + + var buf bytes.Buffer + _, err = io.Copy(&buf, gz) + clErr := gz.Close() + + if err != nil { + return nil, fmt.Errorf("read %q: %v", name, err) + } + if clErr != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +type asset struct { + bytes []byte + info os.FileInfo +} + +type bindataFileInfo struct { + name string + size int64 + mode os.FileMode + modTime time.Time +} + +// Name return file name +func (fi bindataFileInfo) Name() string { + return fi.name +} + +// Size return file size +func (fi bindataFileInfo) Size() int64 { + return fi.size +} + +// Mode return file mode +func (fi bindataFileInfo) Mode() os.FileMode { + return fi.mode +} + +// ModTime return file modify time +func (fi bindataFileInfo) ModTime() time.Time { + return fi.modTime +} + +// IsDir return file whether a directory +func (fi bindataFileInfo) IsDir() bool { + return fi.mode&os.ModeDir != 0 +} + +// Sys return file is sys mode +func (fi bindataFileInfo) Sys() interface{} { + return nil +} + +var __lgraphqlProjectnotificationsGraphql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xcc\x92\xc1\x4a\xc4\x30\x10\x86\xcf\xf6\x29\x46\xf0\xb0\x5e\xfa\x00\xde\x54\x3c\xba\x07\x5b\xf0\xb8\xc4\x74\xd6\x8c\x6d\x67\x6a\x32\x2a\x41\xfa\xee\x92\x96\xc5\xb6\x2a\x45\x50\xd8\xff\x94\xfc\xf9\x66\xf8\x93\xc9\xf3\x0b\xfa\x08\x9b\x0c\xe0\x8c\x4d\x8b\x17\x50\xa8\x27\x7e\x3c\xcd\xce\xe1\x3d\x03\x00\xe8\xbc\x3c\xa1\xd5\xab\xb8\x35\x2d\x6e\x06\x2b\x69\x84\x87\x9a\xc1\x3b\xe0\x49\x54\xcd\xb0\xcf\x8d\x28\xed\xc9\x1a\x25\xe1\x30\xe1\x93\xf2\x3c\x07\x61\xd8\x4e\x90\xa2\x31\xb6\x5e\x60\x49\xbb\x9d\xc6\x0e\x67\x9d\x0f\x7a\xc3\x07\x27\x52\x7f\xf1\xbf\x85\xad\x33\xcc\xd8\xcc\xfc\x7e\x2d\xd4\x9d\xd8\x1a\xf5\xda\x19\x3d\xb6\x64\x37\xad\xa1\xe6\x77\xa1\x30\x95\x5c\x56\x95\xc7\x10\xd6\x93\xad\x26\xb8\x25\xeb\x25\xc8\x5e\x4b\x34\xed\x72\xc0\x7f\xf5\x3e\xab\x29\xee\xc7\x4e\xff\x39\x1e\x61\x45\xd6\x32\x76\xcb\xb3\x93\xe9\x17\x2f\xf0\x15\x3d\x69\x2c\x9d\xc7\xe0\xa4\xa9\x7e\xb8\xc7\xb8\xea\xb3\xfe\x23\x00\x00\xff\xff\x7f\x5a\x0f\xed\x8c\x03\x00\x00") + +func _lgraphqlProjectnotificationsGraphqlBytes() ([]byte, error) { + return bindataRead( + __lgraphqlProjectnotificationsGraphql, + "_lgraphql/projectNotifications.graphql", + ) +} + +func _lgraphqlProjectnotificationsGraphql() (*asset, error) { + bytes, err := _lgraphqlProjectnotificationsGraphqlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "_lgraphql/projectNotifications.graphql", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +// Asset loads and returns the asset for the given name. +// It returns an error if the asset could not be found or +// could not be loaded. +func Asset(name string) ([]byte, error) { + cannonicalName := strings.Replace(name, "\\", "/", -1) + if f, ok := _bindata[cannonicalName]; ok { + a, err := f() + if err != nil { + return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err) + } + return a.bytes, nil + } + return nil, fmt.Errorf("Asset %s not found", name) +} + +// MustAsset is like Asset but panics when Asset would return an error. +// It simplifies safe initialization of global variables. +func MustAsset(name string) []byte { + a, err := Asset(name) + if err != nil { + panic("asset: Asset(" + name + "): " + err.Error()) + } + + return a +} + +// AssetInfo loads and returns the asset info for the given name. +// It returns an error if the asset could not be found or +// could not be loaded. +func AssetInfo(name string) (os.FileInfo, error) { + cannonicalName := strings.Replace(name, "\\", "/", -1) + if f, ok := _bindata[cannonicalName]; ok { + a, err := f() + if err != nil { + return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err) + } + return a.info, nil + } + return nil, fmt.Errorf("AssetInfo %s not found", name) +} + +// AssetNames returns the names of the assets. +func AssetNames() []string { + names := make([]string, 0, len(_bindata)) + for name := range _bindata { + names = append(names, name) + } + return names +} + +// _bindata is a table, holding each asset generator, mapped to its name. +var _bindata = map[string]func() (*asset, error){ + "_lgraphql/projectNotifications.graphql": _lgraphqlProjectnotificationsGraphql, +} + +// AssetDir returns the file names below a certain +// directory embedded in the file by go-bindata. +// For example if you run go-bindata on data/... and data contains the +// following hierarchy: +// data/ +// foo.txt +// img/ +// a.png +// b.png +// then AssetDir("data") would return []string{"foo.txt", "img"} +// AssetDir("data/img") would return []string{"a.png", "b.png"} +// AssetDir("foo.txt") and AssetDir("notexist") would return an error +// AssetDir("") will return []string{"data"}. +func AssetDir(name string) ([]string, error) { + node := _bintree + if len(name) != 0 { + cannonicalName := strings.Replace(name, "\\", "/", -1) + pathList := strings.Split(cannonicalName, "/") + for _, p := range pathList { + node = node.Children[p] + if node == nil { + return nil, fmt.Errorf("Asset %s not found", name) + } + } + } + if node.Func != nil { + return nil, fmt.Errorf("Asset %s not found", name) + } + rv := make([]string, 0, len(node.Children)) + for childName := range node.Children { + rv = append(rv, childName) + } + return rv, nil +} + +type bintree struct { + Func func() (*asset, error) + Children map[string]*bintree +} + +var _bintree = &bintree{nil, map[string]*bintree{ + "_lgraphql": &bintree{nil, map[string]*bintree{ + "projectNotifications.graphql": &bintree{_lgraphqlProjectnotificationsGraphql, map[string]*bintree{}}, + }}, +}} + +// RestoreAsset restores an asset under the given directory +func RestoreAsset(dir, name string) error { + data, err := Asset(name) + if err != nil { + return err + } + info, err := AssetInfo(name) + if err != nil { + return err + } + err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755)) + if err != nil { + return err + } + err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode()) + if err != nil { + return err + } + err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) + if err != nil { + return err + } + return nil +} + +// RestoreAssets restores an asset under the given directory recursively +func RestoreAssets(dir, name string) error { + children, err := AssetDir(name) + // File + if err != nil { + return RestoreAsset(dir, name) + } + // Dir + for _, child := range children { + err = RestoreAssets(dir, filepath.Join(name, child)) + if err != nil { + return err + } + } + return nil +} + +func _filePath(dir, name string) string { + cannonicalName := strings.Replace(name, "\\", "/", -1) + return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...) +} diff --git a/services/logs2notifications/internal/lagoon/client/query.go b/services/logs2notifications/internal/lagoon/client/query.go new file mode 100644 index 0000000000..a1e9a2cdca --- /dev/null +++ b/services/logs2notifications/internal/lagoon/client/query.go @@ -0,0 +1,25 @@ +package client + +import ( + "context" + + "github.com/uselagoon/lagoon/services/logs2notifications/internal/schema" +) + +// NotificationsForProjectByName gets all notifications for a project +func (c *Client) NotificationsForProjectByName( + ctx context.Context, name string, project *schema.Project) error { + req, err := c.newRequest("_lgraphql/projectNotifications.graphql", + map[string]interface{}{ + "name": name, + }) + if err != nil { + return err + } + + return c.client.Run(ctx, req, &struct { + Response *schema.Project `json:"projectByName"` + }{ + Response: project, + }) +} diff --git a/services/logs2notifications/internal/lagoon/jwt/jwt.go b/services/logs2notifications/internal/lagoon/jwt/jwt.go new file mode 100644 index 0000000000..4284685b76 --- /dev/null +++ b/services/logs2notifications/internal/lagoon/jwt/jwt.go @@ -0,0 +1,30 @@ +package jwt + +import ( + "time" + + "github.com/dgrijalva/jwt-go" +) + +// LagoonClaims is a set of JWT claims used by Lagoon. +type LagoonClaims struct { + Role string `json:"role"` + jwt.StandardClaims +} + +// OneMinuteAdminToken returns a JWT admin token valid for one minute. +func OneMinuteAdminToken(secret, audience, subject, issuer string) (string, error) { + now := time.Now() + claims := LagoonClaims{ + Role: "admin", + StandardClaims: jwt.StandardClaims{ + Audience: audience, + ExpiresAt: now.Unix() + 60, + IssuedAt: now.Unix(), + Subject: subject, + Issuer: issuer, + }, + } + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString([]byte(secret)) +} \ No newline at end of file diff --git a/services/logs2notifications/internal/lagoon/project.go b/services/logs2notifications/internal/lagoon/project.go new file mode 100644 index 0000000000..7208fc3501 --- /dev/null +++ b/services/logs2notifications/internal/lagoon/project.go @@ -0,0 +1,20 @@ +// Package lagoon implements high-level functions for interacting with the +// Lagoon API. +package lagoon + +import ( + "context" + + "github.com/uselagoon/lagoon/services/logs2notifications/internal/schema" +) + +// Project interface contains methods for projects in lagoon. +type Project interface { + NotificationsForProjectByName(ctx context.Context, name string, result *schema.Project) error +} + +// NotificationsForProject gets notifications for a project. +func NotificationsForProject(ctx context.Context, name string, m Project) (*schema.Project, error) { + result := schema.Project{} + return &result, m.NotificationsForProjectByName(ctx, name, &result) +} diff --git a/services/logs2notifications/internal/schema/notifications.go b/services/logs2notifications/internal/schema/notifications.go new file mode 100644 index 0000000000..33aeff6f61 --- /dev/null +++ b/services/logs2notifications/internal/schema/notifications.go @@ -0,0 +1,107 @@ +package schema + +import ( + "encoding/json" + "fmt" +) + +// Notifications represents possible Lagoon notification types. +// These are unmarshalled from a projectByName query response. +type Notifications struct { + Slack []NotificationSlack + RocketChat []NotificationRocketChat + Email []NotificationEmail + MicrosoftTeams []NotificationMicrosoftTeams + Webhook []NotificationWebhook +} + +// NotificationSlack is based on the Lagoon API type. +type NotificationSlack struct { + Name string `json:"name"` + Webhook string `json:"webhook"` + Channel string `json:"channel"` +} + +// NotificationRocketChat is based on the Lagoon API type. +type NotificationRocketChat struct { + Name string `json:"name"` + Webhook string `json:"webhook"` + Channel string `json:"channel"` +} + +// NotificationEmail is based on the Lagoon API type. +type NotificationEmail struct { + Name string `json:"name"` + EmailAddress string `json:"emailAddress"` +} + +// NotificationMicrosoftTeams is based on the Lagoon API type. +type NotificationMicrosoftTeams struct { + Name string `json:"name"` + Webhook string `json:"webhook"` +} + +// NotificationWebhook is based on the Lagoon API type. +type NotificationWebhook struct { + Name string `json:"name"` + Webhook string `json:"webhook"` + ContentType string `json:"contentType"` + NotificationSeverityThreshold string `json:"notificationSeverityThreshold"` +} + +// UnmarshalJSON unmashals a quoted json string to the Notification values +// returned from the Lagoon API. +func (n *Notifications) UnmarshalJSON(b []byte) error { + var nArray []map[string]string + err := json.Unmarshal(b, &nArray) + if err != nil { + return err + } + for _, nMap := range nArray { + if len(nMap) == 0 { + // Unsupported notification type returns an empty map. + // This happens when the lagoon API being targeted is actually a higher + // version than configured. + continue + } + switch nMap["__typename"] { + case "NotificationSlack": + n.Slack = append(n.Slack, + NotificationSlack{ + Name: nMap["name"], + Webhook: nMap["webhook"], + Channel: nMap["channel"], + }) + case "NotificationRocketChat": + n.RocketChat = append(n.RocketChat, + NotificationRocketChat{ + Name: nMap["name"], + Webhook: nMap["webhook"], + Channel: nMap["channel"], + }) + case "NotificationEmail": + n.Email = append(n.Email, + NotificationEmail{ + Name: nMap["name"], + EmailAddress: nMap["emailAddress"], + }) + case "NotificationMicrosoftTeams": + n.MicrosoftTeams = append(n.MicrosoftTeams, + NotificationMicrosoftTeams{ + Name: nMap["name"], + Webhook: nMap["webhook"], + }) + case "NotificationWebhook": + n.Webhook = append(n.Webhook, + NotificationWebhook{ + Name: nMap["name"], + Webhook: nMap["webhook"], + ContentType: nMap["contentType"], + NotificationSeverityThreshold: nMap["notificationSeverityThreshold"], + }) + default: + return fmt.Errorf("unknown notification type: %v", nMap["__typename"]) + } + } + return nil +} diff --git a/services/logs2notifications/internal/schema/project.go b/services/logs2notifications/internal/schema/project.go new file mode 100644 index 0000000000..d806ac94a8 --- /dev/null +++ b/services/logs2notifications/internal/schema/project.go @@ -0,0 +1,8 @@ +package schema + +// Project is based on the Lagoon API type. +type Project struct { + ID uint `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Notifications *Notifications `json:"notifications,omitempty"` +} diff --git a/services/logs2notifications/main.go b/services/logs2notifications/main.go new file mode 100644 index 0000000000..1d2a6ded54 --- /dev/null +++ b/services/logs2notifications/main.go @@ -0,0 +1,185 @@ +package main + +import ( + "flag" + "fmt" + "log" + "os" + "strconv" + "time" + + "github.com/cheshir/go-mq" + "github.com/uselagoon/lagoon/services/logs2notifications/internal/handler" +) + +var ( + httpListenPort = os.Getenv("HTTP_LISTEN_PORT") + mqUser string + mqPass string + mqHost string + mqPort string + mqWorkers int + rabbitReconnectRetryInterval int + startupConnectionAttempts int + startupConnectionInterval int + lagoonAPIHost string + lagoonAppID string + jwtTokenSigningKey string + jwtAudience string + actionsQueueName string + actionsExchange string + jwtSubject string + jwtIssuer string +) + +func main() { + flag.StringVar(&lagoonAppID, "lagoon-app-id", "logs2notifications", + "The appID to use that will be sent with messages.") + flag.StringVar(&mqUser, "rabbitmq-username", "guest", + "The username of the rabbitmq user.") + flag.StringVar(&mqPass, "rabbitmq-password", "guest", + "The password for the rabbitmq user.") + flag.StringVar(&mqHost, "rabbitmq-hostname", "localhost", + "The hostname for the rabbitmq host.") + flag.StringVar(&mqPort, "rabbitmq-port", "5672", + "The port for the rabbitmq host.") + flag.IntVar(&mqWorkers, "rabbitmq-queue-workers", 1, + "The number of workers to start with.") + flag.IntVar(&rabbitReconnectRetryInterval, "rabbitmq-reconnect-retry-interval", 30, + "The retry interval for rabbitmq.") + flag.IntVar(&startupConnectionAttempts, "startup-connection-attempts", 10, + "The number of startup attempts before exiting.") + flag.IntVar(&startupConnectionInterval, "startup-connection-interval-seconds", 30, + "The duration between startup attempts.") + flag.StringVar(&lagoonAPIHost, "lagoon-api-host", "http://localhost:3000/graphql", + "The host for the lagoon api.") + flag.StringVar(&jwtTokenSigningKey, "jwt-token-signing-key", "super-secret-string", + "The jwt signing token key or secret.") + flag.StringVar(&jwtAudience, "jwt-audience", "api.dev", + "The jwt audience.") + flag.StringVar(&jwtSubject, "jwt-subject", "logs2notifications", + "The jwt audience.") + flag.StringVar(&jwtIssuer, "jwt-issuer", "logs2notifications", + "The jwt audience.") + flag.StringVar(&actionsQueueName, "actions-queue-name", "lagoon-actions:items", + "The name of the queue in rabbitmq to use.") + flag.StringVar(&actionsExchange, "actions-exchange", "lagoon-actions", + "The name of the exchange in rabbitmq to use.") + flag.Parse() + + // get overrides from environment variables + mqUser = getEnv("RABBITMQ_USERNAME", mqUser) + mqPass = getEnv("RABBITMQ_PASSWORD", mqPass) + mqHost = getEnv("RABBITMQ_ADDRESS", mqHost) + mqPort = getEnv("RABBITMQ_PORT", mqPort) + lagoonAPIHost = getEnv("GRAPHQL_ENDPOINT", lagoonAPIHost) + jwtTokenSigningKey = getEnv("JWT_SECRET", jwtTokenSigningKey) + jwtAudience = getEnv("JWT_AUDIENCE", jwtAudience) + jwtSubject = getEnv("JWT_SUBJECT", jwtSubject) + jwtIssuer = getEnv("JWT_ISSUER", jwtIssuer) + actionsQueueName = getEnv("ACTIONS_QUEUE_NAME", actionsQueueName) + actionsExchange = getEnv("ACTIONS_EXCHANGE", actionsExchange) + + enableDebug := true + + // configure the backup handler settings + broker := handler.RabbitBroker{ + Hostname: fmt.Sprintf("%s:%s", mqHost, mqPort), + Username: mqUser, + Password: mqPass, + QueueName: actionsQueueName, + ExchangeName: actionsExchange, + } + graphQLConfig := handler.LagoonAPI{ + Endpoint: lagoonAPIHost, + TokenSigningKey: jwtTokenSigningKey, + JWTAudience: jwtAudience, + JWTSubject: jwtSubject, + JWTIssuer: jwtIssuer, + } + + log.Println("logs2notifications running") + + config := mq.Config{ + ReconnectDelay: time.Duration(rabbitReconnectRetryInterval) * time.Second, + Exchanges: mq.Exchanges{ + { + Name: "lagoon-logs", + Type: "direct", + Options: mq.Options{ + "durable": true, + "delivery_mode": "2", + "headers": "", + "content_type": "", + }, + }, + }, + Consumers: mq.Consumers{ + { + Name: "notifications-queue", + Queue: "lagoon-logs:notifications", + Workers: mqWorkers, + Options: mq.Options{ + "durable": true, + "delivery_mode": "2", + "headers": "", + "content_type": "", + }, + }, + }, + Queues: mq.Queues{ + { + Name: "lagoon-logs:notifications", + Exchange: "lagoon-logs", + Options: mq.Options{ + "durable": true, + "delivery_mode": "2", + "headers": "", + "content_type": "", + }, + }, + }, + Producers: mq.Producers{ + { + Name: "lagoon-logs", + Exchange: "lagoon-logs", + Options: mq.Options{ + "app_id": lagoonAppID, + "delivery_mode": "2", + "headers": "", + "content_type": "", + }, + }, + }, + DSN: fmt.Sprintf("amqp://%s:%s@%s/", broker.Username, broker.Password, broker.Hostname), + } + + messaging := handler.NewMessaging(config, + graphQLConfig, + startupConnectionAttempts, + startupConnectionInterval, + enableDebug, + lagoonAppID, + ) + + // start the consumer + messaging.Consumer() + +} + +func getEnv(key, fallback string) string { + if value, ok := os.LookupEnv(key); ok { + return value + } + return fallback +} + +// accepts fallback values 1, t, T, TRUE, true, True, 0, f, F, FALSE, false, False +// anything else is false. +func getEnvBool(key string, fallback bool) bool { + if value, ok := os.LookupEnv(key); ok { + rVal, _ := strconv.ParseBool(value) + return rVal + } + return fallback +} From f109e270354ae2a692c995619ef54a808ea4fcbf Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Thu, 28 Oct 2021 09:22:04 +1100 Subject: [PATCH 02/17] feat: add more logs2x services --- services/logs2notifications/Dockerfile | 2 - services/logs2notifications/go.mod | 4 +- services/logs2notifications/go.sum | 16 + .../internal/handler/email_events.go | 421 ++++++++++++++++++ .../internal/handler/main.go | 128 +++--- .../internal/handler/microsoftteams_events.go | 279 ++++++++++++ .../internal/handler/rocketchat_events.go | 165 +++++-- .../internal/handler/s3_events.go | 67 +++ .../internal/handler/slack_events.go | 151 ++++--- services/logs2notifications/main.go | 61 ++- 10 files changed, 1130 insertions(+), 164 deletions(-) create mode 100644 services/logs2notifications/internal/handler/email_events.go create mode 100644 services/logs2notifications/internal/handler/microsoftteams_events.go create mode 100644 services/logs2notifications/internal/handler/s3_events.go diff --git a/services/logs2notifications/Dockerfile b/services/logs2notifications/Dockerfile index 34ef5e9920..6c2bdaff6e 100644 --- a/services/logs2notifications/Dockerfile +++ b/services/logs2notifications/Dockerfile @@ -33,6 +33,4 @@ ENV JWT_SECRET=super-secret-string \ RABBITMQ_PORT=5672 \ RABBITMQ_USERNAME=guest \ RABBITMQ_PASSWORD=guest - -ENTRYPOINT ["/sbin/tini", "--", "/lagoon/entrypoints.sh"] CMD ["/app/logs2notifications"] \ No newline at end of file diff --git a/services/logs2notifications/go.mod b/services/logs2notifications/go.mod index 06e5305c9a..aa3e6ae785 100644 --- a/services/logs2notifications/go.mod +++ b/services/logs2notifications/go.mod @@ -3,6 +3,7 @@ module github.com/uselagoon/lagoon/services/logs2notifications go 1.14 require ( + github.com/aws/aws-sdk-go v1.41.11 github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927 // indirect github.com/cheshir/go-mq v1.0.2 github.com/dgrijalva/jwt-go v3.2.0+incompatible @@ -10,6 +11,7 @@ require ( github.com/machinebox/graphql v0.2.3-0.20181106130121-3a9253180225 github.com/matryer/is v1.4.0 // indirect github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2 + github.com/slack-go/slack v0.9.5 github.com/tiago4orion/conjure v0.0.0-20150908101743-93cb30b9d218 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) @@ -17,4 +19,4 @@ require ( // Fixes for AppID replace github.com/cheshir/go-mq v1.0.2 => github.com/shreddedbacon/go-mq v0.0.0-20200419104937-b8e9af912ead -replace github.com/NeowayLabs/wabbit v0.0.0-20200409220312-12e68ab5b0c6 => github.com/shreddedbacon/wabbit v0.0.0-20200419104837-5b7b769d7204 \ No newline at end of file +replace github.com/NeowayLabs/wabbit v0.0.0-20200409220312-12e68ab5b0c6 => github.com/shreddedbacon/wabbit v0.0.0-20200419104837-5b7b769d7204 diff --git a/services/logs2notifications/go.sum b/services/logs2notifications/go.sum index bcac1aac42..4892db91f2 100644 --- a/services/logs2notifications/go.sum +++ b/services/logs2notifications/go.sum @@ -5,6 +5,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/Microsoft/go-winio v0.4.16-0.20201130162521-d1ffc52c7331/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= github.com/Microsoft/hcsshim v0.8.14/go.mod h1:NtVKoYxQuTLx6gEq0L96c9Ju4JbRJ4nY2ow3VK6a9Lg= +github.com/aws/aws-sdk-go v1.41.11 h1:QLouWsiYQ8i22kD8k58Dpdhio1A0MpT7bg9ZNXqEjuI= +github.com/aws/aws-sdk-go v1.41.11/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927/go.mod h1:h/aW8ynjgkuj+NQRlZcDbAbM1ORAbXjXX77sX7T289U= github.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= @@ -31,6 +33,7 @@ github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5Xh github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/fsouza/go-dockerclient v1.7.3/go.mod h1:8xfZB8o9SptLNJ13VoV5pMiRbZGWkU/Omu5VOu/KC9Y= +github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= @@ -47,8 +50,13 @@ github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= @@ -75,6 +83,7 @@ github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59P github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -89,6 +98,8 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeV github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/slack-go/slack v0.9.5 h1:j7uOUDowybWf9eSgZg/AbGx6J1OPJB6SE8Z5dNl6Mtw= +github.com/slack-go/slack v0.9.5/go.mod h1:wWL//kk0ho+FcQXcBTmEafUI5dz4qz5f4mMk8oIkioQ= github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= @@ -124,6 +135,7 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -145,13 +157,17 @@ golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200922070232-aee5d888a860/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210216224549-f992740a1bac/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201113234701-d7a72108b828/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/services/logs2notifications/internal/handler/email_events.go b/services/logs2notifications/internal/handler/email_events.go new file mode 100644 index 0000000000..e0e805942b --- /dev/null +++ b/services/logs2notifications/internal/handler/email_events.go @@ -0,0 +1,421 @@ +package handler + +import ( + "bytes" + "fmt" + "log" + "net/smtp" + "text/template" +) + +var htmlTemplate = ` + + + {{.Title}} + + + + + + +
+

{{.Emoji}} [{{.ProjectName}}]

+

+ {{.MainHTML}} +

+
+
+

+ {{.Additional}} +

+
+ +` + +// SendToEmail . +func SendToEmail(notification *Notification, emailAddress, appID string) { + + emoji, color, tpl, err := getEmailEvent(notification.Event) + if err != nil { + return + } + var mainHTML, plainText, subject, additional string + switch tpl { + case "mergeRequestOpened": + mainHTML += fmt.Sprintf(`PR #%s (%s opened in %s`, + notification.Meta.PullrequestNumber, + notification.Meta.PullrequestTitle, + notification.Meta.PullrequestURL, + notification.Meta.RepoURL, + notification.Meta.RepoName, + ) + plainText += fmt.Sprintf(`[%s] PR #%s - %s opened in %s`, + notification.Meta.ProjectName, + notification.Meta.PullrequestNumber, + notification.Meta.PullrequestTitle, + notification.Meta.RepoName, + ) + subject += plainText + case "mergeRequestUpdated": + mainHTML += fmt.Sprintf(`PR #%s (%s updated in %s`, + notification.Meta.PullrequestNumber, + notification.Meta.PullrequestTitle, + notification.Meta.PullrequestURL, + notification.Meta.RepoURL, + notification.Meta.RepoName, + ) + plainText += fmt.Sprintf(`[%s] PR #%s - %s updated in %s`, + notification.Meta.ProjectName, + notification.Meta.PullrequestNumber, + notification.Meta.PullrequestTitle, + notification.Meta.RepoName, + ) + subject += plainText + case "mergeRequestClosed": + mainHTML += fmt.Sprintf(`PR #%s (%s closed in %s`, + notification.Meta.PullrequestNumber, + notification.Meta.PullrequestTitle, + notification.Meta.PullrequestURL, + notification.Meta.RepoURL, + notification.Meta.RepoName, + ) + plainText += fmt.Sprintf(`[%s] PR #%s - %s closed in %s`, + notification.Meta.ProjectName, + notification.Meta.PullrequestNumber, + notification.Meta.PullrequestTitle, + notification.Meta.RepoName, + ) + subject += plainText + case "deleteEnvironment": + mainHTML += fmt.Sprintf("Deleted environment %s", + notification.Meta.BranchName, + ) + plainText += fmt.Sprintf("[%s] deleted environment %s", + notification.Meta.ProjectName, + notification.Meta.BranchName, + ) + subject += plainText + case "repoPushHandled": + mainHTML += fmt.Sprintf(`%s`, + notification.Meta.RepoURL, + notification.Meta.BranchName, + notification.Meta.BranchName, + ) + plainText += fmt.Sprintf(`[%s] %s`, + notification.Meta.ProjectName, + notification.Meta.BranchName, + ) + if notification.Meta.ShortSha != "" { + mainHTML += fmt.Sprintf(`%s %s`, + mainHTML, + notification.Meta.CommitURL, + notification.Meta.ShortSha, + ) + plainText += fmt.Sprintf(`%s (%s)`, + plainText, + notification.Meta.ShortSha, + ) + } + mainHTML += fmt.Sprintf(`%s pushed in %s`, + mainHTML, + notification.Meta.RepoURL, + notification.Meta.RepoFullName, + ) + plainText += fmt.Sprintf(`%s pushed in %s`, + plainText, + notification.Meta.RepoFullName, + ) + subject += plainText + case "repoPushSkipped": + mainHTML += fmt.Sprintf(`%s`, + notification.Meta.RepoURL, + notification.Meta.BranchName, + notification.Meta.BranchName, + ) + plainText += fmt.Sprintf(`[%s] %s`, + notification.Meta.ProjectName, + notification.Meta.BranchName, + ) + if notification.Meta.ShortSha != "" { + mainHTML += fmt.Sprintf(`%s %s`, + mainHTML, + notification.Meta.CommitURL, + notification.Meta.ShortSha, + ) + plainText += fmt.Sprintf(`%s (%s)`, + plainText, + notification.Meta.ShortSha, + ) + } + mainHTML += fmt.Sprintf(`%s pushed in %s deployment skipped`, + mainHTML, + notification.Meta.RepoURL, + notification.Meta.RepoFullName, + ) + plainText += fmt.Sprintf(`%s pushed in %s *deployment skipped*`, + plainText, + notification.Meta.RepoFullName, + ) + subject += plainText + case "deployEnvironment": + mainHTML += fmt.Sprintf("Deployment triggered %s", + notification.Meta.BranchName, + ) + plainText += fmt.Sprintf("[%s] Deployment triggered on branch %s", + notification.Meta.ProjectName, + notification.Meta.BranchName, + ) + if notification.Meta.ShortSha != "" { + mainHTML += fmt.Sprintf(`%s (%s)`, + mainHTML, + notification.Meta.ShortSha, + ) + plainText += fmt.Sprintf(`%s (%s)`, + plainText, + notification.Meta.ShortSha, + ) + } + subject += plainText + case "removeFinished": + mainHTML += fmt.Sprintf("Remove %s", notification.Meta.OpenshiftProject) + plainText += fmt.Sprintf("[%s] remove %s", notification.Meta.ProjectName, notification.Meta.OpenshiftProject) + subject += plainText + case "notDeleted": + mainHTML += fmt.Sprintf("%s not deleted.", + notification.Meta.OpenshiftProject, + ) + plainText += fmt.Sprintf("[%s] %s not deleted.", + notification.Meta.ProjectName, + notification.Meta.OpenshiftProject, + ) + subject += plainText + plainText += fmt.Sprintf("%s", notification.Meta.Error) + case "deployError": + mainHTML += fmt.Sprintf("[%s]", + notification.Meta.ProjectName, + ) + plainText += fmt.Sprintf("[%s]", + notification.Meta.ProjectName, + ) + if notification.Meta.ShortSha != "" { + mainHTML += fmt.Sprintf(` %s (%s)`, + notification.Meta.BranchName, + notification.Meta.ShortSha, + ) + plainText += fmt.Sprintf(` %s (%s)`, + notification.Meta.BranchName, + notification.Meta.ShortSha, + ) + } else { + mainHTML += fmt.Sprintf(` %s`, + notification.Meta.BranchName, + ) + plainText += fmt.Sprintf(` %s`, + notification.Meta.BranchName, + ) + } + mainHTML += fmt.Sprintf(` Build %s error.`, + notification.Meta.BuildName, + ) + plainText += fmt.Sprintf(` Build %s error.`, + notification.Meta.BuildName, + ) + subject += plainText + if notification.Meta.LogLink != "" { + mainHTML += fmt.Sprintf(` Logs`, + notification.Meta.LogLink, + ) + plainText += fmt.Sprintf(` [Logs](%s)`, + notification.Meta.LogLink, + ) + } + case "deployFinished": + mainHTML += fmt.Sprintf("[%s]", + notification.Meta.ProjectName, + ) + plainText += fmt.Sprintf("[%s]", + notification.Meta.ProjectName, + ) + if notification.Meta.ShortSha != "" { + mainHTML += fmt.Sprintf(` %s (%s)`, + notification.Meta.BranchName, + notification.Meta.ShortSha, + ) + plainText += fmt.Sprintf(` %s (%s)`, + notification.Meta.BranchName, + notification.Meta.ShortSha, + ) + } else { + mainHTML += fmt.Sprintf(` %s`, + notification.Meta.BranchName, + ) + plainText += fmt.Sprintf(` %s`, + notification.Meta.BranchName, + ) + } + mainHTML += fmt.Sprintf(` Build %s complete.`, + notification.Meta.BuildName, + ) + plainText += fmt.Sprintf(` Build %s complete.`, + notification.Meta.BuildName, + ) + subject += plainText + if notification.Meta.LogLink != "" { + mainHTML += fmt.Sprintf(` Logs`, + notification.Meta.LogLink, + ) + plainText += fmt.Sprintf(` [Logs](%s)`, + notification.Meta.LogLink, + ) + } + additional += fmt.Sprintf(` `) + } + + t, _ := template.New("email").Parse(htmlTemplate) + var body bytes.Buffer + + mimeHeaders := "MIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\n\n" + body.Write([]byte(fmt.Sprintf("From: Lagoon Notifications\nSubject: %s \n%s\n\n", subject, mimeHeaders))) + + t.Execute(&body, struct { + Color string + Emoji string + Title string + ProjectName string + MainHTML string + Additional string + }{ + Title: plainText, + Color: color, + Emoji: emoji, + ProjectName: notification.Meta.ProjectName, + MainHTML: mainHTML, + Additional: additional, + }) + // Configuration + from := "notifications@lagoon.sh" + password := "" + to := []string{emailAddress} + smtpHost := "localhost" + smtpPort := "1025" + + // Create authentication + auth := smtp.PlainAuth("", from, password, smtpHost) + // Send actual message + err = smtp.SendMail(smtpHost+":"+smtpPort, auth, from, to, body.Bytes()) + if err != nil { + log.Printf("Error sending message to email: %v", err) + return + } + log.Println(fmt.Sprintf("Sent %s message to email", notification.Event)) +} + +func getEmailEvent(msgEvent string) (string, string, string, error) { + if val, ok := emailEvent[msgEvent]; ok { + return val.Emoji, val.Color, val.Template, nil + } + return "", "", "", fmt.Errorf("no matching event source") +} + +var emailEvent = map[string]EventMap{ + "github:pull_request:opened:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestOpened"}, + "gitlab:merge_request:opened:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestOpened"}, + "bitbucket:pullrequest:created:opened:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestOpened"}, //not in slack + "bitbucket:pullrequest:created:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestOpened"}, //not in teams + + "github:pull_request:synchronize:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, + "gitlab:merge_request:updated:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, + "bitbucket:pullrequest:updated:opened:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, //not in slack + "bitbucket:pullrequest:updated:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, //not in teams + + "github:pull_request:closed:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestClosed"}, + "bitbucket:pullrequest:fulfilled:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestClosed"}, + "bitbucket:pullrequest:rejected:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestClosed"}, + "gitlab:merge_request:closed:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestClosed"}, + + "github:delete:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "deleteEnvironment"}, + "gitlab:remove:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "deleteEnvironment"}, //not in slack + "bitbucket:delete:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "deleteEnvironment"}, //not in slack + "api:deleteEnvironment": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "deleteEnvironment"}, //not in teams + + "github:push:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "repoPushHandled"}, + "bitbucket:repo:push:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "repoPushHandled"}, + "gitlab:push:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "repoPushHandled"}, + + "github:push:skipped": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "repoPushSkipped"}, + "gitlab:push:skipped": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "repoPushSkipped"}, + "bitbucket:push:skipped": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "repoPushSkipped"}, + + "api:deployEnvironmentLatest": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "deployEnvironment"}, + "api:deployEnvironmentBranch": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "deployEnvironment"}, + + "task:deploy-openshift:finished": {Emoji: "✅", Color: "lawngreen", Template: "deployFinished"}, + "task:remove-openshift-resources:finished": {Emoji: "✅", Color: "lawngreen", Template: "deployFinished"}, + "task:builddeploy-openshift:complete": {Emoji: "✅", Color: "lawngreen", Template: "deployFinished"}, + "task:builddeploy-kubernetes:complete": {Emoji: "✅", Color: "lawngreen", Template: "deployFinished"}, //not in teams + + "task:remove-openshift:finished": {Emoji: "✅", Color: "lawngreen", Template: "removeFinished"}, + "task:remove-kubernetes:finished": {Emoji: "✅", Color: "lawngreen", Template: "removeFinished"}, + + "task:remove-openshift:error": {Emoji: "‼️", Color: "red", Template: "deployError"}, + "task:remove-kubernetes:error": {Emoji: "‼️", Color: "red", Template: "deployError"}, + "task:builddeploy-kubernetes:failed": {Emoji: "‼️", Color: "red", Template: "deployError"}, //not in teams + "task:builddeploy-openshift:failed": {Emoji: "‼️", Color: "red", Template: "deployError"}, + + "github:pull_request:closed:CannotDeleteProductionEnvironment": {Emoji: "⚠️", Color: "gold", Template: "notDeleted"}, + "github:push:CannotDeleteProductionEnvironment": {Emoji: "⚠️", Color: "gold", Template: "notDeleted"}, + "bitbucket:repo:push:CannotDeleteProductionEnvironment": {Emoji: "⚠️", Color: "gold", Template: "notDeleted"}, + "gitlab:push:CannotDeleteProductionEnvironment": {Emoji: "⚠️", Color: "gold", Template: "notDeleted"}, + + // deprecated + // "rest:remove:CannotDeleteProductionEnvironment": {Emoji: "⚠️", Color: "gold"}, + // "rest:deploy:receive": {Emoji: "ℹ️", Color: "#E8E8E8"}, + // "rest:remove:receive": {Emoji: "ℹ️", Color: "#E8E8E8"}, + // "rest:promote:receive": {Emoji: "ℹ️", Color: "#E8E8E8"}, + // "rest:pullrequest:deploy": {Emoji: "ℹ️", Color: "#E8E8E8"}, + // "rest:pullrequest:remove": {Emoji: "ℹ️", Color: "#E8E8E8"}, + + // deprecated + // "task:deploy-openshift:error": {Emoji: "‼️", Color: "red", Template: "deployError"}, + // "task:remove-openshift-resources:error": {Emoji: "‼️", Color: "red", Template: "deployError"}, + + // deprecated + // "task:deploy-openshift:retry": {Emoji: "⚠️", Color: "gold", Template: "removeRetry"}, + // "task:remove-openshift:retry": {Emoji: "⚠️", Color: "gold", Template: "removeRetry"}, + // "task:remove-kubernetes:retry": {Emoji: "⚠️", Color: "gold", Template: "removeRetry"}, + // "task:remove-openshift-resources:retry": {Emoji: "⚠️", Color: "gold", Template: "removeRetry"}, +} diff --git a/services/logs2notifications/internal/handler/main.go b/services/logs2notifications/internal/handler/main.go index 9ccbd7cd1a..d430ff414d 100644 --- a/services/logs2notifications/internal/handler/main.go +++ b/services/logs2notifications/internal/handler/main.go @@ -12,17 +12,14 @@ import ( "github.com/uselagoon/lagoon/services/logs2notifications/internal/lagoon" lclient "github.com/uselagoon/lagoon/services/logs2notifications/internal/lagoon/client" "github.com/uselagoon/lagoon/services/logs2notifications/internal/lagoon/jwt" - // "github.com/uselagoon/lagoon/services/logs2notifications/internal/schema" ) // RabbitBroker . type RabbitBroker struct { - Hostname string `json:"hostname"` - Port string `json:"port"` - Username string `json:"username,omitempty"` - Password string `json:"password,omitempty"` - QueueName string `json:"queueName"` - ExchangeName string `json:"exchangeName"` + Hostname string `json:"hostname"` + Port string `json:"port"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` } // LagoonAPI . @@ -54,6 +51,12 @@ type Messaging struct { ConnectionRetryInterval int EnableDebug bool LagoonAppID string + DisableSlack bool + DisableRocketChat bool + DisableMicrosoftTeams bool + DisableEmail bool + DisableWebhooks bool + DisableS3 bool } // Notification . @@ -75,41 +78,48 @@ type Notification struct { Host string `json:"host"` IPAddress string `json:"ipAddress"` } `json:"headers"` - Project string `json:"project"` - ProjectName string `json:"projectName"` - BranchName string `json:"branchName` - Event string `json:"event"` - Level string `json:"level"` - Message string `json:"message"` - Timestamp string `json:"timestamp"` - ShortSha string `json:"shortSha"` - BuildName string `json:"buildName"` - CommitURL string `json:"commitUrl"` - Environment string `json:"environment"` - EnvironmentID string `json:"environmentId"` - EnvironmentName string `json:"environmentName"` - Error string `json:"error"` - JobName string `json:"jobName"` - LogLink string `json:"logLink"` - Name string `json:"name"` - OpenshiftProject string `json:"openshiftProject"` - PromoteSourceEnvironment string `json:"promoteSourceEnvironment"` - PullrequestNumber string `json:"pullrequestNumber"` - PullrequestTitle string `json:"pullrequestTitle"` - PullrequestURL string `json:"pullrequestUrl"` - RemoteID string `json:"remoteId"` - RepoFullName string `json:"repoFullName"` - RepoName string `json:"repoName"` - RepoURL string `json:"repoUrl"` - Route string `json:"route"` - Routes string `json:"routes"` - Task string `json:"task"` + Project string `json:"project"` + ProjectName string `json:"projectName"` + BranchName string `json:"branchName` + Event string `json:"event"` + Level string `json:"level"` + Message string `json:"message"` + Timestamp string `json:"timestamp"` + ShortSha string `json:"shortSha"` + BuildName string `json:"buildName"` + CommitURL string `json:"commitUrl"` + Environment string `json:"environment"` + EnvironmentID string `json:"environmentId"` + EnvironmentName string `json:"environmentName"` + Error string `json:"error"` + JobName string `json:"jobName"` + LogLink string `json:"logLink"` + Name string `json:"name"` + OpenshiftProject string `json:"openshiftProject"` + PromoteSourceEnvironment string `json:"promoteSourceEnvironment"` + PullrequestNumber string `json:"pullrequestNumber"` + PullrequestTitle string `json:"pullrequestTitle"` + PullrequestURL string `json:"pullrequestUrl"` + RemoteID string `json:"remoteId"` + RepoFullName string `json:"repoFullName"` + RepoName string `json:"repoName"` + RepoURL string `json:"repoUrl"` + Route string `json:"route"` + Routes []string `json:"routes"` + Task string `json:"task"` } `json:"meta"` Message string `json:"message"` } +// EventMap . +type EventMap struct { + Emoji string `json:"emoji"` + Color string `json:"color"` + Template string `json:"template"` +} + // NewMessaging returns a messaging with config -func NewMessaging(config mq.Config, lagoonAPI LagoonAPI, startupAttempts int, startupInterval int, enableDebug bool, appID string) *Messaging { +func NewMessaging(config mq.Config, lagoonAPI LagoonAPI, startupAttempts int, startupInterval int, enableDebug bool, appID string, disableSlack, disableRocketChat, disableMicrosoftTeams, disableEmail, disableWebhooks, disableS3 bool) *Messaging { return &Messaging{ Config: config, LagoonAPI: lagoonAPI, @@ -117,6 +127,12 @@ func NewMessaging(config mq.Config, lagoonAPI LagoonAPI, startupAttempts int, st ConnectionRetryInterval: startupInterval, EnableDebug: enableDebug, LagoonAppID: appID, + DisableSlack: disableSlack, + DisableRocketChat: disableRocketChat, + DisableMicrosoftTeams: disableMicrosoftTeams, + DisableEmail: disableEmail, + DisableWebhooks: disableWebhooks, + DisableS3: disableS3, } } @@ -182,23 +198,31 @@ func (h *Messaging) Consumer() { log.Println(err) break } - // if len(projectNotifications.Notifications.Slack) > 0 { - // fmt.Println(projectNotifications.Notifications.Slack) - // } - if len(projectNotifications.Notifications.RocketChat) > 0 { - for _, rc := range projectNotifications.Notifications.RocketChat { - SendToRocketChat(notification, rc.Channel, rc.Webhook, h.LagoonAppID) + if projectNotifications.Notifications != nil { + if len(projectNotifications.Notifications.Slack) > 0 && !h.DisableSlack { + for _, slack := range projectNotifications.Notifications.Slack { + SendToSlack(notification, slack.Channel, slack.Webhook, message.AppId()) + } + } + if len(projectNotifications.Notifications.RocketChat) > 0 && !h.DisableRocketChat { + for _, rc := range projectNotifications.Notifications.RocketChat { + SendToRocketChat(notification, rc.Channel, rc.Webhook, message.AppId()) + } + } + if len(projectNotifications.Notifications.Email) > 0 && !h.DisableEmail { + for _, email := range projectNotifications.Notifications.Email { + SendToEmail(notification, email.EmailAddress, message.AppId()) + } + } + if len(projectNotifications.Notifications.MicrosoftTeams) > 0 && !h.DisableMicrosoftTeams { + for _, teams := range projectNotifications.Notifications.MicrosoftTeams { + SendToMicrosoftTeams(notification, teams.Webhook, message.AppId()) + } } + // if len(projectNotifications.Notifications.Webhook) > 0 { + // fmt.Println(projectNotifications.Notifications.Webhook) + // } } - // if len(projectNotifications.Notifications.Email) > 0 { - // fmt.Println(projectNotifications.Notifications.Email) - // } - // if len(projectNotifications.Notifications.MicrosoftTeams) > 0 { - // fmt.Println(projectNotifications.Notifications.MicrosoftTeams) - // } - // if len(projectNotifications.Notifications.Webhook) > 0 { - // fmt.Println(projectNotifications.Notifications.Webhook) - // } } message.Ack(false) // ack to remove from queue }) diff --git a/services/logs2notifications/internal/handler/microsoftteams_events.go b/services/logs2notifications/internal/handler/microsoftteams_events.go new file mode 100644 index 0000000000..cc99b05331 --- /dev/null +++ b/services/logs2notifications/internal/handler/microsoftteams_events.go @@ -0,0 +1,279 @@ +package handler + +import ( + "bytes" + "encoding/json" + "fmt" + "log" + "net/http" +) + +// MicrosoftTeamsData . +type MicrosoftTeamsData struct { + Type string `json:"@type"` + Context string `json:"@context"` + Summary string `json:"summary"` + Title string `json:"title"` + ThemeColor string `json:"themeColor"` + Sections []MicrosoftTeamsSection `json:"sections"` +} + +// MicrosoftTeamsSection . +type MicrosoftTeamsSection struct { + ActivityText string `json:"activityText"` + ActivityImage string `json:"activityImage"` +} + +// SendToMicrosoftTeams . +func SendToMicrosoftTeams(notification *Notification, webhook, appID string) { + + emoji, color, template, err := getMicrosoftTeamsEvent(notification.Event) + if err != nil { + return + } + + var text string + switch template { + case "mergeRequestOpened": + text = fmt.Sprintf("PR [#%s (%s)](%s) opened in [%s](%s)", + notification.Meta.PullrequestNumber, + notification.Meta.PullrequestTitle, + notification.Meta.PullrequestURL, + notification.Meta.RepoName, + notification.Meta.RepoURL, + ) + case "mergeRequestUpdated": + text = fmt.Sprintf("PR [#%s (%s)](%s) updated in [%s](%s)", + notification.Meta.PullrequestNumber, + notification.Meta.PullrequestTitle, + notification.Meta.PullrequestURL, + notification.Meta.RepoName, + notification.Meta.RepoURL, + ) + case "mergeRequestClosed": + text = fmt.Sprintf("PR [#%s (%s)](%s) closed in [%s](%s)", + notification.Meta.PullrequestNumber, + notification.Meta.PullrequestTitle, + notification.Meta.PullrequestURL, + notification.Meta.RepoName, + notification.Meta.RepoURL, + ) + case "deleteEnvironment": + text = fmt.Sprintf("Deleting environment `%s`", + notification.Meta.EnvironmentName, + ) + case "repoPushHandled": + text = fmt.Sprintf("[%s](%s/tree/%s)", + notification.Meta.BranchName, + notification.Meta.RepoURL, + notification.Meta.BranchName, + ) + if notification.Meta.ShortSha != "" { + text = fmt.Sprintf("%s ([%s](%s))", + text, + notification.Meta.ShortSha, + notification.Meta.CommitURL, + ) + } + text = fmt.Sprintf("%s pushed in [%s](%s)", + text, + notification.Meta.RepoFullName, + notification.Meta.RepoURL, + ) + case "repoPushSkipped": + text = fmt.Sprintf("[%s](%s/tree/%s)", + notification.Meta.BranchName, + notification.Meta.RepoURL, + notification.Meta.BranchName, + ) + if notification.Meta.ShortSha != "" { + text = fmt.Sprintf("%s ([%s](%s))", + text, + notification.Meta.ShortSha, + notification.Meta.CommitURL, + ) + } + text = fmt.Sprintf("%s pushed in [%s](%s) *deployment skipped*", + text, + notification.Meta.RepoFullName, + notification.Meta.RepoURL, + ) + case "deployEnvironment": + text = fmt.Sprintf("Deployment triggered `%s`", + notification.Meta.BranchName, + ) + if notification.Meta.ShortSha != "" { + text = fmt.Sprintf("%s (%s)", + text, + notification.Meta.ShortSha, + ) + } + case "removeFinished": + text = fmt.Sprintf("Removed `%s`", + notification.Meta.OpenshiftProject, + ) + case "removeRetry": + text = fmt.Sprintf("Removed `%s`", + notification.Meta.OpenshiftProject, + ) + case "notDeleted": + text = fmt.Sprintf("`%s` not deleted. %s", + notification.Meta.BranchName, + notification.Meta.Error, + ) + case "deployError": + if notification.Meta.ShortSha != "" { + text += fmt.Sprintf("`%s` %s", + notification.Meta.BranchName, + notification.Meta.ShortSha, + ) + } else { + text += fmt.Sprintf(" `%s`", + notification.Meta.BranchName, + ) + } + text += fmt.Sprintf(" Build `%s` Failed.", + notification.Meta.BuildName, + ) + if notification.Meta.LogLink != "" { + text += fmt.Sprintf(" [Logs](%s) \r", + notification.Meta.LogLink, + ) + } + case "deployFinished": + if notification.Meta.ShortSha != "" { + text += fmt.Sprintf("`%s` %s", + notification.Meta.BranchName, + notification.Meta.ShortSha, + ) + } else { + text += fmt.Sprintf("`%s`", + notification.Meta.BranchName, + ) + } + text += fmt.Sprintf(" Build `%s` Succeeded.", + notification.Meta.BuildName, + ) + if notification.Meta.LogLink != "" { + text += fmt.Sprintf(" [Logs](%s) \r", + notification.Meta.LogLink, + ) + } + text += fmt.Sprintf("* %s \n", + notification.Meta.Route, + ) + if len(notification.Meta.Routes) != 0 { + for _, r := range notification.Meta.Routes { + if r != notification.Meta.Route { + text += fmt.Sprintf("* %s \n", r) + } + } + } + default: + // do nothing + return + } + + data := MicrosoftTeamsData{ + Type: "MessageCard", + Context: "http://schema.org/extensions", + Summary: text, + Title: notification.Meta.ProjectName, + ThemeColor: color, + Sections: []MicrosoftTeamsSection{ + { + ActivityText: text, + ActivityImage: emoji, + }, + }, + } + + jsonBytes, _ := json.Marshal(data) + req, err := http.NewRequest("POST", webhook, bytes.NewBuffer(jsonBytes)) + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + log.Printf("Error sending message to rocketchat: %v", err) + return + } + defer resp.Body.Close() + log.Println(fmt.Sprintf("Sent %s message to rocketchat", notification.Event)) +} + +func getMicrosoftTeamsEvent(msgEvent string) (string, string, string, error) { + if val, ok := microsoftTeamsEvent[msgEvent]; ok { + return val.Emoji, val.Color, val.Template, nil + } + return "", "", "", fmt.Errorf("no matching event source") +} + +var microsoftTeamsEvent = map[string]EventMap{ + "github:pull_request:opened:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestOpened"}, + "gitlab:merge_request:opened:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestOpened"}, + "bitbucket:pullrequest:created:opened:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestOpened"}, //not in slack + "bitbucket:pullrequest:created:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestOpened"}, //not in teams + + "github:pull_request:synchronize:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, + "gitlab:merge_request:updated:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, + "bitbucket:pullrequest:updated:opened:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, //not in slack + "bitbucket:pullrequest:updated:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, //not in teams + + "github:pull_request:closed:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestClosed"}, + "bitbucket:pullrequest:fulfilled:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestClosed"}, + "bitbucket:pullrequest:rejected:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestClosed"}, + "gitlab:merge_request:closed:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestClosed"}, + + "github:delete:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "deleteEnvironment"}, + "gitlab:remove:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "deleteEnvironment"}, //not in slack + "bitbucket:delete:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "deleteEnvironment"}, //not in slack + "api:deleteEnvironment": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "deleteEnvironment"}, //not in teams + + "github:push:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "repoPushHandled"}, + "bitbucket:repo:push:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "repoPushHandled"}, + "gitlab:push:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "repoPushHandled"}, + + "github:push:skipped": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "repoPushSkipped"}, + "gitlab:push:skipped": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "repoPushSkipped"}, + "bitbucket:push:skipped": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "repoPushSkipped"}, + + "api:deployEnvironmentLatest": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "deployEnvironment"}, + "api:deployEnvironmentBranch": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "deployEnvironment"}, + + "task:deploy-openshift:finished": {Emoji: ":white_check_mark:", Color: "lawngreen", Template: "deployFinished"}, + "task:remove-openshift-resources:finished": {Emoji: ":white_check_mark:", Color: "lawngreen", Template: "deployFinished"}, + "task:builddeploy-openshift:complete": {Emoji: ":white_check_mark:", Color: "lawngreen", Template: "deployFinished"}, + "task:builddeploy-kubernetes:complete": {Emoji: ":white_check_mark:", Color: "lawngreen", Template: "deployFinished"}, //not in teams + + "task:remove-openshift:finished": {Emoji: ":white_check_mark:", Color: "lawngreen", Template: "removeFinished"}, + "task:remove-kubernetes:finished": {Emoji: ":white_check_mark:", Color: "lawngreen", Template: "removeFinished"}, + + "task:remove-openshift:error": {Emoji: ":bangbang:", Color: "red", Template: "deployError"}, + "task:remove-kubernetes:error": {Emoji: ":bangbang:", Color: "red", Template: "deployError"}, + "task:builddeploy-kubernetes:failed": {Emoji: ":bangbang:", Color: "red", Template: "deployError"}, //not in teams + "task:builddeploy-openshift:failed": {Emoji: ":bangbang:", Color: "red", Template: "deployError"}, + + "github:pull_request:closed:CannotDeleteProductionEnvironment": {Emoji: ":warning:", Color: "gold", Template: "notDeleted"}, + "github:push:CannotDeleteProductionEnvironment": {Emoji: ":warning:", Color: "gold", Template: "notDeleted"}, + "bitbucket:repo:push:CannotDeleteProductionEnvironment": {Emoji: ":warning:", Color: "gold", Template: "notDeleted"}, + "gitlab:push:CannotDeleteProductionEnvironment": {Emoji: ":warning:", Color: "gold", Template: "notDeleted"}, + + // deprecated + // "rest:remove:CannotDeleteProductionEnvironment": {Emoji: ":warning:", Color: "gold"}, + // "rest:deploy:receive": {Emoji: ":information_source:", Color: "#E8E8E8"}, + // "rest:remove:receive": {Emoji: ":information_source:", Color: "#E8E8E8"}, + // "rest:promote:receive": {Emoji: ":information_source:", Color: "#E8E8E8"}, + // "rest:pullrequest:deploy": {Emoji: ":information_source:", Color: "#E8E8E8"}, + // "rest:pullrequest:remove": {Emoji: ":information_source:", Color: "#E8E8E8"}, + + // deprecated + // "task:deploy-openshift:error": {Emoji: ":bangbang:", Color: "red", Template: "deployError"}, + // "task:remove-openshift-resources:error": {Emoji: ":bangbang:", Color: "red", Template: "deployError"}, + + // deprecated + // "task:deploy-openshift:retry": {Emoji: ":warning:", Color: "gold", Template: "removeRetry"}, + // "task:remove-openshift:retry": {Emoji: ":warning:", Color: "gold", Template: "removeRetry"}, + // "task:remove-kubernetes:retry": {Emoji: ":warning:", Color: "gold", Template: "removeRetry"}, + // "task:remove-openshift-resources:retry": {Emoji: ":warning:", Color: "gold", Template: "removeRetry"}, +} diff --git a/services/logs2notifications/internal/handler/rocketchat_events.go b/services/logs2notifications/internal/handler/rocketchat_events.go index b8f0b8ce8f..1657a0d150 100644 --- a/services/logs2notifications/internal/handler/rocketchat_events.go +++ b/services/logs2notifications/internal/handler/rocketchat_events.go @@ -32,6 +32,9 @@ type RocketChatAttachmentField struct { func SendToRocketChat(notification *Notification, channel, webhook, appID string) { emoji, color, template, err := getRocketChatEvent(notification.Event) + if err != nil { + return + } var text string switch template { @@ -63,7 +66,7 @@ func SendToRocketChat(notification *Notification, channel, webhook, appID string notification.Meta.RepoURL, ) case "deleteEnvironment": - text = fmt.Sprintf("*[%s]* delete trigger `%s`", + text = fmt.Sprintf("*[%s]* Deleting environment `%s`", notification.Meta.ProjectName, notification.Meta.EnvironmentName, ) @@ -106,7 +109,7 @@ func SendToRocketChat(notification *Notification, channel, webhook, appID string notification.Meta.RepoURL, ) case "deployEnvironment": - text = fmt.Sprintf("*[%s]* API deploy trigger `%s`", + text = fmt.Sprintf("*[%s]* Deployment triggered `%s`", notification.Meta.ProjectName, notification.Meta.BranchName, ) @@ -116,24 +119,97 @@ func SendToRocketChat(notification *Notification, channel, webhook, appID string notification.Meta.ShortSha, ) } - default: - text = fmt.Sprintf("*[%s]* Event received for `%s`", + case "removeFinished": + text = fmt.Sprintf("*[%s]* Removed `%s`", + notification.Meta.ProjectName, + notification.Meta.OpenshiftProject, + ) + case "removeRetry": + text = fmt.Sprintf("*[%s]* Removed `%s`", + notification.Meta.ProjectName, + notification.Meta.OpenshiftProject, + ) + case "notDeleted": + text = fmt.Sprintf("*[%s]* `%s` not deleted. %s", notification.Meta.ProjectName, notification.Meta.BranchName, + notification.Meta.Error, + ) + case "deployError": + text = fmt.Sprintf("*[%s]*", + notification.Meta.ProjectName, ) + if notification.Meta.ShortSha != "" { + text += fmt.Sprintf(" `%s` %s", + notification.Meta.BranchName, + notification.Meta.ShortSha, + ) + } else { + text += fmt.Sprintf(" `%s`", + notification.Meta.BranchName, + ) + } + text += fmt.Sprintf(" Build `%s` Failed.", + notification.Meta.BuildName, + ) + if notification.Meta.LogLink != "" { + text += fmt.Sprintf(" [Logs](%s) \r", + notification.Meta.LogLink, + ) + } + case "deployFinished": + text = fmt.Sprintf("*[%s]*", + notification.Meta.ProjectName, + ) + if notification.Meta.ShortSha != "" { + text += fmt.Sprintf(" `%s` %s", + notification.Meta.BranchName, + notification.Meta.ShortSha, + ) + } else { + text += fmt.Sprintf(" `%s`", + notification.Meta.BranchName, + ) + } + text += fmt.Sprintf(" Build `%s` Succeeded.", + notification.Meta.BuildName, + ) + if notification.Meta.LogLink != "" { + text += fmt.Sprintf(" [Logs](%s) \r", + notification.Meta.LogLink, + ) + } + text += fmt.Sprintf("* %s \n", + notification.Meta.Route, + ) + if len(notification.Meta.Routes) != 0 { + for _, r := range notification.Meta.Routes { + if r != notification.Meta.Route { + text += fmt.Sprintf("* %s \n", r) + } + } + } + default: + // do nothing + return } data := RocketChatData{ Channel: channel, - Attachments: []RocketChatAttachment{{ - Text: fmt.Sprintf("%s %s", emoji, text), - Color: color, - Fields: []RocketChatAttachmentField{{ - Short: true, - Title: "Source", - Value: appID, - }}, - }}, + Attachments: []RocketChatAttachment{ + { + // Text: fmt.Sprintf("%s %s", emoji, notification.Message), + Text: fmt.Sprintf("%s %s", emoji, text), + Color: color, + Fields: []RocketChatAttachmentField{ + { + Short: true, + Title: "Source", + Value: appID, + }, + }, + }, + }, } jsonBytes, _ := json.Marshal(data) @@ -144,7 +220,8 @@ func SendToRocketChat(notification *Notification, channel, webhook, appID string client := &http.Client{} resp, err := client.Do(req) if err != nil { - panic(err) + log.Printf("Error sending message to rocketchat: %v", err) + return } defer resp.Body.Close() log.Println(fmt.Sprintf("Sent %s message to rocketchat", notification.Event)) @@ -157,22 +234,16 @@ func getRocketChatEvent(msgEvent string) (string, string, string, error) { return "", "", "", fmt.Errorf("no matching event source") } -type rocketChatEvent struct { - Emoji string `json:"emoji"` - Color string `json:"color"` - Template string `json:"template"` -} - -var rocketChatEventTypeMap = map[string]rocketChatEvent{ +var rocketChatEventTypeMap = map[string]EventMap{ "github:pull_request:opened:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestOpened"}, "gitlab:merge_request:opened:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestOpened"}, - "bitbucket:pullrequest:created:opened:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestOpened"}, - "bitbucket:pullrequest:created:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestOpened"}, + "bitbucket:pullrequest:created:opened:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestOpened"}, //not in slack + "bitbucket:pullrequest:created:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestOpened"}, //not in teams "github:pull_request:synchronize:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, "gitlab:merge_request:updated:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, - "bitbucket:pullrequest:updated:opened:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, - "bitbucket:pullrequest:updated:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, + "bitbucket:pullrequest:updated:opened:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, //not in slack + "bitbucket:pullrequest:updated:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, //not in teams "github:pull_request:closed:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestClosed"}, "bitbucket:pullrequest:fulfilled:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestClosed"}, @@ -180,9 +251,9 @@ var rocketChatEventTypeMap = map[string]rocketChatEvent{ "gitlab:merge_request:closed:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestClosed"}, "github:delete:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "deleteEnvironment"}, - "gitlab:remove:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "deleteEnvironment"}, - "bitbucket:delete:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "deleteEnvironment"}, - "api:deleteEnvironment": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "deleteEnvironment"}, + "gitlab:remove:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "deleteEnvironment"}, //not in slack + "bitbucket:delete:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "deleteEnvironment"}, //not in slack + "api:deleteEnvironment": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "deleteEnvironment"}, //not in teams "github:push:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "repoPushHandled"}, "bitbucket:repo:push:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "repoPushHandled"}, @@ -198,30 +269,36 @@ var rocketChatEventTypeMap = map[string]rocketChatEvent{ "task:deploy-openshift:finished": {Emoji: ":white_check_mark:", Color: "lawngreen", Template: "deployFinished"}, "task:remove-openshift-resources:finished": {Emoji: ":white_check_mark:", Color: "lawngreen", Template: "deployFinished"}, "task:builddeploy-openshift:complete": {Emoji: ":white_check_mark:", Color: "lawngreen", Template: "deployFinished"}, - "task:builddeploy-kubernetes:complete": {Emoji: ":white_check_mark:", Color: "lawngreen", Template: "deployFinished"}, + "task:builddeploy-kubernetes:complete": {Emoji: ":white_check_mark:", Color: "lawngreen", Template: "deployFinished"}, //not in teams "task:remove-openshift:finished": {Emoji: ":white_check_mark:", Color: "lawngreen", Template: "removeFinished"}, "task:remove-kubernetes:finished": {Emoji: ":white_check_mark:", Color: "lawngreen", Template: "removeFinished"}, - "task:deploy-openshift:retry": {Emoji: ":warning:", Color: "gold", Template: "removeRetry"}, - "task:remove-openshift:retry": {Emoji: ":warning:", Color: "gold", Template: "removeRetry"}, - "task:remove-kubernetes:retry": {Emoji: ":warning:", Color: "gold", Template: "removeRetry"}, - "task:remove-openshift-resources:retry": {Emoji: ":warning:", Color: "gold", Template: "removeRetry"}, - - "task:deploy-openshift:error": {Emoji: ":bangbang:", Color: "red", Template: "deployError"}, - "task:builddeploy-openshift:failed": {Emoji: ":bangbang:", Color: "red", Template: "deployError"}, - "task:builddeploy-kubernetes:failed": {Emoji: ":bangbang:", Color: "red", Template: "deployError"}, - "task:remove-openshift:error": {Emoji: ":bangbang:", Color: "red", Template: "deployError"}, - "task:remove-kubernetes:error": {Emoji: ":bangbang:", Color: "red", Template: "deployError"}, - "task:remove-openshift-resources:error": {Emoji: ":bangbang:", Color: "red", Template: "deployError"}, + "task:remove-openshift:error": {Emoji: ":bangbang:", Color: "red", Template: "deployError"}, + "task:remove-kubernetes:error": {Emoji: ":bangbang:", Color: "red", Template: "deployError"}, + "task:builddeploy-kubernetes:failed": {Emoji: ":bangbang:", Color: "red", Template: "deployError"}, //not in teams + "task:builddeploy-openshift:failed": {Emoji: ":bangbang:", Color: "red", Template: "deployError"}, "github:pull_request:closed:CannotDeleteProductionEnvironment": {Emoji: ":warning:", Color: "gold", Template: "notDeleted"}, "github:push:CannotDeleteProductionEnvironment": {Emoji: ":warning:", Color: "gold", Template: "notDeleted"}, "bitbucket:repo:push:CannotDeleteProductionEnvironment": {Emoji: ":warning:", Color: "gold", Template: "notDeleted"}, "gitlab:push:CannotDeleteProductionEnvironment": {Emoji: ":warning:", Color: "gold", Template: "notDeleted"}, - "rest:remove:CannotDeleteProductionEnvironment": {Emoji: ":warning:", Color: "gold", Template: "notDeleted"}, - // "rest:deploy:receive": {Emoji: ":information_source:", Color: "#E8E8E8"}, - // "rest:remove:receive": {Emoji: ":information_source:", Color: "#E8E8E8"}, - // "rest:promote:receive": {Emoji: ":information_source:", Color: "#E8E8E8"}, + // deprecated + // "rest:remove:CannotDeleteProductionEnvironment": {Emoji: ":warning:", Color: "gold"}, + // "rest:deploy:receive": {Emoji: ":information_source:", Color: "#E8E8E8"}, + // "rest:remove:receive": {Emoji: ":information_source:", Color: "#E8E8E8"}, + // "rest:promote:receive": {Emoji: ":information_source:", Color: "#E8E8E8"}, + // "rest:pullrequest:deploy": {Emoji: ":information_source:", Color: "#E8E8E8"}, + // "rest:pullrequest:remove": {Emoji: ":information_source:", Color: "#E8E8E8"}, + + // deprecated + // "task:deploy-openshift:error": {Emoji: ":bangbang:", Color: "red", Template: "deployError"}, + // "task:remove-openshift-resources:error": {Emoji: ":bangbang:", Color: "red", Template: "deployError"}, + + // deprecated + // "task:deploy-openshift:retry": {Emoji: ":warning:", Color: "gold", Template: "removeRetry"}, + // "task:remove-openshift:retry": {Emoji: ":warning:", Color: "gold", Template: "removeRetry"}, + // "task:remove-kubernetes:retry": {Emoji: ":warning:", Color: "gold", Template: "removeRetry"}, + // "task:remove-openshift-resources:retry": {Emoji: ":warning:", Color: "gold", Template: "removeRetry"}, } diff --git a/services/logs2notifications/internal/handler/s3_events.go b/services/logs2notifications/internal/handler/s3_events.go new file mode 100644 index 0000000000..f53ed93a5c --- /dev/null +++ b/services/logs2notifications/internal/handler/s3_events.go @@ -0,0 +1,67 @@ +package handler + +import ( + "bytes" + "fmt" + "net/http" + "os" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/s3" +) + +var ( + AWS_S3_REGION = "" + AWS_S3_BUCKET = "" +) + +func getS3Event(msgEvent string) (string, string, string, error) { + if val, ok := s3Event[msgEvent]; ok { + return val.Emoji, val.Color, val.Template, nil + } + return "", "", "", fmt.Errorf("no matching event source") +} + +var s3Event = map[string]EventMap{ + "github:pull_request:closed:handled": {Emoji: ":information_source:", Color: "#E8E8E8"}, +} + +// func main() { +// session, err := session.NewSession(&aws.Config{Region: aws.String(AWS_S3_REGION)}) +// if err != nil { +// log.Fatal(err) +// } + +// // Upload Files +// err = uploadFile(session, "test.png") +// if err != nil { +// log.Fatal(err) +// } +// } + +func uploadFile(session *session.Session, uploadFileDir string) error { + + upFile, err := os.Open(uploadFileDir) + if err != nil { + return err + } + defer upFile.Close() + + upFileInfo, _ := upFile.Stat() + var fileSize int64 = upFileInfo.Size() + fileBuffer := make([]byte, fileSize) + upFile.Read(fileBuffer) + + _, err = s3.New(session).PutObject(&s3.PutObjectInput{ + Bucket: aws.String(AWS_S3_BUCKET), + Key: aws.String(uploadFileDir), + ACL: aws.String("private"), + Body: bytes.NewReader(fileBuffer), + ContentLength: aws.Int64(fileSize), + ContentType: aws.String(http.DetectContentType(fileBuffer)), + ContentDisposition: aws.String("attachment"), + ServerSideEncryption: aws.String("AES256"), + }) + return err +} diff --git a/services/logs2notifications/internal/handler/slack_events.go b/services/logs2notifications/internal/handler/slack_events.go index 12b02c3c71..6d153e60a1 100644 --- a/services/logs2notifications/internal/handler/slack_events.go +++ b/services/logs2notifications/internal/handler/slack_events.go @@ -2,63 +2,110 @@ package handler import ( "fmt" + "log" + + "github.com/slack-go/slack" ) -func getSlackEvent(msgEvent string) (string, string, error) { - if val, ok := slackEventTypeMap[msgEvent]; ok { - return val.Emoji, val.Color, nil +// SendToSlack . +func SendToSlack(notification *Notification, channel, webhook, appID string) { + + emoji, color, _, err := getSlackEvent(notification.Event) + if err != nil { + return } - return "", "", fmt.Errorf("no matching event source") + attachment := slack.Attachment{ + Text: fmt.Sprintf("%s %s", emoji, notification.Message), + Color: color, + Footer: appID, + MarkdownIn: []string{"pretext", "text", "fields"}, + } + postMsg := slack.WebhookMessage{ + Attachments: []slack.Attachment{attachment}, + Channel: channel, + } + + err = slack.PostWebhook(webhook, &postMsg) + if err != nil { + // just log any errors + log.Printf("Error sending message to slack: %v", err) + return + } + log.Println(fmt.Sprintf("Sent %s message to slack", notification.Event)) } -type slackEvent struct { - Emoji string `json:"emoji"` - Color string `json:"color"` +func getSlackEvent(msgEvent string) (string, string, string, error) { + if val, ok := slackEventTypeMap[msgEvent]; ok { + return val.Emoji, val.Color, val.Template, nil + } + return "", "", "", fmt.Errorf("no matching event source") } -var slackEventTypeMap = map[string]slackEvent{ - "github:pull_request:closed:handled": {Emoji: ":information_source:", Color: "#E8E8E8"}, - "github:pull_request:opened:handled": {Emoji: ":information_source:", Color: "#E8E8E8"}, - "github:pull_request:synchronize:handled": {Emoji: ":information_source:", Color: "#E8E8E8"}, - "github:delete:handled": {Emoji: ":information_source:", Color: "#E8E8E8"}, - "github:push:handled": {Emoji: ":information_source:", Color: "#E8E8E8"}, - "bitbucket:repo:push:handled": {Emoji: ":information_source:", Color: "#E8E8E8"}, - "bitbucket:pullrequest:created:handled": {Emoji: ":information_source:", Color: "#E8E8E8"}, - "bitbucket:pullrequest:updated:handled": {Emoji: ":information_source:", Color: "#E8E8E8"}, - "bitbucket:pullrequest:fulfilled:handled": {Emoji: ":information_source:", Color: "#E8E8E8"}, - "bitbucket:pullrequest:rejected:handled": {Emoji: ":information_source:", Color: "#E8E8E8"}, - "gitlab:push:handled": {Emoji: ":information_source:", Color: "#E8E8E8"}, - "gitlab:merge_request:opened:handled": {Emoji: ":information_source:", Color: "#E8E8E8"}, - "gitlab:merge_request:updated:handled": {Emoji: ":information_source:", Color: "#E8E8E8"}, - "gitlab:merge_request:closed:handled": {Emoji: ":information_source:", Color: "#E8E8E8"}, - "rest:deploy:receive": {Emoji: ":information_source:", Color: "#E8E8E8"}, - "rest:remove:receive": {Emoji: ":information_source:", Color: "#E8E8E8"}, - "rest:promote:receive": {Emoji: ":information_source:", Color: "#E8E8E8"}, - "api:deployEnvironmentLatest": {Emoji: ":information_source:", Color: "#E8E8E8"}, - "api:deployEnvironmentBranch": {Emoji: ":information_source:", Color: "#E8E8E8"}, - "api:deleteEnvironment": {Emoji: ":information_source:", Color: "#E8E8E8"}, - "github:push:skipped": {Emoji: ":information_source:", Color: "#E8E8E8"}, - "gitlab:push:skipped": {Emoji: ":information_source:", Color: "#E8E8E8"}, - "bitbucket:push:skipped": {Emoji: ":information_source:", Color: "#E8E8E8"}, - "task:deploy-openshift:finished": {Emoji: ":white_check_mark:", Color: "good"}, - "task:remove-openshift:finished": {Emoji: ":white_check_mark:", Color: "good"}, - "task:remove-kubernetes:finished": {Emoji: ":white_check_mark:", Color: "good"}, - "task:remove-openshift-resources:finished": {Emoji: ":white_check_mark:", Color: "good"}, - "task:builddeploy-openshift:complete": {Emoji: ":white_check_mark:", Color: "good"}, - "task:builddeploy-kubernetes:complete": {Emoji: ":white_check_mark:", Color: "good"}, - "task:deploy-openshift:retry": {Emoji: ":warning:", Color: "warning"}, - "task:remove-openshift:retry": {Emoji: ":warning:", Color: "warning"}, - "task:remove-kubernetes:retry": {Emoji: ":warning:", Color: "warning"}, - "task:remove-openshift-resources:retry": {Emoji: ":warning:", Color: "warning"}, - "github:pull_request:closed:CannotDeleteProductionEnvironment": {Emoji: ":warning:", Color: "warning"}, - "github:push:CannotDeleteProductionEnvironment": {Emoji: ":warning:", Color: "warning"}, - "bitbucket:repo:push:CannotDeleteProductionEnvironment": {Emoji: ":warning:", Color: "warning"}, - "gitlab:push:CannotDeleteProductionEnvironment": {Emoji: ":warning:", Color: "warning"}, - "rest:remove:CannotDeleteProductionEnvironment": {Emoji: ":warning:", Color: "warning"}, - "task:deploy-openshift:error": {Emoji: ":bangbang:", Color: "danger"}, - "task:remove-openshift:error": {Emoji: ":bangbang:", Color: "danger"}, - "task:remove-kubernetes:error": {Emoji: ":bangbang:", Color: "danger"}, - "task:remove-openshift-resources:error": {Emoji: ":bangbang:", Color: "danger"}, - "task:builddeploy-openshift:failed": {Emoji: ":bangbang:", Color: "danger"}, - "task:builddeploy-kubernetes:failed": {Emoji: ":bangbang:", Color: "danger"}, +var slackEventTypeMap = map[string]EventMap{ + "github:pull_request:opened:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestOpened"}, + "gitlab:merge_request:opened:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestOpened"}, + "bitbucket:pullrequest:created:opened:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestOpened"}, //not in slack + "bitbucket:pullrequest:created:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestOpened"}, //not in teams + + "github:pull_request:synchronize:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, + "gitlab:merge_request:updated:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, + "bitbucket:pullrequest:updated:opened:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, //not in slack + "bitbucket:pullrequest:updated:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, //not in teams + + "github:pull_request:closed:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestClosed"}, + "bitbucket:pullrequest:fulfilled:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestClosed"}, + "bitbucket:pullrequest:rejected:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestClosed"}, + "gitlab:merge_request:closed:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestClosed"}, + + "github:delete:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "deleteEnvironment"}, + "gitlab:remove:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "deleteEnvironment"}, //not in slack + "bitbucket:delete:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "deleteEnvironment"}, //not in slack + "api:deleteEnvironment": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "deleteEnvironment"}, //not in teams + + "github:push:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "repoPushHandled"}, + "bitbucket:repo:push:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "repoPushHandled"}, + "gitlab:push:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "repoPushHandled"}, + + "github:push:skipped": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "repoPushSkipped"}, + "gitlab:push:skipped": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "repoPushSkipped"}, + "bitbucket:push:skipped": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "repoPushSkipped"}, + + "api:deployEnvironmentLatest": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "deployEnvironment"}, + "api:deployEnvironmentBranch": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "deployEnvironment"}, + + "task:deploy-openshift:finished": {Emoji: ":white_check_mark:", Color: "good", Template: "deployFinished"}, + "task:remove-openshift-resources:finished": {Emoji: ":white_check_mark:", Color: "good", Template: "deployFinished"}, + "task:builddeploy-openshift:complete": {Emoji: ":white_check_mark:", Color: "good", Template: "deployFinished"}, + "task:builddeploy-kubernetes:complete": {Emoji: ":white_check_mark:", Color: "good", Template: "deployFinished"}, //not in teams + + "task:remove-openshift:finished": {Emoji: ":white_check_mark:", Color: "good", Template: "removeFinished"}, + "task:remove-kubernetes:finished": {Emoji: ":white_check_mark:", Color: "good", Template: "removeFinished"}, + + "task:remove-openshift:error": {Emoji: ":bangbang:", Color: "danger", Template: "deployError"}, + "task:remove-kubernetes:error": {Emoji: ":bangbang:", Color: "danger", Template: "deployError"}, + "task:builddeploy-kubernetes:failed": {Emoji: ":bangbang:", Color: "danger", Template: "deployError"}, //not in teams + "task:builddeploy-openshift:failed": {Emoji: ":bangbang:", Color: "danger", Template: "deployError"}, + + "github:pull_request:closed:CannotDeleteProductionEnvironment": {Emoji: ":warning:", Color: "warning", Template: "notDeleted"}, + "github:push:CannotDeleteProductionEnvironment": {Emoji: ":warning:", Color: "warning", Template: "notDeleted"}, + "bitbucket:repo:push:CannotDeleteProductionEnvironment": {Emoji: ":warning:", Color: "warning", Template: "notDeleted"}, + "gitlab:push:CannotDeleteProductionEnvironment": {Emoji: ":warning:", Color: "warning", Template: "notDeleted"}, + + // deprecated + // "rest:remove:CannotDeleteProductionEnvironment": {Emoji: ":warning:", Color: "warning"}, + // "rest:deploy:receive": {Emoji: ":information_source:", Color: "#E8E8E8"}, + // "rest:remove:receive": {Emoji: ":information_source:", Color: "#E8E8E8"}, + // "rest:promote:receive": {Emoji: ":information_source:", Color: "#E8E8E8"}, + // "rest:pullrequest:deploy": {Emoji: ":information_source:", Color: "#E8E8E8"}, + // "rest:pullrequest:remove": {Emoji: ":information_source:", Color: "#E8E8E8"}, + + // deprecated + // "task:deploy-openshift:error": {Emoji: ":bangbang:", Color: "danger", Template: "deployError"}, + // "task:remove-openshift-resources:error": {Emoji: ":bangbang:", Color: "danger", Template: "deployError"}, + + // deprecated + // "task:deploy-openshift:retry": {Emoji: ":warning:", Color: "warning", Template: "removeRetry"}, + // "task:remove-openshift:retry": {Emoji: ":warning:", Color: "warning", Template: "removeRetry"}, + // "task:remove-kubernetes:retry": {Emoji: ":warning:", Color: "warning", Template: "removeRetry"}, + // "task:remove-openshift-resources:retry": {Emoji: ":warning:", Color: "warning", Template: "removeRetry"}, } diff --git a/services/logs2notifications/main.go b/services/logs2notifications/main.go index 1d2a6ded54..789201f56c 100644 --- a/services/logs2notifications/main.go +++ b/services/logs2notifications/main.go @@ -26,10 +26,19 @@ var ( lagoonAppID string jwtTokenSigningKey string jwtAudience string - actionsQueueName string - actionsExchange string jwtSubject string jwtIssuer string + s3FilesAccessKeyID string + s3FilesSecretAccessKey string + s3FilesBucket string + s3FilesRegion string + s3FilesOrigin string + disableSlack bool + disableRocketChat bool + disableMicrosoftTeams bool + disableEmail bool + disableWebhooks bool + disableS3 bool ) func main() { @@ -61,10 +70,28 @@ func main() { "The jwt audience.") flag.StringVar(&jwtIssuer, "jwt-issuer", "logs2notifications", "The jwt audience.") - flag.StringVar(&actionsQueueName, "actions-queue-name", "lagoon-actions:items", - "The name of the queue in rabbitmq to use.") - flag.StringVar(&actionsExchange, "actions-exchange", "lagoon-actions", - "The name of the exchange in rabbitmq to use.") + flag.StringVar(&s3FilesAccessKeyID, "s3-files-access-key", "minio", + "The jwt audience.") + flag.StringVar(&s3FilesSecretAccessKey, "s3-files-secret-access-key", "minio123", + "The jwt audience.") + flag.StringVar(&s3FilesBucket, "s3-files-bucket", "lagoon-files", + "The jwt audience.") + flag.StringVar(&s3FilesRegion, "s3-files-region", "", + "The jwt audience.") + flag.StringVar(&s3FilesOrigin, "s3-files-origin", "http://docker.for.mac.localhost:9000", + "The jwt audience.") + flag.BoolVar(&disableSlack, "disable-slack", false, + "Disable the logs2slack feature.") + flag.BoolVar(&disableRocketChat, "disable-rocketchat", false, + "Disable the logs2rocketchat feature.") + flag.BoolVar(&disableMicrosoftTeams, "disable-microsoft-teams", false, + "Disable the logs2microsoftteams feature.") + flag.BoolVar(&disableEmail, "disable-email", false, + "Disable the logs2email feature.") + flag.BoolVar(&disableWebhooks, "disable-webhooks", false, + "Disable the logs2webhooks feature.") + flag.BoolVar(&disableS3, "disable-s3", false, + "Disable the logs2s3 feature.") flag.Parse() // get overrides from environment variables @@ -77,18 +104,20 @@ func main() { jwtAudience = getEnv("JWT_AUDIENCE", jwtAudience) jwtSubject = getEnv("JWT_SUBJECT", jwtSubject) jwtIssuer = getEnv("JWT_ISSUER", jwtIssuer) - actionsQueueName = getEnv("ACTIONS_QUEUE_NAME", actionsQueueName) - actionsExchange = getEnv("ACTIONS_EXCHANGE", actionsExchange) + + s3FilesAccessKeyID = getEnv("S3_FILES_ACCESS_KEY_ID", s3FilesAccessKeyID) + s3FilesSecretAccessKey = getEnv("S3_FILES_SECRET_ACCESS_KEY", s3FilesSecretAccessKey) + s3FilesBucket = getEnv("S3_FILES_BUCKET", s3FilesBucket) + s3FilesRegion = getEnv("S3_FILES_REGION", s3FilesRegion) + s3FilesOrigin = getEnv("S3_FILES_HOST", s3FilesOrigin) enableDebug := true // configure the backup handler settings broker := handler.RabbitBroker{ - Hostname: fmt.Sprintf("%s:%s", mqHost, mqPort), - Username: mqUser, - Password: mqPass, - QueueName: actionsQueueName, - ExchangeName: actionsExchange, + Hostname: fmt.Sprintf("%s:%s", mqHost, mqPort), + Username: mqUser, + Password: mqPass, } graphQLConfig := handler.LagoonAPI{ Endpoint: lagoonAPIHost, @@ -160,6 +189,12 @@ func main() { startupConnectionInterval, enableDebug, lagoonAppID, + disableSlack, + disableRocketChat, + disableMicrosoftTeams, + disableEmail, + disableWebhooks, + disableS3, ) // start the consumer From 49afad531e00ab8353ef2baa0f217fbc1aac7f7c Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Wed, 23 Mar 2022 09:48:50 +1100 Subject: [PATCH 03/17] feat: add logs2notifications --- services/logs2notifications/Dockerfile | 36 +++ services/logs2notifications/go.mod | 24 ++ services/logs2notifications/go.sum | 207 ++++++++++++ .../internal/handler/email_events.go | 284 +++++++++++++++++ .../internal/handler/main.go | 297 ++++++++++++++++++ .../internal/handler/main_test.go | 209 ++++++++++++ .../internal/handler/microsoftteams_events.go | 201 ++++++++++++ .../internal/handler/rocketchat_events.go | 207 ++++++++++++ .../internal/handler/s3_events.go | 83 +++++ .../internal/handler/slack_events.go | 177 +++++++++++ .../testdata/deleteEnvironment/emailhtml.txt | 1 + .../testdata/deleteEnvironment/emailplain.txt | 1 + .../testdata/deleteEnvironment/rocketchat.txt | 1 + .../testdata/deleteEnvironment/slack.txt | 1 + .../testdata/deleteEnvironment/teams.txt | 1 + .../testdata/deployEnvironment/emailhtml.txt | 1 + .../testdata/deployEnvironment/emailplain.txt | 1 + .../testdata/deployEnvironment/rocketchat.txt | 1 + .../testdata/deployEnvironment/slack.txt | 1 + .../testdata/deployEnvironment/teams.txt | 1 + .../testdata/deployError/emailhtml.txt | 2 + .../testdata/deployError/emailplain.txt | 2 + .../testdata/deployError/rocketchat.txt | 1 + .../handler/testdata/deployError/slack.txt | 1 + .../handler/testdata/deployError/teams.txt | 1 + .../handler/testdata/deployError/webhook.txt | 1 + .../testdata/deployFinished/emailhtml.txt | 10 + .../testdata/deployFinished/emailplain.txt | 4 + .../testdata/deployFinished/rocketchat.txt | 4 + .../handler/testdata/deployFinished/slack.txt | 4 + .../handler/testdata/deployFinished/teams.txt | 4 + .../testdata/deployFinished/webhook.txt | 1 + .../input.deleteEnvironmentGithub.json | 8 + .../testdata/input.deployEnvironment.json | 9 + .../handler/testdata/input.deployError.json | 11 + .../testdata/input.deployFinished.json | 14 + .../testdata/input.repoPushHandledGithub.json | 12 + .../testdata/input.repoPushSkippedGithub.json | 12 + .../handler/testdata/mergeRequestClosed.json | 11 + .../handler/testdata/mergeRequestOpened.json | 11 + .../handler/testdata/mergeRequestUpdated.json | 11 + .../internal/handler/testdata/notDeleted.json | 0 .../testdata/problemNotification.1.json | 12 + .../testdata/problemNotification.2.json | 12 + .../handler/testdata/removeFinished.json | 0 .../testdata/repoPushHandled/emailhtml.txt | 1 + .../testdata/repoPushHandled/emailplain.txt | 1 + .../testdata/repoPushHandled/rocketchat.txt | 1 + .../testdata/repoPushHandled/slack.txt | 1 + .../testdata/repoPushHandled/teams.txt | 1 + .../testdata/repoPushSkipped/emailhtml.txt | 1 + .../testdata/repoPushSkipped/emailplain.txt | 1 + .../testdata/repoPushSkipped/rocketchat.txt | 1 + .../testdata/repoPushSkipped/slack.txt | 1 + .../testdata/repoPushSkipped/teams.txt | 1 + .../internal/handler/webhook_events.go | 150 +++++++++ .../internal/helpers/helpers.go | 77 +++++ .../internal/helpers/helpers_test.go | 160 ++++++++++ .../_lgraphql/projectNotifications.graphql | 41 +++ .../internal/lagoon/client/client.go | 93 ++++++ .../internal/lagoon/client/client_test.go | 81 +++++ .../internal/lagoon/client/helper_test.go | 6 + .../lagoon/client/lgraphql/lgraphql.go | 247 +++++++++++++++ .../internal/lagoon/client/query.go | 25 ++ .../internal/lagoon/jwt/jwt.go | 30 ++ .../internal/lagoon/project.go | 20 ++ .../internal/schema/notifications.go | 107 +++++++ .../internal/schema/project.go | 8 + services/logs2notifications/main.go | 259 +++++++++++++++ 69 files changed, 3207 insertions(+) create mode 100644 services/logs2notifications/Dockerfile create mode 100644 services/logs2notifications/go.mod create mode 100644 services/logs2notifications/go.sum create mode 100644 services/logs2notifications/internal/handler/email_events.go create mode 100644 services/logs2notifications/internal/handler/main.go create mode 100644 services/logs2notifications/internal/handler/main_test.go create mode 100644 services/logs2notifications/internal/handler/microsoftteams_events.go create mode 100644 services/logs2notifications/internal/handler/rocketchat_events.go create mode 100644 services/logs2notifications/internal/handler/s3_events.go create mode 100644 services/logs2notifications/internal/handler/slack_events.go create mode 100644 services/logs2notifications/internal/handler/testdata/deleteEnvironment/emailhtml.txt create mode 100644 services/logs2notifications/internal/handler/testdata/deleteEnvironment/emailplain.txt create mode 100644 services/logs2notifications/internal/handler/testdata/deleteEnvironment/rocketchat.txt create mode 100644 services/logs2notifications/internal/handler/testdata/deleteEnvironment/slack.txt create mode 100644 services/logs2notifications/internal/handler/testdata/deleteEnvironment/teams.txt create mode 100644 services/logs2notifications/internal/handler/testdata/deployEnvironment/emailhtml.txt create mode 100644 services/logs2notifications/internal/handler/testdata/deployEnvironment/emailplain.txt create mode 100644 services/logs2notifications/internal/handler/testdata/deployEnvironment/rocketchat.txt create mode 100644 services/logs2notifications/internal/handler/testdata/deployEnvironment/slack.txt create mode 100644 services/logs2notifications/internal/handler/testdata/deployEnvironment/teams.txt create mode 100644 services/logs2notifications/internal/handler/testdata/deployError/emailhtml.txt create mode 100644 services/logs2notifications/internal/handler/testdata/deployError/emailplain.txt create mode 100644 services/logs2notifications/internal/handler/testdata/deployError/rocketchat.txt create mode 100644 services/logs2notifications/internal/handler/testdata/deployError/slack.txt create mode 100644 services/logs2notifications/internal/handler/testdata/deployError/teams.txt create mode 100644 services/logs2notifications/internal/handler/testdata/deployError/webhook.txt create mode 100644 services/logs2notifications/internal/handler/testdata/deployFinished/emailhtml.txt create mode 100644 services/logs2notifications/internal/handler/testdata/deployFinished/emailplain.txt create mode 100644 services/logs2notifications/internal/handler/testdata/deployFinished/rocketchat.txt create mode 100644 services/logs2notifications/internal/handler/testdata/deployFinished/slack.txt create mode 100644 services/logs2notifications/internal/handler/testdata/deployFinished/teams.txt create mode 100644 services/logs2notifications/internal/handler/testdata/deployFinished/webhook.txt create mode 100644 services/logs2notifications/internal/handler/testdata/input.deleteEnvironmentGithub.json create mode 100644 services/logs2notifications/internal/handler/testdata/input.deployEnvironment.json create mode 100644 services/logs2notifications/internal/handler/testdata/input.deployError.json create mode 100644 services/logs2notifications/internal/handler/testdata/input.deployFinished.json create mode 100644 services/logs2notifications/internal/handler/testdata/input.repoPushHandledGithub.json create mode 100644 services/logs2notifications/internal/handler/testdata/input.repoPushSkippedGithub.json create mode 100644 services/logs2notifications/internal/handler/testdata/mergeRequestClosed.json create mode 100644 services/logs2notifications/internal/handler/testdata/mergeRequestOpened.json create mode 100644 services/logs2notifications/internal/handler/testdata/mergeRequestUpdated.json create mode 100644 services/logs2notifications/internal/handler/testdata/notDeleted.json create mode 100644 services/logs2notifications/internal/handler/testdata/problemNotification.1.json create mode 100644 services/logs2notifications/internal/handler/testdata/problemNotification.2.json create mode 100644 services/logs2notifications/internal/handler/testdata/removeFinished.json create mode 100644 services/logs2notifications/internal/handler/testdata/repoPushHandled/emailhtml.txt create mode 100644 services/logs2notifications/internal/handler/testdata/repoPushHandled/emailplain.txt create mode 100644 services/logs2notifications/internal/handler/testdata/repoPushHandled/rocketchat.txt create mode 100644 services/logs2notifications/internal/handler/testdata/repoPushHandled/slack.txt create mode 100644 services/logs2notifications/internal/handler/testdata/repoPushHandled/teams.txt create mode 100644 services/logs2notifications/internal/handler/testdata/repoPushSkipped/emailhtml.txt create mode 100644 services/logs2notifications/internal/handler/testdata/repoPushSkipped/emailplain.txt create mode 100644 services/logs2notifications/internal/handler/testdata/repoPushSkipped/rocketchat.txt create mode 100644 services/logs2notifications/internal/handler/testdata/repoPushSkipped/slack.txt create mode 100644 services/logs2notifications/internal/handler/testdata/repoPushSkipped/teams.txt create mode 100644 services/logs2notifications/internal/handler/webhook_events.go create mode 100644 services/logs2notifications/internal/helpers/helpers.go create mode 100644 services/logs2notifications/internal/helpers/helpers_test.go create mode 100644 services/logs2notifications/internal/lagoon/client/_lgraphql/projectNotifications.graphql create mode 100644 services/logs2notifications/internal/lagoon/client/client.go create mode 100644 services/logs2notifications/internal/lagoon/client/client_test.go create mode 100644 services/logs2notifications/internal/lagoon/client/helper_test.go create mode 100644 services/logs2notifications/internal/lagoon/client/lgraphql/lgraphql.go create mode 100644 services/logs2notifications/internal/lagoon/client/query.go create mode 100644 services/logs2notifications/internal/lagoon/jwt/jwt.go create mode 100644 services/logs2notifications/internal/lagoon/project.go create mode 100644 services/logs2notifications/internal/schema/notifications.go create mode 100644 services/logs2notifications/internal/schema/project.go create mode 100644 services/logs2notifications/main.go diff --git a/services/logs2notifications/Dockerfile b/services/logs2notifications/Dockerfile new file mode 100644 index 0000000000..49ee341fbb --- /dev/null +++ b/services/logs2notifications/Dockerfile @@ -0,0 +1,36 @@ +# build the binary +ARG GO_VERSION +ARG UPSTREAM_REPO +ARG UPSTREAM_TAG +FROM golang:${GO_VERSION:-1.16.4} AS builder +# bring in all the packages +COPY . /go/src/github.com/uselagoon/lagoon/services/logs2notifications/ +WORKDIR /go/src/github.com/uselagoon/lagoon/services/logs2notifications/ + +# tests currently don't work because mocking rabbit is interesting +RUN GO111MODULE=on go test ./... +# compile +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o logs2notifications . + +# put the binary into container +# use the commons image to get entrypoints +FROM ${UPSTREAM_REPO:-uselagoon}/commons:${UPSTREAM_TAG:-latest} + +ARG LAGOON_VERSION +ENV LAGOON_VERSION=$LAGOON_VERSION + +WORKDIR /app/ + +# bring the auto-idler binary from the builder +COPY --from=builder /go/src/github.com/uselagoon/lagoon/services/logs2notifications/logs2notifications . + +ENV LAGOON=logs2notifications +# set defaults +ENV JWT_SECRET=super-secret-string \ + JWT_AUDIENCE=api.dev \ + GRAPHQL_ENDPOINT="http://api:3000/graphql" \ + RABBITMQ_ADDRESS=broker \ + RABBITMQ_PORT=5672 \ + RABBITMQ_USERNAME=guest \ + RABBITMQ_PASSWORD=guest +CMD ["/app/logs2notifications"] \ No newline at end of file diff --git a/services/logs2notifications/go.mod b/services/logs2notifications/go.mod new file mode 100644 index 0000000000..4382a0c4c7 --- /dev/null +++ b/services/logs2notifications/go.mod @@ -0,0 +1,24 @@ +module github.com/uselagoon/lagoon/services/logs2notifications + +go 1.16 + +require ( + github.com/aws/aws-sdk-go v1.41.11 + github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927 // indirect + github.com/cheshir/go-mq v1.0.2 + github.com/dgrijalva/jwt-go v3.2.0+incompatible + github.com/fsouza/go-dockerclient v1.7.3 // indirect + github.com/machinebox/graphql v0.2.3-0.20181106130121-3a9253180225 + github.com/matryer/is v1.4.0 // indirect + github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2 + github.com/slack-go/slack v0.9.5 + github.com/tiago4orion/conjure v0.0.0-20150908101743-93cb30b9d218 // indirect + gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/mail.v2 v2.3.1 // indirect +) + +// Fixes for AppID +replace github.com/cheshir/go-mq v1.0.2 => github.com/shreddedbacon/go-mq v0.0.0-20200419104937-b8e9af912ead + +replace github.com/NeowayLabs/wabbit v0.0.0-20200409220312-12e68ab5b0c6 => github.com/shreddedbacon/wabbit v0.0.0-20200419104837-5b7b769d7204 diff --git a/services/logs2notifications/go.sum b/services/logs2notifications/go.sum new file mode 100644 index 0000000000..7cbfa00037 --- /dev/null +++ b/services/logs2notifications/go.sum @@ -0,0 +1,207 @@ +bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Microsoft/go-winio v0.4.16-0.20201130162521-d1ffc52c7331/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= +github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= +github.com/Microsoft/hcsshim v0.8.14/go.mod h1:NtVKoYxQuTLx6gEq0L96c9Ju4JbRJ4nY2ow3VK6a9Lg= +github.com/aws/aws-sdk-go v1.41.11 h1:QLouWsiYQ8i22kD8k58Dpdhio1A0MpT7bg9ZNXqEjuI= +github.com/aws/aws-sdk-go v1.41.11/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= +github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927/go.mod h1:h/aW8ynjgkuj+NQRlZcDbAbM1ORAbXjXX77sX7T289U= +github.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59/go.mod h1:pA0z1pT8KYB3TCXK/ocprsh7MAkoW8bZVzPdih9snmM= +github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= +github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.4.3/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= +github.com/containerd/continuity v0.0.0-20210208174643-50096c924a4e/go.mod h1:EXlVlkqNba9rJe3j7w3Xa924itAMLgZH4UD/Q4PExuQ= +github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= +github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= +github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= +github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc= +github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/docker/docker v20.10.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/fsouza/go-dockerclient v1.7.3/go.mod h1:8xfZB8o9SptLNJ13VoV5pMiRbZGWkU/Omu5VOu/KC9Y= +github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/machinebox/graphql v0.2.3-0.20181106130121-3a9253180225 h1:guHWmqIKr4G+gQ4uYU5vcZjsUhhklRA2uOcGVfcfqis= +github.com/machinebox/graphql v0.2.3-0.20181106130121-3a9253180225/go.mod h1:F+kbVMHuwrQ5tYgU9JXlnskM8nOaFxCAEolaQybkjWA= +github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2 h1:JAEbJn3j/FrhdWA9jW8B5ajsLIjeuEHLi8xE4fk997o= +github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2/go.mod h1:0KeJpeMD6o+O4hW7qJOT7vyQPKrWmj26uf5wMc/IiIs= +github.com/moby/sys/mount v0.2.0/go.mod h1:aAivFE2LB3W4bACsUXChRHQ0qKWsetY4Y9V7sxOougM= +github.com/moby/sys/mountinfo v0.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= +github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shreddedbacon/go-mq v0.0.0-20200419104937-b8e9af912ead h1:brBqfI3SWHkBhydQ4zdYbeQj/4Rq68GHO+Me8W7Fji8= +github.com/shreddedbacon/go-mq v0.0.0-20200419104937-b8e9af912ead/go.mod h1:lAwW/xhfO27t6WSVHFtLdgYioymwJvQxMSH19z00/BY= +github.com/shreddedbacon/wabbit v0.0.0-20200419104837-5b7b769d7204 h1:jXml7E4X/d9v6LATMXFPCF1yW6TKrs+o5wMtYTaBdTw= +github.com/shreddedbacon/wabbit v0.0.0-20200419104837-5b7b769d7204/go.mod h1:l7t6U3j3uZUYroWctp1FvWEktRMuGqx2zCcb5cd8cS8= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/slack-go/slack v0.9.5 h1:j7uOUDowybWf9eSgZg/AbGx6J1OPJB6SE8Z5dNl6Mtw= +github.com/slack-go/slack v0.9.5/go.mod h1:wWL//kk0ho+FcQXcBTmEafUI5dz4qz5f4mMk8oIkioQ= +github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/streadway/amqp v0.0.0-20200108173154-1c71cc93ed71 h1:2MR0pKUzlP3SGgj5NYJe/zRYDwOu9ku6YHy+Iw7l5DM= +github.com/streadway/amqp v0.0.0-20200108173154-1c71cc93ed71/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/tiago4orion/conjure v0.0.0-20150908101743-93cb30b9d218/go.mod h1:GQei++1WClbEC7AN1B9ipY1jCjzllM/7UNg0okAh/Z4= +github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/uselagoon/lagoon v1.15.1 h1:/EUJJrWmmNrFVIh4vYm71/+fk2yXlCk+M5g8L+2ll0c= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200922070232-aee5d888a860/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210216224549-f992740a1bac/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201113234701-d7a72108b828/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk= +gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw= +gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= +gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/services/logs2notifications/internal/handler/email_events.go b/services/logs2notifications/internal/handler/email_events.go new file mode 100644 index 0000000000..8004ddb21d --- /dev/null +++ b/services/logs2notifications/internal/handler/email_events.go @@ -0,0 +1,284 @@ +package handler + +import ( + "bytes" + "crypto/tls" + "fmt" + "log" + "strconv" + "strings" + "text/template" + + gomail "gopkg.in/mail.v2" +) + +var htmlTemplate = ` + + + {{.Title}} + + + + + + +
+

{{.Emoji}} [{{.ProjectName}}]

+

+ {{.MainHTML}} +

+
+ +` + +// SendToEmail . +func (h *Messaging) SendToEmail(notification *Notification, emailAddress string) { + emoji, color, subject, mainHTML, plainText, err := h.processEmailTemplates(notification) + if err != nil { + return + } + h.sendEmailMessage(emoji, color, subject, notification.Event, notification.Meta.ProjectName, emailAddress, mainHTML, plainText) +} + +// SendToEmail . +func (h *Messaging) processEmailTemplates(notification *Notification) (string, string, string, string, string, error) { + + emoji, color, tpl, err := getEmailEvent(notification.Event) + if err != nil { + eventSplit := strings.Split(notification.Event, ":") + fmt.Println(eventSplit[0]) + if eventSplit[0] != "problem" { + return "", "", "", "", "", nil + } + if eventSplit[1] == "insert" { + tpl = "problemNotification" + } + } + var mainHTML, plainText, subject, plainTextTpl, mainHTMLTpl string + switch tpl { + case "mergeRequestOpened": + mainHTMLTpl = `PR #{{.PullrequestNumber}} ({{.PullrequestTitle}}) opened in {{.RepoName}}` + plainTextTpl = `[{{.ProjectName}}] PR #{{.PullrequestNumber}} - {{.PullrequestTitle}} opened in {{.RepoName}}` + case "mergeRequestUpdated": + mainHTMLTpl = `PR #{{.PullrequestNumber}} ({{.PullrequestTitle}}) updated in {{.RepoName}}` + plainTextTpl = `[{{.ProjectName}}] PR #{{.PullrequestNumber}} - {{.PullrequestTitle}} updated in {{.RepoName}}` + case "mergeRequestClosed": + mainHTMLTpl = `PR #{{.PullrequestNumber}} ({{.PullrequestTitle}}) closed in {{.RepoName}}` + plainTextTpl = `[{{.ProjectName}}] PR #{{.PullrequestNumber}} - {{.PullrequestTitle}} closed in {{.RepoName}}` + case "deleteEnvironment": + mainHTMLTpl = `Deleted environment {{.EnvironmentName}}` + plainTextTpl = `[{{.ProjectName}}] deleted environment {{.EnvironmentName}}` + case "repoPushHandled": + mainHTMLTpl = `{{.BranchName}}{{ if ne .ShortSha "" }} {{.ShortSha}}{{end}} pushed in {{.RepoFullName}}` + plainTextTpl = `[{{.ProjectName}}] {{.BranchName}}{{ if ne .ShortSha "" }} ({{.ShortSha}}){{end}} pushed in {{.RepoFullName}}` + case "repoPushSkipped": + mainHTMLTpl = `{{.BranchName}}{{ if ne .ShortSha "" }} {{.ShortSha}}{{end}} pushed in {{.RepoFullName}} deployment skipped` + plainTextTpl = `[{{.ProjectName}}] {{.BranchName}}{{ if ne .ShortSha "" }} ({{.ShortSha}}){{end}} pushed in {{.RepoFullName}} *deployment skipped*` + case "deployEnvironment": + mainHTMLTpl = `Deployment triggered {{.BranchName}}{{ if ne .ShortSha "" }} ({{.ShortSha}}){{end}}` + plainTextTpl = `[{{.ProjectName}}] Deployment triggered on branch {{.BranchName}}{{ if ne .ShortSha "" }} ({{.ShortSha}}){{end}}` + case "removeFinished": + mainHTMLTpl = `Remove {{.OpenshiftProject}}` + plainTextTpl = `[{{.ProjectName}] remove {{.OpenshiftProject}}` + case "notDeleted": + mainHTMLTpl = `{{.OpenshiftProject}} not deleted.` + plainTextTpl = `[{{.ProjectName}] {{.OpenshiftProject}} not deleted. {{.Error}}` + case "deployError": + mainHTMLTpl = `[{{.ProjectName}}] {{.BranchName}}{{ if ne .ShortSha "" }} ({{.ShortSha}}){{end}} Build {{.BuildName}} error. +{{if ne .LogLink ""}} Logs{{end}}` + plainTextTpl = `[{{.ProjectName}}] {{.BranchName}}{{ if ne .ShortSha "" }} ({{.ShortSha}}){{end}} Build {{.BuildName}} error. +{{if ne .LogLink ""}} [Logs]({{.LogLink}}){{end}}` + subject += fmt.Sprintf("[%s] %s Build %s error.", + notification.Meta.ProjectName, + notification.Meta.BranchName, + notification.Meta.BuildName, + ) + case "deployFinished": + mainHTMLTpl = `[{{.ProjectName}}] {{.BranchName}}{{ if ne .ShortSha "" }} ({{.ShortSha}}){{end}} Build {{.BuildName}} complete. {{if ne .LogLink ""}}Logs{{end}} +

+ +
+

+

` + plainTextTpl = `[{{.ProjectName}}] {{.BranchName}}{{ if ne .ShortSha "" }} ({{.ShortSha}}){{end}} Build {{.BuildName}} complete. {{if ne .LogLink ""}}[Logs]({{.LogLink}}){{end}} +{{.Route}} +{{range .Routes}}{{if ne . $.Route}}{{.}} +{{end}}{{end}}` + subject += fmt.Sprintf("[%s] %s Build %s complete.", + notification.Meta.ProjectName, + notification.Meta.BranchName, + notification.Meta.BuildName, + ) + case "problemNotification": + eventSplit := strings.Split(notification.Event, ":") + if eventSplit[0] != "problem" && eventSplit[1] == "insert" { + return "", "", "", "", "", nil + } + mainHTMLTpl = `[{{.ProjectName}}] New problem found for {{.EnvironmentName}} +
  • * Service: {{.ServiceName}}
  • {{ if ne .Severity "" }} +
  • * Severity: {{.Severity}}{{end}}
  • {{ if ne .Description "" }} +
  • * Description: {{.Description}}
  • {{end}}
` + plainTextTpl = `[{{.ProjectName}}] New problem found for ` + "`{{.EnvironmentName}}`" + ` +* Service: ` + "`{{.ServiceName}}`" + `{{ if ne .Severity "" }} +* Severity: {{.Severity}}{{end}}{{ if ne .Description "" }} +* Description: {{.Description}}{{end}}` + subject += fmt.Sprintf("[%s] New problem found for environment %s", + notification.Meta.ProjectName, + notification.Meta.EnvironmentName, + ) + default: + return "", "", "", "", "", nil + } + + var body bytes.Buffer + t, _ := template.New("email").Parse(mainHTMLTpl) + t.Execute(&body, notification.Meta) + mainHTML += body.String() + + var plainTextBuffer bytes.Buffer + t, _ = template.New("email").Parse(plainTextTpl) + t.Execute(&plainTextBuffer, notification.Meta) + plainText += plainTextBuffer.String() + if subject == "" { + subject = plainText + } + return emoji, color, subject, mainHTML, plainText, nil +} + +func (h *Messaging) sendEmailMessage(emoji, color, subject, event, project, emailAddress, mainHTML, plainText string) { + var body bytes.Buffer + + // mimeHeaders := "MIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\n\n" + // body.Write([]byte(fmt.Sprintf("From: Lagoon Notifications<%s>\nSubject: %s \n%s\n\n", h.EmailSender, subject, mimeHeaders))) + + t, _ := template.New("email").Parse(htmlTemplate) + t.Execute(&body, struct { + Color string + Emoji string + Title string + ProjectName string + MainHTML string + }{ + Title: plainText, + Color: color, + Emoji: emoji, + ProjectName: project, + MainHTML: mainHTML, + }) + + m := gomail.NewMessage() + m.SetHeader("From", h.EmailSender) + m.SetHeader("To", emailAddress) + m.SetHeader("Subject", subject) + m.SetBody("text/plain", plainText) + m.AddAlternative("text/html", body.String()) + sPort, _ := strconv.Atoi(h.EmailPort) + d := gomail.NewDialer(h.EmailHost, sPort, h.EmailSender, "") + d.TLSConfig = &tls.Config{InsecureSkipVerify: h.EmailInsecureSkipVerify} + if err := d.DialAndSend(m); err != nil { + fmt.Println(err) + panic(err) + } + + // // Create authentication + // auth := smtp.PlainAuth("", sender, password, smtpHost) + // // Send actual message + // err := smtp.SendMail(smtpHost+":"+smtpPort, auth, sender, to, body.Bytes()) + // if err != nil { + // log.Printf("Error sending message to email: %v", err) + // return + // } + log.Println(fmt.Sprintf("Sent %s message to email", event)) +} + +func getEmailEvent(msgEvent string) (string, string, string, error) { + if val, ok := emailEvent[msgEvent]; ok { + return val.Emoji, val.Color, val.Template, nil + } + return "", "", "", fmt.Errorf("no matching event source") +} + +var emailEvent = map[string]EventMap{ + "github:pull_request:opened:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestOpened"}, + "gitlab:merge_request:opened:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestOpened"}, + "bitbucket:pullrequest:created:opened:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestOpened"}, //not in slack + "bitbucket:pullrequest:created:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestOpened"}, //not in teams + + "github:pull_request:synchronize:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, + "gitlab:merge_request:updated:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, + "bitbucket:pullrequest:updated:opened:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, //not in slack + "bitbucket:pullrequest:updated:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, //not in teams + + "github:pull_request:closed:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestClosed"}, + "bitbucket:pullrequest:fulfilled:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestClosed"}, + "bitbucket:pullrequest:rejected:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestClosed"}, + "gitlab:merge_request:closed:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestClosed"}, + + "github:delete:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "deleteEnvironment"}, + "gitlab:remove:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "deleteEnvironment"}, //not in slack + "bitbucket:delete:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "deleteEnvironment"}, //not in slack + "api:deleteEnvironment": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "deleteEnvironment"}, //not in teams + + "github:push:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "repoPushHandled"}, + "bitbucket:repo:push:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "repoPushHandled"}, + "gitlab:push:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "repoPushHandled"}, + + "github:push:skipped": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "repoPushSkipped"}, + "gitlab:push:skipped": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "repoPushSkipped"}, + "bitbucket:push:skipped": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "repoPushSkipped"}, + + "api:deployEnvironmentLatest": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "deployEnvironment"}, + "api:deployEnvironmentBranch": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "deployEnvironment"}, + + "task:deploy-openshift:finished": {Emoji: "✅", Color: "lawngreen", Template: "deployFinished"}, + "task:remove-openshift-resources:finished": {Emoji: "✅", Color: "lawngreen", Template: "deployFinished"}, + "task:builddeploy-openshift:complete": {Emoji: "✅", Color: "lawngreen", Template: "deployFinished"}, + "task:builddeploy-kubernetes:complete": {Emoji: "✅", Color: "lawngreen", Template: "deployFinished"}, //not in teams + + "task:remove-openshift:finished": {Emoji: "✅", Color: "lawngreen", Template: "removeFinished"}, + "task:remove-kubernetes:finished": {Emoji: "✅", Color: "lawngreen", Template: "removeFinished"}, + + "task:remove-openshift:error": {Emoji: "‼️", Color: "red", Template: "deployError"}, + "task:remove-kubernetes:error": {Emoji: "‼️", Color: "red", Template: "deployError"}, + "task:builddeploy-kubernetes:failed": {Emoji: "‼️", Color: "red", Template: "deployError"}, //not in teams + "task:builddeploy-openshift:failed": {Emoji: "‼️", Color: "red", Template: "deployError"}, + + "github:pull_request:closed:CannotDeleteProductionEnvironment": {Emoji: "⚠️", Color: "gold", Template: "notDeleted"}, + "github:push:CannotDeleteProductionEnvironment": {Emoji: "⚠️", Color: "gold", Template: "notDeleted"}, + "bitbucket:repo:push:CannotDeleteProductionEnvironment": {Emoji: "⚠️", Color: "gold", Template: "notDeleted"}, + "gitlab:push:CannotDeleteProductionEnvironment": {Emoji: "⚠️", Color: "gold", Template: "notDeleted"}, + + // deprecated + // "rest:remove:CannotDeleteProductionEnvironment": {Emoji: "⚠️", Color: "gold"}, + // "rest:deploy:receive": {Emoji: "ℹ️", Color: "#E8E8E8"}, + // "rest:remove:receive": {Emoji: "ℹ️", Color: "#E8E8E8"}, + // "rest:promote:receive": {Emoji: "ℹ️", Color: "#E8E8E8"}, + // "rest:pullrequest:deploy": {Emoji: "ℹ️", Color: "#E8E8E8"}, + // "rest:pullrequest:remove": {Emoji: "ℹ️", Color: "#E8E8E8"}, + + // deprecated + // "task:deploy-openshift:error": {Emoji: "‼️", Color: "red", Template: "deployError"}, + // "task:remove-openshift-resources:error": {Emoji: "‼️", Color: "red", Template: "deployError"}, + + // deprecated + // "task:deploy-openshift:retry": {Emoji: "⚠️", Color: "gold", Template: "removeRetry"}, + // "task:remove-openshift:retry": {Emoji: "⚠️", Color: "gold", Template: "removeRetry"}, + // "task:remove-kubernetes:retry": {Emoji: "⚠️", Color: "gold", Template: "removeRetry"}, + // "task:remove-openshift-resources:retry": {Emoji: "⚠️", Color: "gold", Template: "removeRetry"}, +} diff --git a/services/logs2notifications/internal/handler/main.go b/services/logs2notifications/internal/handler/main.go new file mode 100644 index 0000000000..5ce74d3655 --- /dev/null +++ b/services/logs2notifications/internal/handler/main.go @@ -0,0 +1,297 @@ +package handler + +import ( + "context" + "encoding/json" + "fmt" + "log" + "regexp" + "time" + + "github.com/cheshir/go-mq" + "github.com/matryer/try" + "github.com/uselagoon/lagoon/services/logs2notifications/internal/lagoon" + lclient "github.com/uselagoon/lagoon/services/logs2notifications/internal/lagoon/client" + "github.com/uselagoon/lagoon/services/logs2notifications/internal/lagoon/jwt" + "github.com/uselagoon/lagoon/services/logs2notifications/internal/schema" +) + +// RabbitBroker . +type RabbitBroker struct { + Hostname string `json:"hostname"` + Port string `json:"port"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` +} + +// LagoonAPI . +type LagoonAPI struct { + Endpoint string `json:"endpoint"` + JWTAudience string `json:"audience"` + TokenSigningKey string `json:"tokenSigningKey` + JWTSubject string `json:"subject"` + JWTIssuer string `json:"issuer"` +} + +// Action is the structure of an action that is received via the message queue. +type Action struct { + Type string `json:"type"` // defines the action type + EventType string `json:"eventType"` // defines the eventtype field in the event notification + Data map[string]interface{} `json:"data"` // contains the payload for the action, this could be any json so using a map +} + +type messaging interface { + Consumer() + Publish(string, []byte) +} + +// Messaging is used for the config and client information for the messaging queue. +type Messaging struct { + Config mq.Config + LagoonAPI LagoonAPI + ConnectionAttempts int + ConnectionRetryInterval int + EnableDebug bool + LagoonAppID string + DisableSlack bool + DisableRocketChat bool + DisableMicrosoftTeams bool + DisableEmail bool + DisableWebhooks bool + DisableS3 bool + EmailSender string + EmailSenderPassword string + EmailHost string + EmailPort string + EmailInsecureSkipVerify bool + S3FilesAccessKeyID string + S3FilesSecretAccessKey string + S3FilesBucket string + S3FilesRegion string + S3FilesOrigin string +} + +// Notification . +type Notification struct { + Severity string `json:"severity"` + Project string `json:"project"` + UUID string `json:"uuid"` + Event string `json:"event"` + Meta struct { + User struct { + ID string `json:"id"` + PreferredUsername string `json:"preferred_username"` + Email string `json:"email"` + } `json:"user"` + Headers struct { + UserAgent string `json:"user-agent"` + ContentType string `json:"content-type"` + ContentLength string `json:"content-length"` + Host string `json:"host"` + IPAddress string `json:"ipAddress"` + } `json:"headers"` + Project string `json:"project"` + ProjectName string `json:"projectName"` + BranchName string `json:"branchName` + Event string `json:"event"` + Level string `json:"level"` + Message string `json:"message"` + Timestamp string `json:"timestamp"` + ShortSha string `json:"shortSha"` + BuildName string `json:"buildName"` + CommitURL string `json:"commitUrl"` + Environment string `json:"environment"` + EnvironmentID string `json:"environmentId"` + EnvironmentName string `json:"environmentName"` + ServiceName string `json:"serviceName"` + Severity string `json:"severity"` + Description string `json:"description"` + Error string `json:"error"` + JobName string `json:"jobName"` + LogLink string `json:"logLink"` + Name string `json:"name"` + OpenshiftProject string `json:"openshiftProject"` + PromoteSourceEnvironment string `json:"promoteSourceEnvironment"` + PullrequestNumber string `json:"pullrequestNumber"` + PullrequestTitle string `json:"pullrequestTitle"` + PullrequestURL string `json:"pullrequestUrl"` + RemoteID string `json:"remoteId"` + RepoFullName string `json:"repoFullName"` + RepoName string `json:"repoName"` + RepoURL string `json:"repoUrl"` + Route string `json:"route"` + Routes []string `json:"routes"` + Task struct { + ID int `json:"id"` + } `json:"task"` + } `json:"meta"` + Message string `json:"message"` +} + +// EventMap . +type EventMap struct { + Emoji string `json:"emoji"` + Color string `json:"color"` + Template string `json:"template"` +} + +// NewMessaging returns a messaging with config +func NewMessaging(config mq.Config, + lagoonAPI LagoonAPI, + startupAttempts int, + startupInterval int, + enableDebug bool, + appID string, + disableSlack, disableRocketChat, disableMicrosoftTeams, disableEmail, disableWebhooks, disableS3 bool, + emailSender, emailSenderPassword, emailHost, emailPort string, emailInsecureSkipVerify bool, + s3FilesAccessKeyID, s3FilesSecretAccessKey, s3FilesBucket, s3FilesRegion, s3FilesOrigin string) *Messaging { + return &Messaging{ + Config: config, + LagoonAPI: lagoonAPI, + ConnectionAttempts: startupAttempts, + ConnectionRetryInterval: startupInterval, + EnableDebug: enableDebug, + LagoonAppID: appID, + DisableSlack: disableSlack, + DisableRocketChat: disableRocketChat, + DisableMicrosoftTeams: disableMicrosoftTeams, + DisableEmail: disableEmail, + DisableWebhooks: disableWebhooks, + DisableS3: disableS3, + EmailSender: emailSender, + EmailSenderPassword: emailSenderPassword, + EmailHost: emailHost, + EmailPort: emailPort, + EmailInsecureSkipVerify: emailInsecureSkipVerify, + S3FilesAccessKeyID: s3FilesAccessKeyID, + S3FilesSecretAccessKey: s3FilesSecretAccessKey, + S3FilesBucket: s3FilesBucket, + S3FilesRegion: s3FilesRegion, + S3FilesOrigin: s3FilesOrigin, + } +} + +// Consumer handles consuming messages sent to the queue that this action handler is connected to and processes them accordingly +func (h *Messaging) Consumer() { + + var messageQueue mq.MQ + // if no mq is found when the goroutine starts, retry a few times before exiting + // default is 10 retry with 30 second delay = 5 minutes + err := try.Do(func(attempt int) (bool, error) { + var err error + messageQueue, err = mq.New(h.Config) + if err != nil { + log.Println(err, + fmt.Sprintf( + "Failed to initialize message queue manager, retrying in %d seconds, attempt %d/%d", + h.ConnectionRetryInterval, + attempt, + h.ConnectionAttempts, + ), + ) + time.Sleep(time.Duration(h.ConnectionRetryInterval) * time.Second) + } + return attempt < h.ConnectionAttempts, err + }) + if err != nil { + log.Fatalf("Finally failed to initialize message queue manager: %v", err) + } + defer messageQueue.Close() + + go func() { + for err := range messageQueue.Error() { + log.Println(fmt.Sprintf("Caught error from message queue: %v", err)) + } + }() + + forever := make(chan bool) + + // Handle any tasks that go to the queue + log.Println("Listening for messages in queue lagoon-logs:notifications") + err = messageQueue.SetConsumerHandler("notifications-queue", func(message mq.Message) { + h.processMessage(message.Body(), message.AppId()) + message.Ack(false) // ack to remove from queue + }) + if err != nil { + log.Println(fmt.Sprintf("Failed to set handler to consumer `%s`: %v", "items-queue", err)) + } + <-forever +} + +func (h *Messaging) processMessage(message []byte, applicationID string) { + ctx := context.Background() + notification := &Notification{} + json.Unmarshal(message, notification) + + var buildLogs = regexp.MustCompile(`^build-logs:builddeploy-kubernetes:.*`) + var taskLogs = regexp.MustCompile(`^(build|task)-logs:job-kubernetes:.*`) + switch notification.Event { + case buildLogs.FindString(notification.Event): + // if this is a build logs message handle it accordingly + if !h.DisableS3 { + h.SendToS3(notification, buildMessageType) + } + case taskLogs.FindString(notification.Event): + // if this is a task logs message handle it accordingly + if !h.DisableS3 { + h.SendToS3(notification, taskMessageType) + } + default: + // all other events are notifications, so do notification handling with them + // and as long as the event is not a `user_action` (activity logger) + if notification.Project != "" && notification.Meta.Level != "user_action" { + // marshal unmarshal the data into the input we need to use when talking to the lagoon api + projectNotifications, err := h.getProjectNotifictions(ctx, notification.Project) + if err != nil { + log.Println(err) + break + } + if projectNotifications.Notifications != nil { + if len(projectNotifications.Notifications.Slack) > 0 && !h.DisableSlack { + for _, slack := range projectNotifications.Notifications.Slack { + h.SendToSlack(notification, slack.Channel, slack.Webhook, applicationID) + } + } + if len(projectNotifications.Notifications.RocketChat) > 0 && !h.DisableRocketChat { + for _, rc := range projectNotifications.Notifications.RocketChat { + h.SendToRocketChat(notification, rc.Channel, rc.Webhook, applicationID) + } + } + if len(projectNotifications.Notifications.Email) > 0 && !h.DisableEmail { + for _, email := range projectNotifications.Notifications.Email { + h.SendToEmail(notification, email.EmailAddress) + } + } + if len(projectNotifications.Notifications.MicrosoftTeams) > 0 && !h.DisableMicrosoftTeams { + for _, teams := range projectNotifications.Notifications.MicrosoftTeams { + h.SendToMicrosoftTeams(notification, teams.Webhook) + } + } + if len(projectNotifications.Notifications.Webhook) > 0 && !h.DisableWebhooks { + for _, hook := range projectNotifications.Notifications.Webhook { + h.SendToWebhook(notification, hook) + } + } + } + } + } +} + +func (h *Messaging) getProjectNotifictions(ctx context.Context, projectName string) (*schema.Project, error) { + token, err := jwt.OneMinuteAdminToken(h.LagoonAPI.TokenSigningKey, h.LagoonAPI.JWTAudience, h.LagoonAPI.JWTSubject, h.LagoonAPI.JWTIssuer) + if err != nil { + // the token wasn't generated + if h.EnableDebug { + log.Println(err) + } + return nil, err + } + // get all notifications for said project + l := lclient.New(h.LagoonAPI.Endpoint, token, "logs2notifications", false) + projectNotifications, err := lagoon.NotificationsForProject(ctx, projectName, l) + if err != nil { + log.Println(err) + return nil, err + } + return projectNotifications, nil +} diff --git a/services/logs2notifications/internal/handler/main_test.go b/services/logs2notifications/internal/handler/main_test.go new file mode 100644 index 0000000000..29687731ca --- /dev/null +++ b/services/logs2notifications/internal/handler/main_test.go @@ -0,0 +1,209 @@ +package handler + +import ( + "bytes" + "encoding/json" + "io/ioutil" + "reflect" + "testing" + + "github.com/cheshir/go-mq" +) + +func checkEqual(t *testing.T, got, want interface{}, msgs ...interface{}) { + if !reflect.DeepEqual(got, want) { + buf := bytes.Buffer{} + buf.WriteString("got:\n[%v]\nwant:\n[%v]\n") + for _, v := range msgs { + buf.WriteString(v.(string)) + } + t.Errorf(buf.String(), got, want) + } +} + +func TestProcessing(t *testing.T) { + config := mq.Config{} + graphQLConfig := LagoonAPI{ + // Endpoint: svr.URL, + // TokenSigningKey: "jwtTokenSigningKey", + // JWTAudience: "jwtAudience", + // JWTSubject: "jwtSubject", + // JWTIssuer: "jwtIssuer", + } + messaging := NewMessaging(config, + graphQLConfig, + 1, + 1, + true, + "lagoonAppID", + false, + false, + false, + false, + false, + false, + "emailSender", + "emailSenderPassword", + "emailHost", + "emailPort", + true, + "s3FilesAccessKeyID", + "s3FilesSecretAccessKey", + "s3FilesBucket", + "s3FilesRegion", + "s3FilesOrigin", + ) + var testCases = map[string]struct { + input string + description string + slack string + rocketchat string + emailhtml string + emailplain string + teams string + webhook string + }{ + "repoPushHandledGithub": { + description: "test github repo push handled events", + input: "testdata/input.repoPushHandledGithub.json", + slack: "testdata/repoPushHandled/slack.txt", + rocketchat: "testdata/repoPushHandled/rocketchat.txt", + emailhtml: "testdata/repoPushHandled/emailhtml.txt", + emailplain: "testdata/repoPushHandled/emailplain.txt", + teams: "testdata/repoPushHandled/teams.txt", + }, + "repoPushSkippedGithub": { + description: "test github repo push skipped events", + input: "testdata/input.repoPushSkippedGithub.json", + slack: "testdata/repoPushSkipped/slack.txt", + rocketchat: "testdata/repoPushSkipped/rocketchat.txt", + emailhtml: "testdata/repoPushSkipped/emailhtml.txt", + emailplain: "testdata/repoPushSkipped/emailplain.txt", + teams: "testdata/repoPushSkipped/teams.txt", + }, + "deleteEnvironmentGithub": { + description: "test github repo push deleted events", + input: "testdata/input.deleteEnvironmentGithub.json", + slack: "testdata/deleteEnvironment/slack.txt", + rocketchat: "testdata/deleteEnvironment/rocketchat.txt", + emailhtml: "testdata/deleteEnvironment/emailhtml.txt", + emailplain: "testdata/deleteEnvironment/emailplain.txt", + teams: "testdata/deleteEnvironment/teams.txt", + }, + "deployEnvironment": { + description: "test github repo push deleted events", + input: "testdata/input.deployEnvironment.json", + slack: "testdata/deployEnvironment/slack.txt", + rocketchat: "testdata/deployEnvironment/rocketchat.txt", + emailhtml: "testdata/deployEnvironment/emailhtml.txt", + emailplain: "testdata/deployEnvironment/emailplain.txt", + teams: "testdata/deployEnvironment/teams.txt", + }, + "deployError": { + description: "test github repo push deleted events", + input: "testdata/input.deployError.json", + slack: "testdata/deployError/slack.txt", + rocketchat: "testdata/deployError/rocketchat.txt", + emailhtml: "testdata/deployError/emailhtml.txt", + emailplain: "testdata/deployError/emailplain.txt", + teams: "testdata/deployError/teams.txt", + webhook: "testdata/deployError/webhook.txt", + }, + "deployFinished": { + description: "test github repo push deleted events", + input: "testdata/input.deployFinished.json", + slack: "testdata/deployFinished/slack.txt", + rocketchat: "testdata/deployFinished/rocketchat.txt", + emailhtml: "testdata/deployFinished/emailhtml.txt", + emailplain: "testdata/deployFinished/emailplain.txt", + teams: "testdata/deployFinished/teams.txt", + webhook: "testdata/deployFinished/webhook.txt", + }, + } + for name, tc := range testCases { + t.Run(name, func(tt *testing.T) { + // read the input into a the notification struct + inputBytes, err := ioutil.ReadFile(tc.input) // just pass the file name + if err != nil { + tt.Fatalf("unexpected error %v", err) + } + notification := &Notification{} + json.Unmarshal(inputBytes, notification) + + // process slack template + resultBytes, err := ioutil.ReadFile(tc.slack) // just pass the file name + if err != nil { + tt.Fatalf("unexpected error %v", err) + } + _, _, message, err := messaging.processSlackTemplate(notification) + if err != nil { + tt.Fatalf("unexpected error %v", err) + } + if message != string(resultBytes) { + tt.Fatalf("message doesn't match, wanted `%s` got `%s`", message, string(resultBytes)) + } + + // process rocketchat template + resultBytes, err = ioutil.ReadFile(tc.rocketchat) // just pass the file name + if err != nil { + tt.Fatalf("unexpected error %v", err) + } + _, _, message, err = messaging.processRocketChatTemplate(notification) + if err != nil { + tt.Fatalf("unexpected error %v", err) + } + if message != string(resultBytes) { + tt.Fatalf("message doesn't match, wanted `%s` got `%s`", message, string(resultBytes)) + } + + // process email templates + resultBytesHTML, err := ioutil.ReadFile(tc.emailhtml) // just pass the file name + if err != nil { + tt.Fatalf("unexpected error %v", err) + } + resultBytesPlainText, err := ioutil.ReadFile(tc.emailplain) // just pass the file name + if err != nil { + tt.Fatalf("unexpected error %v", err) + } + _, _, _, htmlMessage, plaintextMessage, err := messaging.processEmailTemplates(notification) + if err != nil { + tt.Fatalf("unexpected error %v", err) + } + if htmlMessage != string(resultBytesHTML) { + tt.Fatalf("html doesn't match, wanted `%s` got `%s`", string(htmlMessage), string(resultBytes)) + } + if plaintextMessage != string(resultBytesPlainText) { + tt.Fatalf("plaintextmessage doesn't match, wanted `%s` got `%s`", string(plaintextMessage), string(resultBytes)) + } + + // process teams template + resultBytes, err = ioutil.ReadFile(tc.teams) // just pass the file name + if err != nil { + tt.Fatalf("unexpected error %v", err) + } + _, _, message, err = messaging.processMicrosoftTeamsTemplate(notification) + if err != nil { + tt.Fatalf("unexpected error %v", err) + } + if message != string(resultBytes) { + tt.Fatalf("message doesn't match, wanted `%s` got `%s`", message, string(resultBytes)) + } + + // process webhook template + if tc.webhook != "" { + resultBytes, err = ioutil.ReadFile(tc.webhook) // just pass the file name + if err != nil { + tt.Fatalf("unexpected error %v", err) + } + message, err := messaging.processWebhookTemplate(notification) + if err != nil { + tt.Fatalf("unexpected error %v", err) + } + messageBytes, _ := json.Marshal(&message) + if string(messageBytes) != string(resultBytes) { + tt.Fatalf("message doesn't match, wanted `%s` got `%s`", string(messageBytes), string(resultBytes)) + } + } + }) + } +} diff --git a/services/logs2notifications/internal/handler/microsoftteams_events.go b/services/logs2notifications/internal/handler/microsoftteams_events.go new file mode 100644 index 0000000000..87df357d1a --- /dev/null +++ b/services/logs2notifications/internal/handler/microsoftteams_events.go @@ -0,0 +1,201 @@ +package handler + +import ( + "bytes" + "encoding/json" + "fmt" + "log" + "net/http" + "strings" + "text/template" +) + +// MicrosoftTeamsData . +type MicrosoftTeamsData struct { + Type string `json:"@type"` + Context string `json:"@context"` + Summary string `json:"summary"` + Title string `json:"title"` + ThemeColor string `json:"themeColor"` + Sections []MicrosoftTeamsSection `json:"sections"` +} + +// MicrosoftTeamsSection . +type MicrosoftTeamsSection struct { + ActivityText string `json:"activityText"` + ActivityImage string `json:"activityImage"` +} + +// SendToMicrosoftTeams . +func (h *Messaging) SendToMicrosoftTeams(notification *Notification, webhook string) { + emoji, color, message, err := h.processMicrosoftTeamsTemplate(notification) + if err != nil { + return + } + h.sendMicrosoftTeamsMessage(emoji, color, webhook, notification.Event, notification.Meta.ProjectName, message) +} + +// processMicrosoftTeamsTemplate . +func (h *Messaging) processMicrosoftTeamsTemplate(notification *Notification) (string, string, string, error) { + emoji, color, tpl, err := getMicrosoftTeamsEvent(notification.Event) + if err != nil { + eventSplit := strings.Split(notification.Event, ":") + if eventSplit[0] != "problem" { + return "", "", "", fmt.Errorf("no matching event") + } + if eventSplit[1] == "insert" { + tpl = "problemNotification" + } + } + + var teamsTpl string + switch tpl { + case "mergeRequestOpened": + teamsTpl = `PR [#{{.PullrequestNumber}} ({{.PullrequestTitle}})]({{.PullrequestURL}}) opened in [{{.RepoName}}]({{.RepoURL}})` + case "mergeRequestUpdated": + teamsTpl = `PR [#{{.PullrequestNumber}} ({{.PullrequestTitle}})]({{.PullrequestURL}}) updated in [{{.RepoName}}]({{.RepoURL}})` + case "mergeRequestClosed": + teamsTpl = `PR [#{{.PullrequestNumber}} ({{.PullrequestTitle}})]({{.PullrequestURL}}) closed in [{{.RepoName}}]({{.RepoURL}})` + case "deleteEnvironment": + teamsTpl = `Deleting environment ` + "`{{.EnvironmentName}}`" + case "repoPushHandled": + teamsTpl = `[{{.BranchName}}]({{.RepoURL}}/tree/{{.BranchName}}){{ if ne .ShortSha "" }} ([{{.ShortSha}}]({{.CommitURL}})){{end}} pushed in [{{.RepoFullName}}]({{.RepoURL}})` + case "repoPushSkipped": + teamsTpl = `[{{.BranchName}}]({{.RepoURL}}/tree/{{.BranchName}}){{ if ne .ShortSha "" }} ([{{.ShortSha}}]({{.CommitURL}})){{end}} pushed in [{{.RepoFullName}}]({{.RepoURL}}) *deployment skipped*` + case "deployEnvironment": + teamsTpl = `Deployment triggered ` + "`{{.BranchName}}`" + `{{ if ne .ShortSha "" }} ({{.ShortSha}}){{end}}` + case "removeFinished": + teamsTpl = `Removed ` + "`{{.OpenshiftProject}}`" + `` + case "removeRetry": + teamsTpl = `Removed ` + "`{{.OpenshiftProject}}`" + `` + case "notDeleted": + teamsTpl = "`{{.BranchName}}`" + ` not deleted. {{.Error}}` + case "deployError": + teamsTpl = "`{{.BranchName}}`" + `{{ if ne .ShortSha "" }} ({{.ShortSha}}){{end}} Build ` + "`{{.BuildName}}`" + ` Failed. {{if ne .LogLink ""}} [Logs]({{.LogLink}}){{end}}` + case "deployFinished": + teamsTpl = "`{{.BranchName}}`" + `{{ if ne .ShortSha "" }} ({{.ShortSha}}){{end}} Build ` + "`{{.BuildName}}`" + ` Succeeded. {{if ne .LogLink ""}} [Logs]({{.LogLink}}){{end}} +* {{.Route}}{{range .Routes}}{{if ne . $.Route}}* {{.}}{{end}} +{{end}}` + case "problemNotification": + eventSplit := strings.Split(notification.Event, ":") + if eventSplit[0] != "problem" && eventSplit[1] == "insert" { + return "", "", "", fmt.Errorf("no matching event") + } + teamsTpl = `*[{{.ProjectName}}]* New problem found for ` + "`{{.EnvironmentName}}`" + ` +* Service: ` + "`{{.ServiceName}}`" + `{{ if ne .Severity "" }} +* Severity: {{.Severity}}{{end}}{{ if ne .Description "" }} +* Description: {{.Description}}{{end}}` + default: + return "", "", "", fmt.Errorf("no matching event") + } + + var teamsMsg bytes.Buffer + t, _ := template.New("microsoftteams").Parse(teamsTpl) + t.Execute(&teamsMsg, notification.Meta) + return emoji, color, teamsMsg.String(), nil +} + +func (h *Messaging) sendMicrosoftTeamsMessage(emoji, color, webhook, event, project, message string) { + teamsPayload := MicrosoftTeamsData{ + Type: "MessageCard", + Context: "http://schema.org/extensions", + Summary: message, + Title: project, + ThemeColor: color, + Sections: []MicrosoftTeamsSection{ + { + ActivityText: message, + ActivityImage: emoji, + }, + }, + } + + teamsPayloadBytes, _ := json.Marshal(teamsPayload) + req, err := http.NewRequest("POST", webhook, bytes.NewBuffer(teamsPayloadBytes)) + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + log.Printf("Error sending message to microsoft teams: %v", err) + return + } + defer resp.Body.Close() + log.Println(fmt.Sprintf("Sent %s message to microsoft teams", event)) +} + +func getMicrosoftTeamsEvent(msgEvent string) (string, string, string, error) { + if val, ok := microsoftTeamsEvent[msgEvent]; ok { + return val.Emoji, val.Color, val.Template, nil + } + return "", "", "", fmt.Errorf("no matching event source") +} + +var microsoftTeamsEvent = map[string]EventMap{ + "github:pull_request:opened:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestOpened"}, + "gitlab:merge_request:opened:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestOpened"}, + "bitbucket:pullrequest:created:opened:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestOpened"}, //not in slack + "bitbucket:pullrequest:created:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestOpened"}, //not in teams + + "github:pull_request:synchronize:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, + "gitlab:merge_request:updated:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, + "bitbucket:pullrequest:updated:opened:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, //not in slack + "bitbucket:pullrequest:updated:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, //not in teams + + "github:pull_request:closed:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestClosed"}, + "bitbucket:pullrequest:fulfilled:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestClosed"}, + "bitbucket:pullrequest:rejected:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestClosed"}, + "gitlab:merge_request:closed:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestClosed"}, + + "github:delete:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "deleteEnvironment"}, + "gitlab:remove:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "deleteEnvironment"}, //not in slack + "bitbucket:delete:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "deleteEnvironment"}, //not in slack + "api:deleteEnvironment": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "deleteEnvironment"}, //not in teams + + "github:push:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "repoPushHandled"}, + "bitbucket:repo:push:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "repoPushHandled"}, + "gitlab:push:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "repoPushHandled"}, + + "github:push:skipped": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "repoPushSkipped"}, + "gitlab:push:skipped": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "repoPushSkipped"}, + "bitbucket:push:skipped": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "repoPushSkipped"}, + + "api:deployEnvironmentLatest": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "deployEnvironment"}, + "api:deployEnvironmentBranch": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "deployEnvironment"}, + + "task:deploy-openshift:finished": {Emoji: ":white_check_mark:", Color: "lawngreen", Template: "deployFinished"}, + "task:remove-openshift-resources:finished": {Emoji: ":white_check_mark:", Color: "lawngreen", Template: "deployFinished"}, + "task:builddeploy-openshift:complete": {Emoji: ":white_check_mark:", Color: "lawngreen", Template: "deployFinished"}, + "task:builddeploy-kubernetes:complete": {Emoji: ":white_check_mark:", Color: "lawngreen", Template: "deployFinished"}, //not in teams + + "task:remove-openshift:finished": {Emoji: ":white_check_mark:", Color: "lawngreen", Template: "removeFinished"}, + "task:remove-kubernetes:finished": {Emoji: ":white_check_mark:", Color: "lawngreen", Template: "removeFinished"}, + + "task:remove-openshift:error": {Emoji: ":bangbang:", Color: "red", Template: "deployError"}, + "task:remove-kubernetes:error": {Emoji: ":bangbang:", Color: "red", Template: "deployError"}, + "task:builddeploy-kubernetes:failed": {Emoji: ":bangbang:", Color: "red", Template: "deployError"}, //not in teams + "task:builddeploy-openshift:failed": {Emoji: ":bangbang:", Color: "red", Template: "deployError"}, + + "github:pull_request:closed:CannotDeleteProductionEnvironment": {Emoji: ":warning:", Color: "gold", Template: "notDeleted"}, + "github:push:CannotDeleteProductionEnvironment": {Emoji: ":warning:", Color: "gold", Template: "notDeleted"}, + "bitbucket:repo:push:CannotDeleteProductionEnvironment": {Emoji: ":warning:", Color: "gold", Template: "notDeleted"}, + "gitlab:push:CannotDeleteProductionEnvironment": {Emoji: ":warning:", Color: "gold", Template: "notDeleted"}, + + // deprecated + // "rest:remove:CannotDeleteProductionEnvironment": {Emoji: ":warning:", Color: "gold"}, + // "rest:deploy:receive": {Emoji: ":information_source:", Color: "#E8E8E8"}, + // "rest:remove:receive": {Emoji: ":information_source:", Color: "#E8E8E8"}, + // "rest:promote:receive": {Emoji: ":information_source:", Color: "#E8E8E8"}, + // "rest:pullrequest:deploy": {Emoji: ":information_source:", Color: "#E8E8E8"}, + // "rest:pullrequest:remove": {Emoji: ":information_source:", Color: "#E8E8E8"}, + + // deprecated + // "task:deploy-openshift:error": {Emoji: ":bangbang:", Color: "red", Template: "deployError"}, + // "task:remove-openshift-resources:error": {Emoji: ":bangbang:", Color: "red", Template: "deployError"}, + + // deprecated + // "task:deploy-openshift:retry": {Emoji: ":warning:", Color: "gold", Template: "removeRetry"}, + // "task:remove-openshift:retry": {Emoji: ":warning:", Color: "gold", Template: "removeRetry"}, + // "task:remove-kubernetes:retry": {Emoji: ":warning:", Color: "gold", Template: "removeRetry"}, + // "task:remove-openshift-resources:retry": {Emoji: ":warning:", Color: "gold", Template: "removeRetry"}, +} diff --git a/services/logs2notifications/internal/handler/rocketchat_events.go b/services/logs2notifications/internal/handler/rocketchat_events.go new file mode 100644 index 0000000000..978d0c723e --- /dev/null +++ b/services/logs2notifications/internal/handler/rocketchat_events.go @@ -0,0 +1,207 @@ +package handler + +import ( + "bytes" + "encoding/json" + "fmt" + "log" + "net/http" + "strings" + "text/template" +) + +// RocketChatData . +type RocketChatData struct { + Channel string `json:"channel"` + Attachments []RocketChatAttachment `json:"attachments"` +} + +// RocketChatAttachment . +type RocketChatAttachment struct { + Text string `json:"text"` + Color string `json:"color"` + Fields []RocketChatAttachmentField `json:"fields"` +} + +// RocketChatAttachmentField . +type RocketChatAttachmentField struct { + Short bool `json:"short"` + Title string `json:"title"` + Value string `json:"value"` +} + +// SendToRocketChat . +func (h *Messaging) SendToRocketChat(notification *Notification, channel, webhook, appID string) { + emoji, color, message, err := h.processRocketChatTemplate(notification) + if err != nil { + return + } + h.sendRocketChatMessage(emoji, color, appID, channel, webhook, notification.Event, message) +} + +// SendToRocketChat . +func (h *Messaging) processRocketChatTemplate(notification *Notification) (string, string, string, error) { + emoji, color, tpl, err := getRocketChatEvent(notification.Event) + if err != nil { + eventSplit := strings.Split(notification.Event, ":") + if eventSplit[0] != "problem" { + return "", "", "", fmt.Errorf("no matching event") + } + if eventSplit[1] == "insert" { + tpl = "problemNotification" + } + } + + var rcTpl string + switch tpl { + case "mergeRequestOpened": + rcTpl = `*[{{.ProjectName}}]* PR [#{{.PullrequestNumber}} ({{.PullrequestTitle}})]({{.PullrequestURL}}) opened in [{{.RepoName}}]({{.RepoURL}})` + case "mergeRequestUpdated": + rcTpl = `*[{{.ProjectName}}]* PR [#{{.PullrequestNumber}} ({{.PullrequestTitle}})]({{.PullrequestURL}}) updated in [{{.RepoName}}]({{.RepoURL}})` + case "mergeRequestClosed": + rcTpl = `*[{{.ProjectName}}]* PR [#{{.PullrequestNumber}} ({{.PullrequestTitle}})]({{.PullrequestURL}}) closed in [{{.RepoName}}]({{.RepoURL}})` + case "deleteEnvironment": + rcTpl = `*[{{.ProjectName}}]* Deleting environment ` + "`{{.EnvironmentName}}`" + case "repoPushHandled": + rcTpl = `*[{{.ProjectName}}]* [{{.BranchName}}]({{.RepoURL}}/tree/{{.BranchName}}){{ if ne .ShortSha "" }} ([{{.ShortSha}}]({{.CommitURL}})){{end}} pushed in [{{.RepoFullName}}]({{.RepoURL}})` + case "repoPushSkipped": + rcTpl = `*[{{.ProjectName}}]* [{{.BranchName}}]({{.RepoURL}}/tree/{{.BranchName}}){{ if ne .ShortSha "" }} ([{{.ShortSha}}]({{.CommitURL}})){{end}} pushed in [{{.RepoFullName}}]({{.RepoURL}}) *deployment skipped*` + case "deployEnvironment": + rcTpl = `*[{{.ProjectName}}]* Deployment triggered ` + "`{{.BranchName}}`" + `{{ if ne .ShortSha "" }} ({{.ShortSha}}){{end}}` + case "removeFinished": + rcTpl = `*[{{.ProjectName}}]* Removed ` + "`{{.OpenshiftProject}}`" + `` + case "removeRetry": + rcTpl = `*[{{.ProjectName}}]* Removed ` + "`{{.OpenshiftProject}}`" + `` + case "notDeleted": + rcTpl = `*[{{.ProjectName}}]* ` + "`{{.BranchName}}`" + ` not deleted. {{.Error}}` + case "deployError": + rcTpl = `*[{{.ProjectName}}]* ` + "`{{.BranchName}}`" + `{{ if ne .ShortSha "" }} ({{.ShortSha}}){{end}} Build ` + "`{{.BuildName}}`" + ` Failed. {{if ne .LogLink ""}} [Logs]({{.LogLink}}){{end}}` + case "deployFinished": + rcTpl = `*[{{.ProjectName}}]* ` + "`{{.BranchName}}`" + `{{ if ne .ShortSha "" }} ({{.ShortSha}}){{end}} Build ` + "`{{.BuildName}}`" + ` Succeeded. {{if ne .LogLink ""}} [Logs]({{.LogLink}}){{end}} +* {{.Route}}{{range .Routes}}{{if ne . $.Route}}* {{.}}{{end}} +{{end}}` + case "problemNotification": + eventSplit := strings.Split(notification.Event, ":") + if eventSplit[0] != "problem" && eventSplit[1] == "insert" { + return "", "", "", fmt.Errorf("no matching event") + } + rcTpl = `*[{{.ProjectName}}]* New problem found for ` + "`{{.EnvironmentName}}`" + ` +* Service: ` + "`{{.ServiceName}}`" + `{{ if ne .Severity "" }} +* Severity: {{.Severity}}{{end}}{{ if ne .Description "" }} +* Description: {{.Description}}{{end}}` + default: + return "", "", "", fmt.Errorf("no matching event") + } + var rcMsg bytes.Buffer + t, _ := template.New("rocketchat").Parse(rcTpl) + t.Execute(&rcMsg, notification.Meta) + return emoji, color, rcMsg.String(), nil +} + +func (h *Messaging) sendRocketChatMessage(emoji, color, appID, channel, webhook, event, message string) { + data := RocketChatData{ + Channel: channel, + Attachments: []RocketChatAttachment{ + { + Text: fmt.Sprintf("%s %s", emoji, message), + Color: color, + Fields: []RocketChatAttachmentField{ + { + Short: true, + Title: "Source", + Value: appID, + }, + }, + }, + }, + } + jsonBytes, _ := json.Marshal(data) + req, err := http.NewRequest("POST", webhook, bytes.NewBuffer(jsonBytes)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Content-Length", fmt.Sprintf("%d", len(jsonBytes))) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + log.Printf("Error sending message to rocketchat: %v", err) + return + } + defer resp.Body.Close() + log.Println(fmt.Sprintf("Sent %s message to rocketchat", event)) +} + +func getRocketChatEvent(msgEvent string) (string, string, string, error) { + if val, ok := rocketChatEventTypeMap[msgEvent]; ok { + return val.Emoji, val.Color, val.Template, nil + } + return "", "", "", fmt.Errorf("no matching event source") +} + +var rocketChatEventTypeMap = map[string]EventMap{ + "github:pull_request:opened:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestOpened"}, + "gitlab:merge_request:opened:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestOpened"}, + "bitbucket:pullrequest:created:opened:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestOpened"}, //not in slack + "bitbucket:pullrequest:created:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestOpened"}, //not in teams + + "github:pull_request:synchronize:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, + "gitlab:merge_request:updated:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, + "bitbucket:pullrequest:updated:opened:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, //not in slack + "bitbucket:pullrequest:updated:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, //not in teams + + "github:pull_request:closed:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestClosed"}, + "bitbucket:pullrequest:fulfilled:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestClosed"}, + "bitbucket:pullrequest:rejected:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestClosed"}, + "gitlab:merge_request:closed:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestClosed"}, + + "github:delete:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "deleteEnvironment"}, + "gitlab:remove:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "deleteEnvironment"}, //not in slack + "bitbucket:delete:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "deleteEnvironment"}, //not in slack + "api:deleteEnvironment": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "deleteEnvironment"}, //not in teams + + "github:push:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "repoPushHandled"}, + "bitbucket:repo:push:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "repoPushHandled"}, + "gitlab:push:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "repoPushHandled"}, + + "github:push:skipped": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "repoPushSkipped"}, + "gitlab:push:skipped": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "repoPushSkipped"}, + "bitbucket:push:skipped": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "repoPushSkipped"}, + + "api:deployEnvironmentLatest": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "deployEnvironment"}, + "api:deployEnvironmentBranch": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "deployEnvironment"}, + + "task:deploy-openshift:finished": {Emoji: "✅", Color: "lawngreen", Template: "deployFinished"}, + "task:remove-openshift-resources:finished": {Emoji: "✅", Color: "lawngreen", Template: "deployFinished"}, + "task:builddeploy-openshift:complete": {Emoji: "✅", Color: "lawngreen", Template: "deployFinished"}, + "task:builddeploy-kubernetes:complete": {Emoji: "✅", Color: "lawngreen", Template: "deployFinished"}, //not in teams + + "task:remove-openshift:finished": {Emoji: "✅", Color: "lawngreen", Template: "removeFinished"}, + "task:remove-kubernetes:finished": {Emoji: "✅", Color: "lawngreen", Template: "removeFinished"}, + + "task:remove-openshift:error": {Emoji: "🛑", Color: "red", Template: "deployError"}, + "task:remove-kubernetes:error": {Emoji: "🛑", Color: "red", Template: "deployError"}, + "task:builddeploy-kubernetes:failed": {Emoji: "🛑", Color: "red", Template: "deployError"}, //not in teams + "task:builddeploy-openshift:failed": {Emoji: "🛑", Color: "red", Template: "deployError"}, + + "github:pull_request:closed:CannotDeleteProductionEnvironment": {Emoji: "⚠️", Color: "gold", Template: "notDeleted"}, + "github:push:CannotDeleteProductionEnvironment": {Emoji: "⚠️", Color: "gold", Template: "notDeleted"}, + "bitbucket:repo:push:CannotDeleteProductionEnvironment": {Emoji: "⚠️", Color: "gold", Template: "notDeleted"}, + "gitlab:push:CannotDeleteProductionEnvironment": {Emoji: "⚠️", Color: "gold", Template: "notDeleted"}, + + // deprecated + // "rest:remove:CannotDeleteProductionEnvironment": {Emoji: "⚠️", Color: "gold"}, + // "rest:deploy:receive": {Emoji: "ℹ️", Color: "#E8E8E8"}, + // "rest:remove:receive": {Emoji: "ℹ️", Color: "#E8E8E8"}, + // "rest:promote:receive": {Emoji: "ℹ️", Color: "#E8E8E8"}, + // "rest:pullrequest:deploy": {Emoji: "ℹ️", Color: "#E8E8E8"}, + // "rest:pullrequest:remove": {Emoji: "ℹ️", Color: "#E8E8E8"}, + + // deprecated + // "task:deploy-openshift:error": {Emoji: "🛑", Color: "red", Template: "deployError"}, + // "task:remove-openshift-resources:error": {Emoji: "🛑", Color: "red", Template: "deployError"}, + + // deprecated + // "task:deploy-openshift:retry": {Emoji: "⚠️", Color: "gold", Template: "removeRetry"}, + // "task:remove-openshift:retry": {Emoji: "⚠️", Color: "gold", Template: "removeRetry"}, + // "task:remove-kubernetes:retry": {Emoji: "⚠️", Color: "gold", Template: "removeRetry"}, + // "task:remove-openshift-resources:retry": {Emoji: "⚠️", Color: "gold", Template: "removeRetry"}, +} diff --git a/services/logs2notifications/internal/handler/s3_events.go b/services/logs2notifications/internal/handler/s3_events.go new file mode 100644 index 0000000000..c7f1d41416 --- /dev/null +++ b/services/logs2notifications/internal/handler/s3_events.go @@ -0,0 +1,83 @@ +package handler + +import ( + "bytes" + "fmt" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/s3" + "github.com/uselagoon/lagoon/services/logs2notifications/internal/helpers" +) + +// MessageType . +type MessageType string + +const ( + buildMessageType MessageType = "build" + taskMessageType MessageType = "task" +) + +// SendToS3 . +func (h *Messaging) SendToS3(notification *Notification, msgType MessageType) { + if msgType == buildMessageType { + h.uploadFileS3( + notification.Message, + fmt.Sprintf("buildlogs/%s/%s/%s-%s.txt", + notification.Project, + notification.Meta.BranchName, + notification.Meta.JobName, + notification.Meta.RemoteID, + ), + ) + } else if msgType == taskMessageType { + filePath := fmt.Sprintf("tasklogs/%s/%d-%s.txt", + notification.Project, + notification.Meta.Task.ID, + notification.Meta.RemoteID, + ) + if notification.Meta.Environment != "" { + filePath = fmt.Sprintf("tasklogs/%s/%s/%d-%s.txt", + notification.Project, + helpers.ShortenEnvironment(notification.Project, helpers.MakeSafe(notification.Meta.Environment)), + notification.Meta.Task.ID, + notification.Meta.RemoteID, + ) + + } + h.uploadFileS3( + notification.Message, + filePath, + ) + } +} + +// UploadFileS3 +func (h *Messaging) uploadFileS3(message, fileName string) { + var forcePath bool + forcePath = true + session, err := session.NewSession(&aws.Config{ + Region: aws.String(h.S3FilesRegion), + Endpoint: aws.String(h.S3FilesOrigin), + Credentials: credentials.NewStaticCredentials(h.S3FilesAccessKeyID, h.S3FilesSecretAccessKey, ""), + S3ForcePathStyle: &forcePath, + }) + if err != nil { + log.Fatal(err) + } + + _, err = s3.New(session).PutObject(&s3.PutObjectInput{ + Bucket: aws.String(h.S3FilesBucket), + Key: aws.String(fileName), + ACL: aws.String("private"), + Body: bytes.NewReader([]byte(message)), + ContentType: aws.String("text/plain"), + }) + if err != nil { + log.Println(err) + } + log.Println(fmt.Sprintf("Uploaded file %s", fileName)) + return +} diff --git a/services/logs2notifications/internal/handler/slack_events.go b/services/logs2notifications/internal/handler/slack_events.go new file mode 100644 index 0000000000..413fa75fb3 --- /dev/null +++ b/services/logs2notifications/internal/handler/slack_events.go @@ -0,0 +1,177 @@ +package handler + +import ( + "bytes" + "fmt" + "log" + "strings" + "text/template" + + "github.com/slack-go/slack" +) + +// SendToSlack . +func (h *Messaging) SendToSlack(notification *Notification, channel, webhook, appID string) { + emoji, color, message, err := h.processSlackTemplate(notification) + if err != nil { + return + } + h.sendSlackMessage(emoji, color, appID, channel, webhook, notification.Event, message) +} + +// processSlackTemplate . +func (h *Messaging) processSlackTemplate(notification *Notification) (string, string, string, error) { + emoji, color, tpl, err := getSlackEvent(notification.Event) + if err != nil { + eventSplit := strings.Split(notification.Event, ":") + if eventSplit[0] != "problem" { + return "", "", "", fmt.Errorf("no matching event") + } + if eventSplit[1] == "insert" { + tpl = "problemNotification" + } + } + + var slackTpl string + switch tpl { + case "mergeRequestOpened": + slackTpl = `*[{{.ProjectName}}]* PR [#{{.PullrequestNumber}} ({{.PullrequestTitle}})]({{.PullrequestURL}}) opened in [{{.RepoName}}]({{.RepoURL}})` + case "mergeRequestUpdated": + slackTpl = `*[{{.ProjectName}}]* PR [#{{.PullrequestNumber}} ({{.PullrequestTitle}})]({{.PullrequestURL}}) updated in [{{.RepoName}}]({{.RepoURL}})` + case "mergeRequestClosed": + slackTpl = `*[{{.ProjectName}}]* PR [#{{.PullrequestNumber}} ({{.PullrequestTitle}})]({{.PullrequestURL}}) closed in [{{.RepoName}}]({{.RepoURL}})` + case "deleteEnvironment": + slackTpl = `*[{{.ProjectName}}]* Deleting environment ` + "`{{.EnvironmentName}}`" + case "repoPushHandled": + slackTpl = `*[{{.ProjectName}}]* [{{.BranchName}}]({{.RepoURL}}/tree/{{.BranchName}}){{ if ne .ShortSha "" }} ([{{.ShortSha}}]({{.CommitURL}})){{end}} pushed in [{{.RepoFullName}}]({{.RepoURL}})` + case "repoPushSkipped": + slackTpl = `*[{{.ProjectName}}]* [{{.BranchName}}]({{.RepoURL}}/tree/{{.BranchName}}){{ if ne .ShortSha "" }} ([{{.ShortSha}}]({{.CommitURL}})){{end}} pushed in [{{.RepoFullName}}]({{.RepoURL}}) *deployment skipped*` + case "deployEnvironment": + slackTpl = `*[{{.ProjectName}}]* Deployment triggered ` + "`{{.BranchName}}`" + `{{ if ne .ShortSha "" }} ({{.ShortSha}}){{end}}` + case "removeFinished": + slackTpl = `*[{{.ProjectName}}]* Removed ` + "`{{.OpenshiftProject}}`" + `` + case "removeRetry": + slackTpl = `*[{{.ProjectName}}]* Removed ` + "`{{.OpenshiftProject}}`" + `` + case "notDeleted": + slackTpl = `*[{{.ProjectName}}]* ` + "`{{.BranchName}}`" + ` not deleted. {{.Error}}` + case "deployError": + slackTpl = `*[{{.ProjectName}}]* ` + "`{{.BranchName}}`" + `{{ if ne .ShortSha "" }} ({{.ShortSha}}){{end}} Build ` + "`{{.BuildName}}`" + ` Failed. {{if ne .LogLink ""}} <{{.LogLink}}|Logs>{{end}}` + case "deployFinished": + slackTpl = `*[{{.ProjectName}}]* ` + "`{{.BranchName}}`" + `{{ if ne .ShortSha "" }} ({{.ShortSha}}){{end}} Build ` + "`{{.BuildName}}`" + ` Succeeded. {{if ne .LogLink ""}} <{{.LogLink}}|Logs>{{end}} +* {{.Route}}{{range .Routes}}{{if ne . $.Route}}* {{.}}{{end}} +{{end}}` + case "problemNotification": + eventSplit := strings.Split(notification.Event, ":") + if eventSplit[0] != "problem" && eventSplit[1] == "insert" { + return "", "", "", fmt.Errorf("no matching event") + } + slackTpl = `*[{{.ProjectName}}]* New problem found for ` + "`{{.EnvironmentName}}`" + ` +* Service: ` + "`{{.ServiceName}}`" + `{{ if ne .Severity "" }} +* Severity: {{.Severity}}{{end}}{{ if ne .Description "" }} +* Description: {{.Description}}{{end}}` + default: + return "", "", "", fmt.Errorf("no matching event") + } + + var slackMsg bytes.Buffer + t, _ := template.New("slack").Parse(slackTpl) + t.Execute(&slackMsg, notification.Meta) + return emoji, color, slackMsg.String(), nil +} + +func (h *Messaging) sendSlackMessage(emoji, color, appID, channel, webhook, event, message string) { + attachment := slack.Attachment{ + Text: fmt.Sprintf("%s %s", emoji, message), + Color: color, + Footer: appID, + MarkdownIn: []string{"pretext", "text", "fields"}, + } + postMsg := slack.WebhookMessage{ + Attachments: []slack.Attachment{attachment}, + Channel: channel, + } + + err := slack.PostWebhook(webhook, &postMsg) + if err != nil { + // just log any errors + log.Printf("Error sending message to slack: %v", err) + return + } + log.Println(fmt.Sprintf("Sent %s message to slack", event)) +} + +func getSlackEvent(msgEvent string) (string, string, string, error) { + if val, ok := slackEventTypeMap[msgEvent]; ok { + return val.Emoji, val.Color, val.Template, nil + } + return "", "", "", fmt.Errorf("no matching event source") +} + +var slackEventTypeMap = map[string]EventMap{ + "github:pull_request:opened:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestOpened"}, + "gitlab:merge_request:opened:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestOpened"}, + "bitbucket:pullrequest:created:opened:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestOpened"}, //not in slack + "bitbucket:pullrequest:created:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestOpened"}, //not in teams + + "github:pull_request:synchronize:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, + "gitlab:merge_request:updated:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, + "bitbucket:pullrequest:updated:opened:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, //not in slack + "bitbucket:pullrequest:updated:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, //not in teams + + "github:pull_request:closed:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestClosed"}, + "bitbucket:pullrequest:fulfilled:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestClosed"}, + "bitbucket:pullrequest:rejected:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestClosed"}, + "gitlab:merge_request:closed:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestClosed"}, + + "github:delete:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "deleteEnvironment"}, + "gitlab:remove:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "deleteEnvironment"}, //not in slack + "bitbucket:delete:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "deleteEnvironment"}, //not in slack + "api:deleteEnvironment": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "deleteEnvironment"}, //not in teams + + "github:push:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "repoPushHandled"}, + "bitbucket:repo:push:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "repoPushHandled"}, + "gitlab:push:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "repoPushHandled"}, + + "github:push:skipped": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "repoPushSkipped"}, + "gitlab:push:skipped": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "repoPushSkipped"}, + "bitbucket:push:skipped": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "repoPushSkipped"}, + + "api:deployEnvironmentLatest": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "deployEnvironment"}, + "api:deployEnvironmentBranch": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "deployEnvironment"}, + + "task:deploy-openshift:finished": {Emoji: "✅", Color: "good", Template: "deployFinished"}, + "task:remove-openshift-resources:finished": {Emoji: "✅", Color: "good", Template: "deployFinished"}, + "task:builddeploy-openshift:complete": {Emoji: "✅", Color: "good", Template: "deployFinished"}, + "task:builddeploy-kubernetes:complete": {Emoji: "✅", Color: "good", Template: "deployFinished"}, //not in teams + + "task:remove-openshift:finished": {Emoji: "✅", Color: "good", Template: "removeFinished"}, + "task:remove-kubernetes:finished": {Emoji: "✅", Color: "good", Template: "removeFinished"}, + + "task:remove-openshift:error": {Emoji: "🛑", Color: "danger", Template: "deployError"}, + "task:remove-kubernetes:error": {Emoji: "🛑", Color: "danger", Template: "deployError"}, + "task:builddeploy-kubernetes:failed": {Emoji: "🛑", Color: "danger", Template: "deployError"}, //not in teams + "task:builddeploy-openshift:failed": {Emoji: "🛑", Color: "danger", Template: "deployError"}, + + "github:pull_request:closed:CannotDeleteProductionEnvironment": {Emoji: "⚠️", Color: "warning", Template: "notDeleted"}, + "github:push:CannotDeleteProductionEnvironment": {Emoji: "⚠️", Color: "warning", Template: "notDeleted"}, + "bitbucket:repo:push:CannotDeleteProductionEnvironment": {Emoji: "⚠️", Color: "warning", Template: "notDeleted"}, + "gitlab:push:CannotDeleteProductionEnvironment": {Emoji: "⚠️", Color: "warning", Template: "notDeleted"}, + + // deprecated + // "rest:remove:CannotDeleteProductionEnvironment": {Emoji: "⚠️", Color: "warning"}, + // "rest:deploy:receive": {Emoji: "ℹ️", Color: "#E8E8E8"}, + // "rest:remove:receive": {Emoji: "ℹ️", Color: "#E8E8E8"}, + // "rest:promote:receive": {Emoji: "ℹ️", Color: "#E8E8E8"}, + // "rest:pullrequest:deploy": {Emoji: "ℹ️", Color: "#E8E8E8"}, + // "rest:pullrequest:remove": {Emoji: "ℹ️", Color: "#E8E8E8"}, + + // deprecated + // "task:deploy-openshift:error": {Emoji: "🛑", Color: "danger", Template: "deployError"}, + // "task:remove-openshift-resources:error": {Emoji: "🛑", Color: "danger", Template: "deployError"}, + + // deprecated + // "task:deploy-openshift:retry": {Emoji: "⚠️", Color: "warning", Template: "removeRetry"}, + // "task:remove-openshift:retry": {Emoji: "⚠️", Color: "warning", Template: "removeRetry"}, + // "task:remove-kubernetes:retry": {Emoji: "⚠️", Color: "warning", Template: "removeRetry"}, + // "task:remove-openshift-resources:retry": {Emoji: "⚠️", Color: "warning", Template: "removeRetry"}, +} diff --git a/services/logs2notifications/internal/handler/testdata/deleteEnvironment/emailhtml.txt b/services/logs2notifications/internal/handler/testdata/deleteEnvironment/emailhtml.txt new file mode 100644 index 0000000000..e4312f8960 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/deleteEnvironment/emailhtml.txt @@ -0,0 +1 @@ +Deleted environment lagoon-type-override \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/deleteEnvironment/emailplain.txt b/services/logs2notifications/internal/handler/testdata/deleteEnvironment/emailplain.txt new file mode 100644 index 0000000000..6afbbb9a49 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/deleteEnvironment/emailplain.txt @@ -0,0 +1 @@ +[ci-github-openshift] deleted environment lagoon-type-override \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/deleteEnvironment/rocketchat.txt b/services/logs2notifications/internal/handler/testdata/deleteEnvironment/rocketchat.txt new file mode 100644 index 0000000000..ae882c10ee --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/deleteEnvironment/rocketchat.txt @@ -0,0 +1 @@ +*[ci-github-openshift]* Deleting environment `lagoon-type-override` \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/deleteEnvironment/slack.txt b/services/logs2notifications/internal/handler/testdata/deleteEnvironment/slack.txt new file mode 100644 index 0000000000..ae882c10ee --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/deleteEnvironment/slack.txt @@ -0,0 +1 @@ +*[ci-github-openshift]* Deleting environment `lagoon-type-override` \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/deleteEnvironment/teams.txt b/services/logs2notifications/internal/handler/testdata/deleteEnvironment/teams.txt new file mode 100644 index 0000000000..7236b3ac99 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/deleteEnvironment/teams.txt @@ -0,0 +1 @@ +Deleting environment `lagoon-type-override` \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/deployEnvironment/emailhtml.txt b/services/logs2notifications/internal/handler/testdata/deployEnvironment/emailhtml.txt new file mode 100644 index 0000000000..2df8b891e1 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/deployEnvironment/emailhtml.txt @@ -0,0 +1 @@ +Deployment triggered lagoon-type-override \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/deployEnvironment/emailplain.txt b/services/logs2notifications/internal/handler/testdata/deployEnvironment/emailplain.txt new file mode 100644 index 0000000000..a0d5c2f96a --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/deployEnvironment/emailplain.txt @@ -0,0 +1 @@ +[ci-github-openshift] Deployment triggered on branch lagoon-type-override \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/deployEnvironment/rocketchat.txt b/services/logs2notifications/internal/handler/testdata/deployEnvironment/rocketchat.txt new file mode 100644 index 0000000000..e38caf7ee5 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/deployEnvironment/rocketchat.txt @@ -0,0 +1 @@ +*[ci-github-openshift]* Deployment triggered `lagoon-type-override` \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/deployEnvironment/slack.txt b/services/logs2notifications/internal/handler/testdata/deployEnvironment/slack.txt new file mode 100644 index 0000000000..e38caf7ee5 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/deployEnvironment/slack.txt @@ -0,0 +1 @@ +*[ci-github-openshift]* Deployment triggered `lagoon-type-override` \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/deployEnvironment/teams.txt b/services/logs2notifications/internal/handler/testdata/deployEnvironment/teams.txt new file mode 100644 index 0000000000..80e680018e --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/deployEnvironment/teams.txt @@ -0,0 +1 @@ +Deployment triggered `lagoon-type-override` \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/deployError/emailhtml.txt b/services/logs2notifications/internal/handler/testdata/deployError/emailhtml.txt new file mode 100644 index 0000000000..caa0842373 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/deployError/emailhtml.txt @@ -0,0 +1,2 @@ +[ci-github-openshift] lagoon-type-override Build lagoon-build-1234 error. + Logs \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/deployError/emailplain.txt b/services/logs2notifications/internal/handler/testdata/deployError/emailplain.txt new file mode 100644 index 0000000000..6e842187b3 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/deployError/emailplain.txt @@ -0,0 +1,2 @@ +[ci-github-openshift] lagoon-type-override Build lagoon-build-1234 error. + [Logs](https://logs) \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/deployError/rocketchat.txt b/services/logs2notifications/internal/handler/testdata/deployError/rocketchat.txt new file mode 100644 index 0000000000..3188762d78 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/deployError/rocketchat.txt @@ -0,0 +1 @@ +*[ci-github-openshift]* `lagoon-type-override` Build `lagoon-build-1234` Failed. [Logs](https://logs) \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/deployError/slack.txt b/services/logs2notifications/internal/handler/testdata/deployError/slack.txt new file mode 100644 index 0000000000..cb1126de73 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/deployError/slack.txt @@ -0,0 +1 @@ +*[ci-github-openshift]* `lagoon-type-override` Build `lagoon-build-1234` Failed. \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/deployError/teams.txt b/services/logs2notifications/internal/handler/testdata/deployError/teams.txt new file mode 100644 index 0000000000..ca2fa8daf9 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/deployError/teams.txt @@ -0,0 +1 @@ +`lagoon-type-override` Build `lagoon-build-1234` Failed. [Logs](https://logs) \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/deployError/webhook.txt b/services/logs2notifications/internal/handler/testdata/deployError/webhook.txt new file mode 100644 index 0000000000..5ecba91c37 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/deployError/webhook.txt @@ -0,0 +1 @@ +{"type":"DEPLOYMENT","event":"task:builddeploy-kubernetes:failed","project":"ci-github-openshift","environment":"lagoon-type-override"} \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/deployFinished/emailhtml.txt b/services/logs2notifications/internal/handler/testdata/deployFinished/emailhtml.txt new file mode 100644 index 0000000000..26155d23c4 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/deployFinished/emailhtml.txt @@ -0,0 +1,10 @@ +[ci-github-openshift] lagoon-type-override Build lagoon-build-1234 complete. Logs +

+
+
+

+

\ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/deployFinished/emailplain.txt b/services/logs2notifications/internal/handler/testdata/deployFinished/emailplain.txt new file mode 100644 index 0000000000..3364f45f8b --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/deployFinished/emailplain.txt @@ -0,0 +1,4 @@ +[ci-github-openshift] lagoon-type-override Build lagoon-build-1234 complete. [Logs](https://logs) +https://route1 +https://route2 +https://route3 diff --git a/services/logs2notifications/internal/handler/testdata/deployFinished/rocketchat.txt b/services/logs2notifications/internal/handler/testdata/deployFinished/rocketchat.txt new file mode 100644 index 0000000000..66f5f30f33 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/deployFinished/rocketchat.txt @@ -0,0 +1,4 @@ +*[ci-github-openshift]* `lagoon-type-override` Build `lagoon-build-1234` Succeeded. [Logs](https://logs) +* https://route1 +* https://route2 +* https://route3 diff --git a/services/logs2notifications/internal/handler/testdata/deployFinished/slack.txt b/services/logs2notifications/internal/handler/testdata/deployFinished/slack.txt new file mode 100644 index 0000000000..c2040f005a --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/deployFinished/slack.txt @@ -0,0 +1,4 @@ +*[ci-github-openshift]* `lagoon-type-override` Build `lagoon-build-1234` Succeeded. +* https://route1 +* https://route2 +* https://route3 diff --git a/services/logs2notifications/internal/handler/testdata/deployFinished/teams.txt b/services/logs2notifications/internal/handler/testdata/deployFinished/teams.txt new file mode 100644 index 0000000000..b68e460ad9 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/deployFinished/teams.txt @@ -0,0 +1,4 @@ +`lagoon-type-override` Build `lagoon-build-1234` Succeeded. [Logs](https://logs) +* https://route1 +* https://route2 +* https://route3 diff --git a/services/logs2notifications/internal/handler/testdata/deployFinished/webhook.txt b/services/logs2notifications/internal/handler/testdata/deployFinished/webhook.txt new file mode 100644 index 0000000000..4c9f31e748 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/deployFinished/webhook.txt @@ -0,0 +1 @@ +{"type":"DEPLOYMENT","event":"task:deploy-openshift:finished","project":"ci-github-openshift","environment":"lagoon-type-override"} \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/input.deleteEnvironmentGithub.json b/services/logs2notifications/internal/handler/testdata/input.deleteEnvironmentGithub.json new file mode 100644 index 0000000000..b6df0dcfc5 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/input.deleteEnvironmentGithub.json @@ -0,0 +1,8 @@ +{ + "project":"ci-github-openshift", + "event":"github:delete:handled", + "meta":{ + "projectName":"ci-github-openshift", + "environmentName":"lagoon-type-override" + } +} \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/input.deployEnvironment.json b/services/logs2notifications/internal/handler/testdata/input.deployEnvironment.json new file mode 100644 index 0000000000..ab2a1271c2 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/input.deployEnvironment.json @@ -0,0 +1,9 @@ +{ + "project":"ci-github-openshift", + "event":"api:deployEnvironmentLatest", + "meta":{ + "projectName":"ci-github-openshift", + "branchName":"lagoon-type-override", + "project":"ci-github-openshift" + } +} \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/input.deployError.json b/services/logs2notifications/internal/handler/testdata/input.deployError.json new file mode 100644 index 0000000000..2127536769 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/input.deployError.json @@ -0,0 +1,11 @@ +{ + "project":"ci-github-openshift", + "event":"task:builddeploy-kubernetes:failed", + "meta":{ + "projectName":"ci-github-openshift", + "branchName":"lagoon-type-override", + "project":"ci-github-openshift", + "buildName":"lagoon-build-1234", + "logLink":"https://logs" + } +} \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/input.deployFinished.json b/services/logs2notifications/internal/handler/testdata/input.deployFinished.json new file mode 100644 index 0000000000..be4eb952fd --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/input.deployFinished.json @@ -0,0 +1,14 @@ +{ + "severity":"info", + "project":"ci-github-openshift", + "uuid":"", + "event":"task:deploy-openshift:finished", + "meta":{ + "projectName":"ci-github-openshift", + "branchName":"lagoon-type-override", + "buildName":"lagoon-build-1234", + "route":"https://route1", + "routes":["https://route1","https://route2","https://route3"], + "logLink":"https://logs" + } +} \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/input.repoPushHandledGithub.json b/services/logs2notifications/internal/handler/testdata/input.repoPushHandledGithub.json new file mode 100644 index 0000000000..ef4ca09114 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/input.repoPushHandledGithub.json @@ -0,0 +1,12 @@ +{ + "severity":"info", + "project":"ci-github-openshift", + "uuid":"", + "event":"github:push:handled", + "meta":{ + "projectName":"ci-github-openshift", + "branchName":"lagoon-type-override", + "repoFullName":"owner/repository", + "repoURL":"github.com/owner/repository" + } +} \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/input.repoPushSkippedGithub.json b/services/logs2notifications/internal/handler/testdata/input.repoPushSkippedGithub.json new file mode 100644 index 0000000000..efcc4ccd5b --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/input.repoPushSkippedGithub.json @@ -0,0 +1,12 @@ +{ + "severity":"info", + "project":"ci-github-openshift", + "uuid":"", + "event":"github:push:skipped", + "meta":{ + "projectName":"ci-github-openshift", + "branchName":"lagoon-type-override", + "repoFullName":"owner/repository", + "repoURL":"github.com/owner/repository" + } +} \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/mergeRequestClosed.json b/services/logs2notifications/internal/handler/testdata/mergeRequestClosed.json new file mode 100644 index 0000000000..5d04ed7b12 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/mergeRequestClosed.json @@ -0,0 +1,11 @@ +{ + "project":"ci-github-openshift", + "event":"api:deployEnvironmentBranch", + "meta":{ + "projectName":"ci-github-openshift", + "pullrequestNumber":"", + "pullrequestTitle":"", + "repoURL":"", + "repoName": "" + } +} \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/mergeRequestOpened.json b/services/logs2notifications/internal/handler/testdata/mergeRequestOpened.json new file mode 100644 index 0000000000..5d04ed7b12 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/mergeRequestOpened.json @@ -0,0 +1,11 @@ +{ + "project":"ci-github-openshift", + "event":"api:deployEnvironmentBranch", + "meta":{ + "projectName":"ci-github-openshift", + "pullrequestNumber":"", + "pullrequestTitle":"", + "repoURL":"", + "repoName": "" + } +} \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/mergeRequestUpdated.json b/services/logs2notifications/internal/handler/testdata/mergeRequestUpdated.json new file mode 100644 index 0000000000..5d04ed7b12 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/mergeRequestUpdated.json @@ -0,0 +1,11 @@ +{ + "project":"ci-github-openshift", + "event":"api:deployEnvironmentBranch", + "meta":{ + "projectName":"ci-github-openshift", + "pullrequestNumber":"", + "pullrequestTitle":"", + "repoURL":"", + "repoName": "" + } +} \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/notDeleted.json b/services/logs2notifications/internal/handler/testdata/notDeleted.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/services/logs2notifications/internal/handler/testdata/problemNotification.1.json b/services/logs2notifications/internal/handler/testdata/problemNotification.1.json new file mode 100644 index 0000000000..21fc0fe8c4 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/problemNotification.1.json @@ -0,0 +1,12 @@ +{ + "project":"ci-github-openshift", + "event":"problem:insert:source_name:summary:severity", + "meta":{ + "projectName":"ci-github-openshift", + "environmentName":"lagoon-type-override", + "serviceName":"nginx", + "severity":"danger", + "description":"this is bad" + }, + "message":"sagsdgasdgsdg" +} \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/problemNotification.2.json b/services/logs2notifications/internal/handler/testdata/problemNotification.2.json new file mode 100644 index 0000000000..918ba09e82 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/problemNotification.2.json @@ -0,0 +1,12 @@ +{ + "project":"ci-github-openshift", + "event":"problem:update:source_name:summary:severity", + "meta":{ + "projectName":"ci-github-openshift", + "environmentName":"lagoon-type-override", + "serviceName":"nginx", + "severity":"danger", + "description":"this is bad" + }, + "message":"sagsdgasdgsdg" +} \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/removeFinished.json b/services/logs2notifications/internal/handler/testdata/removeFinished.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/services/logs2notifications/internal/handler/testdata/repoPushHandled/emailhtml.txt b/services/logs2notifications/internal/handler/testdata/repoPushHandled/emailhtml.txt new file mode 100644 index 0000000000..287e649a98 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/repoPushHandled/emailhtml.txt @@ -0,0 +1 @@ +lagoon-type-override pushed in owner/repository \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/repoPushHandled/emailplain.txt b/services/logs2notifications/internal/handler/testdata/repoPushHandled/emailplain.txt new file mode 100644 index 0000000000..93333fb083 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/repoPushHandled/emailplain.txt @@ -0,0 +1 @@ +[ci-github-openshift] lagoon-type-override pushed in owner/repository \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/repoPushHandled/rocketchat.txt b/services/logs2notifications/internal/handler/testdata/repoPushHandled/rocketchat.txt new file mode 100644 index 0000000000..d68f558315 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/repoPushHandled/rocketchat.txt @@ -0,0 +1 @@ +*[ci-github-openshift]* [lagoon-type-override](github.com/owner/repository/tree/lagoon-type-override) pushed in [owner/repository](github.com/owner/repository) \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/repoPushHandled/slack.txt b/services/logs2notifications/internal/handler/testdata/repoPushHandled/slack.txt new file mode 100644 index 0000000000..d68f558315 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/repoPushHandled/slack.txt @@ -0,0 +1 @@ +*[ci-github-openshift]* [lagoon-type-override](github.com/owner/repository/tree/lagoon-type-override) pushed in [owner/repository](github.com/owner/repository) \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/repoPushHandled/teams.txt b/services/logs2notifications/internal/handler/testdata/repoPushHandled/teams.txt new file mode 100644 index 0000000000..efc27fed55 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/repoPushHandled/teams.txt @@ -0,0 +1 @@ +[lagoon-type-override](github.com/owner/repository/tree/lagoon-type-override) pushed in [owner/repository](github.com/owner/repository) \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/repoPushSkipped/emailhtml.txt b/services/logs2notifications/internal/handler/testdata/repoPushSkipped/emailhtml.txt new file mode 100644 index 0000000000..d8bbb51bc0 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/repoPushSkipped/emailhtml.txt @@ -0,0 +1 @@ +lagoon-type-override pushed in owner/repository deployment skipped \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/repoPushSkipped/emailplain.txt b/services/logs2notifications/internal/handler/testdata/repoPushSkipped/emailplain.txt new file mode 100644 index 0000000000..beaf598e56 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/repoPushSkipped/emailplain.txt @@ -0,0 +1 @@ +[ci-github-openshift] lagoon-type-override pushed in owner/repository *deployment skipped* \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/repoPushSkipped/rocketchat.txt b/services/logs2notifications/internal/handler/testdata/repoPushSkipped/rocketchat.txt new file mode 100644 index 0000000000..9dc759260a --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/repoPushSkipped/rocketchat.txt @@ -0,0 +1 @@ +*[ci-github-openshift]* [lagoon-type-override](github.com/owner/repository/tree/lagoon-type-override) pushed in [owner/repository](github.com/owner/repository) *deployment skipped* \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/repoPushSkipped/slack.txt b/services/logs2notifications/internal/handler/testdata/repoPushSkipped/slack.txt new file mode 100644 index 0000000000..9dc759260a --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/repoPushSkipped/slack.txt @@ -0,0 +1 @@ +*[ci-github-openshift]* [lagoon-type-override](github.com/owner/repository/tree/lagoon-type-override) pushed in [owner/repository](github.com/owner/repository) *deployment skipped* \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/repoPushSkipped/teams.txt b/services/logs2notifications/internal/handler/testdata/repoPushSkipped/teams.txt new file mode 100644 index 0000000000..ee43a45568 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/repoPushSkipped/teams.txt @@ -0,0 +1 @@ +[lagoon-type-override](github.com/owner/repository/tree/lagoon-type-override) pushed in [owner/repository](github.com/owner/repository) *deployment skipped* \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/webhook_events.go b/services/logs2notifications/internal/handler/webhook_events.go new file mode 100644 index 0000000000..6e25acf3d1 --- /dev/null +++ b/services/logs2notifications/internal/handler/webhook_events.go @@ -0,0 +1,150 @@ +package handler + +import ( + "bytes" + "encoding/json" + "fmt" + "log" + "net/http" + "strings" + + "github.com/uselagoon/lagoon/services/logs2notifications/internal/schema" +) + +// WebhookData . +type WebhookData struct { + Type string `json:"type"` + Event string `json:"event"` + Project string `json:"project"` + Environment string `json:"environment"` +} + +// SendToWebhook . +func (h *Messaging) SendToWebhook(notification *Notification, webhook schema.NotificationWebhook) { + message, err := h.processWebhookTemplate(notification) + if err != nil { + return + } + h.sendWebhookMessage(*message, webhook) +} + +func (h *Messaging) sendWebhookMessage(data WebhookData, webhook schema.NotificationWebhook) { + message, _ := json.Marshal(data) + req, err := http.NewRequest("POST", webhook.Webhook, bytes.NewBuffer(message)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Content-Length", fmt.Sprintf("%d", len(message))) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + log.Printf("Error sending message to webhook: %v", err) + return + } + defer resp.Body.Close() + log.Println(fmt.Sprintf("Sent %s message to webhook", data.Event)) +} + +// processWebhookTemplate . +func (h *Messaging) processWebhookTemplate(notification *Notification) (*WebhookData, error) { + tpl, err := getWebhookEvent(notification.Event) + if err != nil { + eventSplit := strings.Split(notification.Event, ":") + if eventSplit[0] != "problem" { + return nil, fmt.Errorf("no matching event") + } + if eventSplit[1] == "insert" { + tpl = "problemNotification" + } + } + data := WebhookData{ + Event: notification.Event, + Project: notification.Meta.ProjectName, + Environment: notification.Meta.BranchName, + } + + switch tpl { + case "deployFinished": + data.Type = "DEPLOYMENT" + case "deployError": + data.Type = "DEPLOYMENT" + default: + return nil, fmt.Errorf("no matching event") + } + return &data, nil +} + +func getWebhookEvent(msgEvent string) (string, error) { + if val, ok := webhookEventTypeMap[msgEvent]; ok { + return val.Template, nil + } + return "", fmt.Errorf("no matching event source") +} + +var webhookEventTypeMap = map[string]EventMap{ + "github:pull_request:opened:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestOpened"}, + "gitlab:merge_request:opened:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestOpened"}, + "bitbucket:pullrequest:created:opened:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestOpened"}, //not in slack + "bitbucket:pullrequest:created:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestOpened"}, //not in teams + + "github:pull_request:synchronize:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, + "gitlab:merge_request:updated:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, + "bitbucket:pullrequest:updated:opened:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, //not in slack + "bitbucket:pullrequest:updated:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, //not in teams + + "github:pull_request:closed:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestClosed"}, + "bitbucket:pullrequest:fulfilled:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestClosed"}, + "bitbucket:pullrequest:rejected:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestClosed"}, + "gitlab:merge_request:closed:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestClosed"}, + + "github:delete:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "deleteEnvironment"}, + "gitlab:remove:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "deleteEnvironment"}, //not in slack + "bitbucket:delete:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "deleteEnvironment"}, //not in slack + "api:deleteEnvironment": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "deleteEnvironment"}, //not in teams + + "github:push:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "repoPushHandled"}, + "bitbucket:repo:push:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "repoPushHandled"}, + "gitlab:push:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "repoPushHandled"}, + + "github:push:skipped": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "repoPushSkipped"}, + "gitlab:push:skipped": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "repoPushSkipped"}, + "bitbucket:push:skipped": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "repoPushSkipped"}, + + "api:deployEnvironmentLatest": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "deployEnvironment"}, + "api:deployEnvironmentBranch": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "deployEnvironment"}, + + "task:deploy-openshift:finished": {Emoji: "✅", Color: "good", Template: "deployFinished"}, + "task:remove-openshift-resources:finished": {Emoji: "✅", Color: "good", Template: "deployFinished"}, + "task:builddeploy-openshift:complete": {Emoji: "✅", Color: "good", Template: "deployFinished"}, + "task:builddeploy-kubernetes:complete": {Emoji: "✅", Color: "good", Template: "deployFinished"}, //not in teams + + "task:remove-openshift:finished": {Emoji: "✅", Color: "good", Template: "removeFinished"}, + "task:remove-kubernetes:finished": {Emoji: "✅", Color: "good", Template: "removeFinished"}, + + "task:remove-openshift:error": {Emoji: "🛑", Color: "danger", Template: "deployError"}, + "task:remove-kubernetes:error": {Emoji: "🛑", Color: "danger", Template: "deployError"}, + "task:builddeploy-kubernetes:failed": {Emoji: "🛑", Color: "danger", Template: "deployError"}, //not in teams + "task:builddeploy-openshift:failed": {Emoji: "🛑", Color: "danger", Template: "deployError"}, + + "github:pull_request:closed:CannotDeleteProductionEnvironment": {Emoji: "⚠️", Color: "warning", Template: "notDeleted"}, + "github:push:CannotDeleteProductionEnvironment": {Emoji: "⚠️", Color: "warning", Template: "notDeleted"}, + "bitbucket:repo:push:CannotDeleteProductionEnvironment": {Emoji: "⚠️", Color: "warning", Template: "notDeleted"}, + "gitlab:push:CannotDeleteProductionEnvironment": {Emoji: "⚠️", Color: "warning", Template: "notDeleted"}, + + // deprecated + // "rest:remove:CannotDeleteProductionEnvironment": {Emoji: "⚠️", Color: "warning"}, + // "rest:deploy:receive": {Emoji: "ℹ️", Color: "#E8E8E8"}, + // "rest:remove:receive": {Emoji: "ℹ️", Color: "#E8E8E8"}, + // "rest:promote:receive": {Emoji: "ℹ️", Color: "#E8E8E8"}, + // "rest:pullrequest:deploy": {Emoji: "ℹ️", Color: "#E8E8E8"}, + // "rest:pullrequest:remove": {Emoji: "ℹ️", Color: "#E8E8E8"}, + + // deprecated + // "task:deploy-openshift:error": {Emoji: "🛑", Color: "danger", Template: "deployError"}, + // "task:remove-openshift-resources:error": {Emoji: "🛑", Color: "danger", Template: "deployError"}, + + // deprecated + // "task:deploy-openshift:retry": {Emoji: "⚠️", Color: "warning", Template: "removeRetry"}, + // "task:remove-openshift:retry": {Emoji: "⚠️", Color: "warning", Template: "removeRetry"}, + // "task:remove-kubernetes:retry": {Emoji: "⚠️", Color: "warning", Template: "removeRetry"}, + // "task:remove-openshift-resources:retry": {Emoji: "⚠️", Color: "warning", Template: "removeRetry"}, +} diff --git a/services/logs2notifications/internal/helpers/helpers.go b/services/logs2notifications/internal/helpers/helpers.go new file mode 100644 index 0000000000..46d554bf6f --- /dev/null +++ b/services/logs2notifications/internal/helpers/helpers.go @@ -0,0 +1,77 @@ +package helpers + +import ( + "crypto/sha1" + "fmt" + "regexp" + "strings" +) + +const ( + // DefaultNamespacePattern is what is used when one is not provided. + DefaultNamespacePattern = "${project}-${environment}" +) + +// GenerateNamespaceName handles the generation of the namespace name from environment and project name with prefixes and patterns +func GenerateNamespaceName(pattern, environmentName, projectname, prefix, controllerNamespace string, randomPrefix bool) string { + nsPattern := pattern + if pattern == "" { + nsPattern = DefaultNamespacePattern + } + environmentName = ShortenEnvironment(projectname, MakeSafe(environmentName)) + // lowercase and dnsify the namespace against the namespace pattern + ns := MakeSafe( + strings.Replace( + strings.Replace( + nsPattern, + "${environment}", + environmentName, + -1, + ), + "${project}", + projectname, + -1, + ), + ) + // If there is a namespaceprefix defined, and random prefix is disabled + // then add the prefix to the namespace + if prefix != "" && randomPrefix == false { + ns = fmt.Sprintf("%s-%s", prefix, ns) + } + // If the randomprefix is enabled, then add a prefix based on the hash of the controller namespace + if randomPrefix { + ns = fmt.Sprintf("%s-%s", HashString(controllerNamespace)[0:8], ns) + } + // Once the namespace is fully calculated, then truncate the generated namespace + // to 63 characters to not exceed the kubernetes namespace limit + if len(ns) > 63 { + ns = fmt.Sprintf("%s-%s", ns[0:58], HashString(ns)[0:4]) + } + return ns +} + +// ShortenEnvironment shortens the environment name down the same way that Lagoon does +func ShortenEnvironment(project, environment string) string { + overlength := 58 - len(project) + if len(environment) > overlength { + environment = fmt.Sprintf("%s-%s", environment[0:overlength-5], HashString(environment)[0:4]) + } + return environment +} + +// MakeSafe ensures that any string is dns safe +func MakeSafe(in string) string { + out := regexp.MustCompile(`[^0-9a-z-]`).ReplaceAllString( + strings.ToLower(in), + "$1-$2", + ) + return out +} + +// HashString get the hash of a given string. +func HashString(s string) string { + h := sha1.New() + h.Write([]byte(s)) + bs := h.Sum(nil) + return fmt.Sprintf("%x", bs) +} diff --git a/services/logs2notifications/internal/helpers/helpers_test.go b/services/logs2notifications/internal/helpers/helpers_test.go new file mode 100644 index 0000000000..6967bf4d8d --- /dev/null +++ b/services/logs2notifications/internal/helpers/helpers_test.go @@ -0,0 +1,160 @@ +package helpers + +import ( + "testing" +) + +func TestGenerateNamespaceName(t *testing.T) { + type args struct { + pattern string + environmentName string + projectname string + prefix string + controllerNamespace string + randomPrefix bool + } + tests := []struct { + name string + args args + want string + }{ + { + name: "really long environment name with slash and capitals", + args: args{ + pattern: "", + environmentName: "Feature/Really-Exceedingly-Long-Environment-Name-For-A-Branch", + projectname: "this-is-my-project", + prefix: "", + controllerNamespace: "lagoon", + randomPrefix: false, + }, + want: "this-is-my-project-feature-really-exceedingly-long-env-dc8c", + }, + { + name: "really long environment name with slash and no capitals", + args: args{ + pattern: "", + environmentName: "feature/really-exceedingly-long-environment-name-for-a-branch", + projectname: "this-is-my-project", + prefix: "", + controllerNamespace: "lagoon", + randomPrefix: false, + }, + want: "this-is-my-project-feature-really-exceedingly-long-env-dc8c", + }, + { + name: "short environment name with slash and capitals", + args: args{ + pattern: "", + environmentName: "Feature/Branch", + projectname: "this-is-my-project", + prefix: "", + controllerNamespace: "lagoon", + randomPrefix: false, + }, + want: "this-is-my-project-feature-branch", + }, + { + name: "short environment name with slash and no capitals", + args: args{ + pattern: "", + environmentName: "feature/branch", + projectname: "this-is-my-project", + prefix: "", + controllerNamespace: "lagoon", + randomPrefix: false, + }, + want: "this-is-my-project-feature-branch", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := GenerateNamespaceName(tt.args.pattern, tt.args.environmentName, tt.args.projectname, tt.args.prefix, tt.args.controllerNamespace, tt.args.randomPrefix); got != tt.want { + t.Errorf("GenerateNamespaceName() got %v, want %v", got, tt.want) + } + }) + } +} + +func TestMakeSafe(t *testing.T) { + tests := []struct { + name string + in string + want string + }{ + { + name: "slash in name", + in: "Feature/Branch", + want: "feature-branch", + }, + { + name: "noslash in name", + in: "Feature-Branch", + want: "feature-branch", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := MakeSafe(tt.in); got != tt.want { + t.Errorf("MakeSafe() go %v, want %v", got, tt.want) + } + }) + } +} + +func TestHashString(t *testing.T) { + tests := []struct { + name string + in string + want string + }{ + { + name: "generate hash", + in: "feature-branch", + want: "011122006d017c21d1376add9f7f65b43555a455", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := HashString(tt.in); got != tt.want { + t.Errorf("HashString() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestShortenEnvironment(t *testing.T) { + type args struct { + project string + environment string + } + tests := []struct { + name string + args args + want string + }{ + { + name: "really long environment name with slash and capitals", + args: args{ + environment: MakeSafe("Feature/Really-Exceedingly-Long-Environment-Name-For-A-Branch"), + project: "this-is-my-project", + }, + want: "feature-really-exceedingly-long-env-dc8c", + }, + { + name: "short environment name", + args: args{ + environment: MakeSafe("Feature/Branch"), + project: "this-is-my-project", + }, + want: "feature-branch", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := ShortenEnvironment(tt.args.project, tt.args.environment); got != tt.want { + t.Errorf("ShortenEnvironment() got %v, want %v", got, tt.want) + } + }) + } +} diff --git a/services/logs2notifications/internal/lagoon/client/_lgraphql/projectNotifications.graphql b/services/logs2notifications/internal/lagoon/client/_lgraphql/projectNotifications.graphql new file mode 100644 index 0000000000..a1f4eca381 --- /dev/null +++ b/services/logs2notifications/internal/lagoon/client/_lgraphql/projectNotifications.graphql @@ -0,0 +1,41 @@ +query ( + $name: String! +) { + projectByName( + name: $name + ) { + id + name + notifications { + ... on NotificationSlack { + __typename + webhook + name + channel + } + ... on NotificationRocketChat { + __typename + webhook + name + channel + } + ... on NotificationEmail { + __typename + emailAddress + name + } + ... on NotificationMicrosoftTeams { + __typename + webhook + name + } + ... on NotificationWebhook { + __typename + webhook + name + contentType + notificationSeverityThreshold + } + } + } +} \ No newline at end of file diff --git a/services/logs2notifications/internal/lagoon/client/client.go b/services/logs2notifications/internal/lagoon/client/client.go new file mode 100644 index 0000000000..9ca3bb2624 --- /dev/null +++ b/services/logs2notifications/internal/lagoon/client/client.go @@ -0,0 +1,93 @@ +//go:generate go-bindata -pkg lgraphql -o lgraphql/lgraphql.go -nometadata _lgraphql/ + +// Package client implements the interfaces required by the parent lagoon +// package. +package client + +import ( + "encoding/json" + "fmt" + "log" + "os" + + "github.com/machinebox/graphql" + "github.com/uselagoon/lagoon/services/logs2notifications/internal/lagoon/client/lgraphql" +) + +// Client implements the lagoon package interfaces for the Lagoon GraphQL API. +type Client struct { + userAgent string + token string + client *graphql.Client +} + +// New creates a new Client for the given endpoint. +func New(endpoint, token, userAgent string, debug bool) *Client { + if debug { + return &Client{ + userAgent: userAgent, + token: token, + client: graphql.NewClient(endpoint, + // enable debug logging to stderr + func(c *graphql.Client) { + l := log.New(os.Stderr, "graphql", 0) + c.Log = func(s string) { + l.Println(s) + } + }), + } + } + return &Client{ + userAgent: userAgent, + token: token, + client: graphql.NewClient(endpoint), + } +} + +// newRequest constructs a graphql request. +// assetName is the name of the graphql query template in _graphql/. +// varStruct is converted to a map of variables for the template. +func (c *Client) newRequest( + assetName string, varStruct interface{}) (*graphql.Request, error) { + + q, err := lgraphql.Asset(assetName) + if err != nil { + return nil, fmt.Errorf("couldn't get asset: %w", err) + } + + return c.doRequest(string(q), varStruct) +} + +func (c *Client) doRequest(query string, varStruct interface{}) (*graphql.Request, error) { + vars, err := structToVarMap(varStruct) + if err != nil { + return nil, fmt.Errorf("couldn't convert struct to map: %w", err) + } + + req := graphql.NewRequest(query) + for key, value := range vars { + req.Var(key, value) + } + + headers := map[string]string{ + "User-Agent": c.userAgent, + "Authorization": fmt.Sprintf("Bearer %s", c.token), + } + for key, value := range headers { + req.Header.Set(key, value) + } + + return req, nil +} + +// structToVarMap encodes the given struct to a map. The idea is that by +// round-tripping through Marshal/Unmarshal, omitempty is applied to the +// zero-valued fields. +func structToVarMap( + varStruct interface{}) (vars map[string]interface{}, err error) { + data, err := json.Marshal(varStruct) + if err != nil { + return vars, err + } + return vars, json.Unmarshal(data, &vars) +} diff --git a/services/logs2notifications/internal/lagoon/client/client_test.go b/services/logs2notifications/internal/lagoon/client/client_test.go new file mode 100644 index 0000000000..c0e17ec55b --- /dev/null +++ b/services/logs2notifications/internal/lagoon/client/client_test.go @@ -0,0 +1,81 @@ +package client_test + +import ( + "reflect" + "testing" + + "github.com/uselagoon/lagoon/services/logs2notifications/internal/lagoon/client" +) + +type testStruct0 struct { + Foo string `json:"foo"` + Bar uint `json:"bar"` + Baz string `json:"baz,omitempty"` + Quux uint `json:"quux,omitempty"` +} + +func TestStructToVarMap(t *testing.T) { + var testCases = map[string]struct { + input testStruct0 + expect map[string]interface{} + }{ + "simple struct": { + input: testStruct0{ + Foo: "abc", + Bar: 8, + }, + expect: map[string]interface{}{ + "foo": "abc", + "bar": float64(8), + }, + }, + "keep zero values": { + input: testStruct0{ + Foo: "abc", + Bar: 0, + }, + expect: map[string]interface{}{ + "foo": "abc", + "bar": float64(0), + }, + }, + "omit zero values": { + input: testStruct0{ + Foo: "abc", + Bar: 0, + Baz: "", + Quux: 0, + }, + expect: map[string]interface{}{ + "foo": "abc", + "bar": float64(0), + }, + }, + "keep non-zero values": { + input: testStruct0{ + Foo: "abc", + Bar: 0, + Baz: "hi", + Quux: 9, + }, + expect: map[string]interface{}{ + "foo": "abc", + "bar": float64(0), + "baz": "hi", + "quux": float64(9), + }, + }, + } + for name, tc := range testCases { + t.Run(name, func(tt *testing.T) { + vars, err := client.StructToVarMap(&tc.input) + if err != nil { + tt.Error(err) + } + if !reflect.DeepEqual(vars, tc.expect) { + tt.Logf("result:\n%s\nexpected:\n%s", vars, tc.expect) + tt.Errorf("result does not match expected") + } + }) + } +} \ No newline at end of file diff --git a/services/logs2notifications/internal/lagoon/client/helper_test.go b/services/logs2notifications/internal/lagoon/client/helper_test.go new file mode 100644 index 0000000000..6561a9155d --- /dev/null +++ b/services/logs2notifications/internal/lagoon/client/helper_test.go @@ -0,0 +1,6 @@ +package client + +// StructToVarMap exposes the private client.structToVarMap for tests. +func StructToVarMap(varStruct interface{}) (map[string]interface{}, error) { + return structToVarMap(varStruct) +} \ No newline at end of file diff --git a/services/logs2notifications/internal/lagoon/client/lgraphql/lgraphql.go b/services/logs2notifications/internal/lagoon/client/lgraphql/lgraphql.go new file mode 100644 index 0000000000..3e7a131df5 --- /dev/null +++ b/services/logs2notifications/internal/lagoon/client/lgraphql/lgraphql.go @@ -0,0 +1,247 @@ +// Code generated by go-bindata. (@generated) DO NOT EDIT. + + //Package lgraphql generated by go-bindata.// sources: +// _lgraphql/projectNotifications.graphql +package lgraphql + +import ( + "bytes" + "compress/gzip" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" + "time" +) + +func bindataRead(data []byte, name string) ([]byte, error) { + gz, err := gzip.NewReader(bytes.NewBuffer(data)) + if err != nil { + return nil, fmt.Errorf("read %q: %v", name, err) + } + + var buf bytes.Buffer + _, err = io.Copy(&buf, gz) + clErr := gz.Close() + + if err != nil { + return nil, fmt.Errorf("read %q: %v", name, err) + } + if clErr != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +type asset struct { + bytes []byte + info os.FileInfo +} + +type bindataFileInfo struct { + name string + size int64 + mode os.FileMode + modTime time.Time +} + +// Name return file name +func (fi bindataFileInfo) Name() string { + return fi.name +} + +// Size return file size +func (fi bindataFileInfo) Size() int64 { + return fi.size +} + +// Mode return file mode +func (fi bindataFileInfo) Mode() os.FileMode { + return fi.mode +} + +// ModTime return file modify time +func (fi bindataFileInfo) ModTime() time.Time { + return fi.modTime +} + +// IsDir return file whether a directory +func (fi bindataFileInfo) IsDir() bool { + return fi.mode&os.ModeDir != 0 +} + +// Sys return file is sys mode +func (fi bindataFileInfo) Sys() interface{} { + return nil +} + +var __lgraphqlProjectnotificationsGraphql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xcc\x92\xc1\x4a\xc4\x30\x10\x86\xcf\xf6\x29\x46\xf0\xb0\x5e\xfa\x00\xde\x54\x3c\xba\x07\x5b\xf0\xb8\xc4\x74\xd6\x8c\x6d\x67\x6a\x32\x2a\x41\xfa\xee\x92\x96\xc5\xb6\x2a\x45\x50\xd8\xff\x94\xfc\xf9\x66\xf8\x93\xc9\xf3\x0b\xfa\x08\x9b\x0c\xe0\x8c\x4d\x8b\x17\x50\xa8\x27\x7e\x3c\xcd\xce\xe1\x3d\x03\x00\xe8\xbc\x3c\xa1\xd5\xab\xb8\x35\x2d\x6e\x06\x2b\x69\x84\x87\x9a\xc1\x3b\xe0\x49\x54\xcd\xb0\xcf\x8d\x28\xed\xc9\x1a\x25\xe1\x30\xe1\x93\xf2\x3c\x07\x61\xd8\x4e\x90\xa2\x31\xb6\x5e\x60\x49\xbb\x9d\xc6\x0e\x67\x9d\x0f\x7a\xc3\x07\x27\x52\x7f\xf1\xbf\x85\xad\x33\xcc\xd8\xcc\xfc\x7e\x2d\xd4\x9d\xd8\x1a\xf5\xda\x19\x3d\xb6\x64\x37\xad\xa1\xe6\x77\xa1\x30\x95\x5c\x56\x95\xc7\x10\xd6\x93\xad\x26\xb8\x25\xeb\x25\xc8\x5e\x4b\x34\xed\x72\xc0\x7f\xf5\x3e\xab\x29\xee\xc7\x4e\xff\x39\x1e\x61\x45\xd6\x32\x76\xcb\xb3\x93\xe9\x17\x2f\xf0\x15\x3d\x69\x2c\x9d\xc7\xe0\xa4\xa9\x7e\xb8\xc7\xb8\xea\xb3\xfe\x23\x00\x00\xff\xff\x7f\x5a\x0f\xed\x8c\x03\x00\x00") + +func _lgraphqlProjectnotificationsGraphqlBytes() ([]byte, error) { + return bindataRead( + __lgraphqlProjectnotificationsGraphql, + "_lgraphql/projectNotifications.graphql", + ) +} + +func _lgraphqlProjectnotificationsGraphql() (*asset, error) { + bytes, err := _lgraphqlProjectnotificationsGraphqlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "_lgraphql/projectNotifications.graphql", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +// Asset loads and returns the asset for the given name. +// It returns an error if the asset could not be found or +// could not be loaded. +func Asset(name string) ([]byte, error) { + cannonicalName := strings.Replace(name, "\\", "/", -1) + if f, ok := _bindata[cannonicalName]; ok { + a, err := f() + if err != nil { + return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err) + } + return a.bytes, nil + } + return nil, fmt.Errorf("Asset %s not found", name) +} + +// MustAsset is like Asset but panics when Asset would return an error. +// It simplifies safe initialization of global variables. +func MustAsset(name string) []byte { + a, err := Asset(name) + if err != nil { + panic("asset: Asset(" + name + "): " + err.Error()) + } + + return a +} + +// AssetInfo loads and returns the asset info for the given name. +// It returns an error if the asset could not be found or +// could not be loaded. +func AssetInfo(name string) (os.FileInfo, error) { + cannonicalName := strings.Replace(name, "\\", "/", -1) + if f, ok := _bindata[cannonicalName]; ok { + a, err := f() + if err != nil { + return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err) + } + return a.info, nil + } + return nil, fmt.Errorf("AssetInfo %s not found", name) +} + +// AssetNames returns the names of the assets. +func AssetNames() []string { + names := make([]string, 0, len(_bindata)) + for name := range _bindata { + names = append(names, name) + } + return names +} + +// _bindata is a table, holding each asset generator, mapped to its name. +var _bindata = map[string]func() (*asset, error){ + "_lgraphql/projectNotifications.graphql": _lgraphqlProjectnotificationsGraphql, +} + +// AssetDir returns the file names below a certain +// directory embedded in the file by go-bindata. +// For example if you run go-bindata on data/... and data contains the +// following hierarchy: +// data/ +// foo.txt +// img/ +// a.png +// b.png +// then AssetDir("data") would return []string{"foo.txt", "img"} +// AssetDir("data/img") would return []string{"a.png", "b.png"} +// AssetDir("foo.txt") and AssetDir("notexist") would return an error +// AssetDir("") will return []string{"data"}. +func AssetDir(name string) ([]string, error) { + node := _bintree + if len(name) != 0 { + cannonicalName := strings.Replace(name, "\\", "/", -1) + pathList := strings.Split(cannonicalName, "/") + for _, p := range pathList { + node = node.Children[p] + if node == nil { + return nil, fmt.Errorf("Asset %s not found", name) + } + } + } + if node.Func != nil { + return nil, fmt.Errorf("Asset %s not found", name) + } + rv := make([]string, 0, len(node.Children)) + for childName := range node.Children { + rv = append(rv, childName) + } + return rv, nil +} + +type bintree struct { + Func func() (*asset, error) + Children map[string]*bintree +} + +var _bintree = &bintree{nil, map[string]*bintree{ + "_lgraphql": &bintree{nil, map[string]*bintree{ + "projectNotifications.graphql": &bintree{_lgraphqlProjectnotificationsGraphql, map[string]*bintree{}}, + }}, +}} + +// RestoreAsset restores an asset under the given directory +func RestoreAsset(dir, name string) error { + data, err := Asset(name) + if err != nil { + return err + } + info, err := AssetInfo(name) + if err != nil { + return err + } + err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755)) + if err != nil { + return err + } + err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode()) + if err != nil { + return err + } + err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) + if err != nil { + return err + } + return nil +} + +// RestoreAssets restores an asset under the given directory recursively +func RestoreAssets(dir, name string) error { + children, err := AssetDir(name) + // File + if err != nil { + return RestoreAsset(dir, name) + } + // Dir + for _, child := range children { + err = RestoreAssets(dir, filepath.Join(name, child)) + if err != nil { + return err + } + } + return nil +} + +func _filePath(dir, name string) string { + cannonicalName := strings.Replace(name, "\\", "/", -1) + return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...) +} diff --git a/services/logs2notifications/internal/lagoon/client/query.go b/services/logs2notifications/internal/lagoon/client/query.go new file mode 100644 index 0000000000..a1e9a2cdca --- /dev/null +++ b/services/logs2notifications/internal/lagoon/client/query.go @@ -0,0 +1,25 @@ +package client + +import ( + "context" + + "github.com/uselagoon/lagoon/services/logs2notifications/internal/schema" +) + +// NotificationsForProjectByName gets all notifications for a project +func (c *Client) NotificationsForProjectByName( + ctx context.Context, name string, project *schema.Project) error { + req, err := c.newRequest("_lgraphql/projectNotifications.graphql", + map[string]interface{}{ + "name": name, + }) + if err != nil { + return err + } + + return c.client.Run(ctx, req, &struct { + Response *schema.Project `json:"projectByName"` + }{ + Response: project, + }) +} diff --git a/services/logs2notifications/internal/lagoon/jwt/jwt.go b/services/logs2notifications/internal/lagoon/jwt/jwt.go new file mode 100644 index 0000000000..4284685b76 --- /dev/null +++ b/services/logs2notifications/internal/lagoon/jwt/jwt.go @@ -0,0 +1,30 @@ +package jwt + +import ( + "time" + + "github.com/dgrijalva/jwt-go" +) + +// LagoonClaims is a set of JWT claims used by Lagoon. +type LagoonClaims struct { + Role string `json:"role"` + jwt.StandardClaims +} + +// OneMinuteAdminToken returns a JWT admin token valid for one minute. +func OneMinuteAdminToken(secret, audience, subject, issuer string) (string, error) { + now := time.Now() + claims := LagoonClaims{ + Role: "admin", + StandardClaims: jwt.StandardClaims{ + Audience: audience, + ExpiresAt: now.Unix() + 60, + IssuedAt: now.Unix(), + Subject: subject, + Issuer: issuer, + }, + } + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString([]byte(secret)) +} \ No newline at end of file diff --git a/services/logs2notifications/internal/lagoon/project.go b/services/logs2notifications/internal/lagoon/project.go new file mode 100644 index 0000000000..7208fc3501 --- /dev/null +++ b/services/logs2notifications/internal/lagoon/project.go @@ -0,0 +1,20 @@ +// Package lagoon implements high-level functions for interacting with the +// Lagoon API. +package lagoon + +import ( + "context" + + "github.com/uselagoon/lagoon/services/logs2notifications/internal/schema" +) + +// Project interface contains methods for projects in lagoon. +type Project interface { + NotificationsForProjectByName(ctx context.Context, name string, result *schema.Project) error +} + +// NotificationsForProject gets notifications for a project. +func NotificationsForProject(ctx context.Context, name string, m Project) (*schema.Project, error) { + result := schema.Project{} + return &result, m.NotificationsForProjectByName(ctx, name, &result) +} diff --git a/services/logs2notifications/internal/schema/notifications.go b/services/logs2notifications/internal/schema/notifications.go new file mode 100644 index 0000000000..33aeff6f61 --- /dev/null +++ b/services/logs2notifications/internal/schema/notifications.go @@ -0,0 +1,107 @@ +package schema + +import ( + "encoding/json" + "fmt" +) + +// Notifications represents possible Lagoon notification types. +// These are unmarshalled from a projectByName query response. +type Notifications struct { + Slack []NotificationSlack + RocketChat []NotificationRocketChat + Email []NotificationEmail + MicrosoftTeams []NotificationMicrosoftTeams + Webhook []NotificationWebhook +} + +// NotificationSlack is based on the Lagoon API type. +type NotificationSlack struct { + Name string `json:"name"` + Webhook string `json:"webhook"` + Channel string `json:"channel"` +} + +// NotificationRocketChat is based on the Lagoon API type. +type NotificationRocketChat struct { + Name string `json:"name"` + Webhook string `json:"webhook"` + Channel string `json:"channel"` +} + +// NotificationEmail is based on the Lagoon API type. +type NotificationEmail struct { + Name string `json:"name"` + EmailAddress string `json:"emailAddress"` +} + +// NotificationMicrosoftTeams is based on the Lagoon API type. +type NotificationMicrosoftTeams struct { + Name string `json:"name"` + Webhook string `json:"webhook"` +} + +// NotificationWebhook is based on the Lagoon API type. +type NotificationWebhook struct { + Name string `json:"name"` + Webhook string `json:"webhook"` + ContentType string `json:"contentType"` + NotificationSeverityThreshold string `json:"notificationSeverityThreshold"` +} + +// UnmarshalJSON unmashals a quoted json string to the Notification values +// returned from the Lagoon API. +func (n *Notifications) UnmarshalJSON(b []byte) error { + var nArray []map[string]string + err := json.Unmarshal(b, &nArray) + if err != nil { + return err + } + for _, nMap := range nArray { + if len(nMap) == 0 { + // Unsupported notification type returns an empty map. + // This happens when the lagoon API being targeted is actually a higher + // version than configured. + continue + } + switch nMap["__typename"] { + case "NotificationSlack": + n.Slack = append(n.Slack, + NotificationSlack{ + Name: nMap["name"], + Webhook: nMap["webhook"], + Channel: nMap["channel"], + }) + case "NotificationRocketChat": + n.RocketChat = append(n.RocketChat, + NotificationRocketChat{ + Name: nMap["name"], + Webhook: nMap["webhook"], + Channel: nMap["channel"], + }) + case "NotificationEmail": + n.Email = append(n.Email, + NotificationEmail{ + Name: nMap["name"], + EmailAddress: nMap["emailAddress"], + }) + case "NotificationMicrosoftTeams": + n.MicrosoftTeams = append(n.MicrosoftTeams, + NotificationMicrosoftTeams{ + Name: nMap["name"], + Webhook: nMap["webhook"], + }) + case "NotificationWebhook": + n.Webhook = append(n.Webhook, + NotificationWebhook{ + Name: nMap["name"], + Webhook: nMap["webhook"], + ContentType: nMap["contentType"], + NotificationSeverityThreshold: nMap["notificationSeverityThreshold"], + }) + default: + return fmt.Errorf("unknown notification type: %v", nMap["__typename"]) + } + } + return nil +} diff --git a/services/logs2notifications/internal/schema/project.go b/services/logs2notifications/internal/schema/project.go new file mode 100644 index 0000000000..d806ac94a8 --- /dev/null +++ b/services/logs2notifications/internal/schema/project.go @@ -0,0 +1,8 @@ +package schema + +// Project is based on the Lagoon API type. +type Project struct { + ID uint `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Notifications *Notifications `json:"notifications,omitempty"` +} diff --git a/services/logs2notifications/main.go b/services/logs2notifications/main.go new file mode 100644 index 0000000000..da2e918cb0 --- /dev/null +++ b/services/logs2notifications/main.go @@ -0,0 +1,259 @@ +package main + +import ( + "flag" + "fmt" + "log" + "os" + "strconv" + "time" + + "github.com/cheshir/go-mq" + "github.com/uselagoon/lagoon/services/logs2notifications/internal/handler" +) + +var ( + httpListenPort = os.Getenv("HTTP_LISTEN_PORT") + mqUser string + mqPass string + mqHost string + mqPort string + mqWorkers int + rabbitReconnectRetryInterval int + startupConnectionAttempts int + startupConnectionInterval int + lagoonAPIHost string + lagoonAppID string + jwtTokenSigningKey string + jwtAudience string + jwtSubject string + jwtIssuer string + + s3FilesAccessKeyID string + s3FilesSecretAccessKey string + s3FilesBucket string + s3FilesRegion string + s3FilesOrigin string + + disableSlack bool + disableRocketChat bool + disableMicrosoftTeams bool + disableEmail bool + disableWebhooks bool + disableS3 bool + + emailSender string + emailSenderPassword string + emailHost string + emailPort string + emailInsecureSkipVerify bool +) + +func main() { + flag.StringVar(&lagoonAppID, "lagoon-app-id", "logs2notifications", + "The appID to use that will be sent with messages.") + flag.StringVar(&mqUser, "rabbitmq-username", "guest", + "The username of the rabbitmq user.") + flag.StringVar(&mqPass, "rabbitmq-password", "guest", + "The password for the rabbitmq user.") + flag.StringVar(&mqHost, "rabbitmq-hostname", "localhost", + "The hostname for the rabbitmq host.") + flag.StringVar(&mqPort, "rabbitmq-port", "5672", + "The port for the rabbitmq host.") + flag.IntVar(&mqWorkers, "rabbitmq-queue-workers", 1, + "The number of workers to start with.") + flag.IntVar(&rabbitReconnectRetryInterval, "rabbitmq-reconnect-retry-interval", 30, + "The retry interval for rabbitmq.") + flag.IntVar(&startupConnectionAttempts, "startup-connection-attempts", 10, + "The number of startup attempts before exiting.") + flag.IntVar(&startupConnectionInterval, "startup-connection-interval-seconds", 30, + "The duration between startup attempts.") + flag.StringVar(&lagoonAPIHost, "lagoon-api-host", "http://localhost:3000/graphql", + "The host for the lagoon api.") + flag.StringVar(&jwtTokenSigningKey, "jwt-token-signing-key", "super-secret-string", + "The jwt signing token key or secret.") + flag.StringVar(&jwtAudience, "jwt-audience", "api.dev", + "The jwt audience.") + flag.StringVar(&jwtSubject, "jwt-subject", "logs2notifications", + "The jwt audience.") + flag.StringVar(&jwtIssuer, "jwt-issuer", "logs2notifications", + "The jwt audience.") + + // Other notifications configuration + flag.BoolVar(&disableSlack, "disable-slack", false, + "Disable the logs2slack feature.") + flag.BoolVar(&disableRocketChat, "disable-rocketchat", false, + "Disable the logs2rocketchat feature.") + flag.BoolVar(&disableMicrosoftTeams, "disable-microsoft-teams", false, + "Disable the logs2microsoftteams feature.") + flag.BoolVar(&disableWebhooks, "disable-webhooks", false, + "Disable the logs2webhooks feature.") + + // S3 configuration + flag.BoolVar(&disableS3, "disable-s3", false, + "Disable the logs2s3 feature.") + flag.StringVar(&s3FilesAccessKeyID, "s3-files-access-key", "minio", + "The jwt audience.") + flag.StringVar(&s3FilesSecretAccessKey, "s3-files-secret-access-key", "minio123", + "The jwt audience.") + flag.StringVar(&s3FilesBucket, "s3-files-bucket", "lagoon-files", + "The jwt audience.") + flag.StringVar(&s3FilesRegion, "s3-files-region", "auto", + "The jwt audience.") + flag.StringVar(&s3FilesOrigin, "s3-files-origin", "http://minio.127.0.0.1.nip.io:9000", + "The jwt audience.") + + // Email sending configuration + flag.BoolVar(&disableEmail, "disable-email", false, + "Disable the logs2email feature.") + flag.StringVar(&emailSender, "email-sender-address", "notifications@lagoon.sh", + "The email address to send notifications as.") + flag.StringVar(&emailSenderPassword, "email-sender-password", "", + "The password (if required) for the sending email address.") + flag.StringVar(&emailHost, "email-host", "localhost", + "The host name or address for the email server.") + flag.StringVar(&emailPort, "email-port", "1025", + "The port for the email server.") + flag.BoolVar(&emailInsecureSkipVerify, "email-tls-insecure-skip-verify", true, + "Use TLS verification when talking to the email server.") + flag.Parse() + + // get overrides from environment variables + mqUser = getEnv("RABBITMQ_USERNAME", mqUser) + mqPass = getEnv("RABBITMQ_PASSWORD", mqPass) + mqHost = getEnv("RABBITMQ_ADDRESS", mqHost) + mqPort = getEnv("RABBITMQ_PORT", mqPort) + lagoonAPIHost = getEnv("GRAPHQL_ENDPOINT", lagoonAPIHost) + jwtTokenSigningKey = getEnv("JWT_SECRET", jwtTokenSigningKey) + jwtAudience = getEnv("JWT_AUDIENCE", jwtAudience) + jwtSubject = getEnv("JWT_SUBJECT", jwtSubject) + jwtIssuer = getEnv("JWT_ISSUER", jwtIssuer) + + s3FilesAccessKeyID = getEnv("S3_FILES_ACCESS_KEY_ID", s3FilesAccessKeyID) + s3FilesSecretAccessKey = getEnv("S3_FILES_SECRET_ACCESS_KEY", s3FilesSecretAccessKey) + s3FilesBucket = getEnv("S3_FILES_BUCKET", s3FilesBucket) + s3FilesRegion = getEnv("S3_FILES_REGION", s3FilesRegion) + s3FilesOrigin = getEnv("S3_FILES_HOST", s3FilesOrigin) + + emailSender = getEnv("EMAIL_SENDER_ADDRESS", emailSender) + emailSenderPassword = getEnv("EMAIL_SENDER_PASSWORD", emailSenderPassword) + emailHost = getEnv("EMAIL_HOST", emailHost) + emailPort = getEnv("EMAIL_PORT", emailPort) + + enableDebug := true + + // configure the backup handler settings + broker := handler.RabbitBroker{ + Hostname: fmt.Sprintf("%s:%s", mqHost, mqPort), + Username: mqUser, + Password: mqPass, + } + graphQLConfig := handler.LagoonAPI{ + Endpoint: lagoonAPIHost, + TokenSigningKey: jwtTokenSigningKey, + JWTAudience: jwtAudience, + JWTSubject: jwtSubject, + JWTIssuer: jwtIssuer, + } + + log.Println("logs2notifications running") + + config := mq.Config{ + ReconnectDelay: time.Duration(rabbitReconnectRetryInterval) * time.Second, + Exchanges: mq.Exchanges{ + { + Name: "lagoon-logs", + Type: "direct", + Options: mq.Options{ + "durable": true, + "delivery_mode": "2", + "headers": "", + "content_type": "", + }, + }, + }, + Consumers: mq.Consumers{ + { + Name: "notifications-queue", + Queue: "lagoon-logs:notifications", + Workers: mqWorkers, + Options: mq.Options{ + "durable": true, + "delivery_mode": "2", + "headers": "", + "content_type": "", + }, + }, + }, + Queues: mq.Queues{ + { + Name: "lagoon-logs:notifications", + Exchange: "lagoon-logs", + Options: mq.Options{ + "durable": true, + "delivery_mode": "2", + "headers": "", + "content_type": "", + }, + }, + }, + Producers: mq.Producers{ + { + Name: "lagoon-logs", + Exchange: "lagoon-logs", + Options: mq.Options{ + "app_id": lagoonAppID, + "delivery_mode": "2", + "headers": "", + "content_type": "", + }, + }, + }, + DSN: fmt.Sprintf("amqp://%s:%s@%s/", broker.Username, broker.Password, broker.Hostname), + } + + messaging := handler.NewMessaging(config, + graphQLConfig, + startupConnectionAttempts, + startupConnectionInterval, + enableDebug, + lagoonAppID, + disableSlack, + disableRocketChat, + disableMicrosoftTeams, + disableEmail, + disableWebhooks, + disableS3, + emailSender, + emailSenderPassword, + emailHost, + emailPort, + emailInsecureSkipVerify, + s3FilesAccessKeyID, + s3FilesSecretAccessKey, + s3FilesBucket, + s3FilesRegion, + s3FilesOrigin, + ) + + // start the consumer + messaging.Consumer() + +} + +func getEnv(key, fallback string) string { + if value, ok := os.LookupEnv(key); ok { + return value + } + return fallback +} + +// accepts fallback values 1, t, T, TRUE, true, True, 0, f, F, FALSE, false, False +// anything else is false. +func getEnvBool(key string, fallback bool) bool { + if value, ok := os.LookupEnv(key); ok { + rVal, _ := strconv.ParseBool(value) + return rVal + } + return fallback +} From aab9261895bae3d08ce9181654bfc0e3476b6f5f Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Wed, 23 Mar 2022 09:49:34 +1100 Subject: [PATCH 04/17] chore: disable logs2other services --- Makefile | 19 +++++++------------ docker-compose.yaml | 40 ++-------------------------------------- 2 files changed, 9 insertions(+), 50 deletions(-) diff --git a/Makefile b/Makefile index a28502930e..68c66c5274 100644 --- a/Makefile +++ b/Makefile @@ -193,12 +193,7 @@ services := api \ logs-concentrator \ logs-dispatcher \ logs-tee \ - logs2email \ - logs2microsoftteams \ - logs2rocketchat \ - logs2slack \ - logs2s3 \ - logs2webhook \ + logs2notifications \ storage-calculator \ ui \ webhook-handler \ @@ -218,7 +213,7 @@ $(build-services): touch $@ # Dependencies of Service Images -build/auth-server build/logs2email build/logs2slack build/logs2rocketchat build/logs2s3 build/logs2webhook build/logs2microsoftteams build/backup-handler build/controllerhandler build/webhook-handler build/webhooks2tasks build/api build/ui: build/yarn-workspace-builder +build/auth-server build/logs2notifications build/backup-handler build/controllerhandler build/webhook-handler build/webhooks2tasks build/api build/ui: build/yarn-workspace-builder build/api-db: services/api-db/Dockerfile build/api-redis: services/api-redis/Dockerfile build/actions-handler: services/actions-handler/Dockerfile @@ -298,7 +293,7 @@ wait-for-keycloak: grep -m 1 "Config of Keycloak done." <(docker-compose -p $(CI_BUILD_TAG) --compatibility logs -f keycloak 2>&1) # Define a list of which Lagoon Services are needed for running any deployment testing -main-test-services = actions-handler broker logs2email logs2slack logs2rocketchat logs2microsoftteams logs2s3 logs2webhook api api-db api-redis keycloak keycloak-db ssh auth-server local-git local-api-data-watcher-pusher local-minio +main-test-services = actions-handler broker logs2notifications api api-db api-redis keycloak keycloak-db ssh auth-server local-git local-api-data-watcher-pusher local-minio # List of Lagoon Services needed for webhook endpoint testing webhooks-test-services = webhook-handler webhooks2tasks backup-handler @@ -502,8 +497,8 @@ api-development: build/api build/api-db build/local-api-data-watcher-pusher buil IMAGE_REPO=$(CI_BUILD_TAG) docker-compose -p $(CI_BUILD_TAG) --compatibility up -d api api-db local-api-data-watcher-pusher keycloak keycloak-db broker api-redis .PHONY: ui-logs-development -ui-logs-development: build/actions-handler build/api build/api-db build/local-api-data-watcher-pusher build/ui build/keycloak build/keycloak-db build/broker-single build/api-redis build/logs2s3 build/local-minio - IMAGE_REPO=$(CI_BUILD_TAG) docker-compose -p $(CI_BUILD_TAG) --compatibility up -d api api-db actions-handler local-api-data-watcher-pusher ui keycloak keycloak-db broker api-redis logs2s3 local-minio +ui-logs-development: build/actions-handler build/api build/api-db build/local-api-data-watcher-pusher build/ui build/keycloak build/keycloak-db build/broker-single build/api-redis build/logs2notifications build/local-minio + IMAGE_REPO=$(CI_BUILD_TAG) docker-compose -p $(CI_BUILD_TAG) --compatibility up -d api api-db actions-handler local-api-data-watcher-pusher ui keycloak keycloak-db broker api-redis logs2notifications local-minio ## CI targets @@ -634,7 +629,7 @@ ifeq ($(ARCH), darwin) tcp-listen:32080,fork,reuseaddr tcp-connect:target:32080 endif -KIND_SERVICES = api api-db api-redis auth-server actions-handler broker controllerhandler docker-host drush-alias keycloak keycloak-db logs2s3 webhook-handler webhooks2tasks kubectl-build-deploy-dind local-api-data-watcher-pusher local-git ssh tests ui workflows +KIND_SERVICES = api api-db api-redis auth-server actions-handler broker controllerhandler docker-host drush-alias keycloak keycloak-db logs2notifications logs2s3 webhook-handler webhooks2tasks kubectl-build-deploy-dind local-api-data-watcher-pusher local-git ssh tests ui workflows KIND_TESTS = local-api-data-watcher-pusher local-git tests KIND_TOOLS = kind helm kubectl jq stern @@ -667,7 +662,7 @@ kind/test: kind/cluster helm/repos $(addprefix local-dev/,$(KIND_TOOLS)) $(addpr "quay.io/helmpack/chart-testing:$(CHART_TESTING_VERSION)" \ ct install -LOCAL_DEV_SERVICES = api auth-server controllerhandler logs2email logs2microsoftteams logs2rocketchat logs2slack logs2s3 logs2webhook ui webhook-handler webhooks2tasks +LOCAL_DEV_SERVICES = api auth-server controllerhandler logs2notifications ui webhook-handler webhooks2tasks # install lagoon charts in a Kind cluster .PHONY: kind/setup diff --git a/docker-compose.yaml b/docker-compose.yaml index e63293174e..ea110b3551 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -33,44 +33,8 @@ services: volumes: - ./services/controllerhandler/src:/app/services/controllerhandler/src - ./node-packages:/app/node-packages:delegated - logs2rocketchat: - image: ${IMAGE_REPO:-lagoon}/logs2rocketchat - command: yarn run dev - volumes: - - ./services/logs2rocketchat/src:/app/services/logs2rocketchat/src - - ./node-packages:/app/node-packages:delegated - logs2slack: - image: ${IMAGE_REPO:-lagoon}/logs2slack - command: yarn run dev - volumes: - - ./services/logs2slack/src:/app/services/logs2slack/src - - ./node-packages:/app/node-packages:delegated - logs2webhook: - image: ${IMAGE_REPO:-lagoon}/logs2webhook - command: yarn run dev - volumes: - - ./services/logs2webhook/src:/app/services/logs2webhook/src - - ./node-packages:/app/node-packages:delegated - logs2s3: - image: ${IMAGE_REPO:-lagoon}/logs2s3 - command: yarn run dev - volumes: - - ./services/logs2s3/src:/app/services/logs2s3/src - - ./node-packages:/app/node-packages:delegated - logs2microsoftteams: - image: ${IMAGE_REPO:-lagoon}/logs2microsoftteams - command: yarn run dev - volumes: - - ./services/logs2microsoftteams/src:/app/services/logs2microsoftteams/src - - ./node-packages:/app/node-packages:delegated - logs2email: - image: ${IMAGE_REPO:-lagoon}/logs2email - command: yarn run dev - volumes: - - ./services/logs2email/src:/app/services/logs2email/src - - ./node-packages:/app/node-packages:delegated - depends_on: - - mailhog + logs2notifications: + image: ${IMAGE_REPO:-lagoon}/logs2notifications mailhog: image: mailhog/mailhog ports: From 9590b70fc7a6ef30f17e934c86274905b4628e05 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Wed, 23 Mar 2022 15:56:39 +1100 Subject: [PATCH 05/17] chore: remove old logs2x services --- services/logs2email/.babelrc | 8 - services/logs2email/.gitignore | 2 - services/logs2email/Dockerfile | 42 -- services/logs2email/README.md | 34 -- services/logs2email/entrypoints/50-ssmtp.sh | 48 --- services/logs2email/package.json | 29 -- services/logs2email/src/index.ts | 26 -- services/logs2email/src/readFromRabbitMQ.ts | 365 ------------------ services/logs2email/ssmtp.conf | 4 - services/logs2email/tsconfig.json | 10 - services/logs2microsoftteams/.babelrc | 8 - services/logs2microsoftteams/.gitignore | 2 - services/logs2microsoftteams/Dockerfile | 34 -- services/logs2microsoftteams/README.md | 34 -- services/logs2microsoftteams/package.json | 33 -- services/logs2microsoftteams/src/index.ts | 26 -- .../src/readFromRabbitMQ.ts | 257 ------------ services/logs2microsoftteams/tsconfig.json | 10 - services/logs2rocketchat/.babelrc | 8 - services/logs2rocketchat/.gitignore | 2 - services/logs2rocketchat/Dockerfile | 34 -- services/logs2rocketchat/README.md | 34 -- services/logs2rocketchat/package.json | 33 -- services/logs2rocketchat/src/index.ts | 26 -- .../logs2rocketchat/src/readFromRabbitMQ.ts | 264 ------------- services/logs2rocketchat/tsconfig.json | 10 - services/logs2s3/.gitignore | 2 - services/logs2s3/Dockerfile | 34 -- services/logs2s3/README.md | 31 -- services/logs2s3/package.json | 35 -- services/logs2s3/src/index.ts | 26 -- services/logs2s3/src/readFromRabbitMQ.ts | 96 ----- services/logs2s3/tsconfig.json | 10 - services/logs2slack/.gitignore | 2 - services/logs2slack/Dockerfile | 34 -- services/logs2slack/README.md | 34 -- services/logs2slack/package.json | 34 -- services/logs2slack/src/index.ts | 26 -- services/logs2slack/src/readFromRabbitMQ.ts | 133 ------- services/logs2slack/tsconfig.json | 10 - services/logs2webhook/.gitignore | 2 - services/logs2webhook/Dockerfile | 34 -- services/logs2webhook/README.md | 33 -- services/logs2webhook/package.json | 34 -- services/logs2webhook/src/index.ts | 26 -- services/logs2webhook/src/readFromRabbitMQ.ts | 105 ----- services/logs2webhook/tsconfig.json | 10 - 47 files changed, 2134 deletions(-) delete mode 100644 services/logs2email/.babelrc delete mode 100644 services/logs2email/.gitignore delete mode 100644 services/logs2email/Dockerfile delete mode 100644 services/logs2email/README.md delete mode 100755 services/logs2email/entrypoints/50-ssmtp.sh delete mode 100644 services/logs2email/package.json delete mode 100644 services/logs2email/src/index.ts delete mode 100644 services/logs2email/src/readFromRabbitMQ.ts delete mode 100644 services/logs2email/ssmtp.conf delete mode 100644 services/logs2email/tsconfig.json delete mode 100644 services/logs2microsoftteams/.babelrc delete mode 100644 services/logs2microsoftteams/.gitignore delete mode 100644 services/logs2microsoftteams/Dockerfile delete mode 100644 services/logs2microsoftteams/README.md delete mode 100644 services/logs2microsoftteams/package.json delete mode 100644 services/logs2microsoftteams/src/index.ts delete mode 100644 services/logs2microsoftteams/src/readFromRabbitMQ.ts delete mode 100644 services/logs2microsoftteams/tsconfig.json delete mode 100644 services/logs2rocketchat/.babelrc delete mode 100644 services/logs2rocketchat/.gitignore delete mode 100644 services/logs2rocketchat/Dockerfile delete mode 100644 services/logs2rocketchat/README.md delete mode 100644 services/logs2rocketchat/package.json delete mode 100644 services/logs2rocketchat/src/index.ts delete mode 100644 services/logs2rocketchat/src/readFromRabbitMQ.ts delete mode 100644 services/logs2rocketchat/tsconfig.json delete mode 100644 services/logs2s3/.gitignore delete mode 100644 services/logs2s3/Dockerfile delete mode 100644 services/logs2s3/README.md delete mode 100644 services/logs2s3/package.json delete mode 100644 services/logs2s3/src/index.ts delete mode 100644 services/logs2s3/src/readFromRabbitMQ.ts delete mode 100644 services/logs2s3/tsconfig.json delete mode 100644 services/logs2slack/.gitignore delete mode 100644 services/logs2slack/Dockerfile delete mode 100644 services/logs2slack/README.md delete mode 100644 services/logs2slack/package.json delete mode 100644 services/logs2slack/src/index.ts delete mode 100644 services/logs2slack/src/readFromRabbitMQ.ts delete mode 100644 services/logs2slack/tsconfig.json delete mode 100644 services/logs2webhook/.gitignore delete mode 100644 services/logs2webhook/Dockerfile delete mode 100644 services/logs2webhook/README.md delete mode 100644 services/logs2webhook/package.json delete mode 100644 services/logs2webhook/src/index.ts delete mode 100644 services/logs2webhook/src/readFromRabbitMQ.ts delete mode 100644 services/logs2webhook/tsconfig.json diff --git a/services/logs2email/.babelrc b/services/logs2email/.babelrc deleted file mode 100644 index 5eba7fc969..0000000000 --- a/services/logs2email/.babelrc +++ /dev/null @@ -1,8 +0,0 @@ -{ - "presets": [ - "presets": [["latest-node", { "target": "10" }]] - ], - "plugins": [ - "transform-es2015-modules-commonjs" - ] -} diff --git a/services/logs2email/.gitignore b/services/logs2email/.gitignore deleted file mode 100644 index f06235c460..0000000000 --- a/services/logs2email/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -node_modules -dist diff --git a/services/logs2email/Dockerfile b/services/logs2email/Dockerfile deleted file mode 100644 index 44a3ded8bc..0000000000 --- a/services/logs2email/Dockerfile +++ /dev/null @@ -1,42 +0,0 @@ -ARG LAGOON_GIT_BRANCH -ARG IMAGE_REPO -ARG UPSTREAM_REPO -ARG UPSTREAM_TAG -# STAGE 1: Loading Image lagoon-node-packages-builder which contains node packages shared by all Node Services -FROM ${IMAGE_REPO:-lagoon}/yarn-workspace-builder as yarn-workspace-builder - -# STAGE 2: specific service Image -FROM ${UPSTREAM_REPO:-uselagoon}/node-16:${UPSTREAM_TAG:-latest} - -ARG LAGOON_VERSION -ENV LAGOON_VERSION=$LAGOON_VERSION - -COPY entrypoints/50-ssmtp.sh /lagoon/entrypoints/ -COPY ssmtp.conf /etc/ssmtp/ssmtp.conf - -RUN apk add --no-cache ssmtp - -RUN fix-permissions /etc/ssmtp/ssmtp.conf \ - && fix-permissions /usr/sbin/ssmtp - -# Copying generated node_modules from the first stage -COPY --from=yarn-workspace-builder /app /app - -# Setting the workdir to the service, all following commands will run from here -WORKDIR /app/services/logs2email/ - -# Copying the .env.defaults into the Workdir, as the dotenv system searches within the workdir for it -COPY --from=yarn-workspace-builder /app/.env.defaults . - -# Copying files from our service -COPY . . - -# Verify that all dependencies have been installed via the yarn-workspace-builder -RUN yarn check --verify-tree - -# Making sure we run in production -ENV NODE_ENV production - -RUN yarn build - -CMD ["yarn", "start"] diff --git a/services/logs2email/README.md b/services/logs2email/README.md deleted file mode 100644 index 9e4cced6c2..0000000000 --- a/services/logs2email/README.md +++ /dev/null @@ -1,34 +0,0 @@ -

- -This service is part of amazee.io Lagoon, a Docker build and deploy system for -OpenShift & Kubernetes. Please reference our [documentation] for detailed -information on using, developing, and administering Lagoon. - -# Logs to Email (`logs2email`) - -Watches all the Lagoon logs and checks for events that should trigger an email -notification. Each log message is tied to a Lagoon project, and email -configuration for that project is retrieved from the Lagoon API. - -Examples of events that might trigger an email: GitHub pull request opened, a new -build for a Lagoon project environent has started, a task was completed. - -## Technology - -* Node.js -* Message Queue - -## Related Services - -* API [***dependency***] -* RabbitMQ [***dependency***] - -## Message Queues - -* Consumes: `lagoon-logs`, `lagoon-logs:email` -* Produces: `lagoon-logs:email` - -[documentation]: https://docs.lagoon.sh/ diff --git a/services/logs2email/entrypoints/50-ssmtp.sh b/services/logs2email/entrypoints/50-ssmtp.sh deleted file mode 100755 index c5a51f7ea1..0000000000 --- a/services/logs2email/entrypoints/50-ssmtp.sh +++ /dev/null @@ -1,48 +0,0 @@ -#!/bin/sh - - -if [ ${SSMTP_REWRITEDOMAIN+x} ]; then - echo -e "\nrewriteDomain=${SSMTP_REWRITEDOMAIN}" >> /etc/ssmtp/ssmtp.conf -fi -if [ ${SSMTP_AUTHUSER+x} ]; then - echo -e "\nAuthUser=${SSMTP_AUTHUSER}" >> /etc/ssmtp/ssmtp.conf -fi -if [ ${SSMTP_AUTHPASS+x} ]; then - echo -e "\nAuthPass=${SSMTP_AUTHPASS}" >> /etc/ssmtp/ssmtp.conf -fi -if [ ${SSMTP_USETLS+x} ]; then - echo -e "\nUseTLS=${SSMTP_USETLS}" >> /etc/ssmtp/ssmtp.conf -fi -if [ ${SSMTP_USESTARTTLS+x} ]; then - echo -e "\nUseSTARTTLS=${SSMTP_USESTARTTLS}" >> /etc/ssmtp/ssmtp.conf -fi - -if [ ${SSMTP_MAILHUB+x} ]; then - echo -e "\nmailhub=${SSMTP_MAILHUB}" >> /etc/ssmtp/ssmtp.conf -else - # check if we find a mailhog on 172.17.0.1:1025 - if nc -z -w 1 172.17.0.1 1025 &> /dev/null; then - echo -e "\nmailhub=172.17.0.1:1025" >> /etc/ssmtp/ssmtp.conf - return - fi - # check if mxout.lagoon.svc can do smtp TLS - if nc -z -w 1 mxout.lagoon.svc 465 &> /dev/null; then - echo -e "UseTLS=Yes\nmailhub=mxout.lagoon.svc:465" >> /etc/ssmtp/ssmtp.conf - return - fi - # Fallback: check if mxout.lagoon.svc can do regular 25 smtp - if nc -z -w 1 mxout.lagoon.svc 25 &> /dev/null; then - echo -e "\nmailhub=mxout.lagoon.svc:25" >> /etc/ssmtp/ssmtp.conf - return - fi - # check if mxout.default.svc can do smtp TLS - if nc -z -w 1 mxout.default.svc 465 &> /dev/null; then - echo -e "UseTLS=Yes\nmailhub=mxout.default.svc:465" >> /etc/ssmtp/ssmtp.conf - return - fi - # Fallback: check if mxout.default.svc can do regular 25 smtp - if nc -z -w 1 mxout.default.svc 25 &> /dev/null; then - echo -e "\nmailhub=mxout.default.svc:25" >> /etc/ssmtp/ssmtp.conf - return - fi -fi diff --git a/services/logs2email/package.json b/services/logs2email/package.json deleted file mode 100644 index c6abda9651..0000000000 --- a/services/logs2email/package.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "logs2email", - "version": "0.0.1", - "description": "lagoon handler for email notifications", - "author": "amazee.io (http://www.amazee.io)", - "main": "dist/index.js", - "scripts": { - "build": "tsc --build", - "start": "node dist/index", - "dev": "mkdir -p ../../node-packages/commons/dist && NODE_ENV=development nodemon" - }, - "nodemonConfig": { - "ignore": ["../../node-packages/commons/dist/"], - "watch": ["src", "../../node-packages/"], - "ext": "js,ts,json", - "exec": "yarn build --incremental && yarn start --inspect=0.0.0.0:9229" - }, - "license": "MIT", - "dependencies": { - "@lagoon/commons": "4.0.0", - "amqp-connection-manager": "^1.3.5", - "nodemailer": "^6.3.0" - }, - "devDependencies": { - "@types/amqp-connection-manager": "^2.0.10", - "nodemon": "^1.12.1", - "typescript": "^3.9.3" - } -} diff --git a/services/logs2email/src/index.ts b/services/logs2email/src/index.ts deleted file mode 100644 index d85919133a..0000000000 --- a/services/logs2email/src/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -import amqp, { ChannelWrapper } from 'amqp-connection-manager'; -import { logger } from '@lagoon/commons/dist/local-logging'; -import { readFromRabbitMQ } from './readFromRabbitMQ'; - -const rabbitmqHost = process.env.RABBITMQ_HOST || "broker" -const rabbitmqUsername = process.env.RABBITMQ_USERNAME || "guest" -const rabbitmqPassword = process.env.RABBITMQ_PASSWORD || "guest" -// @ts-ignore -const connection = amqp.connect([`amqp://${rabbitmqUsername}:${rabbitmqPassword}@${rabbitmqHost}`], { json: true }); - -connection.on('connect', ({ url }) => logger.verbose('Connected to %s', url, { action: 'connected', url })); -// @ts-ignore -connection.on('disconnect', params => logger.error('Not connected, error: %s', params.err.code, { action: 'disconnected', reason: params })); - -// Cast any to ChannelWrapper to get type-safetiness through our own code -const channelWrapperLogs: ChannelWrapper = connection.createChannel({ - setup: channel => { - return Promise.all([ - channel.assertExchange('lagoon-logs', 'direct', {durable: true}), - channel.assertQueue('lagoon-logs:email', {durable: true}), - channel.bindQueue('lagoon-logs:email', 'lagoon-logs', ''), - channel.prefetch(1), - channel.consume('lagoon-logs:email', msg => readFromRabbitMQ(msg, channelWrapperLogs), {noAck: false}), - ]); - } -}); diff --git a/services/logs2email/src/readFromRabbitMQ.ts b/services/logs2email/src/readFromRabbitMQ.ts deleted file mode 100644 index a3dbe3d266..0000000000 --- a/services/logs2email/src/readFromRabbitMQ.ts +++ /dev/null @@ -1,365 +0,0 @@ -import nodemailer from 'nodemailer'; -import { ChannelWrapper } from 'amqp-connection-manager'; -import { ConsumeMessage } from 'amqplib'; -import { logger } from '@lagoon/commons/dist/local-logging'; -import { getEmailInfoForProject } from '@lagoon/commons/dist/api'; -import { notificationIntToContentType, notificationContentTypeToInt, parseProblemNotification } from '@lagoon/commons/dist/notificationCommons'; - -let transporter = nodemailer.createTransport({ - sendmail: true, - newline: 'unix', - path: '/usr/sbin/ssmtp' -}); - -export async function readFromRabbitMQ (msg: ConsumeMessage, channelWrapperLogs: ChannelWrapper): Promise { - const logMessage = JSON.parse(msg.content.toString()) - - const { - severity, - project, - uuid, - event, - meta, - message - } = logMessage - - const appId = msg.properties.appId || "" - - logger.verbose(`received ${event} for project ${project}`) - - var messageMeta = { - subject: '', - mainHtml: '', - plainText: '', - additional: '', - color: '#E8E8E8', - emoji: '❔' - } - - switch (event) { - - case "github:pull_request:opened:handled": - case "gitlab:merge_request:opened:handled": - case "bitbucket:pullrequest:created:opened:handled": - messageMeta.color = '#E8E8E8' - messageMeta.emoji = '️ℹ️' - messageMeta.mainHtml = `PR #${meta.pullrequestNumber} (${meta.pullrequestTitle} opened in ${meta.repoName}` - messageMeta.plainText = `[${meta.projectName}] PR #${meta.pullrequestNumber} - ${meta.pullrequestTitle} opened in ${meta.repoName}` - messageMeta.subject = messageMeta.plainText - sendToEmail(project, messageMeta, channelWrapperLogs, msg, appId) - break; - - case "github:pull_request:synchronize:handled": - case "bitbucket:pullrequest:updated:opened:handled": - case "gitlab:merge_request:updated:handled": - messageMeta.color = '#E8E8E8' - messageMeta.emoji = '️ℹ️' - messageMeta.mainHtml = `PR #${meta.pullrequestNumber} (${meta.pullrequestTitle}) updated in ${meta.repoName}` - messageMeta.plainText = `[${meta.projectName}] PR [#${meta.pullrequestNumber} (${meta.pullrequestTitle})](${meta.pullrequestUrl}) updated in ${meta.repoName}` - sendToEmail(project, messageMeta, channelWrapperLogs, msg, appId) - break; - - case "bitbucket:pullrequest:fulfilled:handled": - case "bitbucket:pullrequest:rejected:handled": - case "github:pull_request:closed:handled": - case "gitlab:merge_request:closed:handled": - messageMeta.color = '#E8E8E8' - messageMeta.emoji = '️ℹ️' - messageMeta.mainHtml = `[${meta.projectName}] PR #${meta.pullrequestNumber} (${meta.pullrequestTitle}) closed in ${meta.repoName}` - messageMeta.plainText = `[${meta.projectName}] PR [#${meta.pullrequestNumber} (${meta.pullrequestTitle})](${meta.pullrequestUrl}) closed in ${meta.repoName}` - messageMeta.subject = messageMeta.plainText - sendToEmail(project, messageMeta, channelWrapperLogs, msg, appId) - break; - - case "rest:pullrequest:deploy": - messageMeta.color = '#E8E8E8' - messageMeta.emoji = '️ℹ️' - messageMeta.mainHtml = `REST pullrequest deploy trigger ${meta.pullrequestTitle}` - messageMeta.plainText = `[${meta.projectName}] REST pullrequest deploy trigger for PR : ${meta.pullrequestTitle}` - messageMeta.subject = messageMeta.plainText - sendToEmail(project, messageMeta, channelWrapperLogs, msg, appId) - break; - - case "github:delete:handled": - case "gitlab:remove:handled": - case "bitbucket:delete:handled": - messageMeta.color = '#E8E8E8' - messageMeta.emoji = '️ℹ️' - messageMeta.mainHtml = `Deleted environment ${meta.branchName}` - messageMeta.plainText = `[${meta.projectName}] deleted environment ${meta.branchName}` - messageMeta.subject = messageMeta.plainText - sendToEmail(project, messageMeta, channelWrapperLogs, msg, appId) - break; - - case "rest:remove:receive": - messageMeta.color = '#E8E8E8' - messageMeta.emoji = '️ℹ️' - messageMeta.mainHtml = `REST remove trigger ${meta.branchName}` - messageMeta.plainText = `[${meta.projectName}] REST remove trigger ${meta.branchName}` - messageMeta.subject = messageMeta.plainText - sendToEmail(project, messageMeta, channelWrapperLogs, msg, appId) - break; - - case "bitbucket:repo:push:handled": - case "github:push:handled": - case "gitlab:push:handled": - messageMeta.color = '#E8E8E8' - messageMeta.emoji = '️ℹ️' - messageMeta.mainHtml = `${meta.branchName}` - messageMeta.plainText = `[${meta.projectName}] ${meta.branchName}` - if (meta.shortSha){ - messageMeta.mainHtml = `${messageMeta.plainText} ${meta.shortSha}` - messageMeta.plainText = `${messageMeta.plainText} (${meta.shortSha})` - } - messageMeta.mainHtml = `${messageMeta.mainHtml} pushed in ${meta.repoFullName}` - messageMeta.plainText = `${messageMeta.plainText} pushed in ${meta.repoFullName}` - messageMeta.subject = messageMeta.plainText - sendToEmail(project, messageMeta, channelWrapperLogs, msg, appId) - break; - - case "gitlab:push:skipped": - case "github:push:skipped": - case "bitbucket:push:skipped": - messageMeta.color = '#E8E8E8' - messageMeta.emoji = '️ℹ️' - messageMeta.mainHtml = `${meta.branchName}` - messageMeta.plainText = `[${meta.projectName}] ${meta.branchName}` - if (meta.shortSha){ - messageMeta.mainHtml = `${messageMeta.plainText} ${meta.shortSha}` - messageMeta.plainText = `${messageMeta.plainText} (${meta.shortSha})` - } - messageMeta.mainHtml = `${messageMeta.plainText} pushed in ${meta.repoFullName} deployment skipped` - messageMeta.plainText = `${messageMeta.plainText} pushed in ${meta.repoFullName} *deployment skipped*` - messageMeta.subject = messageMeta.plainText - sendToEmail(project, messageMeta, channelWrapperLogs, msg, appId) - break; - - case "api:deployEnvironmentBranch": - case "api:deployEnvironmentLatest": - messageMeta.color = '#E8E8E8' - messageMeta.emoji = '️ℹ️' - messageMeta.mainHtml = `API deploy trigger ${meta.branchName}` - messageMeta.plainText = `[${meta.projectName}] API deploy trigger on branch: ${meta.branchName}` - if (meta.shortSha) { - messageMeta.mainHtml = `${messageMeta.mainHtml} (${meta.shortSha})` - messageMeta.plainText = `${messageMeta.plainText} (${meta.shortSha})` - } - messageMeta.subject = messageMeta.plainText - sendToEmail(project, messageMeta, channelWrapperLogs, msg, appId) - break; - - case "rest:deploy:receive": - messageMeta.color = '#E8E8E8' - messageMeta.emoji = '️ℹ️' - messageMeta.mainHtml = `REST deploy trigger on branch ${meta.branchName}` - messageMeta.plainText = `[${meta.projectName}] REST deploy trigger on branch: ${meta.branchName}` - if (meta.shortSha) { - messageMeta.mainHtml = `${messageMeta.mainHtml} (${meta.shortSha})` - messageMeta.plainText = `${messageMeta.plainText} (${meta.shortSha})` - } - messageMeta.subject = messageMeta.plainText - sendToEmail(project, messageMeta, channelWrapperLogs, msg, appId) - break; - - case "rest:promote:receive": - messageMeta.color = '#E8E8E8' - messageMeta.emoji = '⚠️' - messageMeta.mainHtml = `REST promote trigger : Branch ${meta.branchName} -> ${meta.promoteSourceEnvironment}` - messageMeta.plainText = `[${meta.projectName}] REST promote trigger : ${meta.branchName} -> ${meta.promoteSourceEnvironment}` - messageMeta.subject = messageMeta.plainText - sendToEmail(project, messageMeta, channelWrapperLogs, msg, appId) - break; - - case "task:deploy-openshift:finished": - case "task:remove-openshift-resources:finished": - case "task:builddeploy-openshift:complete": - messageMeta.color = 'lawngreen' - messageMeta.emoji = '✅' - messageMeta.plainText = `[${meta.projectName}] ` - if (meta.shortSha) { - messageMeta.mainHtml = `${messageMeta.plainText} ${meta.branchName} (${meta.shortSha})` - messageMeta.plainText = `${messageMeta.plainText} ${meta.branchName} (${meta.shortSha})` - } else { - messageMeta.mainHtml = `${messageMeta.plainText} ${meta.branchName}` - messageMeta.plainText = `${messageMeta.plainText} ${meta.branchName}` - } - messageMeta.mainHtml = `${messageMeta.plainText} Build ${meta.buildName} complete.` - messageMeta.plainText = `${messageMeta.plainText} Build ${meta.buildName} complete.` - messageMeta.subject = messageMeta.plainText - if (meta.logLink){ - messageMeta.mainHtml = `${messageMeta.plainText} Logs` - messageMeta.plainText = `${messageMeta.plainText} [Logs](${meta.logLink})\n` - } - messageMeta.plainText = `\n${messageMeta.plainText}${meta.route}\n ${meta.routes.join("\n")}` - messageMeta.additional = `` - sendToEmail(project, messageMeta, channelWrapperLogs, msg, appId) - break; - - case "rest:pullrequest:remove": - messageMeta.color = 'lawngreen' - messageMeta.emoji = '✅' - messageMeta.mainHtml = `REST pullrequest remove trigger ${meta.pullrequestNumber}` - messageMeta.plainText = `[${meta.projectName}] REST pullrequest remove trigger ${meta.pullrequestNumber}` - messageMeta.subject = messageMeta.plainText - sendToEmail(project, messageMeta, channelWrapperLogs, msg, appId) - break; - - case "task:remove-openshift:finished": - case "task:remove-kubernetes:finished": - messageMeta.color = 'lawngreen' - messageMeta.emoji = '✅' - messageMeta.mainHtml = `Remove ${meta.openshiftProject}` - messageMeta.plainText = `[${meta.projectName}] remove ${meta.openshiftProject}` - messageMeta.subject = messageMeta.plainText - sendToEmail(project, messageMeta, channelWrapperLogs, msg, appId) - break; - - case "task:deploy-openshift:retry": - case "task:remove-openshift:retry": - case "task:remove-kubernetes:retry": - case "task:remove-openshift-resources:retry": - messageMeta.color = 'gold' - messageMeta.emoji = '⚠️' - messageMeta.plainText = `[${meta.projectName}]` - if (meta.shortSha) { - messageMeta.mainHtml = `${meta.branchName} (${meta.shortSha})` - messageMeta.plainText = `${messageMeta.plainText} ${meta.branchName} (${meta.shortSha})` - } else { - messageMeta.mainHtml = `${messageMeta.mainHtml} ${meta.branchName}` - messageMeta.plainText = `${messageMeta.plainText} ${meta.branchName}` - } - messageMeta.mainHtml = `${messageMeta.mainHtml} Build ${meta.buildName} retried.` - messageMeta.plainText = `${messageMeta.plainText} Build ${meta.buildName} retried.` - messageMeta.subject = messageMeta.plainText - if (meta.logLink){ - messageMeta.mainHtml = `${messageMeta.mainHtml} ${meta.logLink}` - messageMeta.plainText = `${messageMeta.plainText} ${meta.logLink}` - } - sendToEmail(project, messageMeta, channelWrapperLogs, msg, appId) - break; - - case "task:deploy-openshift:error": - case "task:remove-openshift:error": - case "task:remove-kubernetes:error": - case "task:remove-openshift-resources:error": - case "task:builddeploy-openshift:failed": - messageMeta.color = 'red' - messageMeta.emoji = '‼️' - messageMeta.plainText = `[${meta.projectName}]` - if (meta.shortSha) { - messageMeta.mainHtml = `${meta.branchName} (${meta.shortSha})` - messageMeta.plainText = `${messageMeta.plainText} ${meta.branchName} (${meta.shortSha})` - } else { - messageMeta.mainHtml = `${messageMeta.mainHtml} ${meta.branchName}` - messageMeta.plainText = `${messageMeta.plainText} ${meta.branchName}` - } - messageMeta.mainHtml = `${messageMeta.mainHtml} Build ${meta.buildName} error.` - messageMeta.plainText = `${messageMeta.plainText} Build ${meta.buildName} error.` - messageMeta.subject = messageMeta.plainText - if (meta.logLink){ - messageMeta.mainHtml = `${messageMeta.mainHtml} ${meta.logLink}` - messageMeta.plainText = `${messageMeta.plainText} ${meta.logLink}` - } - sendToEmail(project, messageMeta, channelWrapperLogs, msg, appId) - break; - - case "github:pull_request:closed:CannotDeleteProductionEnvironment": - case "github:push:CannotDeleteProductionEnvironment": - case "bitbucket:repo:push:CannotDeleteProductionEnvironment": - case "gitlab:push:CannotDeleteProductionEnvironment": - case "rest:remove:CannotDeleteProductionEnvironment": - messageMeta.color = 'gold' - messageMeta.emoji = '⚠️' - messageMeta.mainHtml = `${meta.branchName} not deleted. ${meta.error}` - messageMeta.plainText = `[${meta.name}] ${meta.branchName} not deleted.` - messageMeta.subject = messageMeta.plainText - messageMeta.plainText = `${messageMeta.plainText} ${meta.error}` - sendToEmail(project, messageMeta, channelWrapperLogs, msg, appId) - break; - - default: - //since there's no single point of acknowlegement of the msg, we need to keep track of whether we've handled the message - let eventHandledAsProblem = dispatchProblemEventToEmail(event, project, message, messageMeta, channelWrapperLogs, msg, appId); - if(!eventHandledAsProblem) { - return channelWrapperLogs.ack(msg); - } - } -} - -const dispatchProblemEventToEmail = (event, project, message, messageMeta, channelWrapperLogs, msg, appId) => { - const problemEvent = parseProblemNotification(event); - if(problemEvent.isProblem && problemEvent.eventType == 'insert') { - messageMeta.color = 'gold' - messageMeta.emoji = '⚠️' - messageMeta.mainHtml = message - messageMeta.plainText = message - messageMeta.subject = `New Problem of severity ${problemEvent.severityLevel} detected on ${project}` - sendToEmail(project, messageMeta, channelWrapperLogs, msg, appId, 'PROBLEM', problemEvent.severityLevel) - return true; - } - return false; -}; - -const sendToEmail = async (project, messageMeta, channelWrapperLogs, msg, appId, contentType = 'DEPLOYMENT', severityLevel = 'NONE') => { - let projectEmails; - try { - projectEmails = await getEmailInfoForProject(project, contentType) - } - catch (error) { - logger.error(`No Email information found, error: ${error}`) - return channelWrapperLogs.ack(msg) - } - - projectEmails.forEach(projectEmail => { - const { emailAddress } = projectEmail; - - const notificationThresholdMet = notificationContentTypeToInt(projectEmail.notificationSeverityThreshold) <= notificationContentTypeToInt(severityLevel); - if(contentType == 'PROBLEM' && !notificationThresholdMet) { return; } //go to next iteration - - let info = transporter.sendMail({ - from: 'lagoon@amazee.io', - to: emailAddress, - subject: messageMeta.subject, - text: messageMeta.plainText, - html: ` - - - - Test Email Sample - - - - - - -
-

${messageMeta.emoji} [${project}]

-

- ${messageMeta.mainHtml} -

-
-
-

- ${messageMeta.additional} -

-
- -` - }); - }); - channelWrapperLogs.ack(msg) - return -} diff --git a/services/logs2email/ssmtp.conf b/services/logs2email/ssmtp.conf deleted file mode 100644 index 1659e71b49..0000000000 --- a/services/logs2email/ssmtp.conf +++ /dev/null @@ -1,4 +0,0 @@ -# ssmtp config (will be filled during entrypoint 50-ssmtp.sh) - -# Email 'From header's can override the default domain -FromLineOverride=yes diff --git a/services/logs2email/tsconfig.json b/services/logs2email/tsconfig.json deleted file mode 100644 index 6ce2574ab8..0000000000 --- a/services/logs2email/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "outDir": "./dist" - }, - "include": ["./src"], - "references": [ - { "path": "../../node-packages/commons" } - ] -} diff --git a/services/logs2microsoftteams/.babelrc b/services/logs2microsoftteams/.babelrc deleted file mode 100644 index f2314e7303..0000000000 --- a/services/logs2microsoftteams/.babelrc +++ /dev/null @@ -1,8 +0,0 @@ -{ - "presets": [ - "node8-es6" - ], - "plugins": [ - "transform-es2015-modules-commonjs" - ] -} diff --git a/services/logs2microsoftteams/.gitignore b/services/logs2microsoftteams/.gitignore deleted file mode 100644 index f06235c460..0000000000 --- a/services/logs2microsoftteams/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -node_modules -dist diff --git a/services/logs2microsoftteams/Dockerfile b/services/logs2microsoftteams/Dockerfile deleted file mode 100644 index e9cb4af2c3..0000000000 --- a/services/logs2microsoftteams/Dockerfile +++ /dev/null @@ -1,34 +0,0 @@ -ARG LAGOON_GIT_BRANCH -ARG IMAGE_REPO -ARG UPSTREAM_REPO -ARG UPSTREAM_TAG -# STAGE 1: Loading Image lagoon-node-packages-builder which contains node packages shared by all Node Services -FROM ${IMAGE_REPO:-lagoon}/yarn-workspace-builder as yarn-workspace-builder - -# STAGE 2: specific service Image -FROM ${UPSTREAM_REPO:-uselagoon}/node-16:${UPSTREAM_TAG:-latest} - -ARG LAGOON_VERSION -ENV LAGOON_VERSION=$LAGOON_VERSION - -# Copying generated node_modules from the first stage -COPY --from=yarn-workspace-builder /app /app - -# Setting the workdir to the service, all following commands will run from here -WORKDIR /app/services/logs2microsoftteams/ - -# Copying the .env.defaults into the Workdir, as the dotenv system searches within the workdir for it -COPY --from=yarn-workspace-builder /app/.env.defaults . - -# Copying files from our service -COPY . . - -# Verify that all dependencies have been installed via the yarn-workspace-builder -RUN yarn check --verify-tree - -# Making sure we run in production -ENV NODE_ENV production - -RUN yarn build - -CMD ["yarn", "start"] diff --git a/services/logs2microsoftteams/README.md b/services/logs2microsoftteams/README.md deleted file mode 100644 index 2bdd0517c4..0000000000 --- a/services/logs2microsoftteams/README.md +++ /dev/null @@ -1,34 +0,0 @@ -

- -This service is part of amazee.io Lagoon, a Docker build and deploy system for -OpenShift & Kubernetes. Please reference our [documentation] for detailed -information on using, developing, and administering Lagoon. - -# Logs to Microsoft Teams (`logs2microsoftteams`) - -Watches all the Lagoon logs and checks for events that should trigger a -Microsoft Teams message. Each log message is tied to a Lagoon project, and -channel configuration for that project is retrieved from the Lagoon API. - -Examples of events that might trigger a message: GitHub pull request opened, a -new build for a Lagoon project environment has started, a task was completed. - -## Technology - -* Node.js -* Message Queue - -## Related Services - -* API [***dependency***] -* RabbitMQ [***dependency***] - -## Message Queues - -* Consumes: `lagoon-logs`, `lagoon-logs:microsoftTeams` -* Produces: `lagoon-logs:microsoftTeams` - -[documentation]: https://docs.lagoon.sh/ diff --git a/services/logs2microsoftteams/package.json b/services/logs2microsoftteams/package.json deleted file mode 100644 index 83fb4db890..0000000000 --- a/services/logs2microsoftteams/package.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "name": "logs2microsoftTeams", - "version": "0.0.1", - "description": "lagoon handler for logging message to Microsoft Teams", - "author": "amazee.io (http://www.amazee.io)", - "main": "dist/index.js", - "scripts": { - "build": "tsc --build", - "start": "node dist/index", - "dev": "mkdir -p ../../node-packages/commons/dist && NODE_ENV=development nodemon" - }, - "nodemonConfig": { - "ignore": [ - "../../node-packages/commons/dist/" - ], - "watch": [ - "src", - "../../node-packages/" - ], - "ext": "js,ts,json", - "exec": "yarn build --incremental && yarn start --inspect=0.0.0.0:9229" - }, - "license": "MIT", - "dependencies": { - "@lagoon/commons": "4.0.0", - "amqp-connection-manager": "^1.3.5" - }, - "devDependencies": { - "@types/amqp-connection-manager": "^2.0.10", - "nodemon": "^1.12.1", - "typescript": "^3.9.3" - } -} diff --git a/services/logs2microsoftteams/src/index.ts b/services/logs2microsoftteams/src/index.ts deleted file mode 100644 index b4f6dc52cf..0000000000 --- a/services/logs2microsoftteams/src/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -import amqp, { ChannelWrapper } from 'amqp-connection-manager'; -import { logger } from '@lagoon/commons/dist/local-logging'; -import { readFromRabbitMQ } from './readFromRabbitMQ'; - -const rabbitmqHost = process.env.RABBITMQ_HOST || "broker" -const rabbitmqUsername = process.env.RABBITMQ_USERNAME || "guest" -const rabbitmqPassword = process.env.RABBITMQ_PASSWORD || "guest" -// @ts-ignore -const connection = amqp.connect([`amqp://${rabbitmqUsername}:${rabbitmqPassword}@${rabbitmqHost}`], { json: true }); - -connection.on('connect', ({ url }) => logger.verbose('Connected to %s', url, { action: 'connected', url })); -// @ts-ignore -connection.on('disconnect', params => logger.error('Not connected, error: %s', params.err.code, { action: 'disconnected', reason: params })); - -// Cast any to ChannelWrapper to get type-safetiness through our own code -const channelWrapperLogs: ChannelWrapper = connection.createChannel({ - setup: channel => { - return Promise.all([ - channel.assertExchange('lagoon-logs', 'direct', {durable: true}), - channel.assertQueue('lagoon-logs:microsoftTeams', {durable: true}), - channel.bindQueue('lagoon-logs:microsoftTeams', 'lagoon-logs', ''), - channel.prefetch(1), - channel.consume('lagoon-logs:microsoftTeams', msg => readFromRabbitMQ(msg, channelWrapperLogs), {noAck: false}), - ]); - } -}); diff --git a/services/logs2microsoftteams/src/readFromRabbitMQ.ts b/services/logs2microsoftteams/src/readFromRabbitMQ.ts deleted file mode 100644 index f8c328769b..0000000000 --- a/services/logs2microsoftteams/src/readFromRabbitMQ.ts +++ /dev/null @@ -1,257 +0,0 @@ -import { URL } from 'url'; -import http from 'https'; -import { ChannelWrapper } from 'amqp-connection-manager'; -import { ConsumeMessage } from 'amqplib'; -import { logger } from '@lagoon/commons/dist/local-logging'; -import { getMicrosoftTeamsInfoForProject } from '@lagoon/commons/dist/api'; -import { notificationIntToContentType, notificationContentTypeToInt, parseProblemNotification } from '@lagoon/commons/dist/notificationCommons'; - -export async function readFromRabbitMQ (msg: ConsumeMessage, channelWrapperLogs: ChannelWrapper): Promise { - const logMessage = JSON.parse(msg.content.toString()) - - const { - severity, - project, - uuid, - event, - meta, - message - } = logMessage - - const appId = msg.properties.appId || "" - - logger.verbose(`received ${event} for project ${project}`) - - const whiteCheckMark = 'https://statics.teams.microsoft.com/evergreen-assets/emojioneassets/assets-png/2705.png' - const informationSource = 'https://statics.teams.microsoft.com/evergreen-assets/emojioneassets/assets-png/2139.png' - const bangBang = 'https://statics.teams.microsoft.com/evergreen-assets/emojioneassets/assets-png/203c.png' - const warning = 'https://statics.teams.microsoft.com/evergreen-assets/emojioneassets/assets-png/26a0.png' - - var text = '' - - switch (event) { - - case "github:pull_request:opened:handled": - case "gitlab:merge_request:opened:handled": - case "bitbucket:pullrequest:created:opened:handled": - text = `PR [#${meta.pullrequestNumber} (${meta.pullrequestTitle})](${meta.pullrequestUrl}) opened in [${meta.repoName}](${meta.repoUrl})` - sendToMicrosoftTeams(project, text, '#E8E8E8', informationSource, channelWrapperLogs, msg, appId) - break; - - case "github:pull_request:synchronize:handled": - case "bitbucket:pullrequest:updated:opened:handled": - case "gitlab:merge_request:updated:handled": - text = `PR [#${meta.pullrequestNumber} (${meta.pullrequestTitle})](${meta.pullrequestUrl}) updated in [${meta.repoName}](${meta.repoUrl})` - sendToMicrosoftTeams(project, text, '#E8E8E8', informationSource, channelWrapperLogs, msg, appId) - break; - - case "bitbucket:pullrequest:fulfilled:handled": - case "bitbucket:pullrequest:rejected:handled": - case "github:pull_request:closed:handled": - case "gitlab:merge_request:closed:handled": - text = `PR [#${meta.pullrequestNumber} (${meta.pullrequestTitle})](${meta.pullrequestUrl}) closed in [${meta.repoName}](${meta.repoUrl})` - sendToMicrosoftTeams(project, text, '#E8E8E8', informationSource, channelWrapperLogs, msg, appId) - break; - - case "rest:pullrequest:deploy": - text = `REST pullrequest deploy trigger \`${meta.pullrequestTitle}\`` - sendToMicrosoftTeams(project, text, '#E8E8E8', informationSource, channelWrapperLogs, msg, appId) - break; - - case "github:delete:handled": - case "gitlab:remove:handled": - case "bitbucket:delete:handled": - text = `deleted in \`${meta.branchName}\`` - sendToMicrosoftTeams(project, text, '#E8E8E8', informationSource, channelWrapperLogs, msg, appId) - break; - - case "rest:remove:receive": - text = `REST remove trigger \`${meta.branchName}\`` - sendToMicrosoftTeams(project, text, '#E8E8E8', informationSource, channelWrapperLogs, msg, appId) - break; - - case "bitbucket:repo:push:handled": - case "github:push:handled": - case "gitlab:push:handled": - text = `[${meta.branchName}](${meta.repoUrl}/tree/${meta.branchName})` - if (meta.shortSha){ - text = `${text} ([${meta.shortSha}](${meta.commitUrl}))` - } - text = `${text} pushed in [${meta.repoFullName}](${meta.repoUrl})` - sendToMicrosoftTeams(project, text, '#E8E8E8', informationSource, channelWrapperLogs, msg, appId) - break; - - case "gitlab:push:skipped": - case "github:push:skipped": - case "bitbucket:push:skipped": - text = `[${meta.branchName}](${meta.repoUrl}/tree/${meta.branchName})` - if (meta.shortSha){ - text = `${text} ([${meta.shortSha}](${meta.commitUrl}))` - } - text = `${text} pushed in [${meta.repoFullName}](${meta.repoUrl}) *deployment skipped*` - sendToMicrosoftTeams(project, text, '#E8E8E8', informationSource, channelWrapperLogs, msg, appId) - break; - - case "api:deployEnvironmentBranch": - case "api:deployEnvironmentLatest": - text = `API deploy trigger \`${meta.branchName}\`` - if (meta.shortSha) { - text = `${text} (${meta.shortSha})` - } - sendToMicrosoftTeams(project, text, '#E8E8E8', informationSource, channelWrapperLogs, msg, appId) - break; - - case "rest:deploy:receive": - text = `REST deploy trigger \`${meta.branchName}\`` - if (meta.shortSha) { - text = `${text} (${meta.shortSha})` - } - sendToMicrosoftTeams(project, text, '#E8E8E8', informationSource, channelWrapperLogs, msg, appId) - break; - - case "rest:promote:receive": - text = `REST promote trigger \`${meta.branchName}\` -> \`${meta.promoteSourceEnvironment}\`` - sendToMicrosoftTeams(project, text, 'gold', warning, channelWrapperLogs, msg, appId) - break; - - case "task:deploy-openshift:finished": - case "task:remove-openshift-resources:finished": - case "task:builddeploy-openshift:complete": - if (meta.shortSha) { - text = `${text} \`${meta.branchName}\` (${meta.shortSha})` - } else { - text = `${text} \`${meta.branchName}\`` - } - text = `${text} Build \`${meta.buildName}\` complete.

` - if (meta.logLink){ - text = `${text} [Logs](${meta.logLink})

` - } - text = `${text}${meta.route}

${meta.routes.join("

")}` - sendToMicrosoftTeams(project, text, 'lawngreen', whiteCheckMark, channelWrapperLogs, msg, appId) - break; - - case "rest:pullrequest:remove": - text = `REST pullrequest remove trigger \`${meta.pullrequestNumber}\`` - sendToMicrosoftTeams(project, text, 'lawngreen', whiteCheckMark, channelWrapperLogs, msg, appId) - break; - - case "task:remove-openshift:finished": - case "task:remove-kubernetes:finished": - text = `remove \`${meta.openshiftProject}\`` - sendToMicrosoftTeams(project, text, 'lawngreen', whiteCheckMark, channelWrapperLogs, msg, appId) - break; - - case "task:deploy-openshift:retry": - case "task:remove-openshift:retry": - case "task:remove-kubernetes:retry": - case "task:remove-openshift-resources:retry": - if (meta.shortSha) { - text = `${text} \`${meta.branchName}\` (${meta.shortSha})` - } else { - text = `${text} \`${meta.branchName}\`` - } - text = `${text} Build \`${meta.buildName}\` failed.` - if (meta.logLink){ - text = `${text} ${meta.logLink}` - } - sendToMicrosoftTeams(project, message, 'gold', warning, channelWrapperLogs, msg, appId) - break; - - case "task:deploy-openshift:error": - case "task:remove-openshift:error": - case "task:remove-kubernetes:error": - case "task:remove-openshift-resources:error": - case "task:builddeploy-openshift:failed": - if (meta.shortSha) { - text = `${text} \`${meta.branchName}\` (${meta.shortSha})` - } else { - text = `${text} \`${meta.branchName}\`` - } - text = `${text} Build \`${meta.buildName}\` failed.` - if (meta.logLink){ - text = `${text} ${meta.logLink}` - } - sendToMicrosoftTeams(project, text, 'red', bangBang, channelWrapperLogs, msg, appId) - break; - - case "github:pull_request:closed:CannotDeleteProductionEnvironment": - case "github:push:CannotDeleteProductionEnvironment": - case "bitbucket:repo:push:CannotDeleteProductionEnvironment": - case "gitlab:push:CannotDeleteProductionEnvironment": - case "rest:remove:CannotDeleteProductionEnvironment": - text = `*[${meta.name}]* \`${meta.branchName}\` not deleted. ${meta.error}` - sendToMicrosoftTeams(project, message, 'gold', warning, channelWrapperLogs, msg, appId) - break; - default: - //since there's no single point of acknowlegement of the msg, we need to keep track of whether we've handled the message - let eventHandledAsProblem = dispatchProblemEventToTeams(event, project, message, channelWrapperLogs, msg, appId, bangBang); - if(!eventHandledAsProblem) { - return channelWrapperLogs.ack(msg); - } - } -} - -const dispatchProblemEventToTeams = (event, project, message, channelWrapperLogs, msg, appId, errorEmoji) => { - const problemEvent = parseProblemNotification(event); - if(problemEvent.isProblem && problemEvent.eventType == 'insert') { - sendToMicrosoftTeams(project, message, 'red', errorEmoji, channelWrapperLogs, msg, appId, 'PROBLEM', problemEvent.severityLevel) - return true; - } - return false; -}; - -const sendToMicrosoftTeams = async (project, message, color, emoji, channelWrapperLogs, msg, appId, contentType = 'DEPLOYMENT', severityLevel = 'NONE') => { - let projectMicrosoftTeamsNotifications; - try { - projectMicrosoftTeamsNotifications = await getMicrosoftTeamsInfoForProject(project, contentType) - } - catch (error) { - logger.error(`No Microsoft Teams information found, error: ${error}`) - return channelWrapperLogs.ack(msg) - } - - projectMicrosoftTeamsNotifications.forEach(projectMicrosoftTeams => { - const notificationThresholdMet = notificationContentTypeToInt(projectMicrosoftTeams.notificationSeverityThreshold) <= notificationContentTypeToInt(severityLevel); - if(contentType == 'PROBLEM' && !notificationThresholdMet) { return; } //go to next iteration - - const { webhook } = projectMicrosoftTeams; - const webhookUrl = new URL(webhook); - - var data = JSON.stringify( - { - "@type": "MessageCard", - "@context": "http://schema.org/extensions", - "summary": message, - "title": project, - "themeColor": color, - "sections": [ - { - "activityText": message, - "activityImage": emoji - } - ] - } - ); - - var options = { - hostname: webhookUrl.host, - port: webhookUrl.port, - path: webhookUrl.pathname, - method: 'POST', - headers: { - 'Content-Type': 'application/json', - } - }; - - var req = http.request(options, function(res) { - res.setEncoding('utf8'); - }); - - req.on('error', function(e) { - logger.error(`problem with request: ${e.message}`); - }); - req.end(data); - }); - channelWrapperLogs.ack(msg) - return -} diff --git a/services/logs2microsoftteams/tsconfig.json b/services/logs2microsoftteams/tsconfig.json deleted file mode 100644 index 6ce2574ab8..0000000000 --- a/services/logs2microsoftteams/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "outDir": "./dist" - }, - "include": ["./src"], - "references": [ - { "path": "../../node-packages/commons" } - ] -} diff --git a/services/logs2rocketchat/.babelrc b/services/logs2rocketchat/.babelrc deleted file mode 100644 index 5eba7fc969..0000000000 --- a/services/logs2rocketchat/.babelrc +++ /dev/null @@ -1,8 +0,0 @@ -{ - "presets": [ - "presets": [["latest-node", { "target": "10" }]] - ], - "plugins": [ - "transform-es2015-modules-commonjs" - ] -} diff --git a/services/logs2rocketchat/.gitignore b/services/logs2rocketchat/.gitignore deleted file mode 100644 index f06235c460..0000000000 --- a/services/logs2rocketchat/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -node_modules -dist diff --git a/services/logs2rocketchat/Dockerfile b/services/logs2rocketchat/Dockerfile deleted file mode 100644 index 6251f1df82..0000000000 --- a/services/logs2rocketchat/Dockerfile +++ /dev/null @@ -1,34 +0,0 @@ -ARG LAGOON_GIT_BRANCH -ARG IMAGE_REPO -ARG UPSTREAM_REPO -ARG UPSTREAM_TAG -# STAGE 1: Loading Image lagoon-node-packages-builder which contains node packages shared by all Node Services -FROM ${IMAGE_REPO:-lagoon}/yarn-workspace-builder as yarn-workspace-builder - -# STAGE 2: specific service Image -FROM ${UPSTREAM_REPO:-uselagoon}/node-16:${UPSTREAM_TAG:-latest} - -ARG LAGOON_VERSION -ENV LAGOON_VERSION=$LAGOON_VERSION - -# Copying generated node_modules from the first stage -COPY --from=yarn-workspace-builder /app /app - -# Setting the workdir to the service, all following commands will run from here -WORKDIR /app/services/logs2rocketchat/ - -# Copying the .env.defaults into the Workdir, as the dotenv system searches within the workdir for it -COPY --from=yarn-workspace-builder /app/.env.defaults . - -# Copying files from our service -COPY . . - -# Verify that all dependencies have been installed via the yarn-workspace-builder -RUN yarn check --verify-tree - -# Making sure we run in production -ENV NODE_ENV production - -RUN yarn build - -CMD ["yarn", "start"] diff --git a/services/logs2rocketchat/README.md b/services/logs2rocketchat/README.md deleted file mode 100644 index 59358bccfa..0000000000 --- a/services/logs2rocketchat/README.md +++ /dev/null @@ -1,34 +0,0 @@ -

- -This service is part of amazee.io Lagoon, a Docker build and deploy system for -OpenShift & Kubernetes. Please reference our [documentation] for detailed -information on using, developing, and administering Lagoon. - -# Logs to Rocket.Chat (`logs2rocketchat`) - -Watches all the Lagoon logs and checks for events that should trigger a -Rocket.Chat message. Each log message is tied to a Lagoon project, and -channel configuration for that project is retrieved from the Lagoon API. - -Examples of events that might trigger a message: GitHub pull request opened, a -new build for a Lagoon project environent has started, a task was completed. - -## Technology - -* Node.js -* Message Queue - -## Related Services - -* API [***dependency***] -* RabbitMQ [***dependency***] - -## Message Queues - -* Consumes: `lagoon-logs`, `lagoon-logs:rocketchat` -* Produces: `lagoon-logs:rocketchat` - -[documentation]: https://docs.lagoon.sh/ diff --git a/services/logs2rocketchat/package.json b/services/logs2rocketchat/package.json deleted file mode 100644 index 39b3243684..0000000000 --- a/services/logs2rocketchat/package.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "name": "logs2rocketchat", - "version": "0.0.1", - "description": "lagoon handler for webhooks", - "author": "amazee.io (http://www.amazee.io)", - "main": "dist/index.js", - "scripts": { - "build": "tsc --build", - "start": "node dist/index", - "dev": "mkdir -p ../../node-packages/commons/dist && NODE_ENV=development nodemon" - }, - "nodemonConfig": { - "ignore": [ - "../../node-packages/commons/dist/" - ], - "watch": [ - "src", - "../../node-packages/" - ], - "ext": "js,ts,json", - "exec": "yarn build --incremental && yarn start --inspect=0.0.0.0:9229" - }, - "license": "MIT", - "dependencies": { - "@lagoon/commons": "4.0.0", - "amqp-connection-manager": "^1.3.5" - }, - "devDependencies": { - "@types/amqp-connection-manager": "^2.0.10", - "nodemon": "^1.12.1", - "typescript": "^3.9.3" - } -} diff --git a/services/logs2rocketchat/src/index.ts b/services/logs2rocketchat/src/index.ts deleted file mode 100644 index 2feaa363fd..0000000000 --- a/services/logs2rocketchat/src/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -import amqp, { ChannelWrapper } from 'amqp-connection-manager'; -import { logger } from '@lagoon/commons/dist/local-logging'; -import { readFromRabbitMQ } from './readFromRabbitMQ'; - -const rabbitmqHost = process.env.RABBITMQ_HOST || "broker" -const rabbitmqUsername = process.env.RABBITMQ_USERNAME || "guest" -const rabbitmqPassword = process.env.RABBITMQ_PASSWORD || "guest" -// @ts-ignore -const connection = amqp.connect([`amqp://${rabbitmqUsername}:${rabbitmqPassword}@${rabbitmqHost}`], { json: true }); - -connection.on('connect', ({ url }) => logger.verbose('Connected to %s', url, { action: 'connected', url })); -// @ts-ignore -connection.on('disconnect', params => logger.error('Not connected, error: %s', params.err.code, { action: 'disconnected', reason: params })); - -// Cast any to ChannelWrapper to get type-safetiness through our own code -const channelWrapperLogs: ChannelWrapper = connection.createChannel({ - setup: channel => { - return Promise.all([ - channel.assertExchange('lagoon-logs', 'direct', {durable: true}), - channel.assertQueue('lagoon-logs:rocketchat', {durable: true}), - channel.bindQueue('lagoon-logs:rocketchat', 'lagoon-logs', ''), - channel.prefetch(1), - channel.consume('lagoon-logs:rocketchat', msg => readFromRabbitMQ(msg, channelWrapperLogs), {noAck: false}), - ]); - } -}); diff --git a/services/logs2rocketchat/src/readFromRabbitMQ.ts b/services/logs2rocketchat/src/readFromRabbitMQ.ts deleted file mode 100644 index 22972daee0..0000000000 --- a/services/logs2rocketchat/src/readFromRabbitMQ.ts +++ /dev/null @@ -1,264 +0,0 @@ -import { URL } from 'url'; -import http from 'https'; -import { ChannelWrapper } from 'amqp-connection-manager'; -import { ConsumeMessage } from 'amqplib'; -import { logger } from '@lagoon/commons/dist/local-logging'; -import { getRocketChatInfoForProject } from '@lagoon/commons/dist/api'; -import { notificationIntToContentType, notificationContentTypeToInt, parseProblemNotification } from '@lagoon/commons/dist/notificationCommons'; - -export async function readFromRabbitMQ (msg: ConsumeMessage, channelWrapperLogs: ChannelWrapper): Promise { - const logMessage = JSON.parse(msg.content.toString()) - - const { - severity, - project, - uuid, - event, - meta, - message - } = logMessage - - const appId = msg.properties.appId || "" - - logger.verbose(`received ${event} for project ${project}`) - - var text - - switch (event) { - - case "github:pull_request:opened:handled": - case "gitlab:merge_request:opened:handled": - case "bitbucket:pullrequest:created:opened:handled": - text = `*[${meta.projectName}]* PR [#${meta.pullrequestNumber} (${meta.pullrequestTitle})](${meta.pullrequestUrl}) opened in [${meta.repoName}](${meta.repoUrl})` - sendToRocketChat(project, text, '#E8E8E8', ':information_source:', channelWrapperLogs, msg, appId) - break; - - case "github:pull_request:synchronize:handled": - case "bitbucket:pullrequest:updated:opened:handled": - case "gitlab:merge_request:updated:handled": - text = `*[${meta.projectName}]* PR [#${meta.pullrequestNumber} (${meta.pullrequestTitle})](${meta.pullrequestUrl}) updated in [${meta.repoName}](${meta.repoUrl})` - sendToRocketChat(project, text, '#E8E8E8', ':information_source:', channelWrapperLogs, msg, appId) - break; - - case "bitbucket:pullrequest:fulfilled:handled": - case "bitbucket:pullrequest:rejected:handled": - case "github:pull_request:closed:handled": - case "gitlab:merge_request:closed:handled": - text = `*[${meta.projectName}]* PR [#${meta.pullrequestNumber} (${meta.pullrequestTitle})](${meta.pullrequestUrl}) closed in [${meta.repoName}](${meta.repoUrl})` - sendToRocketChat(project, text, '#E8E8E8', ':information_source:', channelWrapperLogs, msg, appId) - break; - - case "rest:pullrequest:deploy": - text = `*[${meta.projectName}]* REST pullrequest deploy trigger \`${meta.pullrequestTitle}\`` - sendToRocketChat(project, text, '#E8E8E8', ':information_source:', channelWrapperLogs, msg, appId) - break; - - case "github:delete:handled": - case "gitlab:remove:handled": - case "bitbucket:delete:handled": - case "api:deleteEnvironment": - text = `*[${meta.projectName}]* delete trigger \`${meta.environmentName}\`` - sendToRocketChat(project, text, '#E8E8E8', ':information_source:', channelWrapperLogs, msg, appId) - break; - - case "rest:remove:receive": - text = `*[${meta.projectName}]* REST remove trigger \`${meta.branchName}\`` - sendToRocketChat(project, text, '#E8E8E8', ':information_source:', channelWrapperLogs, msg, appId) - break; - - case "bitbucket:repo:push:handled": - case "github:push:handled": - case "gitlab:push:handled": - text = `*[${meta.projectName}]* [${meta.branchName}](${meta.repoUrl}/tree/${meta.branchName})` - if (meta.shortSha){ - text = `${text} ([${meta.shortSha}](${meta.commitUrl}))` - } - text = `${text} pushed in [${meta.repoFullName}](${meta.repoUrl})` - sendToRocketChat(project, text, '#E8E8E8', ':information_source:', channelWrapperLogs, msg, appId) - break; - - case "gitlab:push:skipped": - case "github:push:skipped": - case "bitbucket:push:skipped": - text = `*[${meta.projectName}]* [${meta.branchName}](${meta.repoUrl}/tree/${meta.branchName})` - if (meta.shortSha){ - text = `${text} ([${meta.shortSha}](${meta.commitUrl}))` - } - text = `${text} pushed in [${meta.repoFullName}](${meta.repoUrl}) *deployment skipped*` - sendToRocketChat(project, text, '#E8E8E8', ':information_source:', channelWrapperLogs, msg, appId) - break; - - case "api:deployEnvironmentLatest": - case "api:deployEnvironmentBranch": - text = `*[${meta.projectName}]* API deploy trigger \`${meta.branchName}\`` - if (meta.shortSha) { - text = `${text} (${meta.shortSha})` - } - sendToRocketChat(project, text, '#E8E8E8', ':information_source:', channelWrapperLogs, msg, appId) - break; - - case "rest:deploy:receive": - text = `*[${meta.projectName}]* REST deploy trigger \`${meta.branchName}\`` - if (meta.shortSha) { - text = `${text} (${meta.shortSha})` - } - sendToRocketChat(project, text, '#E8E8E8', ':information_source:', channelWrapperLogs, msg, appId) - break; - - case "rest:promote:receive": - text = `*[${meta.projectName}]* REST promote trigger \`${meta.branchName}\` -> \`${meta.promoteSourceEnvironment}\`` - sendToRocketChat(project, text, 'gold', ':warning:', channelWrapperLogs, msg, appId) - break; - - case "task:deploy-openshift:finished": - case "task:remove-openshift-resources:finished": - case "task:builddeploy-openshift:complete": - case "task:builddeploy-kubernetes:complete": - text = `*[${meta.projectName}]* ` - if (meta.shortSha) { - text = `${text} \`${meta.branchName}\` (${meta.shortSha})` - } else { - text = `${text} \`${meta.branchName}\`` - } - text = `${text} Build \`${meta.buildName}\` complete.` - if (meta.logLink){ - text = `${text} [Logs](${meta.logLink})\n` - } - text = `${text}\n ${meta.route}\n` - if (meta.routes) { - text = `${text}\n ${meta.routes.join("\n")}` - } - sendToRocketChat(project, text, 'lawngreen', ':white_check_mark:', channelWrapperLogs, msg, appId) - break; - - case "rest:pullrequest:remove": - text = `*[${meta.projectName}]* REST pullrequest remove trigger \`${meta.pullrequestNumber}\`` - sendToRocketChat(project, text, 'lawngreen', ':white_check_mark:', channelWrapperLogs, msg, appId) - break; - - case "task:remove-openshift:finished": - case "task:remove-kubernetes:finished": - text = `*[${meta.projectName}]* remove \`${meta.openshiftProject}\`` - sendToRocketChat(project, text, 'lawngreen', ':white_check_mark:', channelWrapperLogs, msg, appId) - break; - - case "task:deploy-openshift:retry": - case "task:remove-openshift:retry": - case "task:remove-kubernetes:retry": - case "task:remove-openshift-resources:retry": - text = `*[${meta.projectName}]*` - if (meta.shortSha) { - text = `${text} \`${meta.branchName}\` (${meta.shortSha})` - } else { - text = `${text} \`${meta.branchName}\`` - } - text = `${text} Build \`${meta.buildName}\` failed.` - if (meta.logLink){ - text = `${text} ${meta.logLink}` - } - sendToRocketChat(project, message, 'gold', ':warning:', channelWrapperLogs, msg, appId) - break; - - case "task:deploy-openshift:error": - case "task:remove-openshift:error": - case "task:remove-kubernetes:error": - case "task:remove-openshift-resources:error": - case "task:builddeploy-kubernetes:failed": - case "task:builddeploy-openshift:failed": - text = `*[${meta.projectName}]*` - if (meta.shortSha) { - text = `${text} \`${meta.branchName}\` (${meta.shortSha})` - } else { - text = `${text} \`${meta.branchName}\`` - } - text = `${text} Build \`${meta.buildName}\` failed.` - if (meta.logLink){ - text = `${text} [Logs](${meta.logLink})\n` - } - sendToRocketChat(project, text, 'red', ':bangbang:', channelWrapperLogs, msg, appId) - break; - - case "github:pull_request:closed:CannotDeleteProductionEnvironment": - case "github:push:CannotDeleteProductionEnvironment": - case "bitbucket:repo:push:CannotDeleteProductionEnvironment": - case "gitlab:push:CannotDeleteProductionEnvironment": - case "rest:remove:CannotDeleteProductionEnvironment": - text = `*[${meta.name}]* \`${meta.branchName}\` not deleted. ${meta.error}` - sendToRocketChat(project, message, 'gold', ':warning:', channelWrapperLogs, msg, appId) - break; - - default: - //since there's no single point of acknowlegement of the msg, we need to keep track of whether we've handled the message - let eventHandledAsProblem = dispatchProblemEventToRocketChat(event, project, message, channelWrapperLogs, msg, appId); - if(!eventHandledAsProblem) { - return channelWrapperLogs.ack(msg); - } - } - -} - -const dispatchProblemEventToRocketChat = (event, project, message, channelWrapperLogs, msg, appId) => { - const problemEvent = parseProblemNotification(event); - if(problemEvent.isProblem && problemEvent.eventType == 'insert') { - sendToRocketChat(project, message, 'red', ':warning:', channelWrapperLogs, msg, appId, 'PROBLEM', problemEvent.severityLevel) - return true; - } - return false; -}; - -const sendToRocketChat = async (project, message, color, emoji, channelWrapperLogs, msg, appId, contentType = 'DEPLOYMENT', severityLevel = 'NONE') => { - let projectRocketChats; - try { - projectRocketChats = await getRocketChatInfoForProject(project, contentType) - } - catch (error) { - logger.error(`No RocketChat information found, error: ${error}`) - return channelWrapperLogs.ack(msg) - } - projectRocketChats.forEach(async (projectRocketChat) => { - - const notificationThresholdMet = notificationContentTypeToInt(projectRocketChat.notificationSeverityThreshold) <= notificationContentTypeToInt(severityLevel); - if(contentType == 'PROBLEM' && !notificationThresholdMet) { return; } //go to next iteration - - const { channel, webhook } = projectRocketChat; - const rocketchat = new URL(webhook); - - var data = JSON.stringify({ - channel: `#${channel}`, - attachments: [{ - text: `${emoji} ${message}`, - color: color, - fields: [ - { - "short": true, - "title": "Source", - "value": appId - } - ], - }] - }); - - var options = { - hostname: rocketchat.hostname, - port: rocketchat.port, - path: rocketchat.pathname, - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Content-Length': data.length - } - }; - - var req = http.request(options, function(res) { - res.setEncoding('utf8'); - }); - - req.on('error', function(e) { - logger.error(`problem with request: ${e.message}`); - }); - req.write(data); - req.end(); - }); - channelWrapperLogs.ack(msg) - return -} diff --git a/services/logs2rocketchat/tsconfig.json b/services/logs2rocketchat/tsconfig.json deleted file mode 100644 index 6ce2574ab8..0000000000 --- a/services/logs2rocketchat/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "outDir": "./dist" - }, - "include": ["./src"], - "references": [ - { "path": "../../node-packages/commons" } - ] -} diff --git a/services/logs2s3/.gitignore b/services/logs2s3/.gitignore deleted file mode 100644 index f06235c460..0000000000 --- a/services/logs2s3/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -node_modules -dist diff --git a/services/logs2s3/Dockerfile b/services/logs2s3/Dockerfile deleted file mode 100644 index 7ceda0f379..0000000000 --- a/services/logs2s3/Dockerfile +++ /dev/null @@ -1,34 +0,0 @@ -ARG LAGOON_GIT_BRANCH -ARG IMAGE_REPO -ARG UPSTREAM_REPO -ARG UPSTREAM_TAG -# STAGE 1: Loading Image lagoon-node-packages-builder which contains node packages shared by all Node Services -FROM ${IMAGE_REPO:-lagoon}/yarn-workspace-builder as yarn-workspace-builder - -# STAGE 2: specific service Image -FROM ${UPSTREAM_REPO:-uselagoon}/node-16:${UPSTREAM_TAG:-latest} - -ARG LAGOON_VERSION -ENV LAGOON_VERSION=$LAGOON_VERSION - -# Copying generated node_modules from the first stage -COPY --from=yarn-workspace-builder /app /app - -# Setting the workdir to the service, all following commands will run from here -WORKDIR /app/services/logs2s3/ - -# Copying the .env.defaults into the Workdir, as the dotenv system searches within the workdir for it -COPY --from=yarn-workspace-builder /app/.env.defaults . - -# Copying files from our service -COPY . . - -# Verify that all dependencies have been installed via the yarn-workspace-builder -RUN yarn check --verify-tree - -# Making sure we run in production -ENV NODE_ENV production - -RUN yarn build - -CMD ["yarn", "start"] diff --git a/services/logs2s3/README.md b/services/logs2s3/README.md deleted file mode 100644 index 4323a085cb..0000000000 --- a/services/logs2s3/README.md +++ /dev/null @@ -1,31 +0,0 @@ -

- -This service is part of amazee.io Lagoon, a Docker build and deploy system for -OpenShift & Kubernetes. Please reference our [documentation] for detailed -information on using, developing, and administering Lagoon. - -# Logs to S3 (`logs2s3`) - -Watches all the Lagoon logs and checks for events that should trigger a push to S3. Each log message is tied to a Lagoon project, environment or task. - -This is a requirement for Build and Task logs within Lagoon. - -## Technology - -* Node.js -* Message Queue - -## Related Services - -* API [***dependency***] -* RabbitMQ [***dependency***] - -## Message Queues - -* Consumes: `lagoon-logs`, `lagoon-logs:s3` -* Produces: `lagoon-logs:s3` - -[documentation]: https://docs.lagoon.sh/ diff --git a/services/logs2s3/package.json b/services/logs2s3/package.json deleted file mode 100644 index f5972929ae..0000000000 --- a/services/logs2s3/package.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "logs2s3", - "version": "0.0.1", - "description": "lagoon handler for s3 storage", - "author": "amazee.io (http://www.amazee.io)", - "main": "dist/index.js", - "scripts": { - "build": "tsc --build", - "start": "node dist/index", - "dev": "mkdir -p ../../node-packages/commons/dist && NODE_ENV=development nodemon" - }, - "nodemonConfig": { - "ignore": [ - "../../node-packages/commons/dist/" - ], - "watch": [ - "src", - "../../node-packages/" - ], - "ext": "js,ts,json", - "exec": "yarn build --incremental && yarn start --inspect=0.0.0.0:9229" - }, - "license": "MIT", - "dependencies": { - "@lagoon/commons": "4.0.0", - "@slack/client": "^4.12.0", - "aws-sdk": "^2.378.0", - "amqp-connection-manager": "^1.3.5" - }, - "devDependencies": { - "@types/amqp-connection-manager": "^2.0.10", - "nodemon": "^1.12.1", - "typescript": "^3.9.3" - } -} diff --git a/services/logs2s3/src/index.ts b/services/logs2s3/src/index.ts deleted file mode 100644 index 7ef9dcf4d0..0000000000 --- a/services/logs2s3/src/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -import amqp, { ChannelWrapper } from 'amqp-connection-manager'; -import { logger } from '@lagoon/commons/dist/local-logging'; -import { readFromRabbitMQ } from './readFromRabbitMQ'; - -const rabbitmqHost = process.env.RABBITMQ_HOST || "broker" -const rabbitmqUsername = process.env.RABBITMQ_USERNAME || "guest" -const rabbitmqPassword = process.env.RABBITMQ_PASSWORD || "guest" -// @ts-ignore -const connection = amqp.connect([`amqp://${rabbitmqUsername}:${rabbitmqPassword}@${rabbitmqHost}`], { json: true }); - -connection.on('connect', ({ url }) => logger.verbose('Connected to %s', url, { action: 'connected', url })); -// @ts-ignore -connection.on('disconnect', params => logger.error('Not connected, error: %s', params.err.code, { action: 'disconnected', reason: params })); - -// Cast any to ChannelWrapper to get type-safetiness through our own code -const channelWrapperLogs: ChannelWrapper = connection.createChannel({ - setup: channel => { - return Promise.all([ - channel.assertExchange('lagoon-logs', 'direct', {durable: true}), - channel.assertQueue('lagoon-logs:s3', {durable: true}), - channel.bindQueue('lagoon-logs:s3', 'lagoon-logs', ''), - channel.prefetch(1), - channel.consume('lagoon-logs:s3', msg => readFromRabbitMQ(msg, channelWrapperLogs), {noAck: false}), - ]); - } -}); diff --git a/services/logs2s3/src/readFromRabbitMQ.ts b/services/logs2s3/src/readFromRabbitMQ.ts deleted file mode 100644 index 29003f9bf0..0000000000 --- a/services/logs2s3/src/readFromRabbitMQ.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { ChannelWrapper } from 'amqp-connection-manager'; -import { ConsumeMessage } from 'amqplib'; -import { logger } from '@lagoon/commons/dist/local-logging'; -import S3 from 'aws-sdk/clients/s3'; -import sha1 from 'sha1'; - -const accessKeyId = process.env.S3_FILES_ACCESS_KEY_ID || 'minio' -const secretAccessKey = process.env.S3_FILES_SECRET_ACCESS_KEY || 'minio123' -const bucket = process.env.S3_FILES_BUCKET || 'lagoon-files' -const region = process.env.S3_FILES_REGION -const s3Origin = process.env.S3_FILES_HOST || 'http://docker.for.mac.localhost:9000' - -const config = { - origin: s3Origin, - accessKeyId: accessKeyId, - secretAccessKey: secretAccessKey, - region: region, - bucket: bucket -}; - -const s3Client = new S3({ - endpoint: config.origin, - accessKeyId: config.accessKeyId, - secretAccessKey: config.secretAccessKey, - region: config.region, - params: { - Bucket: config.bucket - }, - s3ForcePathStyle: true, - signatureVersion: 'v4' -}); - -const makeSafe = string => string.toLocaleLowerCase().replace(/[^0-9a-z-]/g,'-') - -export async function readFromRabbitMQ( - msg: ConsumeMessage, - channelWrapperLogs: ChannelWrapper -): Promise { - const logMessage = JSON.parse(msg.content.toString()); - - const { severity, project, uuid, event, meta, message } = logMessage; - - - switch (event) { - // handle builddeploy build logs from lagoon builds - case String(event.match(/^build-logs:builddeploy-kubernetes:.*/)): - logger.verbose(`received ${event} for project ${project} environment ${meta.branchName} - name:${meta.jobName}, remoteId:${meta.remoteId}`); - await s3Client.putObject({ - Bucket: bucket, - Key: 'buildlogs/'+project+'/'+meta.branchName+'/'+meta.jobName+'-'+meta.remoteId+'.txt', - ContentType: 'text/plain', - Body: Buffer.from(message, 'binary') - }).promise(); - - channelWrapperLogs.ack(msg); - break; - // handle tasks events for tasks logs - // yes this says build-logs but it should be task-logs, will be fixed in controller and phased out at some stage - // the build-logs is a flow on from days past - case String(event.match(/^build-logs:job-kubernetes:.*/)): - case String(event.match(/^task-logs:job-kubernetes:.*/)): - if (meta.environment) { - // this value comes back as the actual environment/branch name - // it needs to be made safe just in case - var environmentName = makeSafe(meta.environment) - var overlength = 58 - project.length; - if ( environmentName.length > overlength ) { - var hash = sha1(environmentName).substring(0,4) - environmentName = environmentName.substring(0, overlength-5) - environmentName = environmentName.concat('-' + hash) - } - // if the environment is in the data, then save the log to the environments directory - // some versions of the controller don't send this value in the log meta - // the resolver in the api also knows to check in both locations when trying to load logs - logger.verbose(`received ${event} for project ${project} environment ${environmentName} - id:${meta.task.id}, remoteId:${meta.remoteId}`); - await s3Client.putObject({ - Bucket: bucket, - Key: 'tasklogs/'+project+'/'+environmentName+'/'+meta.task.id+'-'+meta.remoteId+'.txt', - ContentType: 'text/plain', - Body: Buffer.from(message, 'binary') - }).promise(); - } else { - logger.verbose(`received ${event} for project ${project} - id:${meta.task.id}, remoteId:${meta.remoteId}`); - await s3Client.putObject({ - Bucket: bucket, - Key: 'tasklogs/'+project+'/'+meta.task.id+'-'+meta.remoteId+'.txt', - ContentType: 'text/plain', - Body: Buffer.from(message, 'binary') - }).promise(); - } - channelWrapperLogs.ack(msg); - break; - default: - return channelWrapperLogs.ack(msg); - } -} diff --git a/services/logs2s3/tsconfig.json b/services/logs2s3/tsconfig.json deleted file mode 100644 index 6ce2574ab8..0000000000 --- a/services/logs2s3/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "outDir": "./dist" - }, - "include": ["./src"], - "references": [ - { "path": "../../node-packages/commons" } - ] -} diff --git a/services/logs2slack/.gitignore b/services/logs2slack/.gitignore deleted file mode 100644 index f06235c460..0000000000 --- a/services/logs2slack/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -node_modules -dist diff --git a/services/logs2slack/Dockerfile b/services/logs2slack/Dockerfile deleted file mode 100644 index 9aaa1a761f..0000000000 --- a/services/logs2slack/Dockerfile +++ /dev/null @@ -1,34 +0,0 @@ -ARG LAGOON_GIT_BRANCH -ARG IMAGE_REPO -ARG UPSTREAM_REPO -ARG UPSTREAM_TAG -# STAGE 1: Loading Image lagoon-node-packages-builder which contains node packages shared by all Node Services -FROM ${IMAGE_REPO:-lagoon}/yarn-workspace-builder as yarn-workspace-builder - -# STAGE 2: specific service Image -FROM ${UPSTREAM_REPO:-uselagoon}/node-16:${UPSTREAM_TAG:-latest} - -ARG LAGOON_VERSION -ENV LAGOON_VERSION=$LAGOON_VERSION - -# Copying generated node_modules from the first stage -COPY --from=yarn-workspace-builder /app /app - -# Setting the workdir to the service, all following commands will run from here -WORKDIR /app/services/logs2slack/ - -# Copying the .env.defaults into the Workdir, as the dotenv system searches within the workdir for it -COPY --from=yarn-workspace-builder /app/.env.defaults . - -# Copying files from our service -COPY . . - -# Verify that all dependencies have been installed via the yarn-workspace-builder -RUN yarn check --verify-tree - -# Making sure we run in production -ENV NODE_ENV production - -RUN yarn build - -CMD ["yarn", "start"] diff --git a/services/logs2slack/README.md b/services/logs2slack/README.md deleted file mode 100644 index c2c05e7a69..0000000000 --- a/services/logs2slack/README.md +++ /dev/null @@ -1,34 +0,0 @@ -

- -This service is part of amazee.io Lagoon, a Docker build and deploy system for -OpenShift & Kubernetes. Please reference our [documentation] for detailed -information on using, developing, and administering Lagoon. - -# Logs to Slack (`logs2slack`) - -Watches all the Lagoon logs and checks for events that should trigger a Slack -message. Each log message is tied to a Lagoon project, and channel configuration -for that project is retrieved from the Lagoon API. - -Examples of events that might trigger a message: GitHub pull request opened, a -new build for a Lagoon project environent has started, a task was completed. - -## Technology - -* Node.js -* Message Queue - -## Related Services - -* API [***dependency***] -* RabbitMQ [***dependency***] - -## Message Queues - -* Consumes: `lagoon-logs`, `lagoon-logs:slack` -* Produces: `lagoon-logs:slack` - -[documentation]: https://docs.lagoon.sh/ diff --git a/services/logs2slack/package.json b/services/logs2slack/package.json deleted file mode 100644 index 402c985ee1..0000000000 --- a/services/logs2slack/package.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "logs2slack", - "version": "0.9.0", - "description": "lagoon handler for webhooks", - "author": "amazee.io (http://www.amazee.io)", - "main": "dist/index.js", - "scripts": { - "build": "tsc --build", - "start": "node dist/index", - "dev": "mkdir -p ../../node-packages/commons/dist && NODE_ENV=development nodemon" - }, - "nodemonConfig": { - "ignore": [ - "../../node-packages/commons/dist/" - ], - "watch": [ - "src", - "../../node-packages/" - ], - "ext": "js,ts,json", - "exec": "yarn build --incremental && yarn start --inspect=0.0.0.0:9229" - }, - "license": "MIT", - "dependencies": { - "@lagoon/commons": "4.0.0", - "@slack/client": "^4.12.0", - "amqp-connection-manager": "^1.3.5" - }, - "devDependencies": { - "@types/amqp-connection-manager": "^2.0.10", - "nodemon": "^1.12.1", - "typescript": "^3.9.3" - } -} diff --git a/services/logs2slack/src/index.ts b/services/logs2slack/src/index.ts deleted file mode 100644 index 31a1099f15..0000000000 --- a/services/logs2slack/src/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -import amqp, { ChannelWrapper } from 'amqp-connection-manager'; -import { logger } from '@lagoon/commons/dist/local-logging'; -import { readFromRabbitMQ } from './readFromRabbitMQ'; - -const rabbitmqHost = process.env.RABBITMQ_HOST || "broker" -const rabbitmqUsername = process.env.RABBITMQ_USERNAME || "guest" -const rabbitmqPassword = process.env.RABBITMQ_PASSWORD || "guest" -// @ts-ignore -const connection = amqp.connect([`amqp://${rabbitmqUsername}:${rabbitmqPassword}@${rabbitmqHost}`], { json: true }); - -connection.on('connect', ({ url }) => logger.verbose('Connected to %s', url, { action: 'connected', url })); -// @ts-ignore -connection.on('disconnect', params => logger.error('Not connected, error: %s', params.err.code, { action: 'disconnected', reason: params })); - -// Cast any to ChannelWrapper to get type-safetiness through our own code -const channelWrapperLogs: ChannelWrapper = connection.createChannel({ - setup: channel => { - return Promise.all([ - channel.assertExchange('lagoon-logs', 'direct', {durable: true}), - channel.assertQueue('lagoon-logs:slack', {durable: true}), - channel.bindQueue('lagoon-logs:slack', 'lagoon-logs', ''), - channel.prefetch(1), - channel.consume('lagoon-logs:slack', msg => readFromRabbitMQ(msg, channelWrapperLogs), {noAck: false}), - ]); - } -}); diff --git a/services/logs2slack/src/readFromRabbitMQ.ts b/services/logs2slack/src/readFromRabbitMQ.ts deleted file mode 100644 index 5bf8785572..0000000000 --- a/services/logs2slack/src/readFromRabbitMQ.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { IncomingWebhook } from '@slack/client'; -import { ChannelWrapper } from 'amqp-connection-manager'; -import { ConsumeMessage } from 'amqplib'; -import { logger } from '@lagoon/commons/dist/local-logging'; -import { getSlackinfoForProject } from '@lagoon/commons/dist/api'; -import { notificationIntToContentType, notificationContentTypeToInt, parseProblemNotification } from '@lagoon/commons/dist/notificationCommons'; - -export async function readFromRabbitMQ (msg: ConsumeMessage, channelWrapperLogs: ChannelWrapper): Promise { - const logMessage = JSON.parse(msg.content.toString()) - - const { - severity, - project, - uuid, - event, - meta, - message - } = logMessage - - const appId = msg.properties.appId || "" - - logger.verbose(`received ${event} for project ${project}`) - - switch (event) { - - case "github:pull_request:closed:handled": - case "github:pull_request:opened:handled": - case "github:pull_request:synchronize:handled": - case "github:delete:handled": - case "github:push:handled": - case "bitbucket:repo:push:handled": - case "bitbucket:pullrequest:created:handled": - case "bitbucket:pullrequest:updated:handled": - case "bitbucket:pullrequest:fulfilled:handled": - case "bitbucket:pullrequest:rejected:handled": - case "gitlab:push:handled": - case "gitlab:merge_request:opened:handled": - case "gitlab:merge_request:updated:handled": - case "gitlab:merge_request:closed:handled": - case "rest:deploy:receive": - case "rest:remove:receive": - case "rest:promote:receive": - case "api:deployEnvironmentLatest": - case "api:deployEnvironmentBranch": - case "api:deleteEnvironment": - case "github:push:skipped": - case "gitlab:push:skipped": - case "bitbucket:push:skipped": - sendToSlack(project, message, '#E8E8E8', ':information_source:', channelWrapperLogs, msg, appId) - break; - - case "task:deploy-openshift:finished": - case "task:remove-openshift:finished": - case "task:remove-kubernetes:finished": - case "task:remove-openshift-resources:finished": - case "task:builddeploy-openshift:complete": - case "task:builddeploy-kubernetes:complete": - sendToSlack(project, message, 'good', ':white_check_mark:', channelWrapperLogs, msg, appId) - break; - - case "task:deploy-openshift:retry": - case "task:remove-openshift:retry": - case "task:remove-kubernetes:retry": - case "task:remove-openshift-resources:retry": - sendToSlack(project, message, 'warning', ':warning:', channelWrapperLogs, msg, appId) - break; - - case "task:deploy-openshift:error": - case "task:remove-openshift:error": - case "task:remove-kubernetes:error": - case "task:remove-openshift-resources:error": - case "task:builddeploy-openshift:failed": - case "task:builddeploy-kubernetes:failed": - sendToSlack(project, message, 'danger', ':bangbang:', channelWrapperLogs, msg, appId) - break; - - case "github:pull_request:closed:CannotDeleteProductionEnvironment": - case "github:push:CannotDeleteProductionEnvironment": - case "bitbucket:repo:push:CannotDeleteProductionEnvironment": - case "gitlab:push:CannotDeleteProductionEnvironment": - case "rest:remove:CannotDeleteProductionEnvironment": - sendToSlack(project, message, 'warning', ':warning:', channelWrapperLogs, msg, appId) - break; - default: - //since there's no single point of acknowlegement of the msg, we need to keep track of whether we've handled the message - let eventHandledAsProblem = dispatchProblemEventToSlack(event, project, message, channelWrapperLogs, msg, appId); - if(!eventHandledAsProblem) { - return channelWrapperLogs.ack(msg); - } - } -} - -const dispatchProblemEventToSlack = (event, project, message, channelWrapperLogs, msg, appId) => { - const problemEvent = parseProblemNotification(event); - if(problemEvent.isProblem) { - const isNewProblem = problemEvent.eventType == 'insert'; - if(isNewProblem) { - sendToSlack(project, message, 'warning', ':warning:', channelWrapperLogs, msg, appId, 'PROBLEM', problemEvent.severityLevel) - return true; - } - } - return false; -}; - -const sendToSlack = async (project, message, color, emoji, channelWrapperLogs, msg, appId, contentType = 'DEPLOYMENT', severityLevel = 'NONE') => { - - let projectSlacks; - try { - projectSlacks = await getSlackinfoForProject(project, contentType) - } - catch (error) { - logger.error(`No Slack information found, error: ${error}`) - return channelWrapperLogs.ack(msg) - } - projectSlacks.forEach(async (projectSlack) => { - - const notificationThresholdMet = notificationContentTypeToInt(projectSlack.notificationSeverityThreshold) <= notificationContentTypeToInt(severityLevel); - if(contentType == 'PROBLEM' && !notificationThresholdMet) { return; } //go to next iteration - - await new IncomingWebhook(projectSlack.webhook, { - channel: projectSlack.channel, - }).send({ - attachments: [{ - text: `${emoji} ${message}`, - color: color, - "mrkdwn_in": ["pretext", "text", "fields"], - footer: appId - }] - }); - }); - channelWrapperLogs.ack(msg) - return -} diff --git a/services/logs2slack/tsconfig.json b/services/logs2slack/tsconfig.json deleted file mode 100644 index 6ce2574ab8..0000000000 --- a/services/logs2slack/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "outDir": "./dist" - }, - "include": ["./src"], - "references": [ - { "path": "../../node-packages/commons" } - ] -} diff --git a/services/logs2webhook/.gitignore b/services/logs2webhook/.gitignore deleted file mode 100644 index f06235c460..0000000000 --- a/services/logs2webhook/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -node_modules -dist diff --git a/services/logs2webhook/Dockerfile b/services/logs2webhook/Dockerfile deleted file mode 100644 index b3c757e6da..0000000000 --- a/services/logs2webhook/Dockerfile +++ /dev/null @@ -1,34 +0,0 @@ -ARG LAGOON_GIT_BRANCH -ARG IMAGE_REPO -ARG UPSTREAM_REPO -ARG UPSTREAM_TAG -# STAGE 1: Loading Image lagoon-node-packages-builder which contains node packages shared by all Node Services -FROM ${IMAGE_REPO:-lagoon}/yarn-workspace-builder as yarn-workspace-builder - -# STAGE 2: specific service Image -FROM ${UPSTREAM_REPO:-uselagoon}/node-16:${UPSTREAM_TAG:-latest} - -ARG LAGOON_VERSION -ENV LAGOON_VERSION=$LAGOON_VERSION - -# Copying generated node_modules from the first stage -COPY --from=yarn-workspace-builder /app /app - -# Setting the workdir to the service, all following commands will run from here -WORKDIR /app/services/logs2webhook/ - -# Copying the .env.defaults into the Workdir, as the dotenv system searches within the workdir for it -COPY --from=yarn-workspace-builder /app/.env.defaults . - -# Copying files from our service -COPY . . - -# Verify that all dependencies have been installed via the yarn-workspace-builder -RUN yarn check --verify-tree - -# Making sure we run in production -ENV NODE_ENV production - -RUN yarn build - -CMD ["yarn", "start"] diff --git a/services/logs2webhook/README.md b/services/logs2webhook/README.md deleted file mode 100644 index a558778132..0000000000 --- a/services/logs2webhook/README.md +++ /dev/null @@ -1,33 +0,0 @@ -

- -This service is part of amazee.io Lagoon, a Docker build and deploy system for -OpenShift & Kubernetes. Please reference our [documentation] for detailed -information on using, developing, and administering Lagoon. - -# Logs to Webhook (`logs2webhook`) - -Watches all the Lagoon logs and checks for events that should trigger a webhook call. Each log message is tied to a Lagoon project, and channel configuration -for that project is retrieved from the Lagoon API. - -Examples of events that might trigger a message: GitHub pull request opened, a -new build for a Lagoon project environent has started, a task was completed. - -## Technology - -* Node.js -* Message Queue - -## Related Services - -* API [***dependency***] -* RabbitMQ [***dependency***] - -## Message Queues - -* Consumes: `lagoon-logs`, `lagoon-logs:webhook` -* Produces: `lagoon-logs:webhook` - -[documentation]: https://docs.lagoon.sh/ diff --git a/services/logs2webhook/package.json b/services/logs2webhook/package.json deleted file mode 100644 index ac54a3287e..0000000000 --- a/services/logs2webhook/package.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "logs2webhook", - "version": "0.9.0", - "description": "lagoon handler for webhooks", - "author": "amazee.io (http://www.amazee.io)", - "main": "dist/index.js", - "scripts": { - "build": "tsc --build", - "start": "node dist/index", - "dev": "mkdir -p ../../node-packages/commons/dist && NODE_ENV=development nodemon" - }, - "nodemonConfig": { - "ignore": [ - "../../node-packages/commons/dist/" - ], - "watch": [ - "src", - "../../node-packages/" - ], - "ext": "js,ts,json", - "exec": "yarn build --incremental && yarn start --inspect=0.0.0.0:9229" - }, - "license": "MIT", - "dependencies": { - "@lagoon/commons": "4.0.0", - "@slack/client": "^4.12.0", - "amqp-connection-manager": "^1.3.5" - }, - "devDependencies": { - "@types/amqp-connection-manager": "^2.0.10", - "nodemon": "^1.12.1", - "typescript": "^3.9.3" - } -} diff --git a/services/logs2webhook/src/index.ts b/services/logs2webhook/src/index.ts deleted file mode 100644 index 74577bea8c..0000000000 --- a/services/logs2webhook/src/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -import amqp, { ChannelWrapper } from 'amqp-connection-manager'; -import { logger } from '@lagoon/commons/dist/local-logging'; -import { readFromRabbitMQ } from './readFromRabbitMQ'; - -const rabbitmqHost = process.env.RABBITMQ_HOST || "broker" -const rabbitmqUsername = process.env.RABBITMQ_USERNAME || "guest" -const rabbitmqPassword = process.env.RABBITMQ_PASSWORD || "guest" -// @ts-ignore -const connection = amqp.connect([`amqp://${rabbitmqUsername}:${rabbitmqPassword}@${rabbitmqHost}`], { json: true }); - -connection.on('connect', ({ url }) => logger.verbose('Connected to %s', url, { action: 'connected', url })); -// @ts-ignore -connection.on('disconnect', params => logger.error('Not connected, error: %s', params.err.code, { action: 'disconnected', reason: params })); - -// Cast any to ChannelWrapper to get type-safetiness through our own code -const channelWrapperLogs: ChannelWrapper = connection.createChannel({ - setup: channel => { - return Promise.all([ - channel.assertExchange('lagoon-logs', 'direct', {durable: true}), - channel.assertQueue('lagoon-logs:webhook', {durable: true}), - channel.bindQueue('lagoon-logs:webhook', 'lagoon-logs', ''), - channel.prefetch(1), - channel.consume('lagoon-logs:webhook', msg => readFromRabbitMQ(msg, channelWrapperLogs), {noAck: false}), - ]); - } -}); diff --git a/services/logs2webhook/src/readFromRabbitMQ.ts b/services/logs2webhook/src/readFromRabbitMQ.ts deleted file mode 100644 index ec09c7172c..0000000000 --- a/services/logs2webhook/src/readFromRabbitMQ.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { ChannelWrapper } from 'amqp-connection-manager'; -import { ConsumeMessage } from 'amqplib'; -import { logger } from '@lagoon/commons/dist/local-logging'; -import { - getWebhookNotificationInfoForProject, - getEnvironmentById -} from '@lagoon/commons/dist/api'; -import { - notificationIntToContentType, - notificationContentTypeToInt, - parseProblemNotification -} from '@lagoon/commons/dist/notificationCommons'; -import { URL } from 'url'; -import http from 'https'; - -export async function readFromRabbitMQ( - msg: ConsumeMessage, - channelWrapperLogs: ChannelWrapper -): Promise { - const logMessage = JSON.parse(msg.content.toString()); - - const { severity, project, uuid, event, meta, message } = logMessage; - - const appId = msg.properties.appId || ''; - - logger.verbose(`received ${event} for project ${project}`); - - switch (event) { - case 'task:deploy-openshift:finished': - case 'task:remove-openshift:finished': - case 'task:remove-kubernetes:finished': - case 'task:remove-openshift-resources:finished': - case 'task:builddeploy-openshift:complete': - case 'task:builddeploy-kubernetes:complete': - case 'task:deploy-openshift:error': - case 'task:remove-openshift:error': - case 'task:remove-kubernetes:error': - case 'task:remove-openshift-resources:error': - case 'task:builddeploy-openshift:failed': - case 'task:builddeploy-kubernetes:failed': - let payload = { - type: 'DEPLOYMENT', - event: event.split(':').pop(), - project, - environment: '' - }; - if (meta && meta.environmentId) { - const environmentDetails = await getEnvironmentById(meta.environmentId); - payload.environment = environmentDetails.environmentById.name; - } - sendToWebhook(event, project, payload, channelWrapperLogs, msg); - break; - default: - return channelWrapperLogs.ack(msg); - break; - } -} - -const sendToWebhook = async ( - event, - project, - payload, - channelWrapperLogs, - msg -) => { - let projectWebhooks; - try { - projectWebhooks = await getWebhookNotificationInfoForProject( - project, - 'DEPLOYMENT' - ); - } catch (error) { - logger.error(`No Webhook information found, error: ${error}`); - return channelWrapperLogs.ack(msg); - } - - projectWebhooks.forEach(projectWebhook => { - const { webhook } = projectWebhook; - const webhookUrl = new URL(webhook); - - var data = JSON.stringify(payload); - - var options = { - hostname: webhookUrl.host, - port: webhookUrl.port, - path: webhookUrl.pathname, - method: 'POST', - headers: { - 'Content-Type': 'application/json' - } - }; - - var req = http.request(options, function(res) { - res.setEncoding('utf8'); - }); - - req.on('error', function(e) { - logger.error(`problem with request: ${e.message}`); - }); - req.end(data); - }); - - channelWrapperLogs.ack(msg); - return; -}; diff --git a/services/logs2webhook/tsconfig.json b/services/logs2webhook/tsconfig.json deleted file mode 100644 index 6ce2574ab8..0000000000 --- a/services/logs2webhook/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "outDir": "./dist" - }, - "include": ["./src"], - "references": [ - { "path": "../../node-packages/commons" } - ] -} From 6086a4b99a61147ddbd9f708db00a342597e741d Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Wed, 23 Mar 2022 16:01:13 +1100 Subject: [PATCH 06/17] chore: add logs2notifications treeish to makefile --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 68c66c5274..09dad27d72 100644 --- a/Makefile +++ b/Makefile @@ -510,7 +510,7 @@ STERN_VERSION = 2.1.17 CHART_TESTING_VERSION = v3.4.0 KIND_IMAGE = kindest/node:v1.21.1@sha256:69860bda5563ac81e3c0057d654b5253219618a22ec3a346306239bba8cfa1a6 TESTS = [nginx,api,features-kubernetes,bulk-deployment,features-kubernetes-2,features-api-variables,active-standby-kubernetes,tasks,drush,drupal-php80,drupal-postgres,python,gitlab,github,bitbucket,node-mongodb,elasticsearch,workflows] -CHARTS_TREEISH = "main" +CHARTS_TREEISH = "logs2notifications" # Symlink the installed kubectl client if the correct version is already # installed, otherwise downloads it. From 3ec8af6e49556c10d200cf19e14547e355e1b3cc Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Wed, 23 Mar 2022 16:06:25 +1100 Subject: [PATCH 07/17] chore: remove other references to logs2x from yarn builder --- images/yarn-workspace-builder/Dockerfile | 6 ------ 1 file changed, 6 deletions(-) diff --git a/images/yarn-workspace-builder/Dockerfile b/images/yarn-workspace-builder/Dockerfile index cd66e7db1b..9f0fff475f 100644 --- a/images/yarn-workspace-builder/Dockerfile +++ b/images/yarn-workspace-builder/Dockerfile @@ -13,12 +13,6 @@ COPY node-packages /app/node-packages # subdependencies won't be installed COPY services/api/package.json /app/services/api/ COPY services/auth-server/package.json /app/services/auth-server/ -COPY services/logs2email/package.json /app/services/logs2email/ -COPY services/logs2microsoftteams/package.json /app/services/logs2microsoftteams/ -COPY services/logs2rocketchat/package.json /app/services/logs2rocketchat/ -COPY services/logs2slack/package.json /app/services/logs2slack/ -COPY services/logs2s3/package.json /app/services/logs2s3/ -COPY services/logs2webhook/package.json /app/services/logs2webhook/ COPY services/controllerhandler/package.json /app/services/controllerhandler/ COPY services/webhook-handler/package.json /app/services/webhook-handler/ COPY services/webhooks2tasks/package.json /app/services/webhooks2tasks/ From e5d20239ce23cd23a651e5654719d09b754978b8 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Wed, 23 Mar 2022 16:28:44 +1100 Subject: [PATCH 08/17] chore: remove a reference to logs2s3 --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 09dad27d72..a07f1dde9f 100644 --- a/Makefile +++ b/Makefile @@ -629,7 +629,7 @@ ifeq ($(ARCH), darwin) tcp-listen:32080,fork,reuseaddr tcp-connect:target:32080 endif -KIND_SERVICES = api api-db api-redis auth-server actions-handler broker controllerhandler docker-host drush-alias keycloak keycloak-db logs2notifications logs2s3 webhook-handler webhooks2tasks kubectl-build-deploy-dind local-api-data-watcher-pusher local-git ssh tests ui workflows +KIND_SERVICES = api api-db api-redis auth-server actions-handler broker controllerhandler docker-host drush-alias keycloak keycloak-db logs2notifications webhook-handler webhooks2tasks kubectl-build-deploy-dind local-api-data-watcher-pusher local-git ssh tests ui workflows KIND_TESTS = local-api-data-watcher-pusher local-git tests KIND_TOOLS = kind helm kubectl jq stern From 6037845e75894b508c1f7bd96980cce371201146 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Wed, 8 Jun 2022 15:34:46 +1000 Subject: [PATCH 09/17] refactor: add tests to logs2notifications and fix up outputs --- services/logs2notifications/go.mod | 2 + services/logs2notifications/go.sum | 4 + .../internal/handler/email_events.go | 382 ++++++------------ .../internal/handler/main.go | 127 ++++-- .../internal/handler/main_test.go | 179 +++++++- .../internal/handler/microsoftteams_events.go | 196 +++------ .../internal/handler/rocketchat_events.go | 321 +++++---------- .../internal/handler/s3_events.go | 105 ++--- .../internal/handler/slack_events.go | 198 ++++++--- .../testdata/deleteEnvironment/emailhtml.txt | 1 + .../testdata/deleteEnvironment/emailplain.txt | 1 + .../testdata/deleteEnvironment/rocketchat.txt | 1 + .../testdata/deleteEnvironment/slack.txt | 1 + .../testdata/deleteEnvironment/teams.txt | 1 + .../testdata/deployEnvironment/emailhtml.txt | 1 + .../testdata/deployEnvironment/emailplain.txt | 1 + .../testdata/deployEnvironment/rocketchat.txt | 1 + .../testdata/deployEnvironment/slack.txt | 1 + .../testdata/deployEnvironment/teams.txt | 1 + .../testdata/deployError/emailhtml.txt | 2 + .../testdata/deployError/emailplain.txt | 2 + .../testdata/deployError/rocketchat.txt | 1 + .../handler/testdata/deployError/slack.txt | 1 + .../handler/testdata/deployError/teams.txt | 1 + .../testdata/deployFinished/emailhtml.txt | 10 + .../testdata/deployFinished/emailplain.txt | 4 + .../testdata/deployFinished/rocketchat.txt | 4 + .../handler/testdata/deployFinished/slack.txt | 4 + .../handler/testdata/deployFinished/teams.txt | 4 + .../input.deleteEnvironmentGithub.json | 8 + .../testdata/input.deployEnvironment.json | 9 + .../handler/testdata/input.deployError.json | 11 + .../testdata/input.deployFinished.json | 14 + .../testdata/input.repoPushHandledGithub.json | 12 + .../testdata/input.repoPushSkippedGithub.json | 12 + .../handler/testdata/mergeRequestClosed.json | 11 + .../handler/testdata/mergeRequestOpened.json | 11 + .../handler/testdata/mergeRequestUpdated.json | 11 + .../internal/handler/testdata/notDeleted.json | 0 .../testdata/problemNotification.1.json | 12 + .../testdata/problemNotification.2.json | 12 + .../handler/testdata/removeFinished.json | 0 .../testdata/repoPushHandled/emailhtml.txt | 1 + .../testdata/repoPushHandled/emailplain.txt | 1 + .../testdata/repoPushHandled/rocketchat.txt | 1 + .../testdata/repoPushHandled/slack.txt | 1 + .../testdata/repoPushHandled/teams.txt | 1 + .../testdata/repoPushSkipped/emailhtml.txt | 1 + .../testdata/repoPushSkipped/emailplain.txt | 1 + .../testdata/repoPushSkipped/rocketchat.txt | 1 + .../testdata/repoPushSkipped/slack.txt | 1 + .../testdata/repoPushSkipped/teams.txt | 1 + services/logs2notifications/main.go | 85 ++-- 53 files changed, 995 insertions(+), 780 deletions(-) create mode 100644 services/logs2notifications/internal/handler/testdata/deleteEnvironment/emailhtml.txt create mode 100644 services/logs2notifications/internal/handler/testdata/deleteEnvironment/emailplain.txt create mode 100644 services/logs2notifications/internal/handler/testdata/deleteEnvironment/rocketchat.txt create mode 100644 services/logs2notifications/internal/handler/testdata/deleteEnvironment/slack.txt create mode 100644 services/logs2notifications/internal/handler/testdata/deleteEnvironment/teams.txt create mode 100644 services/logs2notifications/internal/handler/testdata/deployEnvironment/emailhtml.txt create mode 100644 services/logs2notifications/internal/handler/testdata/deployEnvironment/emailplain.txt create mode 100644 services/logs2notifications/internal/handler/testdata/deployEnvironment/rocketchat.txt create mode 100644 services/logs2notifications/internal/handler/testdata/deployEnvironment/slack.txt create mode 100644 services/logs2notifications/internal/handler/testdata/deployEnvironment/teams.txt create mode 100644 services/logs2notifications/internal/handler/testdata/deployError/emailhtml.txt create mode 100644 services/logs2notifications/internal/handler/testdata/deployError/emailplain.txt create mode 100644 services/logs2notifications/internal/handler/testdata/deployError/rocketchat.txt create mode 100644 services/logs2notifications/internal/handler/testdata/deployError/slack.txt create mode 100644 services/logs2notifications/internal/handler/testdata/deployError/teams.txt create mode 100644 services/logs2notifications/internal/handler/testdata/deployFinished/emailhtml.txt create mode 100644 services/logs2notifications/internal/handler/testdata/deployFinished/emailplain.txt create mode 100644 services/logs2notifications/internal/handler/testdata/deployFinished/rocketchat.txt create mode 100644 services/logs2notifications/internal/handler/testdata/deployFinished/slack.txt create mode 100644 services/logs2notifications/internal/handler/testdata/deployFinished/teams.txt create mode 100644 services/logs2notifications/internal/handler/testdata/input.deleteEnvironmentGithub.json create mode 100644 services/logs2notifications/internal/handler/testdata/input.deployEnvironment.json create mode 100644 services/logs2notifications/internal/handler/testdata/input.deployError.json create mode 100644 services/logs2notifications/internal/handler/testdata/input.deployFinished.json create mode 100644 services/logs2notifications/internal/handler/testdata/input.repoPushHandledGithub.json create mode 100644 services/logs2notifications/internal/handler/testdata/input.repoPushSkippedGithub.json create mode 100644 services/logs2notifications/internal/handler/testdata/mergeRequestClosed.json create mode 100644 services/logs2notifications/internal/handler/testdata/mergeRequestOpened.json create mode 100644 services/logs2notifications/internal/handler/testdata/mergeRequestUpdated.json create mode 100644 services/logs2notifications/internal/handler/testdata/notDeleted.json create mode 100644 services/logs2notifications/internal/handler/testdata/problemNotification.1.json create mode 100644 services/logs2notifications/internal/handler/testdata/problemNotification.2.json create mode 100644 services/logs2notifications/internal/handler/testdata/removeFinished.json create mode 100644 services/logs2notifications/internal/handler/testdata/repoPushHandled/emailhtml.txt create mode 100644 services/logs2notifications/internal/handler/testdata/repoPushHandled/emailplain.txt create mode 100644 services/logs2notifications/internal/handler/testdata/repoPushHandled/rocketchat.txt create mode 100644 services/logs2notifications/internal/handler/testdata/repoPushHandled/slack.txt create mode 100644 services/logs2notifications/internal/handler/testdata/repoPushHandled/teams.txt create mode 100644 services/logs2notifications/internal/handler/testdata/repoPushSkipped/emailhtml.txt create mode 100644 services/logs2notifications/internal/handler/testdata/repoPushSkipped/emailplain.txt create mode 100644 services/logs2notifications/internal/handler/testdata/repoPushSkipped/rocketchat.txt create mode 100644 services/logs2notifications/internal/handler/testdata/repoPushSkipped/slack.txt create mode 100644 services/logs2notifications/internal/handler/testdata/repoPushSkipped/teams.txt diff --git a/services/logs2notifications/go.mod b/services/logs2notifications/go.mod index aa3e6ae785..2f16b24448 100644 --- a/services/logs2notifications/go.mod +++ b/services/logs2notifications/go.mod @@ -13,7 +13,9 @@ require ( github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2 github.com/slack-go/slack v0.9.5 github.com/tiago4orion/conjure v0.0.0-20150908101743-93cb30b9d218 // indirect + gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/mail.v2 v2.3.1 // indirect ) // Fixes for AppID diff --git a/services/logs2notifications/go.sum b/services/logs2notifications/go.sum index 4892db91f2..7cbfa00037 100644 --- a/services/logs2notifications/go.sum +++ b/services/logs2notifications/go.sum @@ -190,9 +190,13 @@ google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRn google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk= +gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw= gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/services/logs2notifications/internal/handler/email_events.go b/services/logs2notifications/internal/handler/email_events.go index e0e805942b..1c2c8cd86d 100644 --- a/services/logs2notifications/internal/handler/email_events.go +++ b/services/logs2notifications/internal/handler/email_events.go @@ -2,10 +2,14 @@ package handler import ( "bytes" + "crypto/tls" "fmt" "log" - "net/smtp" + "strconv" + "strings" "text/template" + + gomail "gopkg.in/mail.v2" ) var htmlTemplate = ` @@ -37,311 +41,171 @@ var htmlTemplate = `
-
-

- {{.Additional}} -

-
` // SendToEmail . -func SendToEmail(notification *Notification, emailAddress, appID string) { +func (h *Messaging) SendToEmail(notification *Notification, emailAddress string) { - emoji, color, tpl, err := getEmailEvent(notification.Event) + emoji, color, subject, mainHTML, plainText, err := h.processEmailTemplates(notification) if err != nil { return } - var mainHTML, plainText, subject, additional string + h.sendEmailMessage(emoji, color, subject, notification.Event, notification.Meta.ProjectName, emailAddress, mainHTML, plainText) +} + +// SendToEmail . +func (h *Messaging) processEmailTemplates(notification *Notification) (string, string, string, string, string, error) { + + emoji, color, tpl, err := getEmailEvent(notification.Event) + if err != nil { + eventSplit := strings.Split(notification.Event, ":") + fmt.Println(eventSplit[0]) + if eventSplit[0] != "problem" { + return "", "", "", "", "", nil + } + if eventSplit[1] == "insert" { + tpl = "problemNotification" + } + } + var mainHTML, plainText, subject, plainTextTpl, mainHTMLTpl string switch tpl { case "mergeRequestOpened": - mainHTML += fmt.Sprintf(`PR #%s (%s opened in %s`, - notification.Meta.PullrequestNumber, - notification.Meta.PullrequestTitle, - notification.Meta.PullrequestURL, - notification.Meta.RepoURL, - notification.Meta.RepoName, - ) - plainText += fmt.Sprintf(`[%s] PR #%s - %s opened in %s`, - notification.Meta.ProjectName, - notification.Meta.PullrequestNumber, - notification.Meta.PullrequestTitle, - notification.Meta.RepoName, - ) - subject += plainText + mainHTMLTpl = `PR #{{.PullrequestNumber}} ({{.PullrequestTitle}}) opened in {{.RepoName}}` + plainTextTpl = `[{{.ProjectName}}] PR #{{.PullrequestNumber}} - {{.PullrequestTitle}} opened in {{.RepoName}}` case "mergeRequestUpdated": - mainHTML += fmt.Sprintf(`PR #%s (%s updated in %s`, - notification.Meta.PullrequestNumber, - notification.Meta.PullrequestTitle, - notification.Meta.PullrequestURL, - notification.Meta.RepoURL, - notification.Meta.RepoName, - ) - plainText += fmt.Sprintf(`[%s] PR #%s - %s updated in %s`, - notification.Meta.ProjectName, - notification.Meta.PullrequestNumber, - notification.Meta.PullrequestTitle, - notification.Meta.RepoName, - ) - subject += plainText + mainHTMLTpl = `PR #{{.PullrequestNumber}} ({{.PullrequestTitle}}) updated in {{.RepoName}}` + plainTextTpl = `[{{.ProjectName}}] PR #{{.PullrequestNumber}} - {{.PullrequestTitle}} updated in {{.RepoName}}` case "mergeRequestClosed": - mainHTML += fmt.Sprintf(`PR #%s (%s closed in %s`, - notification.Meta.PullrequestNumber, - notification.Meta.PullrequestTitle, - notification.Meta.PullrequestURL, - notification.Meta.RepoURL, - notification.Meta.RepoName, - ) - plainText += fmt.Sprintf(`[%s] PR #%s - %s closed in %s`, - notification.Meta.ProjectName, - notification.Meta.PullrequestNumber, - notification.Meta.PullrequestTitle, - notification.Meta.RepoName, - ) - subject += plainText + mainHTMLTpl = `PR #{{.PullrequestNumber}} ({{.PullrequestTitle}}) closed in {{.RepoName}}` + plainTextTpl = `[{{.ProjectName}}] PR #{{.PullrequestNumber}} - {{.PullrequestTitle}} closed in {{.RepoName}}` case "deleteEnvironment": - mainHTML += fmt.Sprintf("Deleted environment %s", - notification.Meta.BranchName, - ) - plainText += fmt.Sprintf("[%s] deleted environment %s", - notification.Meta.ProjectName, - notification.Meta.BranchName, - ) - subject += plainText + mainHTMLTpl = `Deleted environment {{.EnvironmentName}}` + plainTextTpl = `[{{.ProjectName}}] deleted environment {{.EnvironmentName}}` case "repoPushHandled": - mainHTML += fmt.Sprintf(`%s`, - notification.Meta.RepoURL, - notification.Meta.BranchName, - notification.Meta.BranchName, - ) - plainText += fmt.Sprintf(`[%s] %s`, - notification.Meta.ProjectName, - notification.Meta.BranchName, - ) - if notification.Meta.ShortSha != "" { - mainHTML += fmt.Sprintf(`%s %s`, - mainHTML, - notification.Meta.CommitURL, - notification.Meta.ShortSha, - ) - plainText += fmt.Sprintf(`%s (%s)`, - plainText, - notification.Meta.ShortSha, - ) - } - mainHTML += fmt.Sprintf(`%s pushed in %s`, - mainHTML, - notification.Meta.RepoURL, - notification.Meta.RepoFullName, - ) - plainText += fmt.Sprintf(`%s pushed in %s`, - plainText, - notification.Meta.RepoFullName, - ) - subject += plainText + mainHTMLTpl = `{{.BranchName}}{{ if ne .ShortSha "" }} {{.ShortSha}}{{end}} pushed in {{.RepoFullName}}` + plainTextTpl = `[{{.ProjectName}}] {{.BranchName}}{{ if ne .ShortSha "" }} ({{.ShortSha}}){{end}} pushed in {{.RepoFullName}}` case "repoPushSkipped": - mainHTML += fmt.Sprintf(`%s`, - notification.Meta.RepoURL, - notification.Meta.BranchName, - notification.Meta.BranchName, - ) - plainText += fmt.Sprintf(`[%s] %s`, - notification.Meta.ProjectName, - notification.Meta.BranchName, - ) - if notification.Meta.ShortSha != "" { - mainHTML += fmt.Sprintf(`%s %s`, - mainHTML, - notification.Meta.CommitURL, - notification.Meta.ShortSha, - ) - plainText += fmt.Sprintf(`%s (%s)`, - plainText, - notification.Meta.ShortSha, - ) - } - mainHTML += fmt.Sprintf(`%s pushed in %s deployment skipped`, - mainHTML, - notification.Meta.RepoURL, - notification.Meta.RepoFullName, - ) - plainText += fmt.Sprintf(`%s pushed in %s *deployment skipped*`, - plainText, - notification.Meta.RepoFullName, - ) - subject += plainText + mainHTMLTpl = `{{.BranchName}}{{ if ne .ShortSha "" }} {{.ShortSha}}{{end}} pushed in {{.RepoFullName}} deployment skipped` + plainTextTpl = `[{{.ProjectName}}] {{.BranchName}}{{ if ne .ShortSha "" }} ({{.ShortSha}}){{end}} pushed in {{.RepoFullName}} *deployment skipped*` case "deployEnvironment": - mainHTML += fmt.Sprintf("Deployment triggered %s", - notification.Meta.BranchName, - ) - plainText += fmt.Sprintf("[%s] Deployment triggered on branch %s", - notification.Meta.ProjectName, - notification.Meta.BranchName, - ) - if notification.Meta.ShortSha != "" { - mainHTML += fmt.Sprintf(`%s (%s)`, - mainHTML, - notification.Meta.ShortSha, - ) - plainText += fmt.Sprintf(`%s (%s)`, - plainText, - notification.Meta.ShortSha, - ) - } - subject += plainText + mainHTMLTpl = `Deployment triggered {{.BranchName}}{{ if ne .ShortSha "" }} ({{.ShortSha}}){{end}}` + plainTextTpl = `[{{.ProjectName}}] Deployment triggered on branch {{.BranchName}}{{ if ne .ShortSha "" }} ({{.ShortSha}}){{end}}` case "removeFinished": - mainHTML += fmt.Sprintf("Remove %s", notification.Meta.OpenshiftProject) - plainText += fmt.Sprintf("[%s] remove %s", notification.Meta.ProjectName, notification.Meta.OpenshiftProject) - subject += plainText + mainHTMLTpl = `Remove {{.OpenshiftProject}}` + plainTextTpl = `[{{.ProjectName}] remove {{.OpenshiftProject}}` case "notDeleted": - mainHTML += fmt.Sprintf("%s not deleted.", - notification.Meta.OpenshiftProject, - ) - plainText += fmt.Sprintf("[%s] %s not deleted.", - notification.Meta.ProjectName, - notification.Meta.OpenshiftProject, - ) - subject += plainText - plainText += fmt.Sprintf("%s", notification.Meta.Error) + mainHTMLTpl = `{{.OpenshiftProject}} not deleted.` + plainTextTpl = `[{{.ProjectName}] {{.OpenshiftProject}} not deleted. {{.Error}}` case "deployError": - mainHTML += fmt.Sprintf("[%s]", + mainHTMLTpl = `[{{.ProjectName}}] {{.BranchName}}{{ if ne .ShortSha "" }} ({{.ShortSha}}){{end}} Build {{.BuildName}} error. +{{if ne .LogLink ""}} Logs{{end}}` + plainTextTpl = `[{{.ProjectName}}] {{.BranchName}}{{ if ne .ShortSha "" }} ({{.ShortSha}}){{end}} Build {{.BuildName}} error. +{{if ne .LogLink ""}} [Logs]({{.LogLink}}){{end}}` + subject += fmt.Sprintf("[%s] %s Build %s error.", notification.Meta.ProjectName, - ) - plainText += fmt.Sprintf("[%s]", - notification.Meta.ProjectName, - ) - if notification.Meta.ShortSha != "" { - mainHTML += fmt.Sprintf(` %s (%s)`, - notification.Meta.BranchName, - notification.Meta.ShortSha, - ) - plainText += fmt.Sprintf(` %s (%s)`, - notification.Meta.BranchName, - notification.Meta.ShortSha, - ) - } else { - mainHTML += fmt.Sprintf(` %s`, - notification.Meta.BranchName, - ) - plainText += fmt.Sprintf(` %s`, - notification.Meta.BranchName, - ) - } - mainHTML += fmt.Sprintf(` Build %s error.`, - notification.Meta.BuildName, - ) - plainText += fmt.Sprintf(` Build %s error.`, + notification.Meta.BranchName, notification.Meta.BuildName, ) - subject += plainText - if notification.Meta.LogLink != "" { - mainHTML += fmt.Sprintf(` Logs`, - notification.Meta.LogLink, - ) - plainText += fmt.Sprintf(` [Logs](%s)`, - notification.Meta.LogLink, - ) - } case "deployFinished": - mainHTML += fmt.Sprintf("[%s]", + mainHTMLTpl = `[{{.ProjectName}}] {{.BranchName}}{{ if ne .ShortSha "" }} ({{.ShortSha}}){{end}} Build {{.BuildName}} complete. {{if ne .LogLink ""}}Logs{{end}} +

+ +
+

+

` + plainTextTpl = `[{{.ProjectName}}] {{.BranchName}}{{ if ne .ShortSha "" }} ({{.ShortSha}}){{end}} Build {{.BuildName}} complete. {{if ne .LogLink ""}}[Logs]({{.LogLink}}){{end}} +{{.Route}} +{{range .Routes}}{{if ne . $.Route}}{{.}} +{{end}}{{end}}` + subject += fmt.Sprintf("[%s] %s Build %s complete.", notification.Meta.ProjectName, - ) - plainText += fmt.Sprintf("[%s]", - notification.Meta.ProjectName, - ) - if notification.Meta.ShortSha != "" { - mainHTML += fmt.Sprintf(` %s (%s)`, - notification.Meta.BranchName, - notification.Meta.ShortSha, - ) - plainText += fmt.Sprintf(` %s (%s)`, - notification.Meta.BranchName, - notification.Meta.ShortSha, - ) - } else { - mainHTML += fmt.Sprintf(` %s`, - notification.Meta.BranchName, - ) - plainText += fmt.Sprintf(` %s`, - notification.Meta.BranchName, - ) - } - mainHTML += fmt.Sprintf(` Build %s complete.`, - notification.Meta.BuildName, - ) - plainText += fmt.Sprintf(` Build %s complete.`, + notification.Meta.BranchName, notification.Meta.BuildName, ) - subject += plainText - if notification.Meta.LogLink != "" { - mainHTML += fmt.Sprintf(` Logs`, - notification.Meta.LogLink, - ) - plainText += fmt.Sprintf(` [Logs](%s)`, - notification.Meta.LogLink, - ) + case "problemNotification": + eventSplit := strings.Split(notification.Event, ":") + if eventSplit[0] != "problem" && eventSplit[1] == "insert" { + return "", "", "", "", "", nil } - additional += fmt.Sprintf(`
  • %s
  • `, - notification.Meta.Route, - notification.Meta.Route, - ) - plainText += fmt.Sprintf("%s %s\n", - plainText, - notification.Meta.Route, + mainHTMLTpl = `[{{.ProjectName}}] New problem found for {{.EnvironmentName}} +
    • * Service: {{.ServiceName}}
    • {{ if ne .Severity "" }} +
    • * Severity: {{.Severity}}{{end}}
    • {{ if ne .Description "" }} +
    • * Description: {{.Description}}
    • {{end}}
    ` + plainTextTpl = `[{{.ProjectName}}] New problem found for ` + "`{{.EnvironmentName}}`" + ` +* Service: ` + "`{{.ServiceName}}`" + `{{ if ne .Severity "" }} +* Severity: {{.Severity}}{{end}}{{ if ne .Description "" }} +* Description: {{.Description}}{{end}}` + subject += fmt.Sprintf("[%s] New problem found for environment %s", + notification.Meta.ProjectName, + notification.Meta.EnvironmentName, ) - if len(notification.Meta.Routes) != 0 { - for _, r := range notification.Meta.Routes { - if r != notification.Meta.Route { - additional += fmt.Sprintf(`
  • %s
  • `, - r, - r, - ) - plainText += fmt.Sprintf(" %s\n", - r, - ) - } - } - } - mainHTML += fmt.Sprintf(`
`) + default: + return "", "", "", "", "", nil } - t, _ := template.New("email").Parse(htmlTemplate) + var body bytes.Buffer + t, _ := template.New("email").Parse(mainHTMLTpl) + t.Execute(&body, notification.Meta) + mainHTML += body.String() + + var plainTextBuffer bytes.Buffer + t, _ = template.New("email").Parse(plainTextTpl) + t.Execute(&plainTextBuffer, notification.Meta) + plainText += plainTextBuffer.String() + if subject == "" { + subject = plainText + } + return emoji, color, subject, mainHTML, plainText, nil +} + +func (h *Messaging) sendEmailMessage(emoji, color, subject, event, project, emailAddress, mainHTML, plainText string) { var body bytes.Buffer - mimeHeaders := "MIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\n\n" - body.Write([]byte(fmt.Sprintf("From: Lagoon Notifications\nSubject: %s \n%s\n\n", subject, mimeHeaders))) + // mimeHeaders := "MIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\n\n" + // body.Write([]byte(fmt.Sprintf("From: Lagoon Notifications<%s>\nSubject: %s \n%s\n\n", h.EmailSender, subject, mimeHeaders))) + t, _ := template.New("email").Parse(htmlTemplate) t.Execute(&body, struct { Color string Emoji string Title string ProjectName string MainHTML string - Additional string }{ Title: plainText, Color: color, Emoji: emoji, - ProjectName: notification.Meta.ProjectName, + ProjectName: project, MainHTML: mainHTML, - Additional: additional, }) - // Configuration - from := "notifications@lagoon.sh" - password := "" - to := []string{emailAddress} - smtpHost := "localhost" - smtpPort := "1025" - // Create authentication - auth := smtp.PlainAuth("", from, password, smtpHost) - // Send actual message - err = smtp.SendMail(smtpHost+":"+smtpPort, auth, from, to, body.Bytes()) - if err != nil { - log.Printf("Error sending message to email: %v", err) - return + m := gomail.NewMessage() + m.SetHeader("From", h.EmailSender) + m.SetHeader("To", emailAddress) + m.SetHeader("Subject", subject) + m.SetBody("text/plain", plainText) + m.AddAlternative("text/html", body.String()) + sPort, _ := strconv.Atoi(h.EmailPort) + d := gomail.NewDialer(h.EmailHost, sPort, h.EmailSender, "") + d.TLSConfig = &tls.Config{InsecureSkipVerify: h.EmailInsecureSkipVerify} + if err := d.DialAndSend(m); err != nil { + fmt.Println(err) + panic(err) } - log.Println(fmt.Sprintf("Sent %s message to email", notification.Event)) + + // // Create authentication + // auth := smtp.PlainAuth("", sender, password, smtpHost) + // // Send actual message + // err := smtp.SendMail(smtpHost+":"+smtpPort, auth, sender, to, body.Bytes()) + // if err != nil { + // log.Printf("Error sending message to email: %v", err) + // return + // } + log.Println(fmt.Sprintf("Sent %s message to email", event)) } func getEmailEvent(msgEvent string) (string, string, string, error) { diff --git a/services/logs2notifications/internal/handler/main.go b/services/logs2notifications/internal/handler/main.go index d430ff414d..cecb342057 100644 --- a/services/logs2notifications/internal/handler/main.go +++ b/services/logs2notifications/internal/handler/main.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "log" + "regexp" "time" "github.com/cheshir/go-mq" @@ -12,6 +13,7 @@ import ( "github.com/uselagoon/lagoon/services/logs2notifications/internal/lagoon" lclient "github.com/uselagoon/lagoon/services/logs2notifications/internal/lagoon/client" "github.com/uselagoon/lagoon/services/logs2notifications/internal/lagoon/jwt" + "github.com/uselagoon/lagoon/services/logs2notifications/internal/schema" ) // RabbitBroker . @@ -57,6 +59,16 @@ type Messaging struct { DisableEmail bool DisableWebhooks bool DisableS3 bool + EmailSender string + EmailSenderPassword string + EmailHost string + EmailPort string + EmailInsecureSkipVerify bool + S3FilesAccessKeyID string + S3FilesSecretAccessKey string + S3FilesBucket string + S3FilesRegion string + S3FilesOrigin string } // Notification . @@ -91,6 +103,9 @@ type Notification struct { Environment string `json:"environment"` EnvironmentID string `json:"environmentId"` EnvironmentName string `json:"environmentName"` + ServiceName string `json:"serviceName"` + Severity string `json:"severity"` + Description string `json:"description"` Error string `json:"error"` JobName string `json:"jobName"` LogLink string `json:"logLink"` @@ -106,7 +121,9 @@ type Notification struct { RepoURL string `json:"repoUrl"` Route string `json:"route"` Routes []string `json:"routes"` - Task string `json:"task"` + Task struct { + ID int `json:"id"` + } `json:"task"` } `json:"meta"` Message string `json:"message"` } @@ -119,7 +136,15 @@ type EventMap struct { } // NewMessaging returns a messaging with config -func NewMessaging(config mq.Config, lagoonAPI LagoonAPI, startupAttempts int, startupInterval int, enableDebug bool, appID string, disableSlack, disableRocketChat, disableMicrosoftTeams, disableEmail, disableWebhooks, disableS3 bool) *Messaging { +func NewMessaging(config mq.Config, + lagoonAPI LagoonAPI, + startupAttempts int, + startupInterval int, + enableDebug bool, + appID string, + disableSlack, disableRocketChat, disableMicrosoftTeams, disableEmail, disableWebhooks, disableS3 bool, + emailSender, emailSenderPassword, emailHost, emailPort string, emailInsecureSkipVerify bool, + s3FilesAccessKeyID, s3FilesSecretAccessKey, s3FilesBucket, s3FilesRegion, s3FilesOrigin string) *Messaging { return &Messaging{ Config: config, LagoonAPI: lagoonAPI, @@ -133,12 +158,21 @@ func NewMessaging(config mq.Config, lagoonAPI LagoonAPI, startupAttempts int, st DisableEmail: disableEmail, DisableWebhooks: disableWebhooks, DisableS3: disableS3, + EmailSender: emailSender, + EmailSenderPassword: emailSenderPassword, + EmailHost: emailHost, + EmailPort: emailPort, + EmailInsecureSkipVerify: emailInsecureSkipVerify, + S3FilesAccessKeyID: s3FilesAccessKeyID, + S3FilesSecretAccessKey: s3FilesSecretAccessKey, + S3FilesBucket: s3FilesBucket, + S3FilesRegion: s3FilesRegion, + S3FilesOrigin: s3FilesOrigin, } } // Consumer handles consuming messages sent to the queue that this action handler is connected to and processes them accordingly func (h *Messaging) Consumer() { - ctx := context.TODO() var messageQueue mq.MQ // if no mq is found when the goroutine starts, retry a few times before exiting @@ -175,25 +209,38 @@ func (h *Messaging) Consumer() { // Handle any tasks that go to the queue log.Println("Listening for messages in queue lagoon-logs:notifications") err = messageQueue.SetConsumerHandler("notifications-queue", func(message mq.Message) { - notification := &Notification{} - json.Unmarshal(message.Body(), notification) - switch notification.Event { - // check if this a `deployEnvironmentLatest` type of action - // and perform the steps to run the mutation against the lagoon api - case "api:unknownEvent": - default: + h.processMessage(message.Body(), message.AppId()) + message.Ack(false) // ack to remove from queue + }) + if err != nil { + log.Println(fmt.Sprintf("Failed to set handler to consumer `%s`: %v", "items-queue", err)) + } + <-forever +} + +func (h *Messaging) processMessage(message []byte, applicationID string) { + ctx := context.Background() + notification := &Notification{} + json.Unmarshal(message, notification) + + var buildLogs = regexp.MustCompile(`^build-logs:builddeploy-kubernetes:.*`) + var taskLogs = regexp.MustCompile(`^(build|task)-logs:job-kubernetes:.*`) + switch notification.Event { + case buildLogs.FindString(notification.Event): + // if this is a build logs message handle it accordingly + if !h.DisableS3 { + h.SendToS3(notification, buildMessageType) + } + case taskLogs.FindString(notification.Event): + // if this is a task logs message handle it accordingly + if !h.DisableS3 { + h.SendToS3(notification, taskMessageType) + } + default: + // all other events are notifications, so do notification handling with them + if notification.Project != "" { // marshal unmarshal the data into the input we need to use when talking to the lagoon api - token, err := jwt.OneMinuteAdminToken(h.LagoonAPI.TokenSigningKey, h.LagoonAPI.JWTAudience, h.LagoonAPI.JWTSubject, h.LagoonAPI.JWTIssuer) - if err != nil { - // the token wasn't generated - if h.EnableDebug { - log.Println(err) - } - break - } - // get all notifications for said project - l := lclient.New(h.LagoonAPI.Endpoint, token, "logs2notifications", false) - projectNotifications, err := lagoon.NotificationsForProject(ctx, notification.Project, l) + projectNotifications, err := h.getProjectNotifictions(ctx, notification.Project) if err != nil { log.Println(err) break @@ -201,49 +248,49 @@ func (h *Messaging) Consumer() { if projectNotifications.Notifications != nil { if len(projectNotifications.Notifications.Slack) > 0 && !h.DisableSlack { for _, slack := range projectNotifications.Notifications.Slack { - SendToSlack(notification, slack.Channel, slack.Webhook, message.AppId()) + h.SendToSlack(notification, slack.Channel, slack.Webhook, applicationID) } } if len(projectNotifications.Notifications.RocketChat) > 0 && !h.DisableRocketChat { for _, rc := range projectNotifications.Notifications.RocketChat { - SendToRocketChat(notification, rc.Channel, rc.Webhook, message.AppId()) + h.SendToRocketChat(notification, rc.Channel, rc.Webhook, applicationID) } } if len(projectNotifications.Notifications.Email) > 0 && !h.DisableEmail { for _, email := range projectNotifications.Notifications.Email { - SendToEmail(notification, email.EmailAddress, message.AppId()) + h.SendToEmail(notification, email.EmailAddress) } } if len(projectNotifications.Notifications.MicrosoftTeams) > 0 && !h.DisableMicrosoftTeams { for _, teams := range projectNotifications.Notifications.MicrosoftTeams { - SendToMicrosoftTeams(notification, teams.Webhook, message.AppId()) + h.SendToMicrosoftTeams(notification, teams.Webhook) } } - // if len(projectNotifications.Notifications.Webhook) > 0 { - // fmt.Println(projectNotifications.Notifications.Webhook) + // if len(projectNotifications.Notifications.Webhook) > 0 && !h.DisableWebhooks { + // for _, hook := range projectNotifications.Notifications.Webhook { + // h.SendToWebhook(notification, hook.Webhook) + // } // } } } - message.Ack(false) // ack to remove from queue - }) - if err != nil { - log.Println(fmt.Sprintf("Failed to set handler to consumer `%s`: %v", "items-queue", err)) } - <-forever } -// toLagoonLogs sends logs to the lagoon-logs message queue -func (h *Messaging) toLagoonLogs(messageQueue mq.MQ, message map[string]interface{}) { - msgBytes, err := json.Marshal(message) +func (h *Messaging) getProjectNotifictions(ctx context.Context, projectName string) (*schema.Project, error) { + token, err := jwt.OneMinuteAdminToken(h.LagoonAPI.TokenSigningKey, h.LagoonAPI.JWTAudience, h.LagoonAPI.JWTSubject, h.LagoonAPI.JWTIssuer) if err != nil { + // the token wasn't generated if h.EnableDebug { - log.Println(err, "Unable to encode message as JSON") + log.Println(err) } + return nil, err } - producer, err := messageQueue.AsyncProducer("lagoon-logs") + // get all notifications for said project + l := lclient.New(h.LagoonAPI.Endpoint, token, "logs2notifications", false) + projectNotifications, err := lagoon.NotificationsForProject(ctx, projectName, l) if err != nil { - log.Println(fmt.Sprintf("Failed to get async producer: %v", err)) - return + log.Println(err) + return nil, err } - producer.Produce(msgBytes) + return projectNotifications, nil } diff --git a/services/logs2notifications/internal/handler/main_test.go b/services/logs2notifications/internal/handler/main_test.go index 0adc367c3f..324e619d00 100644 --- a/services/logs2notifications/internal/handler/main_test.go +++ b/services/logs2notifications/internal/handler/main_test.go @@ -2,8 +2,12 @@ package handler import ( "bytes" + "encoding/json" + "io/ioutil" "reflect" "testing" + + "github.com/cheshir/go-mq" ) func checkEqual(t *testing.T, got, want interface{}, msgs ...interface{}) { @@ -15,4 +19,177 @@ func checkEqual(t *testing.T, got, want interface{}, msgs ...interface{}) { } t.Errorf(buf.String(), got, want) } -} \ No newline at end of file +} + +func TestProcessing(t *testing.T) { + config := mq.Config{} + graphQLConfig := LagoonAPI{ + // Endpoint: svr.URL, + // TokenSigningKey: "jwtTokenSigningKey", + // JWTAudience: "jwtAudience", + // JWTSubject: "jwtSubject", + // JWTIssuer: "jwtIssuer", + } + messaging := NewMessaging(config, + graphQLConfig, + 1, + 1, + true, + "lagoonAppID", + false, + false, + false, + false, + false, + false, + "emailSender", + "emailSenderPassword", + "emailHost", + "emailPort", + true, + "s3FilesAccessKeyID", + "s3FilesSecretAccessKey", + "s3FilesBucket", + "s3FilesRegion", + "s3FilesOrigin", + ) + var testCases = map[string]struct { + input string + description string + slack string + rocketchat string + emailhtml string + emailplain string + teams string + }{ + "repoPushHandledGithub": { + description: "test github repo push handled events", + input: "testdata/input.repoPushHandledGithub.json", + slack: "testdata/repoPushHandled/slack.txt", + rocketchat: "testdata/repoPushHandled/rocketchat.txt", + emailhtml: "testdata/repoPushHandled/emailhtml.txt", + emailplain: "testdata/repoPushHandled/emailplain.txt", + teams: "testdata/repoPushHandled/teams.txt", + }, + "repoPushSkippedGithub": { + description: "test github repo push skipped events", + input: "testdata/input.repoPushSkippedGithub.json", + slack: "testdata/repoPushSkipped/slack.txt", + rocketchat: "testdata/repoPushSkipped/rocketchat.txt", + emailhtml: "testdata/repoPushSkipped/emailhtml.txt", + emailplain: "testdata/repoPushSkipped/emailplain.txt", + teams: "testdata/repoPushSkipped/teams.txt", + }, + "deleteEnvironmentGithub": { + description: "test github repo push deleted events", + input: "testdata/input.deleteEnvironmentGithub.json", + slack: "testdata/deleteEnvironment/slack.txt", + rocketchat: "testdata/deleteEnvironment/rocketchat.txt", + emailhtml: "testdata/deleteEnvironment/emailhtml.txt", + emailplain: "testdata/deleteEnvironment/emailplain.txt", + teams: "testdata/deleteEnvironment/teams.txt", + }, + "deployEnvironment": { + description: "test github repo push deleted events", + input: "testdata/input.deployEnvironment.json", + slack: "testdata/deployEnvironment/slack.txt", + rocketchat: "testdata/deployEnvironment/rocketchat.txt", + emailhtml: "testdata/deployEnvironment/emailhtml.txt", + emailplain: "testdata/deployEnvironment/emailplain.txt", + teams: "testdata/deployEnvironment/teams.txt", + }, + "deployError": { + description: "test github repo push deleted events", + input: "testdata/input.deployError.json", + slack: "testdata/deployError/slack.txt", + rocketchat: "testdata/deployError/rocketchat.txt", + emailhtml: "testdata/deployError/emailhtml.txt", + emailplain: "testdata/deployError/emailplain.txt", + teams: "testdata/deployError/teams.txt", + }, + "deployFinished": { + description: "test github repo push deleted events", + input: "testdata/input.deployFinished.json", + slack: "testdata/deployFinished/slack.txt", + rocketchat: "testdata/deployFinished/rocketchat.txt", + emailhtml: "testdata/deployFinished/emailhtml.txt", + emailplain: "testdata/deployFinished/emailplain.txt", + teams: "testdata/deployFinished/teams.txt", + }, + } + for name, tc := range testCases { + t.Run(name, func(tt *testing.T) { + // read the input into a the notification struct + inputBytes, err := ioutil.ReadFile(tc.input) // just pass the file name + if err != nil { + tt.Fatalf("unexpected error %v", err) + } + notification := &Notification{} + json.Unmarshal(inputBytes, notification) + + // process slack template + resultBytes, err := ioutil.ReadFile(tc.slack) // just pass the file name + if err != nil { + tt.Fatalf("unexpected error %v", err) + } + _, _, message, err := messaging.processSlackTemplate(notification) + if err != nil { + tt.Fatalf("unexpected error %v", err) + } + if message != string(resultBytes) { + t.Log(string(message)) + tt.Fatalf("message doesn't match") + } + + // process rocketchat template + resultBytes, err = ioutil.ReadFile(tc.rocketchat) // just pass the file name + if err != nil { + tt.Fatalf("unexpected error %v", err) + } + _, _, message, err = messaging.processRocketChatTemplate(notification) + if err != nil { + tt.Fatalf("unexpected error %v", err) + } + if message != string(resultBytes) { + t.Log(string(message)) + tt.Fatalf("message doesn't match") + } + + // process email templates + resultBytesHTML, err := ioutil.ReadFile(tc.emailhtml) // just pass the file name + if err != nil { + tt.Fatalf("unexpected error %v", err) + } + resultBytesPlainText, err := ioutil.ReadFile(tc.emailplain) // just pass the file name + if err != nil { + tt.Fatalf("unexpected error %v", err) + } + _, _, _, htmlMessage, plaintextMessage, err := messaging.processEmailTemplates(notification) + if err != nil { + tt.Fatalf("unexpected error %v", err) + } + if htmlMessage != string(resultBytesHTML) { + t.Log(string(htmlMessage)) + tt.Fatalf("html message doesn't match") + } + if plaintextMessage != string(resultBytesPlainText) { + t.Log(string(plaintextMessage)) + tt.Fatalf("plaintext message doesn't match") + } + + // process teams template + resultBytes, err = ioutil.ReadFile(tc.teams) // just pass the file name + if err != nil { + tt.Fatalf("unexpected error %v", err) + } + _, _, message, err = messaging.processMicrosoftTeamsTemplate(notification) + if err != nil { + tt.Fatalf("unexpected error %v", err) + } + if message != string(resultBytes) { + t.Log(string(message)) + tt.Fatalf("message doesn't match") + } + }) + } +} diff --git a/services/logs2notifications/internal/handler/microsoftteams_events.go b/services/logs2notifications/internal/handler/microsoftteams_events.go index cc99b05331..87df357d1a 100644 --- a/services/logs2notifications/internal/handler/microsoftteams_events.go +++ b/services/logs2notifications/internal/handler/microsoftteams_events.go @@ -6,6 +6,8 @@ import ( "fmt" "log" "net/http" + "strings" + "text/template" ) // MicrosoftTeamsData . @@ -25,181 +27,101 @@ type MicrosoftTeamsSection struct { } // SendToMicrosoftTeams . -func SendToMicrosoftTeams(notification *Notification, webhook, appID string) { - - emoji, color, template, err := getMicrosoftTeamsEvent(notification.Event) +func (h *Messaging) SendToMicrosoftTeams(notification *Notification, webhook string) { + emoji, color, message, err := h.processMicrosoftTeamsTemplate(notification) if err != nil { return } + h.sendMicrosoftTeamsMessage(emoji, color, webhook, notification.Event, notification.Meta.ProjectName, message) +} - var text string - switch template { +// processMicrosoftTeamsTemplate . +func (h *Messaging) processMicrosoftTeamsTemplate(notification *Notification) (string, string, string, error) { + emoji, color, tpl, err := getMicrosoftTeamsEvent(notification.Event) + if err != nil { + eventSplit := strings.Split(notification.Event, ":") + if eventSplit[0] != "problem" { + return "", "", "", fmt.Errorf("no matching event") + } + if eventSplit[1] == "insert" { + tpl = "problemNotification" + } + } + + var teamsTpl string + switch tpl { case "mergeRequestOpened": - text = fmt.Sprintf("PR [#%s (%s)](%s) opened in [%s](%s)", - notification.Meta.PullrequestNumber, - notification.Meta.PullrequestTitle, - notification.Meta.PullrequestURL, - notification.Meta.RepoName, - notification.Meta.RepoURL, - ) + teamsTpl = `PR [#{{.PullrequestNumber}} ({{.PullrequestTitle}})]({{.PullrequestURL}}) opened in [{{.RepoName}}]({{.RepoURL}})` case "mergeRequestUpdated": - text = fmt.Sprintf("PR [#%s (%s)](%s) updated in [%s](%s)", - notification.Meta.PullrequestNumber, - notification.Meta.PullrequestTitle, - notification.Meta.PullrequestURL, - notification.Meta.RepoName, - notification.Meta.RepoURL, - ) + teamsTpl = `PR [#{{.PullrequestNumber}} ({{.PullrequestTitle}})]({{.PullrequestURL}}) updated in [{{.RepoName}}]({{.RepoURL}})` case "mergeRequestClosed": - text = fmt.Sprintf("PR [#%s (%s)](%s) closed in [%s](%s)", - notification.Meta.PullrequestNumber, - notification.Meta.PullrequestTitle, - notification.Meta.PullrequestURL, - notification.Meta.RepoName, - notification.Meta.RepoURL, - ) + teamsTpl = `PR [#{{.PullrequestNumber}} ({{.PullrequestTitle}})]({{.PullrequestURL}}) closed in [{{.RepoName}}]({{.RepoURL}})` case "deleteEnvironment": - text = fmt.Sprintf("Deleting environment `%s`", - notification.Meta.EnvironmentName, - ) + teamsTpl = `Deleting environment ` + "`{{.EnvironmentName}}`" case "repoPushHandled": - text = fmt.Sprintf("[%s](%s/tree/%s)", - notification.Meta.BranchName, - notification.Meta.RepoURL, - notification.Meta.BranchName, - ) - if notification.Meta.ShortSha != "" { - text = fmt.Sprintf("%s ([%s](%s))", - text, - notification.Meta.ShortSha, - notification.Meta.CommitURL, - ) - } - text = fmt.Sprintf("%s pushed in [%s](%s)", - text, - notification.Meta.RepoFullName, - notification.Meta.RepoURL, - ) + teamsTpl = `[{{.BranchName}}]({{.RepoURL}}/tree/{{.BranchName}}){{ if ne .ShortSha "" }} ([{{.ShortSha}}]({{.CommitURL}})){{end}} pushed in [{{.RepoFullName}}]({{.RepoURL}})` case "repoPushSkipped": - text = fmt.Sprintf("[%s](%s/tree/%s)", - notification.Meta.BranchName, - notification.Meta.RepoURL, - notification.Meta.BranchName, - ) - if notification.Meta.ShortSha != "" { - text = fmt.Sprintf("%s ([%s](%s))", - text, - notification.Meta.ShortSha, - notification.Meta.CommitURL, - ) - } - text = fmt.Sprintf("%s pushed in [%s](%s) *deployment skipped*", - text, - notification.Meta.RepoFullName, - notification.Meta.RepoURL, - ) + teamsTpl = `[{{.BranchName}}]({{.RepoURL}}/tree/{{.BranchName}}){{ if ne .ShortSha "" }} ([{{.ShortSha}}]({{.CommitURL}})){{end}} pushed in [{{.RepoFullName}}]({{.RepoURL}}) *deployment skipped*` case "deployEnvironment": - text = fmt.Sprintf("Deployment triggered `%s`", - notification.Meta.BranchName, - ) - if notification.Meta.ShortSha != "" { - text = fmt.Sprintf("%s (%s)", - text, - notification.Meta.ShortSha, - ) - } + teamsTpl = `Deployment triggered ` + "`{{.BranchName}}`" + `{{ if ne .ShortSha "" }} ({{.ShortSha}}){{end}}` case "removeFinished": - text = fmt.Sprintf("Removed `%s`", - notification.Meta.OpenshiftProject, - ) + teamsTpl = `Removed ` + "`{{.OpenshiftProject}}`" + `` case "removeRetry": - text = fmt.Sprintf("Removed `%s`", - notification.Meta.OpenshiftProject, - ) + teamsTpl = `Removed ` + "`{{.OpenshiftProject}}`" + `` case "notDeleted": - text = fmt.Sprintf("`%s` not deleted. %s", - notification.Meta.BranchName, - notification.Meta.Error, - ) + teamsTpl = "`{{.BranchName}}`" + ` not deleted. {{.Error}}` case "deployError": - if notification.Meta.ShortSha != "" { - text += fmt.Sprintf("`%s` %s", - notification.Meta.BranchName, - notification.Meta.ShortSha, - ) - } else { - text += fmt.Sprintf(" `%s`", - notification.Meta.BranchName, - ) - } - text += fmt.Sprintf(" Build `%s` Failed.", - notification.Meta.BuildName, - ) - if notification.Meta.LogLink != "" { - text += fmt.Sprintf(" [Logs](%s) \r", - notification.Meta.LogLink, - ) - } + teamsTpl = "`{{.BranchName}}`" + `{{ if ne .ShortSha "" }} ({{.ShortSha}}){{end}} Build ` + "`{{.BuildName}}`" + ` Failed. {{if ne .LogLink ""}} [Logs]({{.LogLink}}){{end}}` case "deployFinished": - if notification.Meta.ShortSha != "" { - text += fmt.Sprintf("`%s` %s", - notification.Meta.BranchName, - notification.Meta.ShortSha, - ) - } else { - text += fmt.Sprintf("`%s`", - notification.Meta.BranchName, - ) - } - text += fmt.Sprintf(" Build `%s` Succeeded.", - notification.Meta.BuildName, - ) - if notification.Meta.LogLink != "" { - text += fmt.Sprintf(" [Logs](%s) \r", - notification.Meta.LogLink, - ) - } - text += fmt.Sprintf("* %s \n", - notification.Meta.Route, - ) - if len(notification.Meta.Routes) != 0 { - for _, r := range notification.Meta.Routes { - if r != notification.Meta.Route { - text += fmt.Sprintf("* %s \n", r) - } - } + teamsTpl = "`{{.BranchName}}`" + `{{ if ne .ShortSha "" }} ({{.ShortSha}}){{end}} Build ` + "`{{.BuildName}}`" + ` Succeeded. {{if ne .LogLink ""}} [Logs]({{.LogLink}}){{end}} +* {{.Route}}{{range .Routes}}{{if ne . $.Route}}* {{.}}{{end}} +{{end}}` + case "problemNotification": + eventSplit := strings.Split(notification.Event, ":") + if eventSplit[0] != "problem" && eventSplit[1] == "insert" { + return "", "", "", fmt.Errorf("no matching event") } + teamsTpl = `*[{{.ProjectName}}]* New problem found for ` + "`{{.EnvironmentName}}`" + ` +* Service: ` + "`{{.ServiceName}}`" + `{{ if ne .Severity "" }} +* Severity: {{.Severity}}{{end}}{{ if ne .Description "" }} +* Description: {{.Description}}{{end}}` default: - // do nothing - return + return "", "", "", fmt.Errorf("no matching event") } - data := MicrosoftTeamsData{ + var teamsMsg bytes.Buffer + t, _ := template.New("microsoftteams").Parse(teamsTpl) + t.Execute(&teamsMsg, notification.Meta) + return emoji, color, teamsMsg.String(), nil +} + +func (h *Messaging) sendMicrosoftTeamsMessage(emoji, color, webhook, event, project, message string) { + teamsPayload := MicrosoftTeamsData{ Type: "MessageCard", Context: "http://schema.org/extensions", - Summary: text, - Title: notification.Meta.ProjectName, + Summary: message, + Title: project, ThemeColor: color, Sections: []MicrosoftTeamsSection{ { - ActivityText: text, + ActivityText: message, ActivityImage: emoji, }, }, } - jsonBytes, _ := json.Marshal(data) - req, err := http.NewRequest("POST", webhook, bytes.NewBuffer(jsonBytes)) + teamsPayloadBytes, _ := json.Marshal(teamsPayload) + req, err := http.NewRequest("POST", webhook, bytes.NewBuffer(teamsPayloadBytes)) req.Header.Set("Content-Type", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { - log.Printf("Error sending message to rocketchat: %v", err) + log.Printf("Error sending message to microsoft teams: %v", err) return } defer resp.Body.Close() - log.Println(fmt.Sprintf("Sent %s message to rocketchat", notification.Event)) + log.Println(fmt.Sprintf("Sent %s message to microsoft teams", event)) } func getMicrosoftTeamsEvent(msgEvent string) (string, string, string, error) { diff --git a/services/logs2notifications/internal/handler/rocketchat_events.go b/services/logs2notifications/internal/handler/rocketchat_events.go index 1657a0d150..978d0c723e 100644 --- a/services/logs2notifications/internal/handler/rocketchat_events.go +++ b/services/logs2notifications/internal/handler/rocketchat_events.go @@ -6,6 +6,8 @@ import ( "fmt" "log" "net/http" + "strings" + "text/template" ) // RocketChatData . @@ -29,177 +31,79 @@ type RocketChatAttachmentField struct { } // SendToRocketChat . -func SendToRocketChat(notification *Notification, channel, webhook, appID string) { - - emoji, color, template, err := getRocketChatEvent(notification.Event) +func (h *Messaging) SendToRocketChat(notification *Notification, channel, webhook, appID string) { + emoji, color, message, err := h.processRocketChatTemplate(notification) if err != nil { return } + h.sendRocketChatMessage(emoji, color, appID, channel, webhook, notification.Event, message) +} + +// SendToRocketChat . +func (h *Messaging) processRocketChatTemplate(notification *Notification) (string, string, string, error) { + emoji, color, tpl, err := getRocketChatEvent(notification.Event) + if err != nil { + eventSplit := strings.Split(notification.Event, ":") + if eventSplit[0] != "problem" { + return "", "", "", fmt.Errorf("no matching event") + } + if eventSplit[1] == "insert" { + tpl = "problemNotification" + } + } - var text string - switch template { + var rcTpl string + switch tpl { case "mergeRequestOpened": - text = fmt.Sprintf("*[%s]* PR [#%s (%s)](%s) opened in [%s](%s)", - notification.Meta.ProjectName, - notification.Meta.PullrequestNumber, - notification.Meta.PullrequestTitle, - notification.Meta.PullrequestURL, - notification.Meta.RepoName, - notification.Meta.RepoURL, - ) + rcTpl = `*[{{.ProjectName}}]* PR [#{{.PullrequestNumber}} ({{.PullrequestTitle}})]({{.PullrequestURL}}) opened in [{{.RepoName}}]({{.RepoURL}})` case "mergeRequestUpdated": - text = fmt.Sprintf("*[%s]* PR [#%s (%s)](%s) updated in [%s](%s)", - notification.Meta.ProjectName, - notification.Meta.PullrequestNumber, - notification.Meta.PullrequestTitle, - notification.Meta.PullrequestURL, - notification.Meta.RepoName, - notification.Meta.RepoURL, - ) + rcTpl = `*[{{.ProjectName}}]* PR [#{{.PullrequestNumber}} ({{.PullrequestTitle}})]({{.PullrequestURL}}) updated in [{{.RepoName}}]({{.RepoURL}})` case "mergeRequestClosed": - text = fmt.Sprintf("*[%s]* PR [#%s (%s)](%s) closed in [%s](%s)", - notification.Meta.ProjectName, - notification.Meta.PullrequestNumber, - notification.Meta.PullrequestTitle, - notification.Meta.PullrequestURL, - notification.Meta.RepoName, - notification.Meta.RepoURL, - ) + rcTpl = `*[{{.ProjectName}}]* PR [#{{.PullrequestNumber}} ({{.PullrequestTitle}})]({{.PullrequestURL}}) closed in [{{.RepoName}}]({{.RepoURL}})` case "deleteEnvironment": - text = fmt.Sprintf("*[%s]* Deleting environment `%s`", - notification.Meta.ProjectName, - notification.Meta.EnvironmentName, - ) + rcTpl = `*[{{.ProjectName}}]* Deleting environment ` + "`{{.EnvironmentName}}`" case "repoPushHandled": - text = fmt.Sprintf("*[%s]* [%s](%s/tree/%s)", - notification.Meta.ProjectName, - notification.Meta.BranchName, - notification.Meta.RepoURL, - notification.Meta.BranchName, - ) - if notification.Meta.ShortSha != "" { - text = fmt.Sprintf("%s ([%s](%s))", - text, - notification.Meta.ShortSha, - notification.Meta.CommitURL, - ) - } - text = fmt.Sprintf("%s pushed in [%s](%s)", - text, - notification.Meta.RepoFullName, - notification.Meta.RepoURL, - ) + rcTpl = `*[{{.ProjectName}}]* [{{.BranchName}}]({{.RepoURL}}/tree/{{.BranchName}}){{ if ne .ShortSha "" }} ([{{.ShortSha}}]({{.CommitURL}})){{end}} pushed in [{{.RepoFullName}}]({{.RepoURL}})` case "repoPushSkipped": - text = fmt.Sprintf("*[%s]* [%s](%s/tree/%s)", - notification.Meta.ProjectName, - notification.Meta.BranchName, - notification.Meta.RepoURL, - notification.Meta.BranchName, - ) - if notification.Meta.ShortSha != "" { - text = fmt.Sprintf("%s ([%s](%s))", - text, - notification.Meta.ShortSha, - notification.Meta.CommitURL, - ) - } - text = fmt.Sprintf("%s pushed in [%s](%s) *deployment skipped*", - text, - notification.Meta.RepoFullName, - notification.Meta.RepoURL, - ) + rcTpl = `*[{{.ProjectName}}]* [{{.BranchName}}]({{.RepoURL}}/tree/{{.BranchName}}){{ if ne .ShortSha "" }} ([{{.ShortSha}}]({{.CommitURL}})){{end}} pushed in [{{.RepoFullName}}]({{.RepoURL}}) *deployment skipped*` case "deployEnvironment": - text = fmt.Sprintf("*[%s]* Deployment triggered `%s`", - notification.Meta.ProjectName, - notification.Meta.BranchName, - ) - if notification.Meta.ShortSha != "" { - text = fmt.Sprintf("%s (%s)", - text, - notification.Meta.ShortSha, - ) - } + rcTpl = `*[{{.ProjectName}}]* Deployment triggered ` + "`{{.BranchName}}`" + `{{ if ne .ShortSha "" }} ({{.ShortSha}}){{end}}` case "removeFinished": - text = fmt.Sprintf("*[%s]* Removed `%s`", - notification.Meta.ProjectName, - notification.Meta.OpenshiftProject, - ) + rcTpl = `*[{{.ProjectName}}]* Removed ` + "`{{.OpenshiftProject}}`" + `` case "removeRetry": - text = fmt.Sprintf("*[%s]* Removed `%s`", - notification.Meta.ProjectName, - notification.Meta.OpenshiftProject, - ) + rcTpl = `*[{{.ProjectName}}]* Removed ` + "`{{.OpenshiftProject}}`" + `` case "notDeleted": - text = fmt.Sprintf("*[%s]* `%s` not deleted. %s", - notification.Meta.ProjectName, - notification.Meta.BranchName, - notification.Meta.Error, - ) + rcTpl = `*[{{.ProjectName}}]* ` + "`{{.BranchName}}`" + ` not deleted. {{.Error}}` case "deployError": - text = fmt.Sprintf("*[%s]*", - notification.Meta.ProjectName, - ) - if notification.Meta.ShortSha != "" { - text += fmt.Sprintf(" `%s` %s", - notification.Meta.BranchName, - notification.Meta.ShortSha, - ) - } else { - text += fmt.Sprintf(" `%s`", - notification.Meta.BranchName, - ) - } - text += fmt.Sprintf(" Build `%s` Failed.", - notification.Meta.BuildName, - ) - if notification.Meta.LogLink != "" { - text += fmt.Sprintf(" [Logs](%s) \r", - notification.Meta.LogLink, - ) - } + rcTpl = `*[{{.ProjectName}}]* ` + "`{{.BranchName}}`" + `{{ if ne .ShortSha "" }} ({{.ShortSha}}){{end}} Build ` + "`{{.BuildName}}`" + ` Failed. {{if ne .LogLink ""}} [Logs]({{.LogLink}}){{end}}` case "deployFinished": - text = fmt.Sprintf("*[%s]*", - notification.Meta.ProjectName, - ) - if notification.Meta.ShortSha != "" { - text += fmt.Sprintf(" `%s` %s", - notification.Meta.BranchName, - notification.Meta.ShortSha, - ) - } else { - text += fmt.Sprintf(" `%s`", - notification.Meta.BranchName, - ) - } - text += fmt.Sprintf(" Build `%s` Succeeded.", - notification.Meta.BuildName, - ) - if notification.Meta.LogLink != "" { - text += fmt.Sprintf(" [Logs](%s) \r", - notification.Meta.LogLink, - ) - } - text += fmt.Sprintf("* %s \n", - notification.Meta.Route, - ) - if len(notification.Meta.Routes) != 0 { - for _, r := range notification.Meta.Routes { - if r != notification.Meta.Route { - text += fmt.Sprintf("* %s \n", r) - } - } + rcTpl = `*[{{.ProjectName}}]* ` + "`{{.BranchName}}`" + `{{ if ne .ShortSha "" }} ({{.ShortSha}}){{end}} Build ` + "`{{.BuildName}}`" + ` Succeeded. {{if ne .LogLink ""}} [Logs]({{.LogLink}}){{end}} +* {{.Route}}{{range .Routes}}{{if ne . $.Route}}* {{.}}{{end}} +{{end}}` + case "problemNotification": + eventSplit := strings.Split(notification.Event, ":") + if eventSplit[0] != "problem" && eventSplit[1] == "insert" { + return "", "", "", fmt.Errorf("no matching event") } + rcTpl = `*[{{.ProjectName}}]* New problem found for ` + "`{{.EnvironmentName}}`" + ` +* Service: ` + "`{{.ServiceName}}`" + `{{ if ne .Severity "" }} +* Severity: {{.Severity}}{{end}}{{ if ne .Description "" }} +* Description: {{.Description}}{{end}}` default: - // do nothing - return + return "", "", "", fmt.Errorf("no matching event") } + var rcMsg bytes.Buffer + t, _ := template.New("rocketchat").Parse(rcTpl) + t.Execute(&rcMsg, notification.Meta) + return emoji, color, rcMsg.String(), nil +} +func (h *Messaging) sendRocketChatMessage(emoji, color, appID, channel, webhook, event, message string) { data := RocketChatData{ Channel: channel, Attachments: []RocketChatAttachment{ { - // Text: fmt.Sprintf("%s %s", emoji, notification.Message), - Text: fmt.Sprintf("%s %s", emoji, text), + Text: fmt.Sprintf("%s %s", emoji, message), Color: color, Fields: []RocketChatAttachmentField{ { @@ -211,7 +115,6 @@ func SendToRocketChat(notification *Notification, channel, webhook, appID string }, }, } - jsonBytes, _ := json.Marshal(data) req, err := http.NewRequest("POST", webhook, bytes.NewBuffer(jsonBytes)) req.Header.Set("Content-Type", "application/json") @@ -224,7 +127,7 @@ func SendToRocketChat(notification *Notification, channel, webhook, appID string return } defer resp.Body.Close() - log.Println(fmt.Sprintf("Sent %s message to rocketchat", notification.Event)) + log.Println(fmt.Sprintf("Sent %s message to rocketchat", event)) } func getRocketChatEvent(msgEvent string) (string, string, string, error) { @@ -235,70 +138,70 @@ func getRocketChatEvent(msgEvent string) (string, string, string, error) { } var rocketChatEventTypeMap = map[string]EventMap{ - "github:pull_request:opened:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestOpened"}, - "gitlab:merge_request:opened:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestOpened"}, - "bitbucket:pullrequest:created:opened:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestOpened"}, //not in slack - "bitbucket:pullrequest:created:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestOpened"}, //not in teams - - "github:pull_request:synchronize:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, - "gitlab:merge_request:updated:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, - "bitbucket:pullrequest:updated:opened:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, //not in slack - "bitbucket:pullrequest:updated:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, //not in teams - - "github:pull_request:closed:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestClosed"}, - "bitbucket:pullrequest:fulfilled:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestClosed"}, - "bitbucket:pullrequest:rejected:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestClosed"}, - "gitlab:merge_request:closed:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestClosed"}, - - "github:delete:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "deleteEnvironment"}, - "gitlab:remove:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "deleteEnvironment"}, //not in slack - "bitbucket:delete:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "deleteEnvironment"}, //not in slack - "api:deleteEnvironment": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "deleteEnvironment"}, //not in teams - - "github:push:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "repoPushHandled"}, - "bitbucket:repo:push:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "repoPushHandled"}, - "gitlab:push:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "repoPushHandled"}, - - "github:push:skipped": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "repoPushSkipped"}, - "gitlab:push:skipped": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "repoPushSkipped"}, - "bitbucket:push:skipped": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "repoPushSkipped"}, - - "api:deployEnvironmentLatest": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "deployEnvironment"}, - "api:deployEnvironmentBranch": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "deployEnvironment"}, - - "task:deploy-openshift:finished": {Emoji: ":white_check_mark:", Color: "lawngreen", Template: "deployFinished"}, - "task:remove-openshift-resources:finished": {Emoji: ":white_check_mark:", Color: "lawngreen", Template: "deployFinished"}, - "task:builddeploy-openshift:complete": {Emoji: ":white_check_mark:", Color: "lawngreen", Template: "deployFinished"}, - "task:builddeploy-kubernetes:complete": {Emoji: ":white_check_mark:", Color: "lawngreen", Template: "deployFinished"}, //not in teams - - "task:remove-openshift:finished": {Emoji: ":white_check_mark:", Color: "lawngreen", Template: "removeFinished"}, - "task:remove-kubernetes:finished": {Emoji: ":white_check_mark:", Color: "lawngreen", Template: "removeFinished"}, - - "task:remove-openshift:error": {Emoji: ":bangbang:", Color: "red", Template: "deployError"}, - "task:remove-kubernetes:error": {Emoji: ":bangbang:", Color: "red", Template: "deployError"}, - "task:builddeploy-kubernetes:failed": {Emoji: ":bangbang:", Color: "red", Template: "deployError"}, //not in teams - "task:builddeploy-openshift:failed": {Emoji: ":bangbang:", Color: "red", Template: "deployError"}, - - "github:pull_request:closed:CannotDeleteProductionEnvironment": {Emoji: ":warning:", Color: "gold", Template: "notDeleted"}, - "github:push:CannotDeleteProductionEnvironment": {Emoji: ":warning:", Color: "gold", Template: "notDeleted"}, - "bitbucket:repo:push:CannotDeleteProductionEnvironment": {Emoji: ":warning:", Color: "gold", Template: "notDeleted"}, - "gitlab:push:CannotDeleteProductionEnvironment": {Emoji: ":warning:", Color: "gold", Template: "notDeleted"}, + "github:pull_request:opened:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestOpened"}, + "gitlab:merge_request:opened:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestOpened"}, + "bitbucket:pullrequest:created:opened:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestOpened"}, //not in slack + "bitbucket:pullrequest:created:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestOpened"}, //not in teams + + "github:pull_request:synchronize:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, + "gitlab:merge_request:updated:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, + "bitbucket:pullrequest:updated:opened:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, //not in slack + "bitbucket:pullrequest:updated:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, //not in teams + + "github:pull_request:closed:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestClosed"}, + "bitbucket:pullrequest:fulfilled:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestClosed"}, + "bitbucket:pullrequest:rejected:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestClosed"}, + "gitlab:merge_request:closed:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestClosed"}, + + "github:delete:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "deleteEnvironment"}, + "gitlab:remove:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "deleteEnvironment"}, //not in slack + "bitbucket:delete:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "deleteEnvironment"}, //not in slack + "api:deleteEnvironment": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "deleteEnvironment"}, //not in teams + + "github:push:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "repoPushHandled"}, + "bitbucket:repo:push:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "repoPushHandled"}, + "gitlab:push:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "repoPushHandled"}, + + "github:push:skipped": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "repoPushSkipped"}, + "gitlab:push:skipped": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "repoPushSkipped"}, + "bitbucket:push:skipped": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "repoPushSkipped"}, + + "api:deployEnvironmentLatest": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "deployEnvironment"}, + "api:deployEnvironmentBranch": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "deployEnvironment"}, + + "task:deploy-openshift:finished": {Emoji: "✅", Color: "lawngreen", Template: "deployFinished"}, + "task:remove-openshift-resources:finished": {Emoji: "✅", Color: "lawngreen", Template: "deployFinished"}, + "task:builddeploy-openshift:complete": {Emoji: "✅", Color: "lawngreen", Template: "deployFinished"}, + "task:builddeploy-kubernetes:complete": {Emoji: "✅", Color: "lawngreen", Template: "deployFinished"}, //not in teams + + "task:remove-openshift:finished": {Emoji: "✅", Color: "lawngreen", Template: "removeFinished"}, + "task:remove-kubernetes:finished": {Emoji: "✅", Color: "lawngreen", Template: "removeFinished"}, + + "task:remove-openshift:error": {Emoji: "🛑", Color: "red", Template: "deployError"}, + "task:remove-kubernetes:error": {Emoji: "🛑", Color: "red", Template: "deployError"}, + "task:builddeploy-kubernetes:failed": {Emoji: "🛑", Color: "red", Template: "deployError"}, //not in teams + "task:builddeploy-openshift:failed": {Emoji: "🛑", Color: "red", Template: "deployError"}, + + "github:pull_request:closed:CannotDeleteProductionEnvironment": {Emoji: "⚠️", Color: "gold", Template: "notDeleted"}, + "github:push:CannotDeleteProductionEnvironment": {Emoji: "⚠️", Color: "gold", Template: "notDeleted"}, + "bitbucket:repo:push:CannotDeleteProductionEnvironment": {Emoji: "⚠️", Color: "gold", Template: "notDeleted"}, + "gitlab:push:CannotDeleteProductionEnvironment": {Emoji: "⚠️", Color: "gold", Template: "notDeleted"}, // deprecated - // "rest:remove:CannotDeleteProductionEnvironment": {Emoji: ":warning:", Color: "gold"}, - // "rest:deploy:receive": {Emoji: ":information_source:", Color: "#E8E8E8"}, - // "rest:remove:receive": {Emoji: ":information_source:", Color: "#E8E8E8"}, - // "rest:promote:receive": {Emoji: ":information_source:", Color: "#E8E8E8"}, - // "rest:pullrequest:deploy": {Emoji: ":information_source:", Color: "#E8E8E8"}, - // "rest:pullrequest:remove": {Emoji: ":information_source:", Color: "#E8E8E8"}, + // "rest:remove:CannotDeleteProductionEnvironment": {Emoji: "⚠️", Color: "gold"}, + // "rest:deploy:receive": {Emoji: "ℹ️", Color: "#E8E8E8"}, + // "rest:remove:receive": {Emoji: "ℹ️", Color: "#E8E8E8"}, + // "rest:promote:receive": {Emoji: "ℹ️", Color: "#E8E8E8"}, + // "rest:pullrequest:deploy": {Emoji: "ℹ️", Color: "#E8E8E8"}, + // "rest:pullrequest:remove": {Emoji: "ℹ️", Color: "#E8E8E8"}, // deprecated - // "task:deploy-openshift:error": {Emoji: ":bangbang:", Color: "red", Template: "deployError"}, - // "task:remove-openshift-resources:error": {Emoji: ":bangbang:", Color: "red", Template: "deployError"}, + // "task:deploy-openshift:error": {Emoji: "🛑", Color: "red", Template: "deployError"}, + // "task:remove-openshift-resources:error": {Emoji: "🛑", Color: "red", Template: "deployError"}, // deprecated - // "task:deploy-openshift:retry": {Emoji: ":warning:", Color: "gold", Template: "removeRetry"}, - // "task:remove-openshift:retry": {Emoji: ":warning:", Color: "gold", Template: "removeRetry"}, - // "task:remove-kubernetes:retry": {Emoji: ":warning:", Color: "gold", Template: "removeRetry"}, - // "task:remove-openshift-resources:retry": {Emoji: ":warning:", Color: "gold", Template: "removeRetry"}, + // "task:deploy-openshift:retry": {Emoji: "⚠️", Color: "gold", Template: "removeRetry"}, + // "task:remove-openshift:retry": {Emoji: "⚠️", Color: "gold", Template: "removeRetry"}, + // "task:remove-kubernetes:retry": {Emoji: "⚠️", Color: "gold", Template: "removeRetry"}, + // "task:remove-openshift-resources:retry": {Emoji: "⚠️", Color: "gold", Template: "removeRetry"}, } diff --git a/services/logs2notifications/internal/handler/s3_events.go b/services/logs2notifications/internal/handler/s3_events.go index f53ed93a5c..09600710de 100644 --- a/services/logs2notifications/internal/handler/s3_events.go +++ b/services/logs2notifications/internal/handler/s3_events.go @@ -3,65 +3,80 @@ package handler import ( "bytes" "fmt" - "net/http" - "os" + "log" "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/s3" ) -var ( - AWS_S3_REGION = "" - AWS_S3_BUCKET = "" +// MessageType . +type MessageType string + +const ( + buildMessageType MessageType = "build" + taskMessageType MessageType = "task" ) -func getS3Event(msgEvent string) (string, string, string, error) { - if val, ok := s3Event[msgEvent]; ok { - return val.Emoji, val.Color, val.Template, nil - } - return "", "", "", fmt.Errorf("no matching event source") -} +// SendToS3 . +func (h *Messaging) SendToS3(notification *Notification, msgType MessageType) { + if msgType == buildMessageType { + h.uploadFileS3( + notification.Message, + fmt.Sprintf("buildlogs/%s/%s/%s-%s.txt", + notification.Project, + notification.Meta.BranchName, + notification.Meta.JobName, + notification.Meta.RemoteID, + ), + ) + } else if msgType == taskMessageType { + filePath := fmt.Sprintf("tasklogs/%s/%d-%s.txt", + notification.Project, + notification.Meta.Task.ID, + notification.Meta.RemoteID, + ) + if notification.Meta.Environment != "" { + filePath = fmt.Sprintf("tasklogs/%s/%s/%d-%s.txt", + notification.Project, + notification.Meta.Environment, + notification.Meta.Task.ID, + notification.Meta.RemoteID, + ) -var s3Event = map[string]EventMap{ - "github:pull_request:closed:handled": {Emoji: ":information_source:", Color: "#E8E8E8"}, + } + h.uploadFileS3( + notification.Message, + filePath, + ) + } } -// func main() { -// session, err := session.NewSession(&aws.Config{Region: aws.String(AWS_S3_REGION)}) -// if err != nil { -// log.Fatal(err) -// } - -// // Upload Files -// err = uploadFile(session, "test.png") -// if err != nil { -// log.Fatal(err) -// } -// } - -func uploadFile(session *session.Session, uploadFileDir string) error { - - upFile, err := os.Open(uploadFileDir) +// UploadFileS3 +func (h *Messaging) uploadFileS3(message, fileName string) { + var forcePath bool + forcePath = true + session, err := session.NewSession(&aws.Config{ + Region: aws.String(h.S3FilesRegion), + Endpoint: aws.String(h.S3FilesOrigin), + Credentials: credentials.NewStaticCredentials(h.S3FilesAccessKeyID, h.S3FilesSecretAccessKey, ""), + S3ForcePathStyle: &forcePath, + }) if err != nil { - return err + log.Fatal(err) } - defer upFile.Close() - - upFileInfo, _ := upFile.Stat() - var fileSize int64 = upFileInfo.Size() - fileBuffer := make([]byte, fileSize) - upFile.Read(fileBuffer) _, err = s3.New(session).PutObject(&s3.PutObjectInput{ - Bucket: aws.String(AWS_S3_BUCKET), - Key: aws.String(uploadFileDir), - ACL: aws.String("private"), - Body: bytes.NewReader(fileBuffer), - ContentLength: aws.Int64(fileSize), - ContentType: aws.String(http.DetectContentType(fileBuffer)), - ContentDisposition: aws.String("attachment"), - ServerSideEncryption: aws.String("AES256"), + Bucket: aws.String(h.S3FilesBucket), + Key: aws.String(fileName), + ACL: aws.String("private"), + Body: bytes.NewReader([]byte(message)), + ContentType: aws.String("text/plain"), }) - return err + if err != nil { + log.Println(err) + } + log.Println(fmt.Sprintf("Uploaded file %s", fileName)) + return } diff --git a/services/logs2notifications/internal/handler/slack_events.go b/services/logs2notifications/internal/handler/slack_events.go index 6d153e60a1..413fa75fb3 100644 --- a/services/logs2notifications/internal/handler/slack_events.go +++ b/services/logs2notifications/internal/handler/slack_events.go @@ -1,21 +1,87 @@ package handler import ( + "bytes" "fmt" "log" + "strings" + "text/template" "github.com/slack-go/slack" ) // SendToSlack . -func SendToSlack(notification *Notification, channel, webhook, appID string) { - - emoji, color, _, err := getSlackEvent(notification.Event) +func (h *Messaging) SendToSlack(notification *Notification, channel, webhook, appID string) { + emoji, color, message, err := h.processSlackTemplate(notification) if err != nil { return } + h.sendSlackMessage(emoji, color, appID, channel, webhook, notification.Event, message) +} + +// processSlackTemplate . +func (h *Messaging) processSlackTemplate(notification *Notification) (string, string, string, error) { + emoji, color, tpl, err := getSlackEvent(notification.Event) + if err != nil { + eventSplit := strings.Split(notification.Event, ":") + if eventSplit[0] != "problem" { + return "", "", "", fmt.Errorf("no matching event") + } + if eventSplit[1] == "insert" { + tpl = "problemNotification" + } + } + + var slackTpl string + switch tpl { + case "mergeRequestOpened": + slackTpl = `*[{{.ProjectName}}]* PR [#{{.PullrequestNumber}} ({{.PullrequestTitle}})]({{.PullrequestURL}}) opened in [{{.RepoName}}]({{.RepoURL}})` + case "mergeRequestUpdated": + slackTpl = `*[{{.ProjectName}}]* PR [#{{.PullrequestNumber}} ({{.PullrequestTitle}})]({{.PullrequestURL}}) updated in [{{.RepoName}}]({{.RepoURL}})` + case "mergeRequestClosed": + slackTpl = `*[{{.ProjectName}}]* PR [#{{.PullrequestNumber}} ({{.PullrequestTitle}})]({{.PullrequestURL}}) closed in [{{.RepoName}}]({{.RepoURL}})` + case "deleteEnvironment": + slackTpl = `*[{{.ProjectName}}]* Deleting environment ` + "`{{.EnvironmentName}}`" + case "repoPushHandled": + slackTpl = `*[{{.ProjectName}}]* [{{.BranchName}}]({{.RepoURL}}/tree/{{.BranchName}}){{ if ne .ShortSha "" }} ([{{.ShortSha}}]({{.CommitURL}})){{end}} pushed in [{{.RepoFullName}}]({{.RepoURL}})` + case "repoPushSkipped": + slackTpl = `*[{{.ProjectName}}]* [{{.BranchName}}]({{.RepoURL}}/tree/{{.BranchName}}){{ if ne .ShortSha "" }} ([{{.ShortSha}}]({{.CommitURL}})){{end}} pushed in [{{.RepoFullName}}]({{.RepoURL}}) *deployment skipped*` + case "deployEnvironment": + slackTpl = `*[{{.ProjectName}}]* Deployment triggered ` + "`{{.BranchName}}`" + `{{ if ne .ShortSha "" }} ({{.ShortSha}}){{end}}` + case "removeFinished": + slackTpl = `*[{{.ProjectName}}]* Removed ` + "`{{.OpenshiftProject}}`" + `` + case "removeRetry": + slackTpl = `*[{{.ProjectName}}]* Removed ` + "`{{.OpenshiftProject}}`" + `` + case "notDeleted": + slackTpl = `*[{{.ProjectName}}]* ` + "`{{.BranchName}}`" + ` not deleted. {{.Error}}` + case "deployError": + slackTpl = `*[{{.ProjectName}}]* ` + "`{{.BranchName}}`" + `{{ if ne .ShortSha "" }} ({{.ShortSha}}){{end}} Build ` + "`{{.BuildName}}`" + ` Failed. {{if ne .LogLink ""}} <{{.LogLink}}|Logs>{{end}}` + case "deployFinished": + slackTpl = `*[{{.ProjectName}}]* ` + "`{{.BranchName}}`" + `{{ if ne .ShortSha "" }} ({{.ShortSha}}){{end}} Build ` + "`{{.BuildName}}`" + ` Succeeded. {{if ne .LogLink ""}} <{{.LogLink}}|Logs>{{end}} +* {{.Route}}{{range .Routes}}{{if ne . $.Route}}* {{.}}{{end}} +{{end}}` + case "problemNotification": + eventSplit := strings.Split(notification.Event, ":") + if eventSplit[0] != "problem" && eventSplit[1] == "insert" { + return "", "", "", fmt.Errorf("no matching event") + } + slackTpl = `*[{{.ProjectName}}]* New problem found for ` + "`{{.EnvironmentName}}`" + ` +* Service: ` + "`{{.ServiceName}}`" + `{{ if ne .Severity "" }} +* Severity: {{.Severity}}{{end}}{{ if ne .Description "" }} +* Description: {{.Description}}{{end}}` + default: + return "", "", "", fmt.Errorf("no matching event") + } + + var slackMsg bytes.Buffer + t, _ := template.New("slack").Parse(slackTpl) + t.Execute(&slackMsg, notification.Meta) + return emoji, color, slackMsg.String(), nil +} + +func (h *Messaging) sendSlackMessage(emoji, color, appID, channel, webhook, event, message string) { attachment := slack.Attachment{ - Text: fmt.Sprintf("%s %s", emoji, notification.Message), + Text: fmt.Sprintf("%s %s", emoji, message), Color: color, Footer: appID, MarkdownIn: []string{"pretext", "text", "fields"}, @@ -25,13 +91,13 @@ func SendToSlack(notification *Notification, channel, webhook, appID string) { Channel: channel, } - err = slack.PostWebhook(webhook, &postMsg) + err := slack.PostWebhook(webhook, &postMsg) if err != nil { // just log any errors log.Printf("Error sending message to slack: %v", err) return } - log.Println(fmt.Sprintf("Sent %s message to slack", notification.Event)) + log.Println(fmt.Sprintf("Sent %s message to slack", event)) } func getSlackEvent(msgEvent string) (string, string, string, error) { @@ -42,70 +108,70 @@ func getSlackEvent(msgEvent string) (string, string, string, error) { } var slackEventTypeMap = map[string]EventMap{ - "github:pull_request:opened:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestOpened"}, - "gitlab:merge_request:opened:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestOpened"}, - "bitbucket:pullrequest:created:opened:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestOpened"}, //not in slack - "bitbucket:pullrequest:created:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestOpened"}, //not in teams - - "github:pull_request:synchronize:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, - "gitlab:merge_request:updated:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, - "bitbucket:pullrequest:updated:opened:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, //not in slack - "bitbucket:pullrequest:updated:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, //not in teams - - "github:pull_request:closed:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestClosed"}, - "bitbucket:pullrequest:fulfilled:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestClosed"}, - "bitbucket:pullrequest:rejected:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestClosed"}, - "gitlab:merge_request:closed:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "mergeRequestClosed"}, - - "github:delete:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "deleteEnvironment"}, - "gitlab:remove:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "deleteEnvironment"}, //not in slack - "bitbucket:delete:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "deleteEnvironment"}, //not in slack - "api:deleteEnvironment": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "deleteEnvironment"}, //not in teams - - "github:push:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "repoPushHandled"}, - "bitbucket:repo:push:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "repoPushHandled"}, - "gitlab:push:handled": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "repoPushHandled"}, - - "github:push:skipped": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "repoPushSkipped"}, - "gitlab:push:skipped": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "repoPushSkipped"}, - "bitbucket:push:skipped": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "repoPushSkipped"}, - - "api:deployEnvironmentLatest": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "deployEnvironment"}, - "api:deployEnvironmentBranch": {Emoji: ":information_source:", Color: "#E8E8E8", Template: "deployEnvironment"}, - - "task:deploy-openshift:finished": {Emoji: ":white_check_mark:", Color: "good", Template: "deployFinished"}, - "task:remove-openshift-resources:finished": {Emoji: ":white_check_mark:", Color: "good", Template: "deployFinished"}, - "task:builddeploy-openshift:complete": {Emoji: ":white_check_mark:", Color: "good", Template: "deployFinished"}, - "task:builddeploy-kubernetes:complete": {Emoji: ":white_check_mark:", Color: "good", Template: "deployFinished"}, //not in teams - - "task:remove-openshift:finished": {Emoji: ":white_check_mark:", Color: "good", Template: "removeFinished"}, - "task:remove-kubernetes:finished": {Emoji: ":white_check_mark:", Color: "good", Template: "removeFinished"}, - - "task:remove-openshift:error": {Emoji: ":bangbang:", Color: "danger", Template: "deployError"}, - "task:remove-kubernetes:error": {Emoji: ":bangbang:", Color: "danger", Template: "deployError"}, - "task:builddeploy-kubernetes:failed": {Emoji: ":bangbang:", Color: "danger", Template: "deployError"}, //not in teams - "task:builddeploy-openshift:failed": {Emoji: ":bangbang:", Color: "danger", Template: "deployError"}, - - "github:pull_request:closed:CannotDeleteProductionEnvironment": {Emoji: ":warning:", Color: "warning", Template: "notDeleted"}, - "github:push:CannotDeleteProductionEnvironment": {Emoji: ":warning:", Color: "warning", Template: "notDeleted"}, - "bitbucket:repo:push:CannotDeleteProductionEnvironment": {Emoji: ":warning:", Color: "warning", Template: "notDeleted"}, - "gitlab:push:CannotDeleteProductionEnvironment": {Emoji: ":warning:", Color: "warning", Template: "notDeleted"}, + "github:pull_request:opened:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestOpened"}, + "gitlab:merge_request:opened:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestOpened"}, + "bitbucket:pullrequest:created:opened:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestOpened"}, //not in slack + "bitbucket:pullrequest:created:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestOpened"}, //not in teams + + "github:pull_request:synchronize:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, + "gitlab:merge_request:updated:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, + "bitbucket:pullrequest:updated:opened:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, //not in slack + "bitbucket:pullrequest:updated:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestUpdated"}, //not in teams + + "github:pull_request:closed:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestClosed"}, + "bitbucket:pullrequest:fulfilled:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestClosed"}, + "bitbucket:pullrequest:rejected:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestClosed"}, + "gitlab:merge_request:closed:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "mergeRequestClosed"}, + + "github:delete:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "deleteEnvironment"}, + "gitlab:remove:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "deleteEnvironment"}, //not in slack + "bitbucket:delete:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "deleteEnvironment"}, //not in slack + "api:deleteEnvironment": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "deleteEnvironment"}, //not in teams + + "github:push:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "repoPushHandled"}, + "bitbucket:repo:push:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "repoPushHandled"}, + "gitlab:push:handled": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "repoPushHandled"}, + + "github:push:skipped": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "repoPushSkipped"}, + "gitlab:push:skipped": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "repoPushSkipped"}, + "bitbucket:push:skipped": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "repoPushSkipped"}, + + "api:deployEnvironmentLatest": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "deployEnvironment"}, + "api:deployEnvironmentBranch": {Emoji: "ℹ️", Color: "#E8E8E8", Template: "deployEnvironment"}, + + "task:deploy-openshift:finished": {Emoji: "✅", Color: "good", Template: "deployFinished"}, + "task:remove-openshift-resources:finished": {Emoji: "✅", Color: "good", Template: "deployFinished"}, + "task:builddeploy-openshift:complete": {Emoji: "✅", Color: "good", Template: "deployFinished"}, + "task:builddeploy-kubernetes:complete": {Emoji: "✅", Color: "good", Template: "deployFinished"}, //not in teams + + "task:remove-openshift:finished": {Emoji: "✅", Color: "good", Template: "removeFinished"}, + "task:remove-kubernetes:finished": {Emoji: "✅", Color: "good", Template: "removeFinished"}, + + "task:remove-openshift:error": {Emoji: "🛑", Color: "danger", Template: "deployError"}, + "task:remove-kubernetes:error": {Emoji: "🛑", Color: "danger", Template: "deployError"}, + "task:builddeploy-kubernetes:failed": {Emoji: "🛑", Color: "danger", Template: "deployError"}, //not in teams + "task:builddeploy-openshift:failed": {Emoji: "🛑", Color: "danger", Template: "deployError"}, + + "github:pull_request:closed:CannotDeleteProductionEnvironment": {Emoji: "⚠️", Color: "warning", Template: "notDeleted"}, + "github:push:CannotDeleteProductionEnvironment": {Emoji: "⚠️", Color: "warning", Template: "notDeleted"}, + "bitbucket:repo:push:CannotDeleteProductionEnvironment": {Emoji: "⚠️", Color: "warning", Template: "notDeleted"}, + "gitlab:push:CannotDeleteProductionEnvironment": {Emoji: "⚠️", Color: "warning", Template: "notDeleted"}, // deprecated - // "rest:remove:CannotDeleteProductionEnvironment": {Emoji: ":warning:", Color: "warning"}, - // "rest:deploy:receive": {Emoji: ":information_source:", Color: "#E8E8E8"}, - // "rest:remove:receive": {Emoji: ":information_source:", Color: "#E8E8E8"}, - // "rest:promote:receive": {Emoji: ":information_source:", Color: "#E8E8E8"}, - // "rest:pullrequest:deploy": {Emoji: ":information_source:", Color: "#E8E8E8"}, - // "rest:pullrequest:remove": {Emoji: ":information_source:", Color: "#E8E8E8"}, + // "rest:remove:CannotDeleteProductionEnvironment": {Emoji: "⚠️", Color: "warning"}, + // "rest:deploy:receive": {Emoji: "ℹ️", Color: "#E8E8E8"}, + // "rest:remove:receive": {Emoji: "ℹ️", Color: "#E8E8E8"}, + // "rest:promote:receive": {Emoji: "ℹ️", Color: "#E8E8E8"}, + // "rest:pullrequest:deploy": {Emoji: "ℹ️", Color: "#E8E8E8"}, + // "rest:pullrequest:remove": {Emoji: "ℹ️", Color: "#E8E8E8"}, // deprecated - // "task:deploy-openshift:error": {Emoji: ":bangbang:", Color: "danger", Template: "deployError"}, - // "task:remove-openshift-resources:error": {Emoji: ":bangbang:", Color: "danger", Template: "deployError"}, + // "task:deploy-openshift:error": {Emoji: "🛑", Color: "danger", Template: "deployError"}, + // "task:remove-openshift-resources:error": {Emoji: "🛑", Color: "danger", Template: "deployError"}, // deprecated - // "task:deploy-openshift:retry": {Emoji: ":warning:", Color: "warning", Template: "removeRetry"}, - // "task:remove-openshift:retry": {Emoji: ":warning:", Color: "warning", Template: "removeRetry"}, - // "task:remove-kubernetes:retry": {Emoji: ":warning:", Color: "warning", Template: "removeRetry"}, - // "task:remove-openshift-resources:retry": {Emoji: ":warning:", Color: "warning", Template: "removeRetry"}, + // "task:deploy-openshift:retry": {Emoji: "⚠️", Color: "warning", Template: "removeRetry"}, + // "task:remove-openshift:retry": {Emoji: "⚠️", Color: "warning", Template: "removeRetry"}, + // "task:remove-kubernetes:retry": {Emoji: "⚠️", Color: "warning", Template: "removeRetry"}, + // "task:remove-openshift-resources:retry": {Emoji: "⚠️", Color: "warning", Template: "removeRetry"}, } diff --git a/services/logs2notifications/internal/handler/testdata/deleteEnvironment/emailhtml.txt b/services/logs2notifications/internal/handler/testdata/deleteEnvironment/emailhtml.txt new file mode 100644 index 0000000000..e4312f8960 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/deleteEnvironment/emailhtml.txt @@ -0,0 +1 @@ +Deleted environment lagoon-type-override \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/deleteEnvironment/emailplain.txt b/services/logs2notifications/internal/handler/testdata/deleteEnvironment/emailplain.txt new file mode 100644 index 0000000000..6afbbb9a49 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/deleteEnvironment/emailplain.txt @@ -0,0 +1 @@ +[ci-github-openshift] deleted environment lagoon-type-override \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/deleteEnvironment/rocketchat.txt b/services/logs2notifications/internal/handler/testdata/deleteEnvironment/rocketchat.txt new file mode 100644 index 0000000000..ae882c10ee --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/deleteEnvironment/rocketchat.txt @@ -0,0 +1 @@ +*[ci-github-openshift]* Deleting environment `lagoon-type-override` \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/deleteEnvironment/slack.txt b/services/logs2notifications/internal/handler/testdata/deleteEnvironment/slack.txt new file mode 100644 index 0000000000..ae882c10ee --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/deleteEnvironment/slack.txt @@ -0,0 +1 @@ +*[ci-github-openshift]* Deleting environment `lagoon-type-override` \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/deleteEnvironment/teams.txt b/services/logs2notifications/internal/handler/testdata/deleteEnvironment/teams.txt new file mode 100644 index 0000000000..7236b3ac99 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/deleteEnvironment/teams.txt @@ -0,0 +1 @@ +Deleting environment `lagoon-type-override` \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/deployEnvironment/emailhtml.txt b/services/logs2notifications/internal/handler/testdata/deployEnvironment/emailhtml.txt new file mode 100644 index 0000000000..2df8b891e1 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/deployEnvironment/emailhtml.txt @@ -0,0 +1 @@ +Deployment triggered lagoon-type-override \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/deployEnvironment/emailplain.txt b/services/logs2notifications/internal/handler/testdata/deployEnvironment/emailplain.txt new file mode 100644 index 0000000000..a0d5c2f96a --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/deployEnvironment/emailplain.txt @@ -0,0 +1 @@ +[ci-github-openshift] Deployment triggered on branch lagoon-type-override \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/deployEnvironment/rocketchat.txt b/services/logs2notifications/internal/handler/testdata/deployEnvironment/rocketchat.txt new file mode 100644 index 0000000000..e38caf7ee5 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/deployEnvironment/rocketchat.txt @@ -0,0 +1 @@ +*[ci-github-openshift]* Deployment triggered `lagoon-type-override` \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/deployEnvironment/slack.txt b/services/logs2notifications/internal/handler/testdata/deployEnvironment/slack.txt new file mode 100644 index 0000000000..e38caf7ee5 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/deployEnvironment/slack.txt @@ -0,0 +1 @@ +*[ci-github-openshift]* Deployment triggered `lagoon-type-override` \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/deployEnvironment/teams.txt b/services/logs2notifications/internal/handler/testdata/deployEnvironment/teams.txt new file mode 100644 index 0000000000..80e680018e --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/deployEnvironment/teams.txt @@ -0,0 +1 @@ +Deployment triggered `lagoon-type-override` \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/deployError/emailhtml.txt b/services/logs2notifications/internal/handler/testdata/deployError/emailhtml.txt new file mode 100644 index 0000000000..caa0842373 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/deployError/emailhtml.txt @@ -0,0 +1,2 @@ +[ci-github-openshift] lagoon-type-override Build lagoon-build-1234 error. + Logs \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/deployError/emailplain.txt b/services/logs2notifications/internal/handler/testdata/deployError/emailplain.txt new file mode 100644 index 0000000000..6e842187b3 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/deployError/emailplain.txt @@ -0,0 +1,2 @@ +[ci-github-openshift] lagoon-type-override Build lagoon-build-1234 error. + [Logs](https://logs) \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/deployError/rocketchat.txt b/services/logs2notifications/internal/handler/testdata/deployError/rocketchat.txt new file mode 100644 index 0000000000..3188762d78 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/deployError/rocketchat.txt @@ -0,0 +1 @@ +*[ci-github-openshift]* `lagoon-type-override` Build `lagoon-build-1234` Failed. [Logs](https://logs) \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/deployError/slack.txt b/services/logs2notifications/internal/handler/testdata/deployError/slack.txt new file mode 100644 index 0000000000..cb1126de73 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/deployError/slack.txt @@ -0,0 +1 @@ +*[ci-github-openshift]* `lagoon-type-override` Build `lagoon-build-1234` Failed. \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/deployError/teams.txt b/services/logs2notifications/internal/handler/testdata/deployError/teams.txt new file mode 100644 index 0000000000..ca2fa8daf9 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/deployError/teams.txt @@ -0,0 +1 @@ +`lagoon-type-override` Build `lagoon-build-1234` Failed. [Logs](https://logs) \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/deployFinished/emailhtml.txt b/services/logs2notifications/internal/handler/testdata/deployFinished/emailhtml.txt new file mode 100644 index 0000000000..26155d23c4 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/deployFinished/emailhtml.txt @@ -0,0 +1,10 @@ +[ci-github-openshift] lagoon-type-override Build lagoon-build-1234 complete. Logs +

+
+
+

+

\ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/deployFinished/emailplain.txt b/services/logs2notifications/internal/handler/testdata/deployFinished/emailplain.txt new file mode 100644 index 0000000000..3364f45f8b --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/deployFinished/emailplain.txt @@ -0,0 +1,4 @@ +[ci-github-openshift] lagoon-type-override Build lagoon-build-1234 complete. [Logs](https://logs) +https://route1 +https://route2 +https://route3 diff --git a/services/logs2notifications/internal/handler/testdata/deployFinished/rocketchat.txt b/services/logs2notifications/internal/handler/testdata/deployFinished/rocketchat.txt new file mode 100644 index 0000000000..66f5f30f33 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/deployFinished/rocketchat.txt @@ -0,0 +1,4 @@ +*[ci-github-openshift]* `lagoon-type-override` Build `lagoon-build-1234` Succeeded. [Logs](https://logs) +* https://route1 +* https://route2 +* https://route3 diff --git a/services/logs2notifications/internal/handler/testdata/deployFinished/slack.txt b/services/logs2notifications/internal/handler/testdata/deployFinished/slack.txt new file mode 100644 index 0000000000..c2040f005a --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/deployFinished/slack.txt @@ -0,0 +1,4 @@ +*[ci-github-openshift]* `lagoon-type-override` Build `lagoon-build-1234` Succeeded. +* https://route1 +* https://route2 +* https://route3 diff --git a/services/logs2notifications/internal/handler/testdata/deployFinished/teams.txt b/services/logs2notifications/internal/handler/testdata/deployFinished/teams.txt new file mode 100644 index 0000000000..b68e460ad9 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/deployFinished/teams.txt @@ -0,0 +1,4 @@ +`lagoon-type-override` Build `lagoon-build-1234` Succeeded. [Logs](https://logs) +* https://route1 +* https://route2 +* https://route3 diff --git a/services/logs2notifications/internal/handler/testdata/input.deleteEnvironmentGithub.json b/services/logs2notifications/internal/handler/testdata/input.deleteEnvironmentGithub.json new file mode 100644 index 0000000000..798e855a54 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/input.deleteEnvironmentGithub.json @@ -0,0 +1,8 @@ +{ + "project":"ci-github-openshift", + "event":"github:delete:handled", + "meta":{ + "projectName":"ci-github-openshift", + "environmentName":"lagoon-type-override" + } + } \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/input.deployEnvironment.json b/services/logs2notifications/internal/handler/testdata/input.deployEnvironment.json new file mode 100644 index 0000000000..e38a2e0442 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/input.deployEnvironment.json @@ -0,0 +1,9 @@ +{ + "project":"ci-github-openshift", + "event":"api:deployEnvironmentLatest", + "meta":{ + "projectName":"ci-github-openshift", + "branchName":"lagoon-type-override", + "project":"ci-github-openshift" + } + } \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/input.deployError.json b/services/logs2notifications/internal/handler/testdata/input.deployError.json new file mode 100644 index 0000000000..0f304888b4 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/input.deployError.json @@ -0,0 +1,11 @@ +{ + "project":"ci-github-openshift", + "event":"task:builddeploy-kubernetes:failed", + "meta":{ + "projectName":"ci-github-openshift", + "branchName":"lagoon-type-override", + "project":"ci-github-openshift", + "buildName":"lagoon-build-1234", + "logLink":"https://logs" + } + } \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/input.deployFinished.json b/services/logs2notifications/internal/handler/testdata/input.deployFinished.json new file mode 100644 index 0000000000..be4eb952fd --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/input.deployFinished.json @@ -0,0 +1,14 @@ +{ + "severity":"info", + "project":"ci-github-openshift", + "uuid":"", + "event":"task:deploy-openshift:finished", + "meta":{ + "projectName":"ci-github-openshift", + "branchName":"lagoon-type-override", + "buildName":"lagoon-build-1234", + "route":"https://route1", + "routes":["https://route1","https://route2","https://route3"], + "logLink":"https://logs" + } +} \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/input.repoPushHandledGithub.json b/services/logs2notifications/internal/handler/testdata/input.repoPushHandledGithub.json new file mode 100644 index 0000000000..ef4ca09114 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/input.repoPushHandledGithub.json @@ -0,0 +1,12 @@ +{ + "severity":"info", + "project":"ci-github-openshift", + "uuid":"", + "event":"github:push:handled", + "meta":{ + "projectName":"ci-github-openshift", + "branchName":"lagoon-type-override", + "repoFullName":"owner/repository", + "repoURL":"github.com/owner/repository" + } +} \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/input.repoPushSkippedGithub.json b/services/logs2notifications/internal/handler/testdata/input.repoPushSkippedGithub.json new file mode 100644 index 0000000000..efcc4ccd5b --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/input.repoPushSkippedGithub.json @@ -0,0 +1,12 @@ +{ + "severity":"info", + "project":"ci-github-openshift", + "uuid":"", + "event":"github:push:skipped", + "meta":{ + "projectName":"ci-github-openshift", + "branchName":"lagoon-type-override", + "repoFullName":"owner/repository", + "repoURL":"github.com/owner/repository" + } +} \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/mergeRequestClosed.json b/services/logs2notifications/internal/handler/testdata/mergeRequestClosed.json new file mode 100644 index 0000000000..5d04ed7b12 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/mergeRequestClosed.json @@ -0,0 +1,11 @@ +{ + "project":"ci-github-openshift", + "event":"api:deployEnvironmentBranch", + "meta":{ + "projectName":"ci-github-openshift", + "pullrequestNumber":"", + "pullrequestTitle":"", + "repoURL":"", + "repoName": "" + } +} \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/mergeRequestOpened.json b/services/logs2notifications/internal/handler/testdata/mergeRequestOpened.json new file mode 100644 index 0000000000..5d04ed7b12 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/mergeRequestOpened.json @@ -0,0 +1,11 @@ +{ + "project":"ci-github-openshift", + "event":"api:deployEnvironmentBranch", + "meta":{ + "projectName":"ci-github-openshift", + "pullrequestNumber":"", + "pullrequestTitle":"", + "repoURL":"", + "repoName": "" + } +} \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/mergeRequestUpdated.json b/services/logs2notifications/internal/handler/testdata/mergeRequestUpdated.json new file mode 100644 index 0000000000..5d04ed7b12 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/mergeRequestUpdated.json @@ -0,0 +1,11 @@ +{ + "project":"ci-github-openshift", + "event":"api:deployEnvironmentBranch", + "meta":{ + "projectName":"ci-github-openshift", + "pullrequestNumber":"", + "pullrequestTitle":"", + "repoURL":"", + "repoName": "" + } +} \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/notDeleted.json b/services/logs2notifications/internal/handler/testdata/notDeleted.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/services/logs2notifications/internal/handler/testdata/problemNotification.1.json b/services/logs2notifications/internal/handler/testdata/problemNotification.1.json new file mode 100644 index 0000000000..21fc0fe8c4 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/problemNotification.1.json @@ -0,0 +1,12 @@ +{ + "project":"ci-github-openshift", + "event":"problem:insert:source_name:summary:severity", + "meta":{ + "projectName":"ci-github-openshift", + "environmentName":"lagoon-type-override", + "serviceName":"nginx", + "severity":"danger", + "description":"this is bad" + }, + "message":"sagsdgasdgsdg" +} \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/problemNotification.2.json b/services/logs2notifications/internal/handler/testdata/problemNotification.2.json new file mode 100644 index 0000000000..918ba09e82 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/problemNotification.2.json @@ -0,0 +1,12 @@ +{ + "project":"ci-github-openshift", + "event":"problem:update:source_name:summary:severity", + "meta":{ + "projectName":"ci-github-openshift", + "environmentName":"lagoon-type-override", + "serviceName":"nginx", + "severity":"danger", + "description":"this is bad" + }, + "message":"sagsdgasdgsdg" +} \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/removeFinished.json b/services/logs2notifications/internal/handler/testdata/removeFinished.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/services/logs2notifications/internal/handler/testdata/repoPushHandled/emailhtml.txt b/services/logs2notifications/internal/handler/testdata/repoPushHandled/emailhtml.txt new file mode 100644 index 0000000000..287e649a98 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/repoPushHandled/emailhtml.txt @@ -0,0 +1 @@ +lagoon-type-override pushed in owner/repository \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/repoPushHandled/emailplain.txt b/services/logs2notifications/internal/handler/testdata/repoPushHandled/emailplain.txt new file mode 100644 index 0000000000..93333fb083 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/repoPushHandled/emailplain.txt @@ -0,0 +1 @@ +[ci-github-openshift] lagoon-type-override pushed in owner/repository \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/repoPushHandled/rocketchat.txt b/services/logs2notifications/internal/handler/testdata/repoPushHandled/rocketchat.txt new file mode 100644 index 0000000000..d68f558315 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/repoPushHandled/rocketchat.txt @@ -0,0 +1 @@ +*[ci-github-openshift]* [lagoon-type-override](github.com/owner/repository/tree/lagoon-type-override) pushed in [owner/repository](github.com/owner/repository) \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/repoPushHandled/slack.txt b/services/logs2notifications/internal/handler/testdata/repoPushHandled/slack.txt new file mode 100644 index 0000000000..d68f558315 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/repoPushHandled/slack.txt @@ -0,0 +1 @@ +*[ci-github-openshift]* [lagoon-type-override](github.com/owner/repository/tree/lagoon-type-override) pushed in [owner/repository](github.com/owner/repository) \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/repoPushHandled/teams.txt b/services/logs2notifications/internal/handler/testdata/repoPushHandled/teams.txt new file mode 100644 index 0000000000..efc27fed55 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/repoPushHandled/teams.txt @@ -0,0 +1 @@ +[lagoon-type-override](github.com/owner/repository/tree/lagoon-type-override) pushed in [owner/repository](github.com/owner/repository) \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/repoPushSkipped/emailhtml.txt b/services/logs2notifications/internal/handler/testdata/repoPushSkipped/emailhtml.txt new file mode 100644 index 0000000000..d8bbb51bc0 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/repoPushSkipped/emailhtml.txt @@ -0,0 +1 @@ +lagoon-type-override pushed in owner/repository deployment skipped \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/repoPushSkipped/emailplain.txt b/services/logs2notifications/internal/handler/testdata/repoPushSkipped/emailplain.txt new file mode 100644 index 0000000000..beaf598e56 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/repoPushSkipped/emailplain.txt @@ -0,0 +1 @@ +[ci-github-openshift] lagoon-type-override pushed in owner/repository *deployment skipped* \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/repoPushSkipped/rocketchat.txt b/services/logs2notifications/internal/handler/testdata/repoPushSkipped/rocketchat.txt new file mode 100644 index 0000000000..9dc759260a --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/repoPushSkipped/rocketchat.txt @@ -0,0 +1 @@ +*[ci-github-openshift]* [lagoon-type-override](github.com/owner/repository/tree/lagoon-type-override) pushed in [owner/repository](github.com/owner/repository) *deployment skipped* \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/repoPushSkipped/slack.txt b/services/logs2notifications/internal/handler/testdata/repoPushSkipped/slack.txt new file mode 100644 index 0000000000..9dc759260a --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/repoPushSkipped/slack.txt @@ -0,0 +1 @@ +*[ci-github-openshift]* [lagoon-type-override](github.com/owner/repository/tree/lagoon-type-override) pushed in [owner/repository](github.com/owner/repository) *deployment skipped* \ No newline at end of file diff --git a/services/logs2notifications/internal/handler/testdata/repoPushSkipped/teams.txt b/services/logs2notifications/internal/handler/testdata/repoPushSkipped/teams.txt new file mode 100644 index 0000000000..ee43a45568 --- /dev/null +++ b/services/logs2notifications/internal/handler/testdata/repoPushSkipped/teams.txt @@ -0,0 +1 @@ +[lagoon-type-override](github.com/owner/repository/tree/lagoon-type-override) pushed in [owner/repository](github.com/owner/repository) *deployment skipped* \ No newline at end of file diff --git a/services/logs2notifications/main.go b/services/logs2notifications/main.go index 789201f56c..da2e918cb0 100644 --- a/services/logs2notifications/main.go +++ b/services/logs2notifications/main.go @@ -28,17 +28,25 @@ var ( jwtAudience string jwtSubject string jwtIssuer string - s3FilesAccessKeyID string - s3FilesSecretAccessKey string - s3FilesBucket string - s3FilesRegion string - s3FilesOrigin string - disableSlack bool - disableRocketChat bool - disableMicrosoftTeams bool - disableEmail bool - disableWebhooks bool - disableS3 bool + + s3FilesAccessKeyID string + s3FilesSecretAccessKey string + s3FilesBucket string + s3FilesRegion string + s3FilesOrigin string + + disableSlack bool + disableRocketChat bool + disableMicrosoftTeams bool + disableEmail bool + disableWebhooks bool + disableS3 bool + + emailSender string + emailSenderPassword string + emailHost string + emailPort string + emailInsecureSkipVerify bool ) func main() { @@ -70,28 +78,44 @@ func main() { "The jwt audience.") flag.StringVar(&jwtIssuer, "jwt-issuer", "logs2notifications", "The jwt audience.") - flag.StringVar(&s3FilesAccessKeyID, "s3-files-access-key", "minio", - "The jwt audience.") - flag.StringVar(&s3FilesSecretAccessKey, "s3-files-secret-access-key", "minio123", - "The jwt audience.") - flag.StringVar(&s3FilesBucket, "s3-files-bucket", "lagoon-files", - "The jwt audience.") - flag.StringVar(&s3FilesRegion, "s3-files-region", "", - "The jwt audience.") - flag.StringVar(&s3FilesOrigin, "s3-files-origin", "http://docker.for.mac.localhost:9000", - "The jwt audience.") + + // Other notifications configuration flag.BoolVar(&disableSlack, "disable-slack", false, "Disable the logs2slack feature.") flag.BoolVar(&disableRocketChat, "disable-rocketchat", false, "Disable the logs2rocketchat feature.") flag.BoolVar(&disableMicrosoftTeams, "disable-microsoft-teams", false, "Disable the logs2microsoftteams feature.") - flag.BoolVar(&disableEmail, "disable-email", false, - "Disable the logs2email feature.") flag.BoolVar(&disableWebhooks, "disable-webhooks", false, "Disable the logs2webhooks feature.") + + // S3 configuration flag.BoolVar(&disableS3, "disable-s3", false, "Disable the logs2s3 feature.") + flag.StringVar(&s3FilesAccessKeyID, "s3-files-access-key", "minio", + "The jwt audience.") + flag.StringVar(&s3FilesSecretAccessKey, "s3-files-secret-access-key", "minio123", + "The jwt audience.") + flag.StringVar(&s3FilesBucket, "s3-files-bucket", "lagoon-files", + "The jwt audience.") + flag.StringVar(&s3FilesRegion, "s3-files-region", "auto", + "The jwt audience.") + flag.StringVar(&s3FilesOrigin, "s3-files-origin", "http://minio.127.0.0.1.nip.io:9000", + "The jwt audience.") + + // Email sending configuration + flag.BoolVar(&disableEmail, "disable-email", false, + "Disable the logs2email feature.") + flag.StringVar(&emailSender, "email-sender-address", "notifications@lagoon.sh", + "The email address to send notifications as.") + flag.StringVar(&emailSenderPassword, "email-sender-password", "", + "The password (if required) for the sending email address.") + flag.StringVar(&emailHost, "email-host", "localhost", + "The host name or address for the email server.") + flag.StringVar(&emailPort, "email-port", "1025", + "The port for the email server.") + flag.BoolVar(&emailInsecureSkipVerify, "email-tls-insecure-skip-verify", true, + "Use TLS verification when talking to the email server.") flag.Parse() // get overrides from environment variables @@ -111,6 +135,11 @@ func main() { s3FilesRegion = getEnv("S3_FILES_REGION", s3FilesRegion) s3FilesOrigin = getEnv("S3_FILES_HOST", s3FilesOrigin) + emailSender = getEnv("EMAIL_SENDER_ADDRESS", emailSender) + emailSenderPassword = getEnv("EMAIL_SENDER_PASSWORD", emailSenderPassword) + emailHost = getEnv("EMAIL_HOST", emailHost) + emailPort = getEnv("EMAIL_PORT", emailPort) + enableDebug := true // configure the backup handler settings @@ -195,6 +224,16 @@ func main() { disableEmail, disableWebhooks, disableS3, + emailSender, + emailSenderPassword, + emailHost, + emailPort, + emailInsecureSkipVerify, + s3FilesAccessKeyID, + s3FilesSecretAccessKey, + s3FilesBucket, + s3FilesRegion, + s3FilesOrigin, ) // start the consumer From 4f637d37ae7e9064a90dce3162f7cde34d1337f6 Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Wed, 8 Jun 2022 15:57:39 +1000 Subject: [PATCH 10/17] fix: pass the gcs value through to messaging --- services/logs2notifications/internal/handler/main.go | 3 ++- services/logs2notifications/main.go | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/services/logs2notifications/internal/handler/main.go b/services/logs2notifications/internal/handler/main.go index 23b19a432d..b0b0fe11d8 100644 --- a/services/logs2notifications/internal/handler/main.go +++ b/services/logs2notifications/internal/handler/main.go @@ -145,7 +145,7 @@ func NewMessaging(config mq.Config, appID string, disableSlack, disableRocketChat, disableMicrosoftTeams, disableEmail, disableWebhooks, disableS3 bool, emailSender, emailSenderPassword, emailHost, emailPort string, emailInsecureSkipVerify bool, - s3FilesAccessKeyID, s3FilesSecretAccessKey, s3FilesBucket, s3FilesRegion, s3FilesOrigin string) *Messaging { + s3FilesAccessKeyID, s3FilesSecretAccessKey, s3FilesBucket, s3FilesRegion, s3FilesOrigin string, s3isGCS bool) *Messaging { return &Messaging{ Config: config, LagoonAPI: lagoonAPI, @@ -169,6 +169,7 @@ func NewMessaging(config mq.Config, S3FilesBucket: s3FilesBucket, S3FilesRegion: s3FilesRegion, S3FilesOrigin: s3FilesOrigin, + S3IsGCS: s3isGCS, } } diff --git a/services/logs2notifications/main.go b/services/logs2notifications/main.go index 39f84960ab..d596b6d231 100644 --- a/services/logs2notifications/main.go +++ b/services/logs2notifications/main.go @@ -238,6 +238,7 @@ func main() { s3FilesBucket, s3FilesRegion, s3FilesOrigin, + s3isGCS, ) // start the consumer From 5bbe48abba4bfc88eab8576b3b8a3db3eb399f8d Mon Sep 17 00:00:00 2001 From: Ben Jackson Date: Wed, 8 Jun 2022 16:10:28 +1000 Subject: [PATCH 11/17] test: fix missing bool --- services/logs2notifications/internal/handler/main_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/services/logs2notifications/internal/handler/main_test.go b/services/logs2notifications/internal/handler/main_test.go index 29687731ca..19a35ac43a 100644 --- a/services/logs2notifications/internal/handler/main_test.go +++ b/services/logs2notifications/internal/handler/main_test.go @@ -52,6 +52,7 @@ func TestProcessing(t *testing.T) { "s3FilesBucket", "s3FilesRegion", "s3FilesOrigin", + false, ) var testCases = map[string]struct { input string From d86524d11c05a9ab4c68dd4efd2b8310d9728f7b Mon Sep 17 00:00:00 2001 From: shreddedbacon Date: Fri, 5 Aug 2022 10:29:11 +1000 Subject: [PATCH 12/17] chore: yarn cleanup --- yarn.lock | 131 +----------------------------------------------------- 1 file changed, 2 insertions(+), 129 deletions(-) diff --git a/yarn.lock b/yarn.lock index 9bc025f996..df91a7d349 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3911,33 +3911,6 @@ dependencies: "@sinonjs/commons" "^1.7.0" -"@slack/client@^4.12.0": - version "4.12.0" - resolved "https://registry.yarnpkg.com/@slack/client/-/client-4.12.0.tgz#ec14c84a572cd9afed398bbcf7241c5a83e94c57" - integrity sha512-ltbdkcIWk2eIptCCT/oPmeCGlG8xb3kXfwuPTtvNujioLMo2xXqiPdfl7xK+AeUfnvj3fJLYbpTPuBTscuhgzw== - dependencies: - "@types/form-data" "^2.2.1" - "@types/is-stream" "^1.1.0" - "@types/node" ">=6.0.0" - "@types/p-cancelable" "^1.0.0" - "@types/p-queue" "^2.3.2" - "@types/p-retry" "^3.0.0" - "@types/retry" "^0.12.0" - "@types/ws" "^5.1.1" - axios "^0.18.0" - eventemitter3 "^3.1.0" - finity "^0.5.4" - form-data "^2.3.3" - is-stream "^1.1.0" - object.entries "^1.1.0" - object.getownpropertydescriptors "^2.0.3" - object.values "^1.1.0" - p-cancelable "~1.0.0" - p-queue "^2.4.2" - p-retry "^3.0.1" - retry "^0.12.0" - ws "^5.2.0" - "@storybook/addon-a11y@^5.3.0": version "5.3.22" resolved "https://registry.yarnpkg.com/@storybook/addon-a11y/-/addon-a11y-5.3.22.tgz#3bf1414d7084812278a5fc0d48474430893a9440" @@ -4853,11 +4826,6 @@ dependencies: "@types/node" "*" -"@types/events@*": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7" - integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g== - "@types/express-serve-static-core@*": version "4.17.7" resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.7.tgz#dfe61f870eb549dc6d7e12050901847c7d7e915b" @@ -4892,13 +4860,6 @@ resolved "https://registry.yarnpkg.com/@types/faker/-/faker-4.1.8.tgz#95240361f1379addae0a005ced7c5c77b0b1e1d8" integrity sha512-9ARxZRyXXCMMf8KjYhnD+GHE789HGwIqa1dolCRl/C6ExwvBIUmnIktLeY71TPmFOQiU3DQwrbQEX0pkyqX1tQ== -"@types/form-data@^2.2.1": - version "2.5.0" - resolved "https://registry.yarnpkg.com/@types/form-data/-/form-data-2.5.0.tgz#5025f7433016f923348434c40006d9a797c1b0e8" - integrity sha512-23/wYiuckYYtFpL+4RPWiWmRQH2BjFuqCUi2+N3amB1a1Drv+i/byTrGvlLwRVLFNAZbwpbQ7JvTK+VCAPMbcg== - dependencies: - form-data "*" - "@types/fs-capacitor@*": version "2.0.0" resolved "https://registry.yarnpkg.com/@types/fs-capacitor/-/fs-capacitor-2.0.0.tgz#17113e25817f584f58100fb7a08eed288b81956e" @@ -4955,13 +4916,6 @@ resolved "https://registry.yarnpkg.com/@types/is-function/-/is-function-1.0.0.tgz#1b0b819b1636c7baf0d6785d030d12edf70c3e83" integrity sha512-iTs9HReBu7evG77Q4EC8hZnqRt57irBDkK9nvmHroiOIVwYMQc4IvYvdRgwKfYepunIY7Oh/dBuuld+Gj9uo6w== -"@types/is-stream@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@types/is-stream/-/is-stream-1.1.0.tgz#b84d7bb207a210f2af9bed431dc0fbe9c4143be1" - integrity sha512-jkZatu4QVbR60mpIzjINmtS1ZF4a/FqdTUTBeQDVOQ2PYyidtwFKr0B5G6ERukKwliq+7mIXvxyppwzG5EgRYg== - dependencies: - "@types/node" "*" - "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0": version "2.0.1" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff" @@ -5068,7 +5022,7 @@ "@types/node" "*" form-data "^3.0.0" -"@types/node@*", "@types/node@>=6", "@types/node@>=6.0.0": +"@types/node@*", "@types/node@>=6": version "12.7.8" resolved "https://registry.yarnpkg.com/@types/node/-/node-12.7.8.tgz#cb1bf6800238898bc2ff6ffa5702c3cadd350708" integrity sha512-FMdVn84tJJdV+xe+53sYiZS4R5yn1mAIxfj+DVoNiQjTYz1+OYmjwEZr1ev9nU0axXwda0QDbYl06QHanRVH3A== @@ -5098,25 +5052,6 @@ resolved "https://registry.yarnpkg.com/@types/npmlog/-/npmlog-4.1.2.tgz#d070fe6a6b78755d1092a3dc492d34c3d8f871c4" integrity sha512-4QQmOF5KlwfxJ5IGXFIudkeLCdMABz03RcUXu+LCb24zmln8QW6aDjuGl4d4XPVLf2j+FnjelHTP7dvceAFbhA== -"@types/p-cancelable@^1.0.0": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@types/p-cancelable/-/p-cancelable-1.0.1.tgz#4f0ce8aa3ee0007c2768b9b3e6e22af20a6eecbd" - integrity sha512-MGdhuVx7X2yJe4dgOnDQcZQAYgiC/QK1O5HUPgTMTxWYiOlyWEO5DWmPBlXQBU1F6/JM7aSgYBDrpt7kurC6dw== - dependencies: - p-cancelable "*" - -"@types/p-queue@^2.3.2": - version "2.3.2" - resolved "https://registry.yarnpkg.com/@types/p-queue/-/p-queue-2.3.2.tgz#16bc5fece69ef85efaf2bce8b13f3ebe39c5a1c8" - integrity sha512-eKAv5Ql6k78dh3ULCsSBxX6bFNuGjTmof5Q/T6PiECDq0Yf8IIn46jCyp3RJvCi8owaEmm3DZH1PEImjBMd/vQ== - -"@types/p-retry@^3.0.0": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@types/p-retry/-/p-retry-3.0.1.tgz#44403f405b7b60d108df8ab37d8db81af1ea80f2" - integrity sha512-LkZCWg4JxFdQR/nGNZcMiyKAbNG3DKBRS6nn6Hg4dLS82zxkdBJJcvf4zXFvDCEI+e4dZdQX6wreqs9RDGMRfw== - dependencies: - p-retry "*" - "@types/parse-json@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" @@ -5195,11 +5130,6 @@ dependencies: "@types/node" "*" -"@types/retry@^0.12.0": - version "0.12.0" - resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" - integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== - "@types/serve-static@*": version "1.13.3" resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.3.tgz#eb7e1c41c4468272557e897e9171ded5e2ded9d1" @@ -5238,14 +5168,6 @@ resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.16.3.tgz#b776327a73e561b71e7881d0cd6d34a1424db86a" integrity sha512-9gtOPPkfyNoEqCQgx4qJKkuNm/x0R2hKR7fdl7zvTJyHnIisuE/LfvXOsYWL0o3qq6uiBnKZNNNzi3l0y/X+xw== -"@types/ws@^5.1.1": - version "5.1.2" - resolved "https://registry.yarnpkg.com/@types/ws/-/ws-5.1.2.tgz#f02d3b1cd46db7686734f3ce83bdf46c49decd64" - integrity sha512-NkTXUKTYdXdnPE2aUUbGOXE1XfMK527SCvU/9bj86kyFF6kZ9ZnOQ3mK5jADn98Y2vEUD/7wKDgZa7Qst2wYOg== - dependencies: - "@types/events" "*" - "@types/node" "*" - "@types/ws@^7.0.0": version "7.2.5" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.2.5.tgz#513f28b04a1ea1aa9dc2cad3f26e8e37c88aae49" @@ -10744,11 +10666,6 @@ find-up@^5.0.0: locate-path "^6.0.0" path-exists "^4.0.0" -finity@^0.5.4: - version "0.5.4" - resolved "https://registry.yarnpkg.com/finity/-/finity-0.5.4.tgz#f2a8a9198e8286467328ec32c8bfcc19a2229c11" - integrity sha512-3l+5/1tuw616Lgb0QBimxfdd2TqaDGpfCBpfX6EqtFmqUV3FtQnVEX4Aa62DagYEqnsTIjZcTfbq9msDbXYgyA== - flat-cache@^1.2.1: version "1.3.4" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.3.4.tgz#2c2ef77525cc2929007dfffa1dd314aa9c9dee6f" @@ -10861,15 +10778,6 @@ fork-ts-checker-webpack-plugin@^4.1.0: tapable "^1.0.0" worker-rpc "^0.1.0" -form-data@*, form-data@^2.3.3: - version "2.5.1" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4" - integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.6" - mime-types "^2.1.12" - form-data@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.0.tgz#31b7e39c85f1355b7139ee0c647cf0de7f83c682" @@ -15535,11 +15443,6 @@ node-releases@^2.0.1: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.1.tgz#3d1d395f204f1f2f29a54358b9fb678765ad2fc5" integrity sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA== -nodemailer@^6.3.0: - version "6.3.1" - resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.3.1.tgz#2784beebac6b9f014c424c54dbdcc5c4d1221346" - integrity sha512-j0BsSyaMlyadEDEypK/F+xlne2K5m6wzPYMXS/yxKI0s7jmT1kBx6GEKRVbZmyYfKOsjkeC/TiMVDJBI/w5gMQ== - nodemon@^1.12.1: version "1.19.2" resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.19.2.tgz#b0975147dc99b3761ceb595b3f9277084931dcc0" @@ -15959,21 +15862,11 @@ osenv@^0.1.4: os-homedir "^1.0.0" os-tmpdir "^1.0.0" -p-cancelable@*: - version "2.0.0" - resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.0.0.tgz#4a3740f5bdaf5ed5d7c3e34882c6fb5d6b266a6e" - integrity sha512-wvPXDmbMmu2ksjkB4Z3nZWTSkJEb9lqVdMaCKpZUGJG9TMiNp9XcbG3fn9fPKjem04fJMJnXoyFPk2FmgiaiNg== - p-cancelable@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw== -p-cancelable@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.0.0.tgz#07e9c6d22c31f9c6784cb4f1e1454a79b6d9e2d6" - integrity sha512-USgPoaC6tkTGlS831CxsVdmZmyb8tR1D+hStI84MyckLOzfJlYQUweomrwE3D8T7u5u5GVuW064LT501wHTYYA== - p-each-series@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-1.0.0.tgz#930f3d12dd1f50e7434457a22cd6f04ac6ad7f71" @@ -16057,31 +15950,11 @@ p-map@^3.0.0: dependencies: aggregate-error "^3.0.0" -p-queue@^2.4.2: - version "2.4.2" - resolved "https://registry.yarnpkg.com/p-queue/-/p-queue-2.4.2.tgz#03609826682b743be9a22dba25051bd46724fc34" - integrity sha512-n8/y+yDJwBjoLQe1GSJbbaYQLTI7QHNZI2+rpmCDbe++WLf9HC3gf6iqj5yfPAV71W4UF3ql5W1+UBPXoXTxng== - p-reduce@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-reduce/-/p-reduce-1.0.0.tgz#18c2b0dd936a4690a529f8231f58a0fdb6a47dfa" integrity sha1-GMKw3ZNqRpClKfgjH1ig/bakffo= -p-retry@*: - version "4.1.0" - resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.1.0.tgz#9ce7cef2069e84bf590df3b8ec18d740109338d6" - integrity sha512-oepllyG9gX1qH4Sm20YAKxg1GA7L7puhvGnTfimi31P07zSIj7SDV6YtuAx9nbJF51DES+2CIIRkXs8GKqWJxA== - dependencies: - "@types/retry" "^0.12.0" - retry "^0.12.0" - -p-retry@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-3.0.1.tgz#316b4c8893e2c8dc1cfa891f406c4b422bebf328" - integrity sha512-XE6G4+YTTkT2a0UWb2kjZe8xNwf8bIbnqpc/IS/idOBVhyves0mK5OJgeocjx7q5pvX/6m23xuzVPYT1uGM73w== - dependencies: - retry "^0.12.0" - p-try@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" @@ -18634,7 +18507,7 @@ ret@~0.1.10: resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== -retry@0.12.0, retry@^0.12.0: +retry@0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs= From c487094f5427eb499e1d7e4992a11cb73457a79e Mon Sep 17 00:00:00 2001 From: shreddedbacon Date: Thu, 25 Aug 2022 08:52:53 +1000 Subject: [PATCH 13/17] chore: fix slack deploy finished routes message and use correct var names --- .../logs2notifications/internal/handler/slack_events.go | 2 +- .../internal/handler/testdata/deployFinished/slack.txt | 6 +++--- services/logs2notifications/main.go | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/services/logs2notifications/internal/handler/slack_events.go b/services/logs2notifications/internal/handler/slack_events.go index 413fa75fb3..c847a50e8a 100644 --- a/services/logs2notifications/internal/handler/slack_events.go +++ b/services/logs2notifications/internal/handler/slack_events.go @@ -58,7 +58,7 @@ func (h *Messaging) processSlackTemplate(notification *Notification) (string, st slackTpl = `*[{{.ProjectName}}]* ` + "`{{.BranchName}}`" + `{{ if ne .ShortSha "" }} ({{.ShortSha}}){{end}} Build ` + "`{{.BuildName}}`" + ` Failed. {{if ne .LogLink ""}} <{{.LogLink}}|Logs>{{end}}` case "deployFinished": slackTpl = `*[{{.ProjectName}}]* ` + "`{{.BranchName}}`" + `{{ if ne .ShortSha "" }} ({{.ShortSha}}){{end}} Build ` + "`{{.BuildName}}`" + ` Succeeded. {{if ne .LogLink ""}} <{{.LogLink}}|Logs>{{end}} -* {{.Route}}{{range .Routes}}{{if ne . $.Route}}* {{.}}{{end}} +{{.Route}}{{range .Routes}}{{if ne . $.Route}}{{.}}{{end}} {{end}}` case "problemNotification": eventSplit := strings.Split(notification.Event, ":") diff --git a/services/logs2notifications/internal/handler/testdata/deployFinished/slack.txt b/services/logs2notifications/internal/handler/testdata/deployFinished/slack.txt index c2040f005a..44fa5333ec 100644 --- a/services/logs2notifications/internal/handler/testdata/deployFinished/slack.txt +++ b/services/logs2notifications/internal/handler/testdata/deployFinished/slack.txt @@ -1,4 +1,4 @@ *[ci-github-openshift]* `lagoon-type-override` Build `lagoon-build-1234` Succeeded. -* https://route1 -* https://route2 -* https://route3 +https://route1 +https://route2 +https://route3 diff --git a/services/logs2notifications/main.go b/services/logs2notifications/main.go index d596b6d231..6721dde53c 100644 --- a/services/logs2notifications/main.go +++ b/services/logs2notifications/main.go @@ -69,7 +69,7 @@ func main() { "The number of startup attempts before exiting.") flag.IntVar(&startupConnectionInterval, "startup-connection-interval-seconds", 30, "The duration between startup attempts.") - flag.StringVar(&lagoonAPIHost, "lagoon-api-host", "http://localhost:3000/graphql", + flag.StringVar(&lagoonAPIHost, "lagoon-api-host", "http://localhost:3000", "The host for the lagoon api.") flag.StringVar(&jwtTokenSigningKey, "jwt-token-signing-key", "super-secret-string", "The jwt signing token key or secret.") @@ -124,10 +124,10 @@ func main() { // get overrides from environment variables mqUser = getEnv("RABBITMQ_USERNAME", mqUser) mqPass = getEnv("RABBITMQ_PASSWORD", mqPass) - mqHost = getEnv("RABBITMQ_ADDRESS", mqHost) + mqHost = getEnv("RABBITMQ_HOST", mqHost) mqPort = getEnv("RABBITMQ_PORT", mqPort) lagoonAPIHost = getEnv("GRAPHQL_ENDPOINT", lagoonAPIHost) - jwtTokenSigningKey = getEnv("JWT_SECRET", jwtTokenSigningKey) + jwtTokenSigningKey = getEnv("JWTSECRET", jwtTokenSigningKey) jwtAudience = getEnv("JWT_AUDIENCE", jwtAudience) jwtSubject = getEnv("JWT_SUBJECT", jwtSubject) jwtIssuer = getEnv("JWT_ISSUER", jwtIssuer) @@ -153,7 +153,7 @@ func main() { Password: mqPass, } graphQLConfig := handler.LagoonAPI{ - Endpoint: lagoonAPIHost, + Endpoint: fmt.Sprintf("%s/graphql", lagoonAPIHost), TokenSigningKey: jwtTokenSigningKey, JWTAudience: jwtAudience, JWTSubject: jwtSubject, From fcaec42d4fdda7d90d6a4e6d7d9df04d1a5fda1b Mon Sep 17 00:00:00 2001 From: shreddedbacon Date: Thu, 25 Aug 2022 09:28:20 +1000 Subject: [PATCH 14/17] chore: add some more information for messaging in logs --- .../internal/handler/email_events.go | 2 +- services/logs2notifications/internal/handler/main.go | 2 +- .../internal/handler/microsoftteams_events.go | 4 ++-- .../internal/handler/rocketchat_events.go | 8 ++++---- .../internal/handler/slack_events.go | 7 ++++--- .../internal/handler/webhook_events.go | 10 ++++++++-- 6 files changed, 20 insertions(+), 13 deletions(-) diff --git a/services/logs2notifications/internal/handler/email_events.go b/services/logs2notifications/internal/handler/email_events.go index 8004ddb21d..5a229e83e7 100644 --- a/services/logs2notifications/internal/handler/email_events.go +++ b/services/logs2notifications/internal/handler/email_events.go @@ -204,7 +204,7 @@ func (h *Messaging) sendEmailMessage(emoji, color, subject, event, project, emai // log.Printf("Error sending message to email: %v", err) // return // } - log.Println(fmt.Sprintf("Sent %s message to email", event)) + log.Println(fmt.Sprintf("Sent %s message to email for project %s", event, project)) } func getEmailEvent(msgEvent string) (string, string, string, error) { diff --git a/services/logs2notifications/internal/handler/main.go b/services/logs2notifications/internal/handler/main.go index b0b0fe11d8..1852a029d5 100644 --- a/services/logs2notifications/internal/handler/main.go +++ b/services/logs2notifications/internal/handler/main.go @@ -123,7 +123,7 @@ type Notification struct { Route string `json:"route"` Routes []string `json:"routes"` Task struct { - ID int `json:"id"` + ID string `json:"id"` } `json:"task"` } `json:"meta"` Message string `json:"message"` diff --git a/services/logs2notifications/internal/handler/microsoftteams_events.go b/services/logs2notifications/internal/handler/microsoftteams_events.go index 87df357d1a..4335c16e8c 100644 --- a/services/logs2notifications/internal/handler/microsoftteams_events.go +++ b/services/logs2notifications/internal/handler/microsoftteams_events.go @@ -117,11 +117,11 @@ func (h *Messaging) sendMicrosoftTeamsMessage(emoji, color, webhook, event, proj client := &http.Client{} resp, err := client.Do(req) if err != nil { - log.Printf("Error sending message to microsoft teams: %v", err) + log.Printf("Error sending message to microsoft teams for project %s: %v", project, err) return } defer resp.Body.Close() - log.Println(fmt.Sprintf("Sent %s message to microsoft teams", event)) + log.Println(fmt.Sprintf("Sent %s message to microsoft teams for project %s", event, project)) } func getMicrosoftTeamsEvent(msgEvent string) (string, string, string, error) { diff --git a/services/logs2notifications/internal/handler/rocketchat_events.go b/services/logs2notifications/internal/handler/rocketchat_events.go index 978d0c723e..9e08ccc7a6 100644 --- a/services/logs2notifications/internal/handler/rocketchat_events.go +++ b/services/logs2notifications/internal/handler/rocketchat_events.go @@ -36,7 +36,7 @@ func (h *Messaging) SendToRocketChat(notification *Notification, channel, webhoo if err != nil { return } - h.sendRocketChatMessage(emoji, color, appID, channel, webhook, notification.Event, message) + h.sendRocketChatMessage(emoji, color, appID, channel, webhook, notification.Event, notification.Meta.ProjectName, message) } // SendToRocketChat . @@ -98,7 +98,7 @@ func (h *Messaging) processRocketChatTemplate(notification *Notification) (strin return emoji, color, rcMsg.String(), nil } -func (h *Messaging) sendRocketChatMessage(emoji, color, appID, channel, webhook, event, message string) { +func (h *Messaging) sendRocketChatMessage(emoji, color, appID, channel, webhook, event, project, message string) { data := RocketChatData{ Channel: channel, Attachments: []RocketChatAttachment{ @@ -123,11 +123,11 @@ func (h *Messaging) sendRocketChatMessage(emoji, color, appID, channel, webhook, client := &http.Client{} resp, err := client.Do(req) if err != nil { - log.Printf("Error sending message to rocketchat: %v", err) + log.Printf("Error sending message to rocketchat channel %s for project %s: %v", channel, project, err) return } defer resp.Body.Close() - log.Println(fmt.Sprintf("Sent %s message to rocketchat", event)) + log.Println(fmt.Sprintf("Sent %s message to rocketchat channel %s for project %s", event, channel, project)) } func getRocketChatEvent(msgEvent string) (string, string, string, error) { diff --git a/services/logs2notifications/internal/handler/slack_events.go b/services/logs2notifications/internal/handler/slack_events.go index c847a50e8a..b3d6462742 100644 --- a/services/logs2notifications/internal/handler/slack_events.go +++ b/services/logs2notifications/internal/handler/slack_events.go @@ -16,7 +16,7 @@ func (h *Messaging) SendToSlack(notification *Notification, channel, webhook, ap if err != nil { return } - h.sendSlackMessage(emoji, color, appID, channel, webhook, notification.Event, message) + h.sendSlackMessage(emoji, color, appID, channel, webhook, notification.Event, notification.Meta.ProjectName, message) } // processSlackTemplate . @@ -94,10 +94,11 @@ func (h *Messaging) sendSlackMessage(emoji, color, appID, channel, webhook, even err := slack.PostWebhook(webhook, &postMsg) if err != nil { // just log any errors - log.Printf("Error sending message to slack: %v", err) + log.Printf("Error sending message to slack channel %s for project %s: %v", channel, project, err) return } - log.Println(fmt.Sprintf("Sent %s message to slack", event)) + defer resp.Body.Close() + log.Println(fmt.Sprintf("Sent %s message to slack channel %s for project %s", event, channel, project)) } func getSlackEvent(msgEvent string) (string, string, string, error) { diff --git a/services/logs2notifications/internal/handler/webhook_events.go b/services/logs2notifications/internal/handler/webhook_events.go index 6e25acf3d1..6915809509 100644 --- a/services/logs2notifications/internal/handler/webhook_events.go +++ b/services/logs2notifications/internal/handler/webhook_events.go @@ -25,10 +25,10 @@ func (h *Messaging) SendToWebhook(notification *Notification, webhook schema.Not if err != nil { return } - h.sendWebhookMessage(*message, webhook) + h.sendWebhookMessage(notification.Meta.ProjectName, *message, webhook) } -func (h *Messaging) sendWebhookMessage(data WebhookData, webhook schema.NotificationWebhook) { +func (h *Messaging) sendWebhookMessage(project, data WebhookData, webhook schema.NotificationWebhook) { message, _ := json.Marshal(data) req, err := http.NewRequest("POST", webhook.Webhook, bytes.NewBuffer(message)) req.Header.Set("Content-Type", "application/json") @@ -42,6 +42,12 @@ func (h *Messaging) sendWebhookMessage(data WebhookData, webhook schema.Notifica } defer resp.Body.Close() log.Println(fmt.Sprintf("Sent %s message to webhook", data.Event)) + if err != nil { + log.Printf("Error sending message to webhook for project %s: %v", project, err) + return + } + defer resp.Body.Close() + log.Println(fmt.Sprintf("Sent %s message to webhook for project %s", data.Event, project)) } // processWebhookTemplate . From 40bc60fb3d58c3f6b1f5f00184f5e8f654ccd20c Mon Sep 17 00:00:00 2001 From: shreddedbacon Date: Thu, 25 Aug 2022 09:34:37 +1000 Subject: [PATCH 15/17] fix: missing arguments --- services/logs2notifications/internal/handler/s3_events.go | 4 ++-- services/logs2notifications/internal/handler/slack_events.go | 3 +-- .../logs2notifications/internal/handler/webhook_events.go | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/services/logs2notifications/internal/handler/s3_events.go b/services/logs2notifications/internal/handler/s3_events.go index a8082ec005..ed0132e66c 100644 --- a/services/logs2notifications/internal/handler/s3_events.go +++ b/services/logs2notifications/internal/handler/s3_events.go @@ -33,13 +33,13 @@ func (h *Messaging) SendToS3(notification *Notification, msgType MessageType) { ), ) } else if msgType == taskMessageType { - filePath := fmt.Sprintf("tasklogs/%s/%d-%s.txt", + filePath := fmt.Sprintf("tasklogs/%s/%s-%s.txt", notification.Project, notification.Meta.Task.ID, notification.Meta.RemoteID, ) if notification.Meta.Environment != "" { - filePath = fmt.Sprintf("tasklogs/%s/%s/%d-%s.txt", + filePath = fmt.Sprintf("tasklogs/%s/%s/%s-%s.txt", notification.Project, helpers.ShortenEnvironment(notification.Project, helpers.MakeSafe(notification.Meta.Environment)), notification.Meta.Task.ID, diff --git a/services/logs2notifications/internal/handler/slack_events.go b/services/logs2notifications/internal/handler/slack_events.go index b3d6462742..5134444975 100644 --- a/services/logs2notifications/internal/handler/slack_events.go +++ b/services/logs2notifications/internal/handler/slack_events.go @@ -79,7 +79,7 @@ func (h *Messaging) processSlackTemplate(notification *Notification) (string, st return emoji, color, slackMsg.String(), nil } -func (h *Messaging) sendSlackMessage(emoji, color, appID, channel, webhook, event, message string) { +func (h *Messaging) sendSlackMessage(emoji, color, appID, channel, webhook, event, project, message string) { attachment := slack.Attachment{ Text: fmt.Sprintf("%s %s", emoji, message), Color: color, @@ -97,7 +97,6 @@ func (h *Messaging) sendSlackMessage(emoji, color, appID, channel, webhook, even log.Printf("Error sending message to slack channel %s for project %s: %v", channel, project, err) return } - defer resp.Body.Close() log.Println(fmt.Sprintf("Sent %s message to slack channel %s for project %s", event, channel, project)) } diff --git a/services/logs2notifications/internal/handler/webhook_events.go b/services/logs2notifications/internal/handler/webhook_events.go index 6915809509..f6e76dff8c 100644 --- a/services/logs2notifications/internal/handler/webhook_events.go +++ b/services/logs2notifications/internal/handler/webhook_events.go @@ -28,7 +28,7 @@ func (h *Messaging) SendToWebhook(notification *Notification, webhook schema.Not h.sendWebhookMessage(notification.Meta.ProjectName, *message, webhook) } -func (h *Messaging) sendWebhookMessage(project, data WebhookData, webhook schema.NotificationWebhook) { +func (h *Messaging) sendWebhookMessage(project string, data WebhookData, webhook schema.NotificationWebhook) { message, _ := json.Marshal(data) req, err := http.NewRequest("POST", webhook.Webhook, bytes.NewBuffer(message)) req.Header.Set("Content-Type", "application/json") From 58d72d7edcc380223763f7590a2fd739b0a1ff7b Mon Sep 17 00:00:00 2001 From: shreddedbacon Date: Fri, 26 Aug 2022 10:39:05 +1000 Subject: [PATCH 16/17] chore: bump to go 1.18 --- services/logs2notifications/Dockerfile | 7 +++---- services/logs2notifications/go.mod | 21 ++++++++++++++++----- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/services/logs2notifications/Dockerfile b/services/logs2notifications/Dockerfile index 49ee341fbb..770253a651 100644 --- a/services/logs2notifications/Dockerfile +++ b/services/logs2notifications/Dockerfile @@ -1,8 +1,7 @@ # build the binary -ARG GO_VERSION ARG UPSTREAM_REPO ARG UPSTREAM_TAG -FROM golang:${GO_VERSION:-1.16.4} AS builder +FROM golang:1.18-alpine3.16 AS builder # bring in all the packages COPY . /go/src/github.com/uselagoon/lagoon/services/logs2notifications/ WORKDIR /go/src/github.com/uselagoon/lagoon/services/logs2notifications/ @@ -26,10 +25,10 @@ COPY --from=builder /go/src/github.com/uselagoon/lagoon/services/logs2notificati ENV LAGOON=logs2notifications # set defaults -ENV JWT_SECRET=super-secret-string \ +ENV JWTSECRET=super-secret-string \ JWT_AUDIENCE=api.dev \ GRAPHQL_ENDPOINT="http://api:3000/graphql" \ - RABBITMQ_ADDRESS=broker \ + RABBITMQ_HOST=broker \ RABBITMQ_PORT=5672 \ RABBITMQ_USERNAME=guest \ RABBITMQ_PASSWORD=guest diff --git a/services/logs2notifications/go.mod b/services/logs2notifications/go.mod index df048ac4c4..270f8259f1 100644 --- a/services/logs2notifications/go.mod +++ b/services/logs2notifications/go.mod @@ -1,20 +1,31 @@ module github.com/uselagoon/lagoon/services/logs2notifications -go 1.16 +go 1.18 require ( github.com/aws/aws-sdk-go v1.41.11 - github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927 // indirect github.com/cheshir/go-mq v1.0.2 github.com/dgrijalva/jwt-go v3.2.0+incompatible - github.com/fsouza/go-dockerclient v1.7.3 // indirect github.com/machinebox/graphql v0.2.3-0.20181106130121-3a9253180225 - github.com/matryer/is v1.4.0 // indirect github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2 github.com/slack-go/slack v0.9.5 + gopkg.in/mail.v2 v2.3.1 +) + +require ( + github.com/NeowayLabs/wabbit v0.0.0-20200409220312-12e68ab5b0c6 // indirect + github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927 // indirect + github.com/fsouza/go-dockerclient v1.7.3 // indirect + github.com/google/uuid v1.1.1 // indirect + github.com/gorilla/websocket v1.4.2 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/matryer/is v1.4.0 // indirect + github.com/pborman/uuid v1.2.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/streadway/amqp v0.0.0-20200108173154-1c71cc93ed71 // indirect + github.com/stretchr/testify v1.4.0 // indirect github.com/tiago4orion/conjure v0.0.0-20150908101743-93cb30b9d218 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect - gopkg.in/mail.v2 v2.3.1 ) // Fixes for AppID From aacd0df245d6c68e234f24504063ed82dbe2b049 Mon Sep 17 00:00:00 2001 From: shreddedbacon Date: Fri, 26 Aug 2022 10:41:38 +1000 Subject: [PATCH 17/17] chore: remove test from dockerfile --- services/logs2notifications/Dockerfile | 2 -- 1 file changed, 2 deletions(-) diff --git a/services/logs2notifications/Dockerfile b/services/logs2notifications/Dockerfile index 770253a651..72d59bb61d 100644 --- a/services/logs2notifications/Dockerfile +++ b/services/logs2notifications/Dockerfile @@ -6,8 +6,6 @@ FROM golang:1.18-alpine3.16 AS builder COPY . /go/src/github.com/uselagoon/lagoon/services/logs2notifications/ WORKDIR /go/src/github.com/uselagoon/lagoon/services/logs2notifications/ -# tests currently don't work because mocking rabbit is interesting -RUN GO111MODULE=on go test ./... # compile RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o logs2notifications .