Skip to content

Commit 8125e20

Browse files
committed
Added MQTT integration
1 parent 6bfb8d4 commit 8125e20

File tree

10 files changed

+241
-19
lines changed

10 files changed

+241
-19
lines changed

.github/workflows/release.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,13 @@ jobs:
4343
virtualzone/compose-updater:latest
4444
cache-from: type=local,src=/tmp/.buildx-cache
4545
cache-to: type=local,dest=/tmp/.buildx-cache-new
46+
- name: Docker Hub Description
47+
uses: peter-evans/dockerhub-description@v2
48+
with:
49+
username: virtualzone
50+
password: ${{ secrets.CI_REGISTRY_PASSWORD }}
51+
repository: virtualzone/compose-updater
52+
readme-filepath: ./README.md
4653
- name: Create Release
4754
uses: actions/create-release@v1
4855
env:

README.md

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@
99
A solution for watching your Docker® containers running via Docker Compose for image updates and automatically restarting the compositions whenever an image is refreshed.
1010

1111
## Overview
12-
Compose Updater is an application which continuously monitors your running docker containers. When a base image is changed, the updated version gets pulled (or built via --pull) from the registry and the docker compose composition gets restarted (via down and up -d).
12+
Compose Updater is an application which continuously monitors your running docker containers. When an image is updated, the updated version gets pulled (or built via --pull) from the registry and the docker compose composition gets restarted (via down and up -d).
13+
14+
Compose Updater is useful for your when you're using image tags which are updated regularly (such as ```image:latest``` or a specific major version like ```image:v3```).
15+
16+
Currently, Compose Updater doesn't help you when your're using image tags that won't change (such as an unchangable SemVer, i.e. ```image:1.2.3```). It won't update your Docker Compose files to use newer image tags.
1317

1418
## Usage
1519
### 1. Prepare your services
@@ -67,8 +71,32 @@ CLEANUP | -cleanup | 0 | Run docker system prune -a -f after each run
6771
ONCE | -once | 0 | Run once and exit
6872
PRINT_SETTINGS | -printSettings | 1 | Print settings on start
6973
UPDATE_LOG | -updateLog | '' | Log file for updates and restarts
70-
71-
# License
74+
BUILD | -build | 0 | Build the image of a service with "build:" section in YAML file every run
75+
MQTT_BROKER | -mqttBroker | '' | MQTT Broker address (i.e. tcp://127.0.0.1:1883)
76+
MQTT_CLIENT_ID | -mqttClientId | composeupdater | MQTT Client ID
77+
MQTT_TOPIC_PREFIX | -mqttTopicPrefix | composeupdater | MQTT Topic Prefix
78+
MQTT_USERNAME | -mqttUsername | '' | MQTT Username
79+
MQTT_PASSWORD | -mqttPassword | '' | MQTT Password
80+
81+
## Connecting an MQTT Broker
82+
You can connect Compose Updater to an MQTT Broker (such as Eclispe Mosquitto or HiveMQ). This way, the actions of each run (i.e. image pulls, composition restarts) are published to an MQTT topic. You can use these informations to send push notifications using a solution like [mqttwarn](https://github.com/jpmens/mqttwarn).
83+
84+
To connect to an MQTT broker, specify the required connection parameters in the settings (see above).
85+
86+
Compose Updater published the following topics:
87+
88+
Topic | Corresponding event | Example content
89+
--- | --- | ---
90+
update | On update run start and done | 'start' or 'done'
91+
update/composition/start | On start checking for updates for a specific Docker Composition | YAML File Path
92+
update/composition/restart/dry | On skipping composition restart due to dry-run | YAML File Path
93+
update/composition/restart/skip | On skipping composition restart due to no updated found | YAML File Path
94+
update/composition/restart/start | On restarting a composition | ```{"composeFile": "/path/to/docker-compose.yml", "services":[{"name": "service1", "image": "image:tag"}]}```
95+
update/composition/restart/done | On finished restarting a composition | ```{"composeFile": "/path/to/docker-compose.yml", "services":[{"name": "service1", "image": "image:tag"}]}```
96+
update/composition/service/built | On service's image built | ```{"composeFile": "/path/to/docker-compose.yml", "services":[{"name": "service1", "image": "image:tag"}]}```
97+
update/composition/service/pulled | On service's image pulled | ```{"composeFile": "/path/to/docker-compose.yml", "services":[{"name": "service1", "image": "image:tag"}]}```
98+
99+
## License
72100
GNU General Public License v3.0
73101

74102
Docker® is a trademark of Docker, Inc.

src/go.mod

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,7 @@ module github.com/virtualzone/compose-updater
22

33
go 1.16
44

5-
require gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
5+
require (
6+
github.com/eclipse/paho.mqtt.golang v1.3.5 // indirect
7+
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
8+
)

src/go.sum

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
github.com/eclipse/paho.mqtt.golang v1.3.5 h1:sWtmgNxYM9P2sP+xEItMozsR3w0cqZFlqnNN1bdl41Y=
2+
github.com/eclipse/paho.mqtt.golang v1.3.5/go.mod h1:eTzb4gxwwyWpqBUHGQZ4ABAV7+Jgm1PklsYT/eo8Hcc=
3+
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
4+
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
5+
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
6+
golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0 h1:Jcxah/M+oLZ/R4/z5RzfPzGbPXnVDPkEDtf2JnuxN+U=
7+
golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
8+
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
9+
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
10+
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
111
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
212
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
313
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=

src/settings.go

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,20 @@ import (
1010

1111
// Settings holds the program runtime configuration
1212
type Settings struct {
13-
Cleanup bool
14-
Dry bool
15-
Help bool
16-
Interval int64
17-
Once bool
18-
PrintSettings bool
19-
UpdateLog string
13+
Cleanup bool
14+
Dry bool
15+
Help bool
16+
Interval int64
17+
Once bool
18+
PrintSettings bool
19+
UpdateLog string
20+
Build bool
21+
MqttBroker string
22+
MqttClientID string
23+
MqttTopicPrefix string
24+
MqttUsername string
25+
MqttPassword string
2026
//CompleteStop bool
21-
Build bool
2227
}
2328

2429
var GlobalSettings *Settings = nil
@@ -34,6 +39,11 @@ func ReadSettings() {
3439
s.stringFlagEnv(&s.UpdateLog, "updateLog", "UPDATE_LOG", "", "update log file")
3540
//s.boolFlagEnv(&s.CompleteStop, "completeStop", "COMPLETE_STOP", false, "Restart all services in docker-compose.yml (even unmanaged) after a new image is pulled")
3641
s.boolFlagEnv(&s.Build, "build", "BUILD", false, "Rebuild images of services with 'build' definition")
42+
s.stringFlagEnv(&s.MqttBroker, "mqttBroker", "MQTT_BROKER", "", "MQTT Broker address (i.e. tcp://127.0.0.1:1883)")
43+
s.stringFlagEnv(&s.MqttClientID, "mqttClientId", "MQTT_CLIENT_ID", "composeupdater", "MQTT Client ID")
44+
s.stringFlagEnv(&s.MqttTopicPrefix, "mqttTopicPrefix", "MQTT_TOPIC_PREFIX", "composeupdater", "MQTT Topic Prefix")
45+
s.stringFlagEnv(&s.MqttUsername, "mqttUsername", "MQTT_USERNAME", "", "MQTT Username")
46+
s.stringFlagEnv(&s.MqttPassword, "mqttPassword", "MQTT_PASSWORD", "", "MQTT Password")
3747
flag.Parse()
3848
GlobalSettings = s
3949
}
@@ -71,5 +81,9 @@ func (settings *Settings) Print() {
7181
log.Printf(" printSettings ... %t\n", settings.PrintSettings)
7282
//log.Printf(" completeStop .... %t\n", settings.CompleteStop)
7383
log.Printf(" build ........... %t\n", settings.Build)
74-
log.Printf(" updateLog ....... %s\n", settings.UpdateLog)
84+
log.Printf(" mqttBroker ...... %s\n", settings.MqttBroker)
85+
log.Printf(" mqttClientId .... %s\n", settings.MqttClientID)
86+
log.Printf(" mqttTopicPrefix . %s\n", settings.MqttTopicPrefix)
87+
log.Printf(" mqttUsername .... %s\n", settings.MqttUsername)
88+
log.Printf(" mqttPassword .... %s\n", "(hidden)")
7589
}

src/update-event.go

Lines changed: 115 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,76 @@
11
package main
22

3-
import "log"
3+
import (
4+
"encoding/json"
5+
"log"
6+
"strings"
47

5-
type UpdateEvent struct{}
8+
mqtt "github.com/eclipse/paho.mqtt.golang"
9+
)
10+
11+
type UpdateEvent struct {
12+
MqttClient mqtt.Client
13+
}
14+
15+
type MqttComposeServiceEvent struct {
16+
Name string `json:"name"`
17+
Image string `json:"image"`
18+
}
19+
20+
type MqttComposeFileEvent struct {
21+
ComposeFile string `json:"composeFile"`
22+
Services []*MqttComposeServiceEvent `json:"services"`
23+
}
624

725
var EventBus *UpdateEvent = nil
826

927
func CreateEventBus() {
10-
EventBus = &UpdateEvent{}
28+
e := &UpdateEvent{}
29+
e.connectMqtt()
30+
EventBus = e
1131
}
1232

1333
func (e *UpdateEvent) OnPerformUpdatesStart() {
1434
log.Println("Gathering details about running containers...")
35+
e.publishMqtt("update", "start")
1536
}
1637

1738
func (e *UpdateEvent) OnPerformUpdatesComplete() {
1839
log.Println("Done.")
40+
e.publishMqttWait("update", "done")
1941
}
2042

2143
func (e *UpdateEvent) OnProcessComposeFileStart(composeFile *ComposeFile) {
2244
log.Printf("Checking for updates of services in %s...\n", composeFile.YamlFilePath)
45+
e.publishMqtt("update/composition/start", composeFile.YamlFilePath)
2346
}
2447

2548
func (e *UpdateEvent) OnSkipRestartComposeFileDryMode(composeFile *ComposeFile) {
2649
log.Printf("Dry-Mode enabled, not restarting services in %s\n", composeFile.YamlFilePath)
50+
e.publishMqtt("update/composition/restart/dry", composeFile.YamlFilePath)
2751
}
2852

2953
func (e *UpdateEvent) OnSkipRestartComposeFileNoUpdates(composeFile *ComposeFile) {
3054
log.Printf("No need to restart services in %s\n", composeFile.YamlFilePath)
55+
e.publishMqtt("update/composition/restart/skip", composeFile.YamlFilePath)
3156
}
3257

3358
func (e *UpdateEvent) OnRestartComposeFile(composeFile *ComposeFile) {
3459
log.Printf("Restarting services in %s...\n", composeFile.YamlFilePath)
60+
msg := &MqttComposeFileEvent{
61+
ComposeFile: composeFile.YamlFilePath,
62+
Services: e.servicesToStringArray(composeFile),
63+
}
64+
e.publishMqttJSON("update/composition/restart/start", msg)
3565
}
3666

3767
func (e *UpdateEvent) OnRestartComposeFileComplete(composeFile *ComposeFile) {
3868
log.Printf("Restarted services in %s\n", composeFile.YamlFilePath)
69+
msg := &MqttComposeFileEvent{
70+
ComposeFile: composeFile.YamlFilePath,
71+
Services: e.servicesToStringArray(composeFile),
72+
}
73+
e.publishMqttJSON("update/composition/restart/done", msg)
3974
}
4075

4176
func (e *UpdateEvent) OnProcessServiceStart(service *ComposeService) {
@@ -44,12 +79,89 @@ func (e *UpdateEvent) OnProcessServiceStart(service *ComposeService) {
4479

4580
func (e *UpdateEvent) OnServiceNewImageBuilt(service *ComposeService) {
4681
log.Printf("Built new image for service %s\n", service.Name)
82+
msg := &MqttComposeFileEvent{
83+
ComposeFile: service.ComposeFile.YamlFilePath,
84+
Services: []*MqttComposeServiceEvent{e.servicesToString(service)},
85+
}
86+
e.publishMqttJSON("update/composition/service/built", msg)
4787
}
4888

4989
func (e *UpdateEvent) OnServiceNewImagePulled(service *ComposeService) {
5090
log.Printf("Pulled new image %s for service %s\n", service.ImageName, service.Name)
91+
msg := &MqttComposeFileEvent{
92+
ComposeFile: service.ComposeFile.YamlFilePath,
93+
Services: []*MqttComposeServiceEvent{e.servicesToString(service)},
94+
}
95+
e.publishMqttJSON("update/composition/service/pulled", msg)
5196
}
5297

5398
func (e *UpdateEvent) OnImagePruneStart() {
5499
log.Println("Removing unused images...")
55100
}
101+
102+
func (e *UpdateEvent) OnMqttConnect(opts *mqtt.ClientOptions) {
103+
log.Printf("Connecting to MQTT Broker %s...\n", opts.Servers[0])
104+
}
105+
106+
func (e *UpdateEvent) connectMqtt() {
107+
if GlobalSettings.MqttBroker == "" {
108+
return
109+
}
110+
opts := mqtt.NewClientOptions().AddBroker(GlobalSettings.MqttBroker).SetClientID(GlobalSettings.MqttClientID)
111+
if GlobalSettings.MqttUsername != "" {
112+
opts.SetUsername(GlobalSettings.MqttUsername)
113+
}
114+
if GlobalSettings.MqttPassword != "" {
115+
opts.SetPassword(GlobalSettings.MqttPassword)
116+
}
117+
e.OnMqttConnect(opts)
118+
client := mqtt.NewClient(opts)
119+
if token := client.Connect(); token.Wait() && token.Error() != nil {
120+
log.Fatalf("Could not connect to MQTT broker: %s\n", token.Error())
121+
}
122+
GlobalSettings.MqttTopicPrefix = strings.TrimSuffix(GlobalSettings.MqttTopicPrefix, "/")
123+
e.MqttClient = client
124+
}
125+
126+
func (e *UpdateEvent) publishMqttInternal(topic string, s string, wait bool) {
127+
if e.MqttClient == nil {
128+
return
129+
}
130+
token := e.MqttClient.Publish(GlobalSettings.MqttTopicPrefix+"/"+topic, 0, false, s)
131+
if wait {
132+
token.Wait()
133+
}
134+
}
135+
136+
func (e *UpdateEvent) publishMqtt(topic string, s string) {
137+
e.publishMqttInternal(topic, s, false)
138+
}
139+
140+
func (e *UpdateEvent) publishMqttWait(topic string, s string) {
141+
e.publishMqttInternal(topic, s, true)
142+
}
143+
144+
func (e *UpdateEvent) publishMqttJSON(topic string, v interface{}) error {
145+
json, err := json.Marshal(v)
146+
if err != nil {
147+
return err
148+
}
149+
e.publishMqttInternal(topic, string(json), false)
150+
return nil
151+
}
152+
153+
func (e *UpdateEvent) servicesToString(service *ComposeService) *MqttComposeServiceEvent {
154+
item := &MqttComposeServiceEvent{
155+
Name: service.Name,
156+
Image: service.ImageName,
157+
}
158+
return item
159+
}
160+
161+
func (e *UpdateEvent) servicesToStringArray(composeFile *ComposeFile) []*MqttComposeServiceEvent {
162+
services := make([]*MqttComposeServiceEvent, 0)
163+
for _, service := range composeFile.Services {
164+
services = append(services, e.servicesToString(service))
165+
}
166+
return services
167+
}

src/version.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
package main
22

33
// BuildVersion is the version
4-
const BuildVersion = "2.0.2"
4+
const BuildVersion = "2.1.0"

test/mosquitto.conf

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
per_listener_settings true
2+
3+
listener 1883
4+
allow_anonymous false
5+
password_file /mosquitto/config/mqpass
6+
require_certificate false

test/mqpass

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
compose-updater:$7$101$/EAt7Hm36wbI6jXp$B+LItdHbG64HCjGg5Bbuz3k/r6HtZNaZozSHTs7RUmw6d/HuPt9k/q2ZVAj606rFphljf0XdWR5AZ9AtDbl7Kw==

0 commit comments

Comments
 (0)