// Package traefik — клиент Traefik API (GET /api/http/routers). package traefik import ( "context" "encoding/json" "fmt" "net/http" "net/url" "strconv" "strings" "time" "github.com/realmanual/traefik-selectel/internal/httpx" ) // Router — подмножество полей роутера Traefik. type Router struct { Name string `json:"name"` Rule string `json:"rule"` Status string `json:"status"` Provider string `json:"provider"` } // RouterSource — источник роутов (интерфейс для тестов). type RouterSource interface { Routers(ctx context.Context) ([]Router, error) } // Client читает роутеры из Traefik API. type Client struct { baseURL string http *http.Client retry httpx.Retrier } // NewClient создаёт клиент. timeout — таймаут одного HTTP-запроса. func NewClient(apiURL string, timeout time.Duration) (*Client, error) { u, err := url.Parse(apiURL) if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" { return nil, fmt.Errorf("traefik: некорректный apiURL %q", apiURL) } return &Client{ baseURL: strings.TrimRight(u.String(), "/"), http: &http.Client{Timeout: timeout}, retry: httpx.DefaultRetrier(), }, nil } const perPage = 500 const maxPages = 100 // Routers возвращает все HTTP-роутеры (с учётом пагинации X-Next-Page). func (c *Client) Routers(ctx context.Context) ([]Router, error) { var all []Router page := 1 for i := 0; i < maxPages; i++ { u := fmt.Sprintf("%s/api/http/routers?page=%d&per_page=%d", c.baseURL, page, perPage) resp, err := c.retry.Do(ctx, c.http, func() (*http.Request, error) { req, err := http.NewRequest(http.MethodGet, u, nil) if err != nil { return nil, err } req.Header.Set("Accept", "application/json") return req, nil }) if err != nil { return nil, fmt.Errorf("traefik: запрос роутеров: %w", err) } body, err := httpx.ReadBody(resp) resp.Body.Close() if err != nil { return nil, fmt.Errorf("traefik: чтение ответа: %w", err) } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("traefik: статус %d: %s", resp.StatusCode, httpx.Snippet(body, 200)) } var chunk []Router if err := json.Unmarshal(body, &chunk); err != nil { return nil, fmt.Errorf("traefik: разбор JSON: %w", err) } all = append(all, chunk...) next, _ := strconv.Atoi(resp.Header.Get("X-Next-Page")) if next <= page { return all, nil } page = next } return nil, fmt.Errorf("traefik: слишком много страниц роутеров") }