mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-28 12:03:33 +00:00
fix(caddy): proxy to only healthy containers
This commit is contained in:
@@ -15,7 +15,6 @@ import (
|
|||||||
"github.com/caddyserver/caddy/v2/caddyconfig"
|
"github.com/caddyserver/caddy/v2/caddyconfig"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy"
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp/reverseproxy"
|
||||||
"github.com/psviderski/uncloud/internal/machine/docker"
|
|
||||||
"github.com/psviderski/uncloud/pkg/api"
|
"github.com/psviderski/uncloud/pkg/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -24,34 +23,38 @@ func GenerateConfig(containers []api.ServiceContainer, verifyResponse string) (*
|
|||||||
httpHostUpstreams := make(map[string][]string)
|
httpHostUpstreams := make(map[string][]string)
|
||||||
httpsHostUpstreams := make(map[string][]string)
|
httpsHostUpstreams := make(map[string][]string)
|
||||||
for _, ctr := range containers {
|
for _, ctr := range containers {
|
||||||
logger := slog.With("container", ctr.ID)
|
if !ctr.Healthy() {
|
||||||
network, ok := ctr.NetworkSettings.Networks[docker.NetworkName]
|
|
||||||
if !ok {
|
|
||||||
// Container is not connected to the uncloud Docker network (could be host network).
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if network.IPAddress == "" {
|
|
||||||
logger.Error("Container has no IPv4 address.")
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ip := ctr.UncloudNetworkIP()
|
||||||
|
if !ip.IsValid() {
|
||||||
|
// Container is not connected to the uncloud Docker network (could be host network).
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log := slog.With("container", ctr.ID)
|
||||||
|
|
||||||
ports, err := ctr.ServicePorts()
|
ports, err := ctr.ServicePorts()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Failed to parse service ports for container.", "err", err)
|
log.Error("Failed to parse service ports for container.", "err", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, port := range ports {
|
for _, port := range ports {
|
||||||
|
if port.Mode != api.PortModeIngress {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
switch port.Protocol {
|
switch port.Protocol {
|
||||||
case api.ProtocolHTTP:
|
case api.ProtocolHTTP:
|
||||||
upstream := net.JoinHostPort(network.IPAddress, strconv.Itoa(int(port.ContainerPort)))
|
upstream := net.JoinHostPort(ip.String(), strconv.Itoa(int(port.ContainerPort)))
|
||||||
httpHostUpstreams[port.Hostname] = append(httpHostUpstreams[port.Hostname], upstream)
|
httpHostUpstreams[port.Hostname] = append(httpHostUpstreams[port.Hostname], upstream)
|
||||||
case api.ProtocolHTTPS:
|
case api.ProtocolHTTPS:
|
||||||
upstream := net.JoinHostPort(network.IPAddress, strconv.Itoa(int(port.ContainerPort)))
|
upstream := net.JoinHostPort(ip.String(), strconv.Itoa(int(port.ContainerPort)))
|
||||||
httpsHostUpstreams[port.Hostname] = append(httpsHostUpstreams[port.Hostname], upstream)
|
httpsHostUpstreams[port.Hostname] = append(httpsHostUpstreams[port.Hostname], upstream)
|
||||||
default:
|
default:
|
||||||
// TODO: implement L4 ingress routing for TCP and UDP.
|
// TODO: implement L4 ingress routing for TCP and UDP.
|
||||||
logger.Error("Unsupported protocol for ingress port.", "port", port)
|
log.Error("Unsupported protocol for ingress port.", "port", port)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,25 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestGenerateConfig(t *testing.T) {
|
func TestGenerateConfig(t *testing.T) {
|
||||||
|
configWithoutServices := `{
|
||||||
|
"servers": {
|
||||||
|
"http": {
|
||||||
|
"listen": [":80"],
|
||||||
|
"routes": [{
|
||||||
|
"match": [{"path": ["/.uncloud-verify"]}],
|
||||||
|
"handle": [{
|
||||||
|
"body": "verification-response-body",
|
||||||
|
"handler": "static_response",
|
||||||
|
"status_code": 200
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
"https": {
|
||||||
|
"listen": [":443"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
containers []api.ServiceContainer
|
containers []api.ServiceContainer
|
||||||
@@ -23,25 +42,8 @@ func TestGenerateConfig(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "empty containers",
|
name: "empty containers",
|
||||||
containers: []api.ServiceContainer{},
|
containers: []api.ServiceContainer{},
|
||||||
want: `{
|
want: configWithoutServices,
|
||||||
"servers": {
|
wantErr: false,
|
||||||
"http": {
|
|
||||||
"listen": [":80"],
|
|
||||||
"routes": [{
|
|
||||||
"match": [{"path": ["/.uncloud-verify"]}],
|
|
||||||
"handle": [{
|
|
||||||
"body": "verification-response-body",
|
|
||||||
"handler": "static_response",
|
|
||||||
"status_code": 200
|
|
||||||
}]
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
"https": {
|
|
||||||
"listen": [":443"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
wantErr: false,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -231,24 +233,7 @@ func TestGenerateConfig(t *testing.T) {
|
|||||||
containers: []api.ServiceContainer{
|
containers: []api.ServiceContainer{
|
||||||
newContainerWithoutNetwork("ignored.example.com:8080/http"),
|
newContainerWithoutNetwork("ignored.example.com:8080/http"),
|
||||||
},
|
},
|
||||||
want: `{
|
want: configWithoutServices,
|
||||||
"servers": {
|
|
||||||
"http": {
|
|
||||||
"listen": [":80"],
|
|
||||||
"routes": [{
|
|
||||||
"match": [{"path": ["/.uncloud-verify"]}],
|
|
||||||
"handle": [{
|
|
||||||
"body": "verification-response-body",
|
|
||||||
"handler": "static_response",
|
|
||||||
"status_code": 200
|
|
||||||
}]
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
"https": {
|
|
||||||
"listen": [":443"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -256,45 +241,63 @@ func TestGenerateConfig(t *testing.T) {
|
|||||||
containers: []api.ServiceContainer{
|
containers: []api.ServiceContainer{
|
||||||
newContainer("10.210.0.2", "invalid-port"),
|
newContainer("10.210.0.2", "invalid-port"),
|
||||||
},
|
},
|
||||||
want: `{
|
want: configWithoutServices,
|
||||||
"servers": {
|
|
||||||
"http": {
|
|
||||||
"listen": [":80"],
|
|
||||||
"routes": [{
|
|
||||||
"match": [{"path": ["/.uncloud-verify"]}],
|
|
||||||
"handle": [{
|
|
||||||
"body": "verification-response-body",
|
|
||||||
"handler": "static_response",
|
|
||||||
"status_code": 200
|
|
||||||
}]
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
"https": {
|
|
||||||
"listen": [":443"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`,
|
|
||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "containers with unsupported protocols ignored",
|
name: "containers with unsupported protocols and host mode ignored",
|
||||||
containers: []api.ServiceContainer{
|
containers: []api.ServiceContainer{
|
||||||
newContainer("10.210.0.2", "5000/tcp"),
|
newContainer("10.210.0.2", "5000/tcp"),
|
||||||
newContainer("10.210.0.3", "5000/udp"),
|
newContainer("10.210.0.3", "5000/udp"),
|
||||||
newContainer("10.210.0.4", "80:8080/tcp@host"),
|
newContainer("10.210.0.4", "80:8080/tcp@host"),
|
||||||
},
|
},
|
||||||
|
want: configWithoutServices,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "restarting container ignored",
|
||||||
|
containers: []api.ServiceContainer{
|
||||||
|
newRestartingContainer("10.210.0.2", "app.example.com:8080/http"),
|
||||||
|
},
|
||||||
|
want: configWithoutServices,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "stopped container ignored",
|
||||||
|
containers: []api.ServiceContainer{
|
||||||
|
newStoppedContainer("10.210.0.2", "app.example.com:8080/http"),
|
||||||
|
},
|
||||||
|
want: configWithoutServices,
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mix of running, restarting, and stopped containers",
|
||||||
|
containers: []api.ServiceContainer{
|
||||||
|
newContainer("10.210.0.2", "app.example.com:8080/http"),
|
||||||
|
newRestartingContainer("10.210.0.3", "app.example.com:8080/http"),
|
||||||
|
newStoppedContainer("10.210.0.4", "app.example.com:8080/http"),
|
||||||
|
},
|
||||||
want: `{
|
want: `{
|
||||||
"servers": {
|
"servers": {
|
||||||
"http": {
|
"http": {
|
||||||
"listen": [":80"],
|
"listen": [":80"],
|
||||||
"routes": [{
|
"routes": [
|
||||||
"match": [{"path": ["/.uncloud-verify"]}],
|
{
|
||||||
"handle": [{
|
"match": [{"host": ["app.example.com"]}],
|
||||||
"body": "verification-response-body",
|
"handle": [{
|
||||||
"handler": "static_response",
|
"handler": "reverse_proxy",
|
||||||
"status_code": 200
|
"upstreams": [{"dial": "10.210.0.2:8080"}]
|
||||||
}]
|
}]
|
||||||
}]
|
},
|
||||||
|
{
|
||||||
|
"match": [{"path": ["/.uncloud-verify"]}],
|
||||||
|
"handle": [{
|
||||||
|
"body": "verification-response-body",
|
||||||
|
"handler": "static_response",
|
||||||
|
"status_code": 200
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"https": {
|
"https": {
|
||||||
"listen": [":443"]
|
"listen": [":443"]
|
||||||
@@ -326,7 +329,11 @@ func TestGenerateConfig(t *testing.T) {
|
|||||||
func newContainer(ip string, ports ...string) api.ServiceContainer {
|
func newContainer(ip string, ports ...string) api.ServiceContainer {
|
||||||
portsLabel := strings.Join(ports, ",")
|
portsLabel := strings.Join(ports, ",")
|
||||||
return api.ServiceContainer{Container: api.Container{ContainerJSON: types.ContainerJSON{
|
return api.ServiceContainer{Container: api.Container{ContainerJSON: types.ContainerJSON{
|
||||||
ContainerJSONBase: &types.ContainerJSONBase{},
|
ContainerJSONBase: &types.ContainerJSONBase{
|
||||||
|
State: &types.ContainerState{
|
||||||
|
Running: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
NetworkSettings: &types.NetworkSettings{
|
NetworkSettings: &types.NetworkSettings{
|
||||||
Networks: map[string]*network.EndpointSettings{
|
Networks: map[string]*network.EndpointSettings{
|
||||||
docker.NetworkName: {
|
docker.NetworkName: {
|
||||||
@@ -345,7 +352,11 @@ func newContainer(ip string, ports ...string) api.ServiceContainer {
|
|||||||
func newContainerWithoutNetwork(ports ...string) api.ServiceContainer {
|
func newContainerWithoutNetwork(ports ...string) api.ServiceContainer {
|
||||||
portsLabel := strings.Join(ports, ",")
|
portsLabel := strings.Join(ports, ",")
|
||||||
return api.ServiceContainer{Container: api.Container{ContainerJSON: types.ContainerJSON{
|
return api.ServiceContainer{Container: api.Container{ContainerJSON: types.ContainerJSON{
|
||||||
ContainerJSONBase: &types.ContainerJSONBase{},
|
ContainerJSONBase: &types.ContainerJSONBase{
|
||||||
|
State: &types.ContainerState{
|
||||||
|
Running: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
NetworkSettings: &types.NetworkSettings{
|
NetworkSettings: &types.NetworkSettings{
|
||||||
Networks: map[string]*network.EndpointSettings{
|
Networks: map[string]*network.EndpointSettings{
|
||||||
"other-network": {
|
"other-network": {
|
||||||
@@ -360,3 +371,15 @@ func newContainerWithoutNetwork(ports ...string) api.ServiceContainer {
|
|||||||
},
|
},
|
||||||
}}}
|
}}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newRestartingContainer(ip string, ports ...string) api.ServiceContainer {
|
||||||
|
ctr := newContainer(ip, ports...)
|
||||||
|
ctr.Container.State.Restarting = true
|
||||||
|
return ctr
|
||||||
|
}
|
||||||
|
|
||||||
|
func newStoppedContainer(ip string, ports ...string) api.ServiceContainer {
|
||||||
|
ctr := newContainer(ip, ports...)
|
||||||
|
ctr.Container.State.Running = false
|
||||||
|
return ctr
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user