/
main.go
146 lines (133 loc) · 3.56 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"os/signal"
"strings"
"syscall"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
"go.uber.org/zap"
"github.com/hack-fan/config"
"github.com/hack-fan/skadigo"
)
var cli *client.Client
var log *zap.SugaredLogger
var ctx = context.Background()
var settings = new(Settings)
type Settings struct {
Debug bool `default:"false"`
Token string
Server string `default:"https://api.letserver.run"`
// default service name to update
Default string
}
// up: update default service
// up <service>: update the service
func handler(msg string) (string, error) {
log.Infof("job received: %s", msg)
// default error
e := fmt.Errorf("unsupported command: %s", msg)
// parse command
switch {
// update
case strings.HasPrefix(msg, "up"):
args := strings.Split(msg, " ")
service := settings.Default
if len(args) == 1 {
if settings.Default == "" {
log.Error("missing default setting")
return "", errors.New("default service is not defined")
}
} else if len(args) == 2 {
service = strings.TrimSpace(args[1])
} else {
log.Error(e)
return "", e
}
warning, err := update(service)
if err != nil {
log.Error(e)
return "", err
}
if warning != "" {
log.Warnf("service [%s] update warning:\n%s", service, warning)
return fmt.Sprintf("update service [%s] successful with warnings:\n%s", service, warning), nil
}
log.Infof("succeeded: %s", msg)
return fmt.Sprintf("update service [%s] successful", service), nil
// other
default:
log.Error(e)
return "", e
}
}
// update docker service
func update(service string) (string, error) {
resp, _, err := cli.ServiceInspectWithRaw(ctx, service, types.ServiceInspectOptions{})
if err != nil {
return "", fmt.Errorf("check service [%s] failed: %w", service, err)
}
// if you start service from cli, the image will be locked to a version, change it to tag only
image := resp.Spec.TaskTemplate.ContainerSpec.Image
i := strings.Index(image, "@")
if i > 0 {
resp.Spec.TaskTemplate.ContainerSpec.Image = image[0:i]
}
respLog, _ := json.MarshalIndent(resp, "", " ")
log.Debugf("service: %s", string(respLog))
// this field must greater than prev state to take effect
resp.Spec.TaskTemplate.ForceUpdate += 1
// then force update
res, err := cli.ServiceUpdate(ctx, service, resp.Version, resp.Spec, types.ServiceUpdateOptions{})
if err != nil {
return "", fmt.Errorf("update service [%s] failed: %w", service, err)
}
if len(res.Warnings) > 0 {
warning := strings.Join(res.Warnings, "\n")
return warning, nil
}
return "", nil
}
func main() {
var err error
// load config
config.MustLoad(settings)
// logger
var logger *zap.Logger
if settings.Debug {
logger, err = zap.NewDevelopment()
} else {
logger, err = zap.NewProduction()
}
if err != nil {
panic(err)
}
defer logger.Sync() // nolint
log = logger.Sugar()
// docker cli
cli, err = client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
panic(err)
}
// context
ctx, cancel := context.WithCancel(context.Background())
// system signals - for graceful shutdown or restart
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
go func() {
sig := <-sigs
log.Infof("%s signal received, send cancel to worker context.", sig)
cancel()
}()
// skadi agent
agent := skadigo.New(settings.Token, settings.Server, &skadigo.Options{
Logger: log,
})
log.Info("Skadi agent start")
agent.StartWorker(ctx, handler, 0)
log.Info("this worker have been safety stopped.")
}