рабочий вариант, но скороть 10 МБит
build / backend (push) Has been cancelled
build / node-agent (push) Has been cancelled
build / worker (push) Has been cancelled

This commit is contained in:
2026-05-22 21:46:49 +03:00
parent 469fa0e860
commit 20d361a886
280 changed files with 954890 additions and 18524 deletions
+29 -367
View File
@@ -1,22 +1,11 @@
package client
import (
"bytes"
"context"
"encoding/binary"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
)
type Client struct {
baseURL string
httpClient *http.Client
}
type Client struct{}
type RawControlRequest struct {
Method string `json:"method"`
@@ -45,19 +34,19 @@ type EnrollResponse struct {
JoinRequest json.RawMessage `json:"join_request"`
}
type EnrollmentBootstrapRequest struct {
type EnrollmentJoinRequest struct {
ClusterID string `json:"cluster_id"`
NodeFingerprint string `json:"node_fingerprint"`
PublicKey string `json:"public_key"`
}
type EnrollmentBootstrapResponse struct {
Status string `json:"status"`
JoinRequest json.RawMessage `json:"join_request"`
Bootstrap *NodeBootstrap `json:"node_bootstrap,omitempty"`
type EnrollmentJoinResponse struct {
Status string `json:"status"`
JoinRequest json.RawMessage `json:"join_request"`
JoinContract *NodeJoinContract `json:"node_join,omitempty"`
}
type NodeBootstrap struct {
type NodeJoinContract struct {
NodeID string `json:"node_id"`
ClusterID string `json:"cluster_id"`
IdentityStatus string `json:"identity_status"`
@@ -84,15 +73,19 @@ type HeartbeatResponse struct {
}
type NodeUpdateHint struct {
SchemaVersion string `json:"schema_version"`
Generation string `json:"generation,omitempty"`
CheckNow bool `json:"check_now"`
Products []string `json:"products,omitempty"`
Reason string `json:"reason,omitempty"`
DeliveryMode string `json:"delivery_mode,omitempty"`
SubscriptionStatus string `json:"subscription_status,omitempty"`
UpdateService *NodeUpdateServiceAssignment `json:"update_service,omitempty"`
FallbackPollSeconds int `json:"fallback_poll_seconds,omitempty"`
SchemaVersion string `json:"schema_version"`
Generation string `json:"generation,omitempty"`
CheckNow bool `json:"check_now"`
Products []string `json:"products,omitempty"`
TargetVersions map[string]string `json:"target_versions,omitempty"`
Reason string `json:"reason,omitempty"`
DeliveryMode string `json:"delivery_mode,omitempty"`
SubscriptionStatus string `json:"subscription_status,omitempty"`
UpdateService *NodeUpdateServiceAssignment `json:"update_service,omitempty"`
UpdateServiceCandidates []NodeUpdateServiceAssignment `json:"update_service_candidates,omitempty"`
RescuePollSeconds int `json:"rescue_poll_seconds,omitempty"`
AuthorityPayload json.RawMessage `json:"authority_payload,omitempty"`
AuthoritySignature *ClusterSignature `json:"authority_signature,omitempty"`
}
type NodeUpdateServiceAssignment struct {
@@ -207,6 +200,13 @@ type NodeVPNAssignmentLease struct {
}
type NodeVPNAssignment struct {
TunnelID string `json:"tunnel_id,omitempty"`
PoolID string `json:"pool_id,omitempty"`
ServiceID string `json:"service_id,omitempty"`
LocalServiceID string `json:"local_service_id,omitempty"`
RemoteServiceID string `json:"remote_service_id,omitempty"`
ServiceKind string `json:"service_kind,omitempty"`
ServiceClass string `json:"service_class,omitempty"`
VPNConnectionID string `json:"vpn_connection_id"`
ClusterID string `json:"cluster_id"`
OrganizationID string `json:"organization_id"`
@@ -624,6 +624,7 @@ type EndpointCandidateHealthObservation struct {
EndpointID string `json:"endpoint_id"`
Source string `json:"source,omitempty"`
ReporterNodeID string `json:"reporter_node_id,omitempty"`
ReporterRegion string `json:"reporter_region,omitempty"`
LastLatencyMs int64 `json:"last_latency_ms,omitempty"`
SuccessCount uint64 `json:"success_count,omitempty"`
FailureCount uint64 `json:"failure_count,omitempty"`
@@ -632,343 +633,4 @@ type EndpointCandidateHealthObservation struct {
ObservedAt time.Time `json:"observed_at,omitempty"`
}
func New(baseURL string) *Client {
return &Client{
baseURL: baseURL,
httpClient: &http.Client{
Timeout: 15 * time.Second,
},
}
}
func (c *Client) Enroll(ctx context.Context, request EnrollRequest) (EnrollResponse, error) {
var response EnrollResponse
if err := c.postJSON(ctx, "/node-agents/enroll", request, &response); err != nil {
return EnrollResponse{}, err
}
return response, nil
}
func (c *Client) BootstrapEnrollment(ctx context.Context, joinRequestID string, request EnrollmentBootstrapRequest) (EnrollmentBootstrapResponse, error) {
var response EnrollmentBootstrapResponse
path := fmt.Sprintf("/node-agents/enrollments/%s/bootstrap", joinRequestID)
if err := c.postJSON(ctx, path, request, &response); err != nil {
return EnrollmentBootstrapResponse{}, err
}
return response, nil
}
func (c *Client) Heartbeat(ctx context.Context, clusterID, nodeID string, request HeartbeatRequest) (HeartbeatResponse, error) {
var response HeartbeatResponse
path := fmt.Sprintf("/clusters/%s/nodes/%s/heartbeats", clusterID, nodeID)
if err := c.postJSON(ctx, path, request, &response); err != nil {
return HeartbeatResponse{}, err
}
return response, nil
}
func (c *Client) NodeUpdatePlan(ctx context.Context, clusterID, nodeID string, request NodeUpdatePlanRequest) (NodeUpdatePlan, error) {
values := url.Values{}
values.Set("product", request.Product)
values.Set("current_version", request.CurrentVersion)
values.Set("os", request.OS)
values.Set("arch", request.Arch)
values.Set("install_type", request.InstallType)
if request.Channel != "" {
values.Set("channel", request.Channel)
}
var response NodeUpdatePlanResponse
path := fmt.Sprintf("/clusters/%s/nodes/%s/updates/plan?%s", clusterID, nodeID, values.Encode())
if err := c.getJSON(ctx, path, &response); err != nil {
return NodeUpdatePlan{}, err
}
return response.Plan, nil
}
func (c *Client) ReportNodeUpdateStatus(ctx context.Context, clusterID, nodeID string, request NodeUpdateStatusRequest) error {
path := fmt.Sprintf("/clusters/%s/nodes/%s/updates/status", clusterID, nodeID)
return c.postJSON(ctx, path, request, nil)
}
func (c *Client) DesiredWorkloads(ctx context.Context, clusterID, nodeID string) ([]DesiredWorkload, error) {
var response struct {
DesiredWorkloads []DesiredWorkload `json:"desired_workloads"`
}
path := fmt.Sprintf("/clusters/%s/nodes/%s/workloads/desired", clusterID, nodeID)
if err := c.getJSON(ctx, path, &response); err != nil {
return nil, err
}
return response.DesiredWorkloads, nil
}
func (c *Client) ReportWorkloadStatus(ctx context.Context, clusterID, nodeID, serviceType string, request WorkloadStatusRequest) error {
path := fmt.Sprintf("/clusters/%s/nodes/%s/workloads/%s/status", clusterID, nodeID, serviceType)
return c.postJSON(ctx, path, request, nil)
}
func (c *Client) NodeVPNAssignments(ctx context.Context, clusterID, nodeID string) ([]NodeVPNAssignment, error) {
var response struct {
Assignments []NodeVPNAssignment `json:"vpn_assignments"`
}
path := fmt.Sprintf("/clusters/%s/nodes/%s/vpn/assignments", clusterID, nodeID)
if err := c.getJSON(ctx, path, &response); err != nil {
return nil, err
}
return response.Assignments, nil
}
func (c *Client) ReportNodeVPNAssignmentStatus(ctx context.Context, clusterID, nodeID, vpnConnectionID string, request NodeVPNAssignmentStatusRequest) error {
path := fmt.Sprintf("/clusters/%s/nodes/%s/vpn/assignments/%s/status", clusterID, nodeID, vpnConnectionID)
return c.postJSON(ctx, path, request, nil)
}
func (c *Client) AcquireNodeVPNAssignmentLease(ctx context.Context, clusterID, nodeID, vpnConnectionID string, request NodeVPNAssignmentLeaseAcquireRequest) (*NodeVPNAssignmentLease, error) {
var response struct {
Lease NodeVPNAssignmentLease `json:"lease"`
}
path := fmt.Sprintf("/clusters/%s/nodes/%s/vpn/assignments/%s/lease/acquire", clusterID, nodeID, vpnConnectionID)
if err := c.postJSON(ctx, path, request, &response); err != nil {
return nil, err
}
return &response.Lease, nil
}
func (c *Client) RenewNodeVPNAssignmentLease(ctx context.Context, clusterID, nodeID, vpnConnectionID, leaseID string, request NodeVPNAssignmentLeaseRenewRequest) error {
path := fmt.Sprintf("/clusters/%s/nodes/%s/vpn/assignments/%s/lease/%s/renew", clusterID, nodeID, vpnConnectionID, leaseID)
return c.postJSON(ctx, path, request, nil)
}
func (c *Client) SendVPNGatewayPacket(ctx context.Context, clusterID, vpnConnectionID string, packet []byte) error {
if len(packet) == 0 {
return nil
}
path := fmt.Sprintf("/clusters/%s/vpn-connections/%s/tunnel/gateway/packets", clusterID, vpnConnectionID)
return c.postBytes(ctx, path, packet)
}
func (c *Client) SendVPNGatewayPacketBatch(ctx context.Context, clusterID, vpnConnectionID string, packets [][]byte) error {
packets = cleanVPNPacketBatch(packets)
if len(packets) == 0 {
return nil
}
path := fmt.Sprintf("/clusters/%s/vpn-connections/%s/tunnel/gateway/packets?batch=true", clusterID, vpnConnectionID)
return c.postBytes(ctx, path, encodeVPNPacketBatch(packets))
}
func (c *Client) ReceiveVPNGatewayPacket(ctx context.Context, clusterID, vpnConnectionID string, timeout time.Duration) ([]byte, bool, error) {
path := fmt.Sprintf("/clusters/%s/vpn-connections/%s/tunnel/gateway/packets?timeout_ms=%d", clusterID, vpnConnectionID, timeout.Milliseconds())
return c.getBytes(ctx, path)
}
func (c *Client) ReceiveVPNGatewayPacketBatch(ctx context.Context, clusterID, vpnConnectionID string, timeout time.Duration) ([][]byte, error) {
path := fmt.Sprintf("/clusters/%s/vpn-connections/%s/tunnel/gateway/packets?batch=true&timeout_ms=%d", clusterID, vpnConnectionID, timeout.Milliseconds())
payload, ok, err := c.getBytes(ctx, path)
if err != nil || !ok {
return nil, err
}
return decodeVPNPacketBatch(payload)
}
func (c *Client) ReportMeshLink(ctx context.Context, clusterID string, request MeshLinkObservationRequest) error {
path := fmt.Sprintf("/clusters/%s/mesh/links", clusterID)
return c.postJSON(ctx, path, request, nil)
}
func (c *Client) ReportTelemetry(ctx context.Context, clusterID, nodeID string, request TelemetryRequest) error {
path := fmt.Sprintf("/clusters/%s/nodes/%s/telemetry", clusterID, nodeID)
return c.postJSON(ctx, path, request, nil)
}
func (c *Client) SyntheticMeshConfig(ctx context.Context, clusterID, nodeID string) (SyntheticMeshConfig, error) {
var response struct {
Config SyntheticMeshConfig `json:"synthetic_mesh_config"`
}
path := fmt.Sprintf("/clusters/%s/nodes/%s/mesh/synthetic-config", clusterID, nodeID)
if err := c.getJSON(ctx, path, &response); err != nil {
return SyntheticMeshConfig{}, err
}
return response.Config, nil
}
func (c *Client) AdminRuntimeProjection(ctx context.Context, clusterID, nodeID string, request AdminRuntimeProjectionRequest) (AdminRuntimeProjectionResponse, error) {
var response AdminRuntimeProjectionResponse
path := fmt.Sprintf("/clusters/%s/nodes/%s/admin-runtime/projection", clusterID, nodeID)
if err := c.postJSON(ctx, path, request, &response); err != nil {
return AdminRuntimeProjectionResponse{}, err
}
return response, nil
}
func (c *Client) RawControl(ctx context.Context, request RawControlRequest) (RawControlResponse, error) {
method := strings.ToUpper(strings.TrimSpace(request.Method))
if method == "" {
method = http.MethodGet
}
path := strings.TrimSpace(request.Path)
if !strings.HasPrefix(path, "/") {
return RawControlResponse{}, fmt.Errorf("control path must be relative")
}
var body io.Reader
if len(request.Body) > 0 && string(request.Body) != "null" {
body = bytes.NewReader(request.Body)
}
httpReq, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, body)
if err != nil {
return RawControlResponse{}, err
}
if body != nil {
httpReq.Header.Set("Content-Type", "application/json")
}
httpResp, err := c.httpClient.Do(httpReq)
if err != nil {
return RawControlResponse{}, err
}
defer httpResp.Body.Close()
payload, err := io.ReadAll(io.LimitReader(httpResp.Body, 2*1024*1024))
if err != nil {
return RawControlResponse{}, err
}
return RawControlResponse{StatusCode: httpResp.StatusCode, Body: json.RawMessage(payload)}, nil
}
func (c *Client) getJSON(ctx context.Context, path string, response any) error {
httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+path, nil)
if err != nil {
return err
}
httpResp, err := c.httpClient.Do(httpReq)
if err != nil {
return err
}
defer httpResp.Body.Close()
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
return fmt.Errorf("backend returned status %d", httpResp.StatusCode)
}
if response == nil {
return nil
}
return json.NewDecoder(httpResp.Body).Decode(response)
}
func (c *Client) getBytes(ctx context.Context, path string) ([]byte, bool, error) {
httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+path, nil)
if err != nil {
return nil, false, err
}
httpResp, err := c.httpClient.Do(httpReq)
if err != nil {
return nil, false, err
}
defer httpResp.Body.Close()
if httpResp.StatusCode == http.StatusNoContent {
return nil, false, nil
}
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
return nil, false, fmt.Errorf("backend returned status %d", httpResp.StatusCode)
}
payload, err := io.ReadAll(io.LimitReader(httpResp.Body, vpnPacketBatchMaxBytes))
if err != nil {
return nil, false, err
}
if len(payload) == 0 {
return nil, false, nil
}
return payload, true, nil
}
func (c *Client) postBytes(ctx context.Context, path string, payload []byte) error {
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+path, bytes.NewReader(payload))
if err != nil {
return err
}
httpReq.Header.Set("Content-Type", "application/octet-stream")
httpResp, err := c.httpClient.Do(httpReq)
if err != nil {
return err
}
defer httpResp.Body.Close()
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
return fmt.Errorf("backend returned status %d", httpResp.StatusCode)
}
return nil
}
func (c *Client) postJSON(ctx context.Context, path string, request any, response any) error {
payload, err := json.Marshal(request)
if err != nil {
return err
}
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+path, bytes.NewReader(payload))
if err != nil {
return err
}
httpReq.Header.Set("Content-Type", "application/json")
httpResp, err := c.httpClient.Do(httpReq)
if err != nil {
return err
}
defer httpResp.Body.Close()
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
return fmt.Errorf("backend returned status %d", httpResp.StatusCode)
}
if response == nil {
return nil
}
return json.NewDecoder(httpResp.Body).Decode(response)
}
const (
vpnPacketMaxBytes = 65535
vpnPacketBatchMaxBytes = 4 * 1024 * 1024
)
func encodeVPNPacketBatch(packets [][]byte) []byte {
packets = cleanVPNPacketBatch(packets)
total := 0
for _, packet := range packets {
total += 4 + len(packet)
}
out := make([]byte, total)
offset := 0
for _, packet := range packets {
binary.BigEndian.PutUint32(out[offset:offset+4], uint32(len(packet)))
offset += 4
copy(out[offset:offset+len(packet)], packet)
offset += len(packet)
}
return out
}
func decodeVPNPacketBatch(payload []byte) ([][]byte, error) {
var packets [][]byte
for offset := 0; offset < len(payload); {
if offset+4 > len(payload) {
return nil, fmt.Errorf("truncated vpn packet batch header")
}
size := int(binary.BigEndian.Uint32(payload[offset : offset+4]))
offset += 4
if size <= 0 || size > vpnPacketMaxBytes {
return nil, fmt.Errorf("invalid vpn packet batch item size")
}
if offset+size > len(payload) {
return nil, fmt.Errorf("truncated vpn packet batch item")
}
packets = append(packets, append([]byte(nil), payload[offset:offset+size]...))
offset += size
}
return cleanVPNPacketBatch(packets), nil
}
func cleanVPNPacketBatch(packets [][]byte) [][]byte {
if len(packets) == 0 {
return nil
}
cleaned := make([][]byte, 0, len(packets))
for _, packet := range packets {
if len(packet) == 0 {
continue
}
cleaned = append(cleaned, append([]byte(nil), packet...))
}
return cleaned
}
func New(_ string) *Client { return &Client{} }