Add tracked vpnruntime implementation for CI guard tests
This commit is contained in:
@@ -3,9 +3,12 @@ package client
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -64,6 +67,95 @@ type HeartbeatRequest struct {
|
||||
type HeartbeatResponse struct {
|
||||
Heartbeat json.RawMessage `json:"heartbeat"`
|
||||
TestingFlags EffectiveTestingFlags `json:"testing_flags"`
|
||||
UpdateHint *NodeUpdateHint `json:"update_hint,omitempty"`
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
type NodeUpdateServiceAssignment struct {
|
||||
SchemaVersion string `json:"schema_version"`
|
||||
NodeID string `json:"node_id,omitempty"`
|
||||
NodeName string `json:"node_name,omitempty"`
|
||||
Endpoint string `json:"endpoint,omitempty"`
|
||||
Region string `json:"region,omitempty"`
|
||||
Status string `json:"status"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
AssignedAt time.Time `json:"assigned_at"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
}
|
||||
|
||||
type NodeUpdatePlanRequest struct {
|
||||
Product string
|
||||
CurrentVersion string
|
||||
OS string
|
||||
Arch string
|
||||
InstallType string
|
||||
Channel string
|
||||
}
|
||||
|
||||
type NodeUpdatePlanResponse struct {
|
||||
Plan NodeUpdatePlan `json:"node_update_plan"`
|
||||
}
|
||||
|
||||
type NodeUpdatePlan struct {
|
||||
SchemaVersion string `json:"schema_version"`
|
||||
ClusterID string `json:"cluster_id"`
|
||||
NodeID string `json:"node_id"`
|
||||
Product string `json:"product"`
|
||||
CurrentVersion string `json:"current_version,omitempty"`
|
||||
Action string `json:"action"`
|
||||
Reason string `json:"reason"`
|
||||
TargetVersion string `json:"target_version,omitempty"`
|
||||
Channel string `json:"channel,omitempty"`
|
||||
Strategy string `json:"strategy,omitempty"`
|
||||
RollbackAllowed bool `json:"rollback_allowed"`
|
||||
HealthWindowSec int `json:"health_window_seconds,omitempty"`
|
||||
Artifact *ReleaseArtifact `json:"artifact,omitempty"`
|
||||
AuthorityPayload json.RawMessage `json:"authority_payload,omitempty"`
|
||||
AuthoritySignature *ClusterSignature `json:"authority_signature,omitempty"`
|
||||
ProductionForwarding bool `json:"production_forwarding"`
|
||||
}
|
||||
|
||||
type ReleaseArtifact struct {
|
||||
ID string `json:"id"`
|
||||
ReleaseID string `json:"release_id"`
|
||||
ClusterID string `json:"cluster_id"`
|
||||
Product string `json:"product"`
|
||||
Version string `json:"version"`
|
||||
OS string `json:"os"`
|
||||
Arch string `json:"arch"`
|
||||
InstallType string `json:"install_type"`
|
||||
Kind string `json:"kind"`
|
||||
URL string `json:"url"`
|
||||
URLs []string `json:"urls,omitempty"`
|
||||
SHA256 string `json:"sha256"`
|
||||
SizeBytes int64 `json:"size_bytes"`
|
||||
Signature *string `json:"signature,omitempty"`
|
||||
Metadata json.RawMessage `json:"metadata"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type NodeUpdateStatusRequest struct {
|
||||
Product string `json:"product"`
|
||||
CurrentVersion string `json:"current_version,omitempty"`
|
||||
TargetVersion string `json:"target_version,omitempty"`
|
||||
Phase string `json:"phase"`
|
||||
Status string `json:"status"`
|
||||
AttemptID string `json:"attempt_id,omitempty"`
|
||||
ErrorMessage *string `json:"error_message,omitempty"`
|
||||
RollbackVersion *string `json:"rollback_version,omitempty"`
|
||||
Payload map[string]any `json:"payload,omitempty"`
|
||||
ObservedAt time.Time `json:"observed_at,omitempty"`
|
||||
}
|
||||
|
||||
type EffectiveTestingFlags struct {
|
||||
@@ -91,6 +183,45 @@ type WorkloadStatusRequest struct {
|
||||
StatusPayload map[string]any `json:"status_payload"`
|
||||
}
|
||||
|
||||
type NodeVPNAssignmentLease struct {
|
||||
LeaseID string `json:"lease_id"`
|
||||
OwnerNodeID string `json:"owner_node_id"`
|
||||
LeaseGeneration int64 `json:"lease_generation"`
|
||||
Status string `json:"status"`
|
||||
RenewedAt time.Time `json:"renewed_at"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
}
|
||||
|
||||
type NodeVPNAssignment struct {
|
||||
VPNConnectionID string `json:"vpn_connection_id"`
|
||||
ClusterID string `json:"cluster_id"`
|
||||
OrganizationID string `json:"organization_id"`
|
||||
Name string `json:"name"`
|
||||
TargetEndpoint json.RawMessage `json:"target_endpoint"`
|
||||
ProtocolFamily string `json:"protocol_family"`
|
||||
Mode string `json:"mode"`
|
||||
DesiredState string `json:"desired_state"`
|
||||
RoutingUsage json.RawMessage `json:"routing_usage"`
|
||||
RoutePolicy json.RawMessage `json:"route_policy"`
|
||||
QoSPolicy json.RawMessage `json:"qos_policy"`
|
||||
PlacementPolicy json.RawMessage `json:"placement_policy"`
|
||||
Status string `json:"status"`
|
||||
HasCredentialRef bool `json:"has_credential_ref"`
|
||||
AssignmentReason string `json:"assignment_reason"`
|
||||
ActiveLease *NodeVPNAssignmentLease `json:"active_lease,omitempty"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type NodeVPNAssignmentStatusRequest struct {
|
||||
ObservedStatus string `json:"observed_status"`
|
||||
StatusPayload map[string]any `json:"status_payload"`
|
||||
ObservedAt time.Time `json:"observed_at,omitempty"`
|
||||
}
|
||||
|
||||
type NodeVPNAssignmentLeaseRenewRequest struct {
|
||||
TTLSeconds int `json:"ttl_seconds"`
|
||||
}
|
||||
|
||||
type MeshLinkObservationRequest struct {
|
||||
SourceNodeID string `json:"source_node_id"`
|
||||
TargetNodeID string `json:"target_node_id"`
|
||||
@@ -129,26 +260,155 @@ type SyntheticMeshRouteConfig struct {
|
||||
}
|
||||
|
||||
type SyntheticMeshConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
SchemaVersion string `json:"schema_version"`
|
||||
ClusterID string `json:"cluster_id"`
|
||||
LocalNodeID string `json:"local_node_id"`
|
||||
AuthorityRequired bool `json:"authority_required"`
|
||||
ClusterAuthority *ClusterAuthorityDescriptor `json:"cluster_authority,omitempty"`
|
||||
AuthorityPayload json.RawMessage `json:"authority_payload,omitempty"`
|
||||
AuthoritySignature *ClusterSignature `json:"authority_signature,omitempty"`
|
||||
ConfigVersion string `json:"config_version,omitempty"`
|
||||
PeerDirectoryVersion string `json:"peer_directory_version,omitempty"`
|
||||
PolicyVersion string `json:"policy_version,omitempty"`
|
||||
PeerEndpoints map[string]string `json:"peer_endpoints"`
|
||||
PeerEndpointCandidates map[string][]PeerEndpointCandidate `json:"peer_endpoint_candidates,omitempty"`
|
||||
PeerDirectory []PeerDirectoryEntry `json:"peer_directory,omitempty"`
|
||||
RecoverySeeds []PeerRecoverySeed `json:"recovery_seeds,omitempty"`
|
||||
RendezvousLeases []PeerRendezvousLease `json:"rendezvous_leases,omitempty"`
|
||||
RendezvousRelayPolicy *RendezvousRelayPolicyReport `json:"rendezvous_relay_policy,omitempty"`
|
||||
RoutePathDecisions *RoutePathDecisionReport `json:"route_path_decisions,omitempty"`
|
||||
Routes []SyntheticMeshRouteConfig `json:"routes"`
|
||||
ProductionForwarding bool `json:"production_forwarding"`
|
||||
Enabled bool `json:"enabled"`
|
||||
SchemaVersion string `json:"schema_version"`
|
||||
ClusterID string `json:"cluster_id"`
|
||||
LocalNodeID string `json:"local_node_id"`
|
||||
AuthorityRequired bool `json:"authority_required"`
|
||||
ClusterAuthority *ClusterAuthorityDescriptor `json:"cluster_authority,omitempty"`
|
||||
AuthorityPayload json.RawMessage `json:"authority_payload,omitempty"`
|
||||
AuthoritySignature *ClusterSignature `json:"authority_signature,omitempty"`
|
||||
ConfigVersion string `json:"config_version,omitempty"`
|
||||
PeerDirectoryVersion string `json:"peer_directory_version,omitempty"`
|
||||
PolicyVersion string `json:"policy_version,omitempty"`
|
||||
PeerEndpoints map[string]string `json:"peer_endpoints"`
|
||||
PeerEndpointCandidates map[string][]PeerEndpointCandidate `json:"peer_endpoint_candidates,omitempty"`
|
||||
PeerDirectory []PeerDirectoryEntry `json:"peer_directory,omitempty"`
|
||||
RecoverySeeds []PeerRecoverySeed `json:"recovery_seeds,omitempty"`
|
||||
RendezvousLeases []PeerRendezvousLease `json:"rendezvous_leases,omitempty"`
|
||||
RendezvousRelayPolicy *RendezvousRelayPolicyReport `json:"rendezvous_relay_policy,omitempty"`
|
||||
RoutePathDecisions *RoutePathDecisionReport `json:"route_path_decisions,omitempty"`
|
||||
ServiceChannelFeedback *FabricServiceChannelFeedbackReport `json:"service_channel_route_feedback,omitempty"`
|
||||
ServiceChannelAdaptivePolicy *FabricServiceChannelAdaptivePolicy `json:"service_channel_adaptive_policy,omitempty"`
|
||||
ServiceChannelRemediationCommands []FabricServiceChannelRemediationCommand `json:"service_channel_remediation_commands,omitempty"`
|
||||
MeshListener *MeshListenerConfig `json:"mesh_listener,omitempty"`
|
||||
Routes []SyntheticMeshRouteConfig `json:"routes"`
|
||||
ProductionForwarding bool `json:"production_forwarding"`
|
||||
}
|
||||
|
||||
type FabricServiceChannelRemediationCommand struct {
|
||||
SchemaVersion string `json:"schema_version"`
|
||||
CommandID string `json:"command_id"`
|
||||
Action string `json:"action"`
|
||||
ClusterID string `json:"cluster_id"`
|
||||
ChannelID string `json:"channel_id"`
|
||||
ResourceID string `json:"resource_id,omitempty"`
|
||||
ServiceClass string `json:"service_class"`
|
||||
EntryNodeID string `json:"entry_node_id,omitempty"`
|
||||
ExitNodeID string `json:"exit_node_id,omitempty"`
|
||||
PrimaryRouteID string `json:"primary_route_id,omitempty"`
|
||||
ReplacementRouteID string `json:"replacement_route_id,omitempty"`
|
||||
ReplacementRouteStatus string `json:"replacement_route_status,omitempty"`
|
||||
PoolPolicyFingerprint string `json:"pool_policy_fingerprint,omitempty"`
|
||||
GuardStatus string `json:"guard_status,omitempty"`
|
||||
GuardReason string `json:"guard_reason,omitempty"`
|
||||
ExecutionStatus string `json:"execution_status,omitempty"`
|
||||
ExecutionReason string `json:"execution_reason,omitempty"`
|
||||
ExecutionGeneration string `json:"execution_generation,omitempty"`
|
||||
ExecutionObservedAt string `json:"execution_observed_at,omitempty"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
OperatorAction string `json:"operator_action,omitempty"`
|
||||
IssuedAt time.Time `json:"issued_at"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
}
|
||||
|
||||
type FabricServiceChannelFeedbackReport struct {
|
||||
SchemaVersion string `json:"schema_version"`
|
||||
GeneratedAt time.Time `json:"generated_at"`
|
||||
FeedbackMaxAgeSeconds int `json:"feedback_max_age_seconds"`
|
||||
RecoveryPolicy *FabricServiceChannelRecoveryPolicy `json:"recovery_policy,omitempty"`
|
||||
MissingProvenanceCount int `json:"missing_provenance_count,omitempty"`
|
||||
StalePolicyCount int `json:"stale_policy_count,omitempty"`
|
||||
StaleGenerationCount int `json:"stale_generation_count,omitempty"`
|
||||
ObservationCount int `json:"observation_count"`
|
||||
FencedRouteCount int `json:"fenced_route_count"`
|
||||
DegradedRouteCount int `json:"degraded_route_count"`
|
||||
HealthyRouteCount int `json:"healthy_route_count"`
|
||||
RecoveredRouteCount int `json:"recovered_route_count,omitempty"`
|
||||
RecoveryHysteresisCount int `json:"recovery_hysteresis_count,omitempty"`
|
||||
RecoveryPromotedCount int `json:"recovery_promoted_count,omitempty"`
|
||||
RecoveryDemotedCount int `json:"recovery_demoted_count,omitempty"`
|
||||
Observations []FabricServiceChannelFeedbackObservation `json:"observations,omitempty"`
|
||||
}
|
||||
|
||||
type FabricServiceChannelAdaptivePolicy struct {
|
||||
SchemaVersion string `json:"schema_version"`
|
||||
Fingerprint string `json:"fingerprint,omitempty"`
|
||||
MaxParallelWindow int `json:"max_parallel_window"`
|
||||
BulkPressureChannelThreshold int `json:"bulk_pressure_channel_threshold"`
|
||||
QueuePressureHighWatermark int `json:"queue_pressure_high_watermark"`
|
||||
QueuePressureMaxInFlight int `json:"queue_pressure_max_in_flight"`
|
||||
ClassWindows map[string]int `json:"class_windows"`
|
||||
Source string `json:"source"`
|
||||
UpdatedByUserID *string `json:"updated_by_user_id,omitempty"`
|
||||
UpdatedAt string `json:"updated_at,omitempty"`
|
||||
ControlPlaneOnly bool `json:"control_plane_only"`
|
||||
ProductionForwarding bool `json:"production_forwarding"`
|
||||
}
|
||||
|
||||
type FabricServiceChannelRecoveryPolicy struct {
|
||||
SchemaVersion string `json:"schema_version"`
|
||||
Fingerprint string `json:"fingerprint,omitempty"`
|
||||
HysteresisPenalty int `json:"hysteresis_penalty"`
|
||||
PromotionMinSamples int `json:"promotion_min_samples"`
|
||||
DemotionFailureThreshold int `json:"demotion_failure_threshold"`
|
||||
DemotionDropThreshold int `json:"demotion_drop_threshold"`
|
||||
DemotionSlowThreshold int `json:"demotion_slow_threshold"`
|
||||
DemotionRebuildEnabled bool `json:"demotion_rebuild_enabled"`
|
||||
DemotionFencedEnabled bool `json:"demotion_fenced_enabled"`
|
||||
Source string `json:"source"`
|
||||
UpdatedByUserID *string `json:"updated_by_user_id,omitempty"`
|
||||
UpdatedAt time.Time `json:"updated_at,omitempty"`
|
||||
ControlPlaneOnly bool `json:"control_plane_only"`
|
||||
ProductionForwarding bool `json:"production_forwarding"`
|
||||
}
|
||||
|
||||
type FabricServiceChannelFeedbackObservation struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
ClusterID string `json:"cluster_id"`
|
||||
ReporterNodeID string `json:"reporter_node_id"`
|
||||
RouteID string `json:"route_id"`
|
||||
ServiceClass string `json:"service_class"`
|
||||
FeedbackStatus string `json:"feedback_status"`
|
||||
ScoreAdjustment int `json:"score_adjustment"`
|
||||
EffectiveScoreAdjustment int `json:"effective_score_adjustment,omitempty"`
|
||||
Reasons []string `json:"reasons,omitempty"`
|
||||
LastError string `json:"last_error,omitempty"`
|
||||
ConsecutiveFailures int `json:"consecutive_failures,omitempty"`
|
||||
StallCount int `json:"stall_count,omitempty"`
|
||||
LastSendDurationMs int64 `json:"last_send_duration_ms,omitempty"`
|
||||
RecoveryState string `json:"recovery_state,omitempty"`
|
||||
ObservedPolicyFingerprint string `json:"observed_policy_fingerprint,omitempty"`
|
||||
EffectivePolicyFingerprint string `json:"effective_policy_fingerprint,omitempty"`
|
||||
ObservedRouteGeneration string `json:"observed_route_generation,omitempty"`
|
||||
EffectiveRouteGeneration string `json:"effective_route_generation,omitempty"`
|
||||
ProvenanceMissing bool `json:"provenance_missing,omitempty"`
|
||||
StalePolicy bool `json:"stale_policy,omitempty"`
|
||||
StaleGeneration bool `json:"stale_generation,omitempty"`
|
||||
StaleReason string `json:"stale_reason,omitempty"`
|
||||
Payload json.RawMessage `json:"payload"`
|
||||
ObservedAt time.Time `json:"observed_at"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
}
|
||||
|
||||
type MeshListenerConfig struct {
|
||||
SchemaVersion string `json:"schema_version"`
|
||||
Source string `json:"source"`
|
||||
DesiredState string `json:"desired_state"`
|
||||
ListenAddr string `json:"listen_addr"`
|
||||
ListenPortMode string `json:"listen_port_mode"`
|
||||
AutoPortStart int `json:"auto_port_start,omitempty"`
|
||||
AutoPortEnd int `json:"auto_port_end,omitempty"`
|
||||
AdvertiseEndpoint string `json:"advertise_endpoint,omitempty"`
|
||||
AdvertiseTransport string `json:"advertise_transport,omitempty"`
|
||||
ConnectivityMode string `json:"connectivity_mode,omitempty"`
|
||||
NATType string `json:"nat_type,omitempty"`
|
||||
Region string `json:"region,omitempty"`
|
||||
ConfigVersion string `json:"config_version,omitempty"`
|
||||
UpdatedByUserID string `json:"updated_by_user_id,omitempty"`
|
||||
UpdatedAt string `json:"updated_at,omitempty"`
|
||||
ControlPlaneOnly bool `json:"control_plane_only"`
|
||||
ProductionForwarding bool `json:"production_forwarding"`
|
||||
}
|
||||
|
||||
type ClusterAuthorityDescriptor struct {
|
||||
@@ -233,6 +493,11 @@ type RendezvousRelayPolicyReport struct {
|
||||
type RoutePathDecision struct {
|
||||
DecisionID string `json:"decision_id"`
|
||||
RouteID string `json:"route_id"`
|
||||
ReplacementRouteID string `json:"replacement_route_id,omitempty"`
|
||||
RebuildRequestID string `json:"rebuild_request_id,omitempty"`
|
||||
RebuildStatus string `json:"rebuild_status,omitempty"`
|
||||
RebuildReason string `json:"rebuild_reason,omitempty"`
|
||||
RebuildAttempt int `json:"rebuild_attempt,omitempty"`
|
||||
ClusterID string `json:"cluster_id"`
|
||||
LocalNodeID string `json:"local_node_id"`
|
||||
SourceNodeID string `json:"source_node_id"`
|
||||
@@ -258,14 +523,21 @@ type RoutePathDecision struct {
|
||||
}
|
||||
|
||||
type RoutePathDecisionReport struct {
|
||||
SchemaVersion string `json:"schema_version"`
|
||||
DecisionMode string `json:"decision_mode"`
|
||||
Generation string `json:"generation"`
|
||||
DecisionCount int `json:"decision_count"`
|
||||
ReplacementDecisionCount int `json:"replacement_decision_count"`
|
||||
ControlPlaneOnly bool `json:"control_plane_only"`
|
||||
ProductionForwarding bool `json:"production_forwarding"`
|
||||
Decisions []RoutePathDecision `json:"decisions,omitempty"`
|
||||
SchemaVersion string `json:"schema_version"`
|
||||
DecisionMode string `json:"decision_mode"`
|
||||
Generation string `json:"generation"`
|
||||
RecoveryPolicy *FabricServiceChannelRecoveryPolicy `json:"recovery_policy,omitempty"`
|
||||
DecisionCount int `json:"decision_count"`
|
||||
ReplacementDecisionCount int `json:"replacement_decision_count"`
|
||||
DegradedDecisionCount int `json:"degraded_decision_count"`
|
||||
RebuildRequestCount int `json:"rebuild_request_count,omitempty"`
|
||||
RebuildAppliedCount int `json:"rebuild_applied_count,omitempty"`
|
||||
RecoveryHysteresisCount int `json:"recovery_hysteresis_count,omitempty"`
|
||||
RecoveryPromotedCount int `json:"recovery_promoted_count,omitempty"`
|
||||
RecoveryDemotedCount int `json:"recovery_demoted_count,omitempty"`
|
||||
ControlPlaneOnly bool `json:"control_plane_only"`
|
||||
ProductionForwarding bool `json:"production_forwarding"`
|
||||
Decisions []RoutePathDecision `json:"decisions,omitempty"`
|
||||
}
|
||||
|
||||
type PeerEndpointCandidate struct {
|
||||
@@ -319,6 +591,29 @@ func (c *Client) Heartbeat(ctx context.Context, clusterID, nodeID string, reques
|
||||
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"`
|
||||
@@ -335,6 +630,58 @@ func (c *Client) ReportWorkloadStatus(ctx context.Context, clusterID, nodeID, se
|
||||
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) 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)
|
||||
@@ -375,6 +722,49 @@ func (c *Client) getJSON(ctx context.Context, path string, response any) error {
|
||||
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 {
|
||||
@@ -398,3 +788,59 @@ func (c *Client) postJSON(ctx context.Context, path string, request any, respons
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user