Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 53 additions & 12 deletions internal/pkg/api/handleCheckin.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,12 +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
req *CheckinRequest
dur time.Duration
rawMeta []byte
rawComp []byte
seqno sqn.SeqNo
unhealthyReason *[]string
rawAvailableRollbacks []byte
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure this was RFCed but could this be part of upgrade meta? like scheduled_at or error_msg so we don't pollute root object with upgrade specific stuff?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i imagine you don't need this info outside of upgrade window anyways

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if you are referring to the checkin request body, the validatedCheckin struct or the ES document structure.

The main reason that I didn't mix this information with existing upgrade stuff is that there are assumptions about upgrade details and I didn't know if there are more around other other upgrade fields.

The information about available rollbacks must survive beyond the duration of a single upgrade (and fleet detects the conclusion of an upgrade by the upgrade_details field disappearing in the checkin body) and it's also not a stable intrinsic characteristic of agent so that it can go in something like local_metadata.

The RFC will be updated with the final schema we are gonna have in this PR.

}

func (ct *CheckinT) validateRequest(zlog zerolog.Logger, w http.ResponseWriter, r *http.Request, start time.Time, agent *model.Agent) (validatedCheckin, error) {
Expand Down Expand Up @@ -251,13 +252,19 @@ func (ct *CheckinT) validateRequest(zlog zerolog.Logger, w http.ResponseWriter,
return val, err
}

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,
req: &req,
dur: pollDuration,
rawMeta: rawMeta,
rawComp: rawComponents,
seqno: seqno,
unhealthyReason: unhealthyReason,
rawAvailableRollbacks: rawRollbacks,
}, nil
}

Expand Down Expand Up @@ -290,6 +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.WithAvailableRollbacks(validated.rawAvailableRollbacks),
}

revID, opts, err := ct.processPolicyDetails(r.Context(), zlog, agent, req)
Expand Down Expand Up @@ -1132,6 +1140,39 @@ func parseComponents(zlog zerolog.Logger, agent *model.Agent, req *CheckinReques
return outComponents, &unhealthyReason, nil
}

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.AvailableRollbacks) {
zlog.Trace().
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 available rollbacks: %w", err)
}
outRollbacks = marshalled
}
return outRollbacks, nil
}

func calcUnhealthyReason(reqComponents []model.ComponentsItems) []string {
var unhealthyReason []string
hasUnhealthyInput := false
Expand Down
21 changes: 20 additions & 1 deletion internal/pkg/api/handleCheckin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"}]`),
},
},
}
Expand All @@ -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
Expand Down
12 changes: 12 additions & 0 deletions internal/pkg/api/openapi.gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 19 additions & 5 deletions internal/pkg/checkin/bulk.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,12 +125,22 @@ func WithPolicyRevisionIDX(idx int64) Option {
}
}

func WithAvailableRollbacks(availableRollbacks []byte) Option {
return func(pending *pendingT) {
if pending.extra == nil {
pending.extra = &extraT{}
}
pending.extra.availableRollbacks = availableRollbacks
}
}

type extraT struct {
meta []byte
seqNo sqn.SeqNo
ver string
components []byte
deleteAudit bool
meta []byte
seqNo sqn.SeqNo
ver string
components []byte
deleteAudit bool
availableRollbacks []byte
}

// Minimize the size of this structure.
Expand Down Expand Up @@ -358,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()
}
Expand Down
22 changes: 11 additions & 11 deletions internal/pkg/dl/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
9 changes: 9 additions & 0 deletions internal/pkg/model/schema.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 19 additions & 0 deletions model/openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -431,6 +448,8 @@ components:
The revision of the policy that the agent is currently running.
type: integer
format: int64
available_rollbacks:
$ref: "#/components/schemas/available_rollbacks"
actionSignature:
description: Optional action signing data.
type: object
Expand Down
21 changes: 21 additions & 0 deletions model/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,20 @@
}
},

"available_rollback": {
"title": "AvailableRollback",
"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",
Expand Down Expand Up @@ -717,6 +731,13 @@
"replace_token": {
"description": "hash of token provided during enrollment that allows replacement by another enrollment with same ID",
"type": "string"
},
"available_rollbacks": {
"description": "list of available rollbacks for the agent",
"type": "array",
"items": {
"$ref": "#/definitions/available_rollback"
}
}
},
"required": ["_id", "type", "active", "enrolled_at", "status"]
Expand Down
12 changes: 12 additions & 0 deletions pkg/api/types.gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.