Skip to content

Commit 6ab0c9a

Browse files
sjmiller609rgarcia
andauthored
Ingress (#24)
* Ingress feature * fixes - Fix ID type - latest envoy - configurable if envoy stops or not - dedupe instance lookup by id, name, id prefix to inside instance manager * Host port configurable by ingress resource * Auto download envoy when missing * envoy starting * Avoid shared memory conflicts between envoys on same host * Use hostname for routing * Update default config * review tweaks * context logger * Config validation and OTEL * Fix otel * Envoy provides stats to otel, not per-request tracing * Update stainless * Update stainless again * naming things is hard * Address pr review comments * Use file-based simple xDS instead of static config+reload * Fix config validation for dynamic resources, add tests, fix import cycle * Otel cluster should be static * e2e test should actually do a http request * Give ingress 5s to be ready * Up to 30s * Delete comment * Fix dynamic config update race condition --------- Co-authored-by: Rafael Garcia <[email protected]>
1 parent af3dfb0 commit 6ab0c9a

34 files changed

+5246
-314
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,6 @@ lib/vmm/binaries/cloud-hypervisor/*/*/cloud-hypervisor
2020
cloud-hypervisor
2121
cloud-hypervisor/**
2222
lib/system/exec_agent/exec-agent
23+
24+
# Envoy binaries
25+
lib/ingress/binaries/envoy/*/*/envoy

Makefile

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
SHELL := /bin/bash
2-
.PHONY: oapi-generate generate-vmm-client generate-wire generate-all dev build test install-tools gen-jwt download-ch-binaries download-ch-spec ensure-ch-binaries
2+
.PHONY: oapi-generate generate-vmm-client generate-wire generate-all dev build test install-tools gen-jwt download-ch-binaries download-ch-spec ensure-ch-binaries download-envoy-binaries ensure-envoy-binaries
33

44
# Directory where local binaries will be installed
55
BIN_DIR ?= $(CURDIR)/bin
@@ -49,6 +49,19 @@ download-ch-binaries:
4949
@chmod +x lib/vmm/binaries/cloud-hypervisor/v*/*/cloud-hypervisor
5050
@echo "Binaries downloaded successfully"
5151

52+
# Download Envoy binaries
53+
download-envoy-binaries:
54+
@echo "Downloading Envoy binaries..."
55+
@mkdir -p lib/ingress/binaries/envoy/v1.36/{x86_64,aarch64}
56+
@echo "Downloading Envoy v1.36.3 for x86_64..."
57+
@curl -L -o lib/ingress/binaries/envoy/v1.36/x86_64/envoy \
58+
https://github.com/envoyproxy/envoy/releases/download/v1.36.3/envoy-1.36.3-linux-x86_64
59+
@echo "Downloading Envoy v1.36.3 for aarch64..."
60+
@curl -L -o lib/ingress/binaries/envoy/v1.36/aarch64/envoy \
61+
https://github.com/envoyproxy/envoy/releases/download/v1.36.3/envoy-1.36.3-linux-aarch_64
62+
@chmod +x lib/ingress/binaries/envoy/v1.36/*/envoy
63+
@echo "Envoy binaries downloaded successfully"
64+
5265
# Download Cloud Hypervisor API spec
5366
download-ch-spec:
5467
@echo "Downloading Cloud Hypervisor API spec..."
@@ -86,21 +99,29 @@ generate-grpc:
8699
# Generate all code
87100
generate-all: oapi-generate generate-vmm-client generate-wire generate-grpc
88101

89-
# Check if binaries exist, download if missing
102+
# Check if CH binaries exist, download if missing
90103
.PHONY: ensure-ch-binaries
91104
ensure-ch-binaries:
92105
@if [ ! -f lib/vmm/binaries/cloud-hypervisor/v48.0/x86_64/cloud-hypervisor ]; then \
93106
echo "Cloud Hypervisor binaries not found, downloading..."; \
94107
$(MAKE) download-ch-binaries; \
95108
fi
96109

110+
# Check if Envoy binaries exist, download if missing
111+
.PHONY: ensure-envoy-binaries
112+
ensure-envoy-binaries:
113+
@if [ ! -f lib/ingress/binaries/envoy/v1.36/x86_64/envoy ]; then \
114+
echo "Envoy binaries not found, downloading..."; \
115+
$(MAKE) download-envoy-binaries; \
116+
fi
117+
97118
# Build exec-agent (guest binary) into its own directory for embedding
98119
lib/system/exec_agent/exec-agent: lib/system/exec_agent/main.go
99120
@echo "Building exec-agent..."
100121
cd lib/system/exec_agent && CGO_ENABLED=0 go build -ldflags="-s -w" -o exec-agent .
101122

102123
# Build the binary
103-
build: ensure-ch-binaries lib/system/exec_agent/exec-agent | $(BIN_DIR)
124+
build: ensure-ch-binaries ensure-envoy-binaries lib/system/exec_agent/exec-agent | $(BIN_DIR)
104125
go build -tags containers_image_openpgp -o $(BIN_DIR)/hypeman ./cmd/api
105126

106127
# Build exec CLI
@@ -118,7 +139,7 @@ dev: $(AIR)
118139
# Compile test binaries and grant network capabilities (runs as user, not root)
119140
# Usage: make test - runs all tests
120141
# make test TEST=TestCreateInstanceWithNetwork - runs specific test
121-
test: ensure-ch-binaries lib/system/exec_agent/exec-agent
142+
test: ensure-ch-binaries ensure-envoy-binaries lib/system/exec_agent/exec-agent
122143
@echo "Building test binaries..."
123144
@mkdir -p $(BIN_DIR)/tests
124145
@for pkg in $$(go list -tags containers_image_openpgp ./...); do \

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,24 @@ getcap ./bin/hypeman
5858

5959
**Note:** These capabilities must be reapplied after each rebuild. For production deployments, set capabilities on the installed binary. For local testing, this is handled automatically in `make test`.
6060

61+
**File Descriptor Limits:**
62+
63+
Envoy (used for ingress) requires a higher file descriptor limit than the default on some systems (root defaults to 1024 on many systems). If you see "Too many open files" errors, increase the limit:
64+
65+
```bash
66+
# Check current limit (also check with: sudo bash -c 'ulimit -n')
67+
ulimit -n
68+
69+
# Increase temporarily (current session)
70+
ulimit -n 65536
71+
72+
# For persistent changes, add to /etc/security/limits.conf:
73+
* soft nofile 65536
74+
* hard nofile 65536
75+
root soft nofile 65536
76+
root hard nofile 65536
77+
```
78+
6179
### Configuration
6280

6381
#### Environment variables
@@ -81,6 +99,10 @@ Hypeman can be configured using the following environment variables:
8199
| `OTEL_SERVICE_INSTANCE_ID` | Instance ID for telemetry (differentiates multiple servers) | hostname |
82100
| `LOG_LEVEL` | Default log level (debug, info, warn, error) | `info` |
83101
| `LOG_LEVEL_<SUBSYSTEM>` | Per-subsystem log level (API, IMAGES, INSTANCES, NETWORK, VOLUMES, VMM, SYSTEM, EXEC) | inherits default |
102+
| `ENVOY_LISTEN_ADDRESS` | Address for Envoy ingress listeners | `0.0.0.0` |
103+
| `ENVOY_ADMIN_ADDRESS` | Address for Envoy admin API | `127.0.0.1` |
104+
| `ENVOY_ADMIN_PORT` | Port for Envoy admin API | `9901` |
105+
| `ENVOY_STOP_ON_SHUTDOWN` | Stop Envoy when hypeman shuts down (if false, Envoy continues running) | `false` |
84106

85107
**Important: Subnet Configuration**
86108

cmd/api/api/api.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package api
33
import (
44
"github.com/onkernel/hypeman/cmd/api/config"
55
"github.com/onkernel/hypeman/lib/images"
6+
"github.com/onkernel/hypeman/lib/ingress"
67
"github.com/onkernel/hypeman/lib/instances"
78
"github.com/onkernel/hypeman/lib/network"
89
"github.com/onkernel/hypeman/lib/oapi"
@@ -16,6 +17,7 @@ type ApiService struct {
1617
InstanceManager instances.Manager
1718
VolumeManager volumes.Manager
1819
NetworkManager network.Manager
20+
IngressManager ingress.Manager
1921
}
2022

2123
var _ oapi.StrictServerInterface = (*ApiService)(nil)
@@ -27,13 +29,14 @@ func New(
2729
instanceManager instances.Manager,
2830
volumeManager volumes.Manager,
2931
networkManager network.Manager,
32+
ingressManager ingress.Manager,
3033
) *ApiService {
3134
return &ApiService{
3235
Config: config,
3336
ImageManager: imageManager,
3437
InstanceManager: instanceManager,
3538
VolumeManager: volumeManager,
3639
NetworkManager: networkManager,
40+
IngressManager: ingressManager,
3741
}
3842
}
39-

cmd/api/api/ingress.go

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
package api
2+
3+
import (
4+
"context"
5+
"errors"
6+
7+
"github.com/onkernel/hypeman/lib/ingress"
8+
"github.com/onkernel/hypeman/lib/logger"
9+
"github.com/onkernel/hypeman/lib/oapi"
10+
)
11+
12+
// ListIngresses lists all ingress resources
13+
func (s *ApiService) ListIngresses(ctx context.Context, request oapi.ListIngressesRequestObject) (oapi.ListIngressesResponseObject, error) {
14+
log := logger.FromContext(ctx)
15+
16+
ingresses, err := s.IngressManager.List(ctx)
17+
if err != nil {
18+
log.ErrorContext(ctx, "failed to list ingresses", "error", err)
19+
return oapi.ListIngresses500JSONResponse{
20+
Code: "internal_error",
21+
Message: "failed to list ingresses",
22+
}, nil
23+
}
24+
25+
oapiIngresses := make([]oapi.Ingress, len(ingresses))
26+
for i, ing := range ingresses {
27+
oapiIngresses[i] = ingressToOAPI(ing)
28+
}
29+
30+
return oapi.ListIngresses200JSONResponse(oapiIngresses), nil
31+
}
32+
33+
// CreateIngress creates a new ingress resource
34+
func (s *ApiService) CreateIngress(ctx context.Context, request oapi.CreateIngressRequestObject) (oapi.CreateIngressResponseObject, error) {
35+
log := logger.FromContext(ctx)
36+
37+
// Convert OAPI request to domain request
38+
domainReq := ingress.CreateIngressRequest{
39+
Name: request.Body.Name,
40+
Rules: make([]ingress.IngressRule, len(request.Body.Rules)),
41+
}
42+
43+
for i, rule := range request.Body.Rules {
44+
matchPort := 80
45+
if rule.Match.Port != nil {
46+
matchPort = *rule.Match.Port
47+
}
48+
domainReq.Rules[i] = ingress.IngressRule{
49+
Match: ingress.IngressMatch{
50+
Hostname: rule.Match.Hostname,
51+
Port: matchPort,
52+
},
53+
Target: ingress.IngressTarget{
54+
Instance: rule.Target.Instance,
55+
Port: rule.Target.Port,
56+
},
57+
}
58+
}
59+
60+
ing, err := s.IngressManager.Create(ctx, domainReq)
61+
if err != nil {
62+
switch {
63+
case errors.Is(err, ingress.ErrInvalidRequest):
64+
return oapi.CreateIngress400JSONResponse{
65+
Code: "bad_request",
66+
Message: err.Error(),
67+
}, nil
68+
case errors.Is(err, ingress.ErrAlreadyExists):
69+
return oapi.CreateIngress409JSONResponse{
70+
Code: "already_exists",
71+
Message: err.Error(),
72+
}, nil
73+
case errors.Is(err, ingress.ErrHostnameInUse):
74+
return oapi.CreateIngress409JSONResponse{
75+
Code: "hostname_in_use",
76+
Message: err.Error(),
77+
}, nil
78+
case errors.Is(err, ingress.ErrInstanceNotFound):
79+
return oapi.CreateIngress400JSONResponse{
80+
Code: "instance_not_found",
81+
Message: err.Error(),
82+
}, nil
83+
default:
84+
log.ErrorContext(ctx, "failed to create ingress", "error", err, "name", request.Body.Name)
85+
return oapi.CreateIngress500JSONResponse{
86+
Code: "internal_error",
87+
Message: "failed to create ingress",
88+
}, nil
89+
}
90+
}
91+
92+
return oapi.CreateIngress201JSONResponse(ingressToOAPI(*ing)), nil
93+
}
94+
95+
// GetIngress gets ingress details by ID or name
96+
func (s *ApiService) GetIngress(ctx context.Context, request oapi.GetIngressRequestObject) (oapi.GetIngressResponseObject, error) {
97+
log := logger.FromContext(ctx)
98+
99+
ing, err := s.IngressManager.Get(ctx, request.Id)
100+
if err != nil {
101+
if errors.Is(err, ingress.ErrNotFound) {
102+
return oapi.GetIngress404JSONResponse{
103+
Code: "not_found",
104+
Message: "ingress not found",
105+
}, nil
106+
}
107+
log.ErrorContext(ctx, "failed to get ingress", "error", err, "id", request.Id)
108+
return oapi.GetIngress500JSONResponse{
109+
Code: "internal_error",
110+
Message: "failed to get ingress",
111+
}, nil
112+
}
113+
114+
return oapi.GetIngress200JSONResponse(ingressToOAPI(*ing)), nil
115+
}
116+
117+
// DeleteIngress deletes an ingress by ID or name
118+
func (s *ApiService) DeleteIngress(ctx context.Context, request oapi.DeleteIngressRequestObject) (oapi.DeleteIngressResponseObject, error) {
119+
log := logger.FromContext(ctx)
120+
121+
err := s.IngressManager.Delete(ctx, request.Id)
122+
if err != nil {
123+
if errors.Is(err, ingress.ErrNotFound) {
124+
return oapi.DeleteIngress404JSONResponse{
125+
Code: "not_found",
126+
Message: "ingress not found",
127+
}, nil
128+
}
129+
log.ErrorContext(ctx, "failed to delete ingress", "error", err, "id", request.Id)
130+
return oapi.DeleteIngress500JSONResponse{
131+
Code: "internal_error",
132+
Message: "failed to delete ingress",
133+
}, nil
134+
}
135+
136+
return oapi.DeleteIngress204Response{}, nil
137+
}
138+
139+
// ingressToOAPI converts a domain Ingress to the OAPI type
140+
func ingressToOAPI(ing ingress.Ingress) oapi.Ingress {
141+
rules := make([]oapi.IngressRule, len(ing.Rules))
142+
for i, rule := range ing.Rules {
143+
port := rule.Match.GetPort()
144+
rules[i] = oapi.IngressRule{
145+
Match: oapi.IngressMatch{
146+
Hostname: rule.Match.Hostname,
147+
Port: &port,
148+
},
149+
Target: oapi.IngressTarget{
150+
Instance: rule.Target.Instance,
151+
Port: rule.Target.Port,
152+
},
153+
}
154+
}
155+
156+
return oapi.Ingress{
157+
Id: ing.ID,
158+
Name: ing.Name,
159+
Rules: rules,
160+
CreatedAt: ing.CreatedAt,
161+
}
162+
}

0 commit comments

Comments
 (0)