From 816c8f9ad9b8114711e621817f17c845f4072c1e Mon Sep 17 00:00:00 2001 From: Paolo Chila Date: Mon, 17 Nov 2025 12:08:03 +0100 Subject: [PATCH 1/5] add available rollbacks to checkin body --- internal/pkg/api/openapi.gen.go | 12 ++++++++++++ model/openapi.yml | 19 +++++++++++++++++++ pkg/api/types.gen.go | 12 ++++++++++++ 3 files changed, 43 insertions(+) diff --git a/internal/pkg/api/openapi.gen.go b/internal/pkg/api/openapi.gen.go index 0bc7b6d9d3..48edde26e5 100644 --- a/internal/pkg/api/openapi.gen.go +++ b/internal/pkg/api/openapi.gen.go @@ -308,6 +308,15 @@ type AuditUnenrollRequest struct { // AuditUnenrollRequestReason The unenroll reason type AuditUnenrollRequestReason string +// AvailableRollbacks Target versions available for a rollback +type AvailableRollbacks = []struct { + // ValidUntil timestamp indicating when the rollback target will expire + ValidUntil time.Time `json:"valid_until"` + + // Version version of the available rollback target, represented as string + Version string `json:"version"` +} + // CheckinRequest defines model for checkinRequest. type CheckinRequest struct { // AckToken The ack_token form a previous response if the agent has checked in before. @@ -340,6 +349,9 @@ type CheckinRequest struct { // If specified fleet-server will set its poll timeout to `max(1m, poll_timeout-2m)` and its write timeout to `max(2m, poll_timout-1m)`. PollTimeout *string `json:"poll_timeout,omitempty"` + // Rollbacks Target versions available for a rollback + Rollbacks *AvailableRollbacks `json:"rollbacks,omitempty"` + // Status The agent state, inferred from agent control protocol states. Status CheckinRequestStatus `json:"status"` diff --git a/model/openapi.yml b/model/openapi.yml index 6d10389128..c14dfe5082 100644 --- a/model/openapi.yml +++ b/model/openapi.yml @@ -371,6 +371,23 @@ components: - $ref: "#/components/schemas/upgrade_metadata_scheduled" - $ref: "#/components/schemas/upgrade_metadata_downloading" - $ref: "#/components/schemas/upgrade_metadata_failed" + available_rollbacks: + description: | + Target versions available for a rollback + type: array + items: + type: object + required: + - version + - valid_until + properties: + version: + description: version of the available rollback target, represented as string + type: string + valid_until: + description: timestamp indicating when the rollback target will expire + type: string + format: date-time checkinRequest: type: object required: @@ -431,6 +448,8 @@ components: The revision of the policy that the agent is currently running. type: integer format: int64 + rollbacks: + $ref: "#/components/schemas/available_rollbacks" actionSignature: description: Optional action signing data. type: object diff --git a/pkg/api/types.gen.go b/pkg/api/types.gen.go index 9f9f78e24f..0406c15552 100644 --- a/pkg/api/types.gen.go +++ b/pkg/api/types.gen.go @@ -305,6 +305,15 @@ type AuditUnenrollRequest struct { // AuditUnenrollRequestReason The unenroll reason type AuditUnenrollRequestReason string +// AvailableRollbacks Target versions available for a rollback +type AvailableRollbacks = []struct { + // ValidUntil timestamp indicating when the rollback target will expire + ValidUntil time.Time `json:"valid_until"` + + // Version version of the available rollback target, represented as string + Version string `json:"version"` +} + // CheckinRequest defines model for checkinRequest. type CheckinRequest struct { // AckToken The ack_token form a previous response if the agent has checked in before. @@ -337,6 +346,9 @@ type CheckinRequest struct { // If specified fleet-server will set its poll timeout to `max(1m, poll_timeout-2m)` and its write timeout to `max(2m, poll_timout-1m)`. PollTimeout *string `json:"poll_timeout,omitempty"` + // Rollbacks Target versions available for a rollback + Rollbacks *AvailableRollbacks `json:"rollbacks,omitempty"` + // Status The agent state, inferred from agent control protocol states. Status CheckinRequestStatus `json:"status"` From 08b95a856937a128d6e64f90b5da1a478b122251 Mon Sep 17 00:00:00 2001 From: Paolo Chila Date: Mon, 24 Nov 2025 11:23:44 +0100 Subject: [PATCH 2/5] add rollbacks to ES document --- internal/pkg/api/handleCheckin.go | 44 +++++++++++++++++++++++++++++++ internal/pkg/checkin/bulk.go | 19 +++++++++++++ internal/pkg/model/schema.go | 16 +++++++++++ model/schema.json | 21 +++++++++++++++ 4 files changed, 100 insertions(+) diff --git a/internal/pkg/api/handleCheckin.go b/internal/pkg/api/handleCheckin.go index 1aa720f8a7..d03d7a72c6 100644 --- a/internal/pkg/api/handleCheckin.go +++ b/internal/pkg/api/handleCheckin.go @@ -22,6 +22,8 @@ import ( "sync" "time" + "github.com/dustin/go-humanize" + "github.com/elastic/fleet-server/v7/internal/pkg/action" "github.com/elastic/fleet-server/v7/internal/pkg/bulk" "github.com/elastic/fleet-server/v7/internal/pkg/cache" @@ -173,6 +175,7 @@ type validatedCheckin struct { rawComp []byte seqno sqn.SeqNo unhealthyReason *[]string + rawRollbacks []byte } func (ct *CheckinT) validateRequest(zlog zerolog.Logger, w http.ResponseWriter, r *http.Request, start time.Time, agent *model.Agent) (validatedCheckin, error) { @@ -251,6 +254,11 @@ func (ct *CheckinT) validateRequest(zlog zerolog.Logger, w http.ResponseWriter, return val, err } + rawRollbacks, err := parseRollbacks(zlog, agent, &req) + if err != nil { + return val, err + } + return validatedCheckin{ req: &req, dur: pollDuration, @@ -258,6 +266,7 @@ func (ct *CheckinT) validateRequest(zlog zerolog.Logger, w http.ResponseWriter, rawComp: rawComponents, seqno: seqno, unhealthyReason: unhealthyReason, + rawRollbacks: rawRollbacks, }, nil } @@ -290,6 +299,7 @@ func (ct *CheckinT) ProcessRequest(zlog zerolog.Logger, w http.ResponseWriter, r checkin.WithVer(ver), checkin.WithUnhealthyReason(unhealthyReason), checkin.WithDeleteAudit(agent.AuditUnenrolledReason != "" || agent.UnenrolledAt != ""), + checkin.WithRollbacks(req.Rollbacks), } revID, opts, err := ct.processPolicyDetails(r.Context(), zlog, agent, req) @@ -1132,6 +1142,40 @@ func parseComponents(zlog zerolog.Logger, agent *model.Agent, req *CheckinReques return outComponents, &unhealthyReason, nil } +func parseRollbacks(zlog zerolog.Logger, agent *model.Agent, req *CheckinRequest) ([]byte, error) { + if req.Rollbacks == nil && agent.Rollbacks == nil { + return nil, nil + } + + var reqRollbacks []model.Rollback + if req.Rollbacks != nil { + reqRollbacks = make([]model.Rollback, len(*req.Rollbacks)) + for i, rr := range *req.Rollbacks { + reqRollbacks[i] = model.Rollback{ + ValidUntil: rr.ValidUntil.UTC().Format(time.RFC3339), + Version: rr.Version, + } + } + } + + var outRollbacks []byte + // Compare the deserialized meta structures and return the bytes to update if different + if !reflect.DeepEqual(reqRollbacks, agent.Rollbacks) { + zlog.Trace(). + Any("oldRollbacks", agent.Rollbacks). + Any("req.Rollbacks", req.Rollbacks). + Msg("available rollback data is not equal") + + zlog.Info().Msg("applying new rollback data") + marshalled, err := json.Marshal(reqRollbacks) + if err != nil { + return nil, fmt.Errorf("marshalling rollbacks: %w", err) + } + outRollbacks = marshalled + } + return outRollbacks, nil +} + func calcUnhealthyReason(reqComponents []model.ComponentsItems) []string { var unhealthyReason []string hasUnhealthyInput := false diff --git a/internal/pkg/checkin/bulk.go b/internal/pkg/checkin/bulk.go index a2304ffd85..7e56fe00db 100644 --- a/internal/pkg/checkin/bulk.go +++ b/internal/pkg/checkin/bulk.go @@ -6,6 +6,7 @@ package checkin import ( + "bytes" "context" _ "embed" "encoding/json" @@ -14,6 +15,7 @@ import ( "sync" "time" + "github.com/elastic/fleet-server/v7/internal/pkg/api" "github.com/elastic/fleet-server/v7/internal/pkg/bulk" "github.com/elastic/fleet-server/v7/internal/pkg/dl" "github.com/elastic/fleet-server/v7/internal/pkg/sqn" @@ -125,12 +127,29 @@ func WithPolicyRevisionIDX(idx int64) Option { } } +func WithRollbacks(rollbacks *api.AvailableRollbacks) Option { + return func(pending *pendingT) { + if pending.extra == nil { + pending.extra = &extraT{} + } + if rollbacks == nil { + pending.extra.rollbacks = nil + return + } + // FIXME this should be done in the validated checkin + buf := bytes.NewBuffer(nil) + json. + pending.extra.rollbacks = rollbacks + } +} + type extraT struct { meta []byte seqNo sqn.SeqNo ver string components []byte deleteAudit bool + rollbacks []byte } // Minimize the size of this structure. diff --git a/internal/pkg/model/schema.go b/internal/pkg/model/schema.go index f462dbc71d..1612a339bf 100644 --- a/internal/pkg/model/schema.go +++ b/internal/pkg/model/schema.go @@ -198,6 +198,9 @@ type Agent struct { // hash of token provided during enrollment that allows replacement by another enrollment with same ID ReplaceToken string `json:"replace_token,omitempty"` + // list of available rollbacks for the agent + Rollbacks []Rollback `json:"rollbacks,omitempty"` + // Shared ID SharedID string `json:"shared_id,omitempty"` @@ -286,6 +289,13 @@ type Artifact struct { PackageName string `json:"package_name,omitempty"` } +// AvailableRollback +type AvailableRollback struct { + ESDocument + ValidUntil string `json:"valid_until,omitempty"` + Version string `json:"version,omitempty"` +} + // Checkin An Elastic Agent checkin to Fleet type Checkin struct { ESDocument @@ -520,6 +530,12 @@ type PolicyOutput struct { Type string `json:"type"` } +// Rollback +type Rollback struct { + ValidUntil string `json:"valid_until,omitempty"` + Version string `json:"version,omitempty"` +} + // SecretReferencesItems type SecretReferencesItems struct { ID string `json:"id"` diff --git a/model/schema.json b/model/schema.json index bc65f19dbd..ce9d416c7a 100644 --- a/model/schema.json +++ b/model/schema.json @@ -514,6 +514,20 @@ } }, + "available_rollback": { + "title": "Rollback", + "type": "object", + "properties": { + "version": { + "type": "string" + }, + "valid_until": { + "type": "string", + "format": "date-time" + } + } + }, + "agent": { "title": "Agent", "description": "An Elastic Agent that has enrolled into Fleet", @@ -717,6 +731,13 @@ "replace_token": { "description": "hash of token provided during enrollment that allows replacement by another enrollment with same ID", "type": "string" + }, + "rollbacks": { + "description": "list of available rollbacks for the agent", + "type": "array", + "items": { + "$ref": "#/definitions/available_rollback" + } } }, "required": ["_id", "type", "active", "enrolled_at", "status"] From 6839f22c744a06254bcb8e93005e1503be56475a Mon Sep 17 00:00:00 2001 From: Paolo Chila Date: Wed, 26 Nov 2025 18:04:07 +0100 Subject: [PATCH 3/5] Refactor names and add bulk handling of available rollbacks --- internal/pkg/api/handleCheckin.go | 61 +++++++++++++++---------------- internal/pkg/api/openapi.gen.go | 6 +-- internal/pkg/checkin/bulk.go | 29 ++++++--------- internal/pkg/dl/constants.go | 22 +++++------ internal/pkg/model/schema.go | 13 ++----- model/openapi.yml | 2 +- model/schema.json | 2 +- pkg/api/types.gen.go | 6 +-- 8 files changed, 63 insertions(+), 78 deletions(-) diff --git a/internal/pkg/api/handleCheckin.go b/internal/pkg/api/handleCheckin.go index d03d7a72c6..95ed9a847b 100644 --- a/internal/pkg/api/handleCheckin.go +++ b/internal/pkg/api/handleCheckin.go @@ -22,8 +22,6 @@ import ( "sync" "time" - "github.com/dustin/go-humanize" - "github.com/elastic/fleet-server/v7/internal/pkg/action" "github.com/elastic/fleet-server/v7/internal/pkg/bulk" "github.com/elastic/fleet-server/v7/internal/pkg/cache" @@ -169,13 +167,13 @@ func invalidateAPIKeysOfInactiveAgent(ctx context.Context, zlog zerolog.Logger, // validatedCheckin is a struct to wrap all the things that validateRequest returns. type validatedCheckin struct { - req *CheckinRequest - dur time.Duration - rawMeta []byte - rawComp []byte - seqno sqn.SeqNo - unhealthyReason *[]string - rawRollbacks []byte + req *CheckinRequest + dur time.Duration + rawMeta []byte + rawComp []byte + seqno sqn.SeqNo + unhealthyReason *[]string + rawAvailableRollbacks []byte } func (ct *CheckinT) validateRequest(zlog zerolog.Logger, w http.ResponseWriter, r *http.Request, start time.Time, agent *model.Agent) (validatedCheckin, error) { @@ -254,19 +252,19 @@ func (ct *CheckinT) validateRequest(zlog zerolog.Logger, w http.ResponseWriter, return val, err } - rawRollbacks, err := parseRollbacks(zlog, agent, &req) + rawRollbacks, err := parseAvailableRollbacks(zlog, agent, &req) if err != nil { return val, err } return validatedCheckin{ - req: &req, - dur: pollDuration, - rawMeta: rawMeta, - rawComp: rawComponents, - seqno: seqno, - unhealthyReason: unhealthyReason, - rawRollbacks: rawRollbacks, + req: &req, + dur: pollDuration, + rawMeta: rawMeta, + rawComp: rawComponents, + seqno: seqno, + unhealthyReason: unhealthyReason, + rawAvailableRollbacks: rawRollbacks, }, nil } @@ -299,7 +297,7 @@ func (ct *CheckinT) ProcessRequest(zlog zerolog.Logger, w http.ResponseWriter, r checkin.WithVer(ver), checkin.WithUnhealthyReason(unhealthyReason), checkin.WithDeleteAudit(agent.AuditUnenrolledReason != "" || agent.UnenrolledAt != ""), - checkin.WithRollbacks(req.Rollbacks), + checkin.WithAvailableRollbacks(validated.rawAvailableRollbacks), } revID, opts, err := ct.processPolicyDetails(r.Context(), zlog, agent, req) @@ -1142,34 +1140,33 @@ func parseComponents(zlog zerolog.Logger, agent *model.Agent, req *CheckinReques return outComponents, &unhealthyReason, nil } -func parseRollbacks(zlog zerolog.Logger, agent *model.Agent, req *CheckinRequest) ([]byte, error) { - if req.Rollbacks == nil && agent.Rollbacks == nil { - return nil, nil - } - - var reqRollbacks []model.Rollback - if req.Rollbacks != nil { - reqRollbacks = make([]model.Rollback, len(*req.Rollbacks)) - for i, rr := range *req.Rollbacks { - reqRollbacks[i] = model.Rollback{ +func parseAvailableRollbacks(zlog zerolog.Logger, agent *model.Agent, req *CheckinRequest) ([]byte, error) { + var reqRollbacks []model.AvailableRollback + if req.AvailableRollbacks != nil { + reqRollbacks = make([]model.AvailableRollback, len(*req.AvailableRollbacks)) + for i, rr := range *req.AvailableRollbacks { + reqRollbacks[i] = model.AvailableRollback{ ValidUntil: rr.ValidUntil.UTC().Format(time.RFC3339), Version: rr.Version, } } + } else { + // still set an empty slice in order to clear obsolete information, if any + reqRollbacks = []model.AvailableRollback{} } var outRollbacks []byte // Compare the deserialized meta structures and return the bytes to update if different - if !reflect.DeepEqual(reqRollbacks, agent.Rollbacks) { + if !reflect.DeepEqual(reqRollbacks, agent.AvailableRollbacks) { zlog.Trace(). - Any("oldRollbacks", agent.Rollbacks). - Any("req.Rollbacks", req.Rollbacks). + Any("oldAvailableRollbacks", agent.AvailableRollbacks). + Any("req.AvailableRollbacks", req.AvailableRollbacks). Msg("available rollback data is not equal") zlog.Info().Msg("applying new rollback data") marshalled, err := json.Marshal(reqRollbacks) if err != nil { - return nil, fmt.Errorf("marshalling rollbacks: %w", err) + return nil, fmt.Errorf("marshalling available rollbacks: %w", err) } outRollbacks = marshalled } diff --git a/internal/pkg/api/openapi.gen.go b/internal/pkg/api/openapi.gen.go index 48edde26e5..856f60c461 100644 --- a/internal/pkg/api/openapi.gen.go +++ b/internal/pkg/api/openapi.gen.go @@ -326,6 +326,9 @@ type CheckinRequest struct { // AgentPolicyId The ID of the policy that the agent is currently running. AgentPolicyId *string `json:"agent_policy_id,omitempty"` + // AvailableRollbacks Target versions available for a rollback + AvailableRollbacks *AvailableRollbacks `json:"available_rollbacks,omitempty"` + // Components An embedded JSON object that holds component information that the agent is running. // Defined in fleet-server as a `json.RawMessage`, defined as an object in the elastic-agent. // fleet-server will update the components in an agent record if they differ from this object. @@ -349,9 +352,6 @@ type CheckinRequest struct { // If specified fleet-server will set its poll timeout to `max(1m, poll_timeout-2m)` and its write timeout to `max(2m, poll_timout-1m)`. PollTimeout *string `json:"poll_timeout,omitempty"` - // Rollbacks Target versions available for a rollback - Rollbacks *AvailableRollbacks `json:"rollbacks,omitempty"` - // Status The agent state, inferred from agent control protocol states. Status CheckinRequestStatus `json:"status"` diff --git a/internal/pkg/checkin/bulk.go b/internal/pkg/checkin/bulk.go index 7e56fe00db..eddf8678d0 100644 --- a/internal/pkg/checkin/bulk.go +++ b/internal/pkg/checkin/bulk.go @@ -6,7 +6,6 @@ package checkin import ( - "bytes" "context" _ "embed" "encoding/json" @@ -15,7 +14,6 @@ import ( "sync" "time" - "github.com/elastic/fleet-server/v7/internal/pkg/api" "github.com/elastic/fleet-server/v7/internal/pkg/bulk" "github.com/elastic/fleet-server/v7/internal/pkg/dl" "github.com/elastic/fleet-server/v7/internal/pkg/sqn" @@ -127,29 +125,22 @@ func WithPolicyRevisionIDX(idx int64) Option { } } -func WithRollbacks(rollbacks *api.AvailableRollbacks) Option { +func WithAvailableRollbacks(availableRollbacks []byte) Option { return func(pending *pendingT) { if pending.extra == nil { pending.extra = &extraT{} } - if rollbacks == nil { - pending.extra.rollbacks = nil - return - } - // FIXME this should be done in the validated checkin - buf := bytes.NewBuffer(nil) - json. - pending.extra.rollbacks = rollbacks + pending.extra.availableRollbacks = availableRollbacks } } type extraT struct { - meta []byte - seqNo sqn.SeqNo - ver string - components []byte - deleteAudit bool - rollbacks []byte + meta []byte + seqNo sqn.SeqNo + ver string + components []byte + deleteAudit bool + availableRollbacks []byte } // Minimize the size of this structure. @@ -377,6 +368,10 @@ func toUpdateBody(now string, pending pendingT) ([]byte, error) { if pending.extra.seqNo.IsSet() { fields[dl.FieldActionSeqNo] = pending.extra.seqNo } + + if pending.extra.availableRollbacks != nil { + fields[dl.FieldAvailableRollbacks] = json.RawMessage(pending.extra.availableRollbacks) + } } return fields.Marshal() } diff --git a/internal/pkg/dl/constants.go b/internal/pkg/dl/constants.go index 7937d62877..615699c099 100644 --- a/internal/pkg/dl/constants.go +++ b/internal/pkg/dl/constants.go @@ -45,17 +45,17 @@ const ( FieldUnenrolledReason = "unenrolled_reason" FiledType = "type" FieldUnhealthyReason = "unhealthy_reason" - - FieldActive = "active" - FieldNamespaces = "namespaces" - FieldTags = "tags" - FieldUpdatedAt = "updated_at" - FieldUnenrolledAt = "unenrolled_at" - FieldUpgradedAt = "upgraded_at" - FieldUpgradeStartedAt = "upgrade_started_at" - FieldUpgradeStatus = "upgrade_status" - FieldUpgradeDetails = "upgrade_details" - FieldUpgradeAttempts = "upgrade_attempts" + FieldAvailableRollbacks = "available_rollbacks" + FieldActive = "active" + FieldNamespaces = "namespaces" + FieldTags = "tags" + FieldUpdatedAt = "updated_at" + FieldUnenrolledAt = "unenrolled_at" + FieldUpgradedAt = "upgraded_at" + FieldUpgradeStartedAt = "upgrade_started_at" + FieldUpgradeStatus = "upgrade_status" + FieldUpgradeDetails = "upgrade_details" + FieldUpgradeAttempts = "upgrade_attempts" FieldAuditUnenrolledTime = "audit_unenrolled_time" FieldAuditUnenrolledReason = "audit_unenrolled_reason" diff --git a/internal/pkg/model/schema.go b/internal/pkg/model/schema.go index 1612a339bf..8dd564c0f6 100644 --- a/internal/pkg/model/schema.go +++ b/internal/pkg/model/schema.go @@ -141,6 +141,9 @@ type Agent struct { // Agent timestamp for audit unenroll/uninstall action AuditUnenrolledTime string `json:"audit_unenrolled_time,omitempty"` + // list of available rollbacks for the agent + AvailableRollbacks []AvailableRollback `json:"available_rollbacks,omitempty"` + // Elastic Agent components detailed status information Components json.RawMessage `json:"components,omitempty"` @@ -198,9 +201,6 @@ type Agent struct { // hash of token provided during enrollment that allows replacement by another enrollment with same ID ReplaceToken string `json:"replace_token,omitempty"` - // list of available rollbacks for the agent - Rollbacks []Rollback `json:"rollbacks,omitempty"` - // Shared ID SharedID string `json:"shared_id,omitempty"` @@ -291,7 +291,6 @@ type Artifact struct { // AvailableRollback type AvailableRollback struct { - ESDocument ValidUntil string `json:"valid_until,omitempty"` Version string `json:"version,omitempty"` } @@ -530,12 +529,6 @@ type PolicyOutput struct { Type string `json:"type"` } -// Rollback -type Rollback struct { - ValidUntil string `json:"valid_until,omitempty"` - Version string `json:"version,omitempty"` -} - // SecretReferencesItems type SecretReferencesItems struct { ID string `json:"id"` diff --git a/model/openapi.yml b/model/openapi.yml index c14dfe5082..34ef6d5e75 100644 --- a/model/openapi.yml +++ b/model/openapi.yml @@ -448,7 +448,7 @@ components: The revision of the policy that the agent is currently running. type: integer format: int64 - rollbacks: + available_rollbacks: $ref: "#/components/schemas/available_rollbacks" actionSignature: description: Optional action signing data. diff --git a/model/schema.json b/model/schema.json index ce9d416c7a..3ad9d1ee9f 100644 --- a/model/schema.json +++ b/model/schema.json @@ -732,7 +732,7 @@ "description": "hash of token provided during enrollment that allows replacement by another enrollment with same ID", "type": "string" }, - "rollbacks": { + "available_rollbacks": { "description": "list of available rollbacks for the agent", "type": "array", "items": { diff --git a/pkg/api/types.gen.go b/pkg/api/types.gen.go index 0406c15552..a09d6740ff 100644 --- a/pkg/api/types.gen.go +++ b/pkg/api/types.gen.go @@ -323,6 +323,9 @@ type CheckinRequest struct { // AgentPolicyId The ID of the policy that the agent is currently running. AgentPolicyId *string `json:"agent_policy_id,omitempty"` + // AvailableRollbacks Target versions available for a rollback + AvailableRollbacks *AvailableRollbacks `json:"available_rollbacks,omitempty"` + // Components An embedded JSON object that holds component information that the agent is running. // Defined in fleet-server as a `json.RawMessage`, defined as an object in the elastic-agent. // fleet-server will update the components in an agent record if they differ from this object. @@ -346,9 +349,6 @@ type CheckinRequest struct { // If specified fleet-server will set its poll timeout to `max(1m, poll_timeout-2m)` and its write timeout to `max(2m, poll_timout-1m)`. PollTimeout *string `json:"poll_timeout,omitempty"` - // Rollbacks Target versions available for a rollback - Rollbacks *AvailableRollbacks `json:"rollbacks,omitempty"` - // Status The agent state, inferred from agent control protocol states. Status CheckinRequestStatus `json:"status"` From 5eeb78bc0b6ac96736ef8e3f3d9fb964d0230824 Mon Sep 17 00:00:00 2001 From: Paolo Chila Date: Thu, 27 Nov 2025 18:32:12 +0100 Subject: [PATCH 4/5] fixup! Refactor names and add bulk handling of available rollbacks --- model/schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/model/schema.json b/model/schema.json index 3ad9d1ee9f..f87a8a60c3 100644 --- a/model/schema.json +++ b/model/schema.json @@ -515,7 +515,7 @@ }, "available_rollback": { - "title": "Rollback", + "title": "AvailableRollback", "type": "object", "properties": { "version": { From 68867eea6c833d26ecf64bce51b570ed61c2f19d Mon Sep 17 00:00:00 2001 From: Paolo Chila Date: Mon, 1 Dec 2025 09:28:20 +0100 Subject: [PATCH 5/5] Add a simple unit test for available_rollbacks in checkin body --- internal/pkg/api/handleCheckin_test.go | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/internal/pkg/api/handleCheckin_test.go b/internal/pkg/api/handleCheckin_test.go index a02bd9c0e7..7181f1eeb7 100644 --- a/internal/pkg/api/handleCheckin_test.go +++ b/internal/pkg/api/handleCheckin_test.go @@ -1149,7 +1149,25 @@ func TestValidateCheckinRequest(t *testing.T) { }, }, expValid: validatedCheckin{ - rawMeta: []byte(`{"elastic": {"agent": {"id": "testid", "fips": true}}}`), + rawMeta: []byte(`{"elastic": {"agent": {"id": "testid", "fips": true}}}`), + rawAvailableRollbacks: []byte(`[]`), + }, + }, + { + name: "Available rollbacks are correctly parsed", + req: &http.Request{ + Body: io.NopCloser(strings.NewReader(`{"validJson": "test", "status": "test", "message": "test message", "available_rollbacks": [{"version": "1.2.3-SNAPSHOT", "valid_until": "2025-11-27T15:12:44Z"}]}`)), + }, + cfg: &config.Server{ + Limits: config.ServerLimits{ + CheckinLimit: config.Limit{ + MaxBody: 0, + }, + }, + }, + expErr: nil, + expValid: validatedCheckin{ + rawAvailableRollbacks: []byte(`[{"version": "1.2.3-SNAPSHOT", "valid_until": "2025-11-27T15:12:44Z"}]`), }, }, } @@ -1164,6 +1182,7 @@ func TestValidateCheckinRequest(t *testing.T) { if tc.expErr == nil { assert.NoError(t, err) assert.Equal(t, tc.expValid.rawMeta, valid.rawMeta) + assert.JSONEq(t, string(tc.expValid.rawAvailableRollbacks), string(valid.rawAvailableRollbacks)) } else { // Asserting error messages prior to ErrorAs becuase ErrorAs modifies // the target error. If we assert error messages after calling ErrorAs