Skip to content

Commit 99cc378

Browse files
mhoranNatalie Arellano
authored andcommitted
Make the diego healthcheck work on Windows
Because Windows "containers" don't have network namespaces, we have to map the internal port for health check to the external port that is actually used by Windows applications. This is in an effort to bring the windows_app_lifecycle in line with the buildpackapplifecycle, and ultimately get rid of the WAL. Signed-off-by: Natalie Arellano <[email protected]>
1 parent 6506afd commit 99cc378

File tree

5 files changed

+190
-1
lines changed

5 files changed

+190
-1
lines changed

cmd/healthcheck/healthcheck_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
// +build !windows
2+
13
package main_test
24

35
import (
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package main_test
2+
3+
import (
4+
"fmt"
5+
"net"
6+
"os"
7+
"os/exec"
8+
"strconv"
9+
10+
. "github.com/onsi/ginkgo"
11+
. "github.com/onsi/gomega"
12+
"github.com/onsi/gomega/gbytes"
13+
"github.com/onsi/gomega/gexec"
14+
"github.com/onsi/gomega/ghttp"
15+
)
16+
17+
var _ = Describe("HealthCheck", func() {
18+
var (
19+
server *ghttp.Server
20+
serverAddr string
21+
)
22+
23+
itExitsWithCode := func(healthCheck func() *gexec.Session, code int, reason string) {
24+
It("exits with code "+strconv.Itoa(code)+" and logs reason", func() {
25+
session := healthCheck()
26+
Eventually(session).Should(gexec.Exit(code))
27+
Expect(session.Out).To(gbytes.Say(reason))
28+
})
29+
}
30+
31+
BeforeEach(func() {
32+
ip := getNonLoopbackIP()
33+
server = ghttp.NewUnstartedServer()
34+
listener, err := net.Listen("tcp", ip+":0")
35+
Expect(err).NotTo(HaveOccurred())
36+
37+
server.HTTPTestServer.Listener = listener
38+
serverAddr = listener.Addr().String()
39+
server.Start()
40+
})
41+
42+
Describe("fails when parsing flags", func() {
43+
It("exits with code 2", func() {
44+
session, _ := gexec.Start(exec.Command(healthCheck, "-invalid_flag"), GinkgoWriter, GinkgoWriter)
45+
Eventually(session).Should(gexec.Exit(2))
46+
})
47+
})
48+
49+
Describe("port healthcheck", func() {
50+
var port string
51+
var err error
52+
53+
portHealthCheck := func() *gexec.Session {
54+
command := exec.Command(healthCheck, "-port", "8080", "-timeout", "100ms")
55+
command.Env = append(
56+
os.Environ(),
57+
fmt.Sprintf(`CF_INSTANCE_PORTS=[{"external":%s,"internal":%s}]`, port, "8080"),
58+
)
59+
session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)
60+
Expect(err).NotTo(HaveOccurred())
61+
return session
62+
}
63+
64+
BeforeEach(func() {
65+
_, port, err = net.SplitHostPort(serverAddr)
66+
Expect(err).NotTo(HaveOccurred())
67+
})
68+
69+
Context("when the address is listening", func() {
70+
itExitsWithCode(portHealthCheck, 0, "healthcheck passed")
71+
})
72+
73+
Context("when the address is not listening", func() {
74+
BeforeEach(func() {
75+
port = "-1"
76+
})
77+
78+
itExitsWithCode(portHealthCheck, 4, "failure to make TCP connection")
79+
})
80+
})
81+
82+
Describe("http healthcheck", func() {
83+
var port string
84+
var err error
85+
86+
httpHealthCheck := func() *gexec.Session {
87+
command := exec.Command(healthCheck, "-uri", "/api/_ping", "-port", "8080", "-timeout", "100ms")
88+
command.Env = append(
89+
os.Environ(),
90+
fmt.Sprintf(`CF_INSTANCE_PORTS=[{"external":%s,"internal":%s}]`, port, "8080"),
91+
)
92+
session, err := gexec.Start(command, GinkgoWriter, GinkgoWriter)
93+
Expect(err).NotTo(HaveOccurred())
94+
return session
95+
}
96+
97+
BeforeEach(func() {
98+
_, port, err = net.SplitHostPort(serverAddr)
99+
Expect(err).NotTo(HaveOccurred())
100+
})
101+
102+
Context("when the healthcheck is properly invoked", func() {
103+
BeforeEach(func() {
104+
server.RouteToHandler("GET", "/api/_ping", ghttp.VerifyRequest("GET", "/api/_ping"))
105+
})
106+
107+
Context("when the address is listening", func() {
108+
itExitsWithCode(httpHealthCheck, 0, "healthcheck passed")
109+
})
110+
111+
Context("when the address returns error http code", func() {
112+
BeforeEach(func() {
113+
server.RouteToHandler("GET", "/api/_ping", ghttp.RespondWith(500, ""))
114+
})
115+
116+
itExitsWithCode(httpHealthCheck, 6, "failure to get valid HTTP status code: 500")
117+
})
118+
})
119+
})
120+
})
121+
122+
func getNonLoopbackIP() string {
123+
interfaces, err := net.Interfaces()
124+
Expect(err).NotTo(HaveOccurred())
125+
for _, intf := range interfaces {
126+
addrs, err := intf.Addrs()
127+
if err != nil {
128+
continue
129+
}
130+
131+
for _, a := range addrs {
132+
if ipnet, ok := a.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
133+
if ipnet.IP.To4() != nil {
134+
return ipnet.IP.String()
135+
}
136+
}
137+
}
138+
}
139+
Fail("no non-loopback address found")
140+
panic("non-reachable")
141+
}

cmd/healthcheck/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ func main() {
4444
return
4545
}
4646

47-
h := healthcheck.NewHealthCheck(*network, *uri, *port, *timeout)
47+
h := newHealthCheck(*network, *uri, *port, *timeout)
4848
err = h.CheckInterfaces(interfaces)
4949
if err == nil {
5050
fmt.Println("healthcheck passed")

cmd/healthcheck/main_unix.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// +build !windows
2+
3+
package main
4+
5+
import (
6+
"time"
7+
8+
"code.cloudfoundry.org/healthcheck"
9+
)
10+
11+
func newHealthCheck(
12+
network, uri, port string,
13+
timeout time.Duration,
14+
) healthcheck.HealthCheck {
15+
return healthcheck.NewHealthCheck(network, uri, port, timeout)
16+
}

cmd/healthcheck/main_windows.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"os"
6+
"strconv"
7+
"time"
8+
9+
"code.cloudfoundry.org/healthcheck"
10+
)
11+
12+
type PortMapping struct {
13+
Internal int `json:"internal"`
14+
External int `json:"external"`
15+
}
16+
17+
func newHealthCheck(
18+
network, uri, port string,
19+
timeout time.Duration,
20+
) healthcheck.HealthCheck {
21+
jsonPortMappings := os.Getenv("CF_INSTANCE_PORTS")
22+
var portMappings []PortMapping
23+
json.Unmarshal([]byte(jsonPortMappings), &portMappings)
24+
for _, mapping := range portMappings {
25+
if strconv.Itoa(mapping.Internal) == port {
26+
port = strconv.Itoa(mapping.External)
27+
}
28+
}
29+
return healthcheck.NewHealthCheck(network, uri, port, timeout)
30+
}

0 commit comments

Comments
 (0)