Support PATCH updates for telemetry status
Send only changing fields for status updates and add server-side update flow. - Trimmed telemetry JSON payload in misc/api.func to include only status, error, and exit_code (removed static fields and pve_version) so updates are minimal. - Added TelemetryStatusUpdate type and new PBClient methods: FindRecordByRandomID, UpdateTelemetryStatus, and UpsertTelemetry in misc/data/service.go. UpsertTelemetry creates a record for status="installing", otherwise finds the record by random_id and PATCHes only status/error/exit_code (fallbacks to create if not found). - Relaxed validation logic in validate(): detect updates (status != "installing") and skip certain strict numeric checks for update requests while keeping required fields and other validations. - Main handler now calls UpsertTelemetry instead of CreateTelemetry and logs generic errors. These changes allow idempotent, minimal updates to existing telemetry records and avoid repeatedly sending/storing unchanged metadata.
This commit is contained in:
parent
878672a8df
commit
88a540e457
@ -378,12 +378,8 @@ post_update_to_api() {
|
||||
[[ -z "$error" ]] && error="Unknown error"
|
||||
fi
|
||||
|
||||
# Get PVE version for complete record
|
||||
local pve_version=""
|
||||
if command -v pveversion &>/dev/null; then
|
||||
pve_version=$(pveversion 2>/dev/null | awk -F'[/ ]' '{print $2}') || true
|
||||
fi
|
||||
|
||||
# Update payload: only fields that change (status, error, exit_code)
|
||||
# The Go service will find the record by random_id and PATCH only these fields
|
||||
local JSON_PAYLOAD
|
||||
JSON_PAYLOAD=$(
|
||||
cat <<EOF
|
||||
@ -392,14 +388,6 @@ post_update_to_api() {
|
||||
"type": "${TELEMETRY_TYPE:-lxc}",
|
||||
"nsapp": "${NSAPP:-unknown}",
|
||||
"status": "${pb_status}",
|
||||
"ct_type": ${CT_TYPE:-1},
|
||||
"disk_size": ${DISK_SIZE:-0},
|
||||
"core_count": ${CORE_COUNT:-0},
|
||||
"ram_size": ${RAM_SIZE:-0},
|
||||
"os_type": "${var_os:-}",
|
||||
"os_version": "${var_version:-}",
|
||||
"pve_version": "${pve_version}",
|
||||
"method": "${METHOD:-default}",
|
||||
"exit_code": ${exit_code},
|
||||
"error": "${error}"
|
||||
}
|
||||
|
||||
@ -82,6 +82,13 @@ type TelemetryOut struct {
|
||||
ExitCode int `json:"exit_code,omitempty"`
|
||||
}
|
||||
|
||||
// TelemetryStatusUpdate contains only fields needed for status updates
|
||||
type TelemetryStatusUpdate struct {
|
||||
Status string `json:"status"`
|
||||
Error string `json:"error,omitempty"`
|
||||
ExitCode int `json:"exit_code"`
|
||||
}
|
||||
|
||||
type PBClient struct {
|
||||
baseURL string
|
||||
authCollection string
|
||||
@ -159,6 +166,109 @@ func (p *PBClient) ensureAuth(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// FindRecordByRandomID searches for an existing record by random_id
|
||||
func (p *PBClient) FindRecordByRandomID(ctx context.Context, randomID string) (string, error) {
|
||||
if err := p.ensureAuth(ctx); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// URL encode the filter
|
||||
filter := fmt.Sprintf("random_id='%s'", randomID)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet,
|
||||
fmt.Sprintf("%s/api/collections/%s/records?filter=%s&fields=id&perPage=1",
|
||||
p.baseURL, p.targetColl, filter),
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+p.token)
|
||||
|
||||
resp, err := p.http.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return "", fmt.Errorf("pocketbase search failed: %s", resp.Status)
|
||||
}
|
||||
|
||||
var result struct {
|
||||
Items []struct {
|
||||
ID string `json:"id"`
|
||||
} `json:"items"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(result.Items) == 0 {
|
||||
return "", nil // Not found
|
||||
}
|
||||
return result.Items[0].ID, nil
|
||||
}
|
||||
|
||||
// UpdateTelemetryStatus updates only status, error, and exit_code of an existing record
|
||||
func (p *PBClient) UpdateTelemetryStatus(ctx context.Context, recordID string, update TelemetryStatusUpdate) error {
|
||||
if err := p.ensureAuth(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b, _ := json.Marshal(update)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPatch,
|
||||
fmt.Sprintf("%s/api/collections/%s/records/%s", p.baseURL, p.targetColl, recordID),
|
||||
bytes.NewReader(b),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+p.token)
|
||||
|
||||
resp, err := p.http.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
rb, _ := io.ReadAll(io.LimitReader(resp.Body, 8<<10))
|
||||
return fmt.Errorf("pocketbase update failed: %s: %s", resp.Status, strings.TrimSpace(string(rb)))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpsertTelemetry handles both creation and updates intelligently
|
||||
// - status="installing": Always creates a new record
|
||||
// - status!="installing": Updates existing record (found by random_id) with status/error/exit_code only
|
||||
func (p *PBClient) UpsertTelemetry(ctx context.Context, payload TelemetryOut) error {
|
||||
// For "installing" status, always create new record
|
||||
if payload.Status == "installing" {
|
||||
return p.CreateTelemetry(ctx, payload)
|
||||
}
|
||||
|
||||
// For status updates (sucess/failed/unknown), find and update existing record
|
||||
recordID, err := p.FindRecordByRandomID(ctx, payload.RandomID)
|
||||
if err != nil {
|
||||
// Search failed, log and return error
|
||||
return fmt.Errorf("cannot find record to update: %w", err)
|
||||
}
|
||||
|
||||
if recordID == "" {
|
||||
// Record not found - this shouldn't happen normally
|
||||
// Create a full record as fallback
|
||||
return p.CreateTelemetry(ctx, payload)
|
||||
}
|
||||
|
||||
// Update only status, error, and exit_code
|
||||
update := TelemetryStatusUpdate{
|
||||
Status: payload.Status,
|
||||
Error: payload.Error,
|
||||
ExitCode: payload.ExitCode,
|
||||
}
|
||||
return p.UpdateTelemetryStatus(ctx, recordID, update)
|
||||
}
|
||||
|
||||
func (p *PBClient) CreateTelemetry(ctx context.Context, payload TelemetryOut) error {
|
||||
if err := p.ensureAuth(ctx); err != nil {
|
||||
return err
|
||||
@ -350,7 +460,7 @@ func validate(in *TelemetryIn) error {
|
||||
// IMPORTANT: "error" must be short and not contain identifiers/logs
|
||||
in.Error = sanitizeShort(in.Error, 120)
|
||||
|
||||
// Required fields
|
||||
// Required fields for all requests
|
||||
if in.RandomID == "" || in.Type == "" || in.NSAPP == "" || in.Status == "" {
|
||||
return errors.New("missing required fields: random_id, type, nsapp, status")
|
||||
}
|
||||
@ -363,6 +473,10 @@ func validate(in *TelemetryIn) error {
|
||||
return errors.New("invalid status")
|
||||
}
|
||||
|
||||
// For status updates (not installing), skip numeric field validation
|
||||
// These are only required for initial creation
|
||||
isUpdate := in.Status != "installing"
|
||||
|
||||
// os_type is optional but if provided must be valid
|
||||
if in.OsType != "" && !allowedOsType[in.OsType] {
|
||||
return errors.New("invalid os_type")
|
||||
@ -371,9 +485,11 @@ func validate(in *TelemetryIn) error {
|
||||
// method is optional and flexible - just sanitized, no strict validation
|
||||
// Values like "default", "advanced", "mydefaults-global", "mydefaults-app" are all valid
|
||||
|
||||
// Validate numeric ranges
|
||||
if in.CTType < 0 || in.CTType > 2 {
|
||||
return errors.New("invalid ct_type (must be 0, 1, or 2)")
|
||||
// Validate numeric ranges (only strict for new records)
|
||||
if !isUpdate {
|
||||
if in.CTType < 0 || in.CTType > 2 {
|
||||
return errors.New("invalid ct_type (must be 0, 1, or 2)")
|
||||
}
|
||||
}
|
||||
if in.DiskSize < 0 || in.DiskSize > 100000 {
|
||||
return errors.New("invalid disk_size")
|
||||
@ -511,7 +627,8 @@ func main() {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), cfg.RequestTimeout)
|
||||
defer cancel()
|
||||
|
||||
if err := pb.CreateTelemetry(ctx, out); err != nil {
|
||||
// Upsert: Creates new record if random_id doesn't exist, updates if it does
|
||||
if err := pb.UpsertTelemetry(ctx, out); err != nil {
|
||||
// GDPR: don't log raw payload, don't log IPs; log only generic error
|
||||
log.Printf("pocketbase write failed: %v", err)
|
||||
http.Error(w, "upstream error", http.StatusBadGateway)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user