// Package hostnginx manages the exact backend upstream block used by the current host Nginx deployment. package hostnginx import ( "errors" "fmt" "net" "strconv" "strings" ) const ( managedBegin = "# yms-update managed upstream begin" managedEnd = "# yms-update managed upstream end" port8080 = 8080 port8081 = 8081 ) // ActiveBackendPort reads the one uncommented backend server in the managed upstream block. func ActiveBackendPort(content []byte) (int, error) { block, err := parseManagedBlock(content) if err != nil { return 0, err } active := 0 for _, server := range block.servers { if !server.commented { if active != 0 { return 0, errors.New("managed Nginx upstream contains more than one active backend server") } active = server.port } } if active == 0 { return 0, errors.New("managed Nginx upstream does not contain an active backend server") } return active, nil } // RenderBackendPort returns a complete configuration with only activePort uncommented. func RenderBackendPort(content []byte, activePort int) ([]byte, error) { if activePort != port8080 && activePort != port8081 { return nil, fmt.Errorf("host Nginx backend port must be 8080 or 8081: %d", activePort) } block, err := parseManagedBlock(content) if err != nil { return nil, err } lines := block.lines for _, server := range block.servers { indentLength := len(lines[server.line]) - len(strings.TrimLeft(lines[server.line], " \t")) indent := lines[server.line][:indentLength] serverText := strings.TrimSpace(lines[server.line]) serverText = strings.TrimPrefix(serverText, "# ") if server.port == activePort { lines[server.line] = indent + serverText } else { lines[server.line] = indent + "# " + serverText } } rendered := []byte(strings.Join(lines, "\n")) if _, err := ActiveBackendPort(rendered); err != nil { return nil, fmt.Errorf("validate rendered host Nginx backend upstream: %w", err) } return rendered, nil } type managedBlock struct { lines []string servers []managedServer } type managedServer struct { line int port int commented bool } func parseManagedBlock(content []byte) (managedBlock, error) { if len(content) == 0 { return managedBlock{}, errors.New("host Nginx configuration is empty") } lines := strings.Split(string(content), "\n") beginLine := -1 endLine := -1 for index, line := range lines { switch strings.TrimSpace(line) { case managedBegin: if beginLine != -1 { return managedBlock{}, errors.New("host Nginx configuration contains duplicate managed upstream begin markers") } beginLine = index case managedEnd: if endLine != -1 { return managedBlock{}, errors.New("host Nginx configuration contains duplicate managed upstream end markers") } endLine = index } } if beginLine == -1 || endLine == -1 || endLine <= beginLine { return managedBlock{}, errors.New("host Nginx configuration requires one ordered managed upstream marker pair") } servers := make([]managedServer, 0, 2) seenPorts := make(map[int]struct{}, 2) for index := beginLine + 1; index < endLine; index++ { server, found, err := parseManagedServer(lines[index], index) if err != nil { return managedBlock{}, err } if !found { if strings.TrimSpace(lines[index]) != "" { return managedBlock{}, fmt.Errorf("managed Nginx upstream contains an unexpected line: %q", strings.TrimSpace(lines[index])) } continue } if _, duplicate := seenPorts[server.port]; duplicate { return managedBlock{}, fmt.Errorf("managed Nginx upstream contains duplicate port %d", server.port) } seenPorts[server.port] = struct{}{} servers = append(servers, server) } for _, port := range []int{port8080, port8081} { if _, found := seenPorts[port]; !found { return managedBlock{}, fmt.Errorf("managed Nginx upstream is missing port %d", port) } } if len(servers) != 2 { return managedBlock{}, fmt.Errorf("managed Nginx upstream must contain exactly two backend servers, got %d", len(servers)) } return managedBlock{lines: lines, servers: servers}, nil } func parseManagedServer(line string, lineIndex int) (managedServer, bool, error) { trimmed := strings.TrimSpace(line) commented := strings.HasPrefix(trimmed, "# server ") active := strings.HasPrefix(trimmed, "server ") if !commented && !active { return managedServer{}, false, nil } serverText := strings.TrimPrefix(trimmed, "# ") fields := strings.Fields(serverText) if len(fields) != 4 || fields[0] != "server" || fields[2] != "max_fails=1" || fields[3] != "fail_timeout=2s;" { return managedServer{}, false, fmt.Errorf("managed Nginx upstream server line has an unsupported format: %q", trimmed) } _, portText, err := net.SplitHostPort(fields[1]) if err != nil { return managedServer{}, false, fmt.Errorf("parse managed Nginx upstream address %q: %w", fields[1], err) } port, err := strconv.Atoi(portText) if err != nil || (port != port8080 && port != port8081) { return managedServer{}, false, fmt.Errorf("managed Nginx upstream contains unsupported backend port %q", portText) } return managedServer{line: lineIndex, port: port, commented: commented}, true, nil }