рабочий вариант, но скороть 10 МБит
This commit is contained in:
@@ -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{} }
|
||||
|
||||
Reference in New Issue
Block a user