forked from aschzero/hera
-
Notifications
You must be signed in to change notification settings - Fork 0
/
handler.go
190 lines (151 loc) · 4.08 KB
/
handler.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
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
package main
import (
"fmt"
"golang.org/x/net/publicsuffix"
"net"
"time"
"github.com/spf13/afero"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/events"
)
const (
heraHostname = "hera.hostname"
heraPort = "hera.port"
)
// A Handler is responsible for responding to container start and die events
type Handler struct {
Client *Client
}
// NewHandler returns a new Handler instance
func NewHandler(client *Client) *Handler {
handler := &Handler{
Client: client,
}
return handler
}
// HandleEvent dispatches an event to the appropriate handler method depending on its status
func (h *Handler) HandleEvent(event events.Message) {
switch status := event.Status; status {
case "start":
err := h.handleStartEvent(event)
if err != nil {
log.Error(err.Error())
}
case "die":
err := h.handleDieEvent(event)
if err != nil {
log.Error(err.Error())
}
}
}
// HandleContainer allows immediate tunnel creation when hera is started by treating existing
// containers as start events
func (h *Handler) HandleContainer(id string) error {
event := events.Message{
ID: id,
}
err := h.handleStartEvent(event)
if err != nil {
return err
}
return nil
}
// handleStartEvent inspects the container from a start event and creates a tunnel if the container
// has been appropriately labeled and a certificate exists for its hostname
func (h *Handler) handleStartEvent(event events.Message) error {
container, err := h.Client.Inspect(event.ID)
if err != nil {
return err
}
hostname := getLabel(heraHostname, container)
port := getLabel(heraPort, container)
if hostname == "" || port == "" {
return nil
}
log.Infof("Container found, connecting to %s...", container.ID[:12])
ip, err := h.resolveHostname(container)
if err != nil {
return err
}
cert, err := getCertificate(hostname)
if err != nil {
return err
}
config := &TunnelConfig{
IP: ip,
Hostname: hostname,
Port: port,
}
tunnel := NewTunnel(config, cert)
tunnel.Start()
return nil
}
// handleDieEvent inspects the container from a die event and stops the tunnel if one exists.
// An error is returned if a tunnel cannot be found or if the tunnel fails to stop
func (h *Handler) handleDieEvent(event events.Message) error {
container, err := h.Client.Inspect(event.ID)
if err != nil {
return err
}
hostname := getLabel("hera.hostname", container)
if hostname == "" {
return nil
}
tunnel, err := GetTunnelForHost(hostname)
if err != nil {
return err
}
err = tunnel.Stop()
if err != nil {
return err
}
return nil
}
// resolveHostname returns the IP address of a container from its hostname.
// An error is returned if the hostname cannot be resolved after five attempts.
func (h *Handler) resolveHostname(container types.ContainerJSON) (string, error) {
var resolved []string
var err error
attempts := 0
maxAttempts := 5
for attempts < maxAttempts {
attempts++
resolved, err = net.LookupHost(container.Config.Hostname)
if err != nil {
time.Sleep(2 * time.Second)
log.Infof("Unable to connect, retrying... (%d/%d)", attempts, maxAttempts)
continue
}
return resolved[0], nil
}
return "", fmt.Errorf("Unable to connect to %s", container.ID[:12])
}
// getLabel returns the label value from a given label name and container JSON.
func getLabel(name string, container types.ContainerJSON) string {
value, ok := container.Config.Labels[name]
if !ok {
return ""
}
return value
}
// getCertificate returns a Certificate for a given hostname.
// An error is returned if the root hostname cannot be parsed or if the certificate cannot be found.
func getCertificate(hostname string) (*Certificate, error) {
rootHostname, err := getRootDomain(hostname)
if err != nil {
return nil, err
}
cert, err := FindCertificateForHost(rootHostname, afero.NewOsFs())
if err != nil {
return nil, err
}
return cert, nil
}
// getRootDomain returns the root domain for a given hostname
func getRootDomain(hostname string) (string, error) {
domain, err := publicsuffix.EffectiveTLDPlusOne(hostname)
if err != nil {
return "", err
}
return domain, nil
}