Add TCP/UDP port mapping support

- Add app_ports table for storing port mappings per app
- Add Port model with CRUD operations
- Add handlers for adding/deleting port mappings
- Add ports section to app detail template
- Update Docker client to configure port bindings when creating containers
- Support both TCP and UDP protocols
This commit is contained in:
2025-12-30 12:11:57 +07:00
parent 4ece7431af
commit bc275f7b9c
9 changed files with 398 additions and 5 deletions

View File

@@ -121,6 +121,7 @@ func (h *Handlers) HandleAppDetail() http.HandlerFunc {
envVars, _ := application.GetEnvVars(request.Context())
labels, _ := application.GetLabels(request.Context())
volumes, _ := application.GetVolumes(request.Context())
ports, _ := application.GetPorts(request.Context())
deployments, _ := application.GetDeployments(
request.Context(),
recentDeploymentsLimit,
@@ -135,6 +136,7 @@ func (h *Handlers) HandleAppDetail() http.HandlerFunc {
"EnvVars": envVars,
"Labels": labels,
"Volumes": volumes,
"Ports": ports,
"Deployments": deployments,
"WebhookURL": webhookURL,
"DeployKey": deployKey,
@@ -685,6 +687,96 @@ func (h *Handlers) HandleVolumeDelete() http.HandlerFunc {
}
}
// HandlePortAdd handles adding a port mapping.
func (h *Handlers) HandlePortAdd() http.HandlerFunc {
return func(writer http.ResponseWriter, request *http.Request) {
appID := chi.URLParam(request, "id")
application, findErr := models.FindApp(request.Context(), h.db, appID)
if findErr != nil || application == nil {
http.NotFound(writer, request)
return
}
parseErr := request.ParseForm()
if parseErr != nil {
http.Error(writer, "Bad Request", http.StatusBadRequest)
return
}
hostPort, containerPort, valid := parsePortValues(
request.FormValue("host_port"),
request.FormValue("container_port"),
)
if !valid {
http.Redirect(writer, request, "/apps/"+application.ID, http.StatusSeeOther)
return
}
protocol := request.FormValue("protocol")
if protocol != "tcp" && protocol != "udp" {
protocol = "tcp"
}
port := models.NewPort(h.db)
port.AppID = application.ID
port.HostPort = hostPort
port.ContainerPort = containerPort
port.Protocol = models.PortProtocol(protocol)
saveErr := port.Save(request.Context())
if saveErr != nil {
h.log.Error("failed to save port", "error", saveErr)
}
http.Redirect(writer, request, "/apps/"+application.ID, http.StatusSeeOther)
}
}
// parsePortValues parses and validates host and container port strings.
func parsePortValues(hostPortStr, containerPortStr string) (int, int, bool) {
hostPort, hostErr := strconv.Atoi(hostPortStr)
containerPort, containerErr := strconv.Atoi(containerPortStr)
if hostErr != nil || containerErr != nil || hostPort <= 0 || containerPort <= 0 {
return 0, 0, false
}
return hostPort, containerPort, true
}
// HandlePortDelete handles deleting a port mapping.
func (h *Handlers) HandlePortDelete() http.HandlerFunc {
return func(writer http.ResponseWriter, request *http.Request) {
appID := chi.URLParam(request, "id")
portIDStr := chi.URLParam(request, "portID")
portID, parseErr := strconv.ParseInt(portIDStr, 10, 64)
if parseErr != nil {
http.NotFound(writer, request)
return
}
port, findErr := models.FindPort(request.Context(), h.db, portID)
if findErr != nil || port == nil {
http.NotFound(writer, request)
return
}
deleteErr := port.Delete(request.Context())
if deleteErr != nil {
h.log.Error("failed to delete port", "error", deleteErr)
}
http.Redirect(writer, request, "/apps/"+appID, http.StatusSeeOther)
}
}
// formatDeployKey formats an SSH public key with a descriptive comment.
// Format: ssh-ed25519 AAAA... upaas-2025-01-15-example.com.
func formatDeployKey(pubKey string, createdAt time.Time, host string) string {