Skip to content
Merged
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
75 changes: 63 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
}

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,20 @@ func (ct *CheckinT) validateRequest(zlog zerolog.Logger, w http.ResponseWriter,
return val, err
}

rawRollbacks, err := parseAvailableRollbacks(zlog, agent.Upgrade, &req)
if err != nil {
zlog.Warn().Err(err).Msg("unable to parse available rollbacks")
rawRollbacks = nil
}

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 @@ -292,6 +300,10 @@ func (ct *CheckinT) ProcessRequest(zlog zerolog.Logger, w http.ResponseWriter, r
checkin.WithDeleteAudit(agent.AuditUnenrolledReason != "" || agent.UnenrolledAt != ""),
}

if validated.rawAvailableRollbacks != nil {
initialOpts = append(initialOpts, checkin.WithAvailableRollbacks(validated.rawAvailableRollbacks))
}

revID, opts, err := ct.processPolicyDetails(r.Context(), zlog, agent, req)
if err != nil {
return fmt.Errorf("failed to update policy details: %w", err)
Expand Down Expand Up @@ -1131,6 +1143,45 @@ func parseComponents(zlog zerolog.Logger, agent *model.Agent, req *CheckinReques
return outComponents, &unhealthyReason, nil
}

// parseAvailableRollbacks will pull the available rollbacks contained in the checkin request and compare them to what
// we have currently in the model, returning the value that we want to persist expressed as a []byte.
// If the value needs to be updated, this function will return a non-nil []byte (possibly empty if we need to clear the information)
// Nil []byte returned means that no storage operation should happen for the available rollbacks (it means that we already have
// the correct value on the model). See ProcessRequest and checkin.WithAvailableRollbacks for reference.
func parseAvailableRollbacks(zlog zerolog.Logger, upgradeInfo *model.Upgrade, req *CheckinRequest) ([]byte, error) {

reqUpgradeInfo := model.Upgrade{Rollbacks: []model.AvailableRollback{}}
if len(req.Upgrade) > 0 {
err := json.Unmarshal(req.Upgrade, &reqUpgradeInfo)
if err != nil {
return nil, fmt.Errorf("parsing request upgrade information: %w", err)
}
}

var outRollbacks []byte

var agentRollbacks []model.AvailableRollback
if upgradeInfo != nil {
agentRollbacks = upgradeInfo.Rollbacks
}

// Compare the deserialized meta structures and return the bytes to update if different
if !reflect.DeepEqual(reqUpgradeInfo.Rollbacks, agentRollbacks) {
zlog.Trace().
Any("oldAvailableRollbacks", agentRollbacks).
Any("reqAvailableRollbacks", reqUpgradeInfo.Rollbacks).
Msg("available rollback data is not equal")

zlog.Info().Msg("applying new rollback data")
marshalled, err := json.Marshal(reqUpgradeInfo.Rollbacks)
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
42 changes: 41 additions & 1 deletion internal/pkg/api/handleCheckin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1149,7 +1149,42 @@ 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", "upgrade":{ "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"}]`),
},
},
{
name: "Available rollbacks are incorrectly formatted (string instead of array): no error returned but the rawAvailableRollbacks are set to nil",
req: &http.Request{
Body: io.NopCloser(strings.NewReader(`{"validJson": "test", "status": "test", "message": "test message", "upgrade":{"rollbacks": "foobar"}}`)),
},
cfg: &config.Server{
Limits: config.ServerLimits{
CheckinLimit: config.Limit{
MaxBody: 0,
},
},
},
expErr: nil,
expValid: validatedCheckin{
rawAvailableRollbacks: nil,
},
},
}
Expand All @@ -1164,6 +1199,11 @@ func TestValidateCheckinRequest(t *testing.T) {
if tc.expErr == nil {
assert.NoError(t, err)
assert.Equal(t, tc.expValid.rawMeta, valid.rawMeta)
if tc.expValid.rawAvailableRollbacks == nil {
assert.Nil(t, valid.rawAvailableRollbacks)
} else {
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
15 changes: 15 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.

28 changes: 23 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,14 @@ func toUpdateBody(now string, pending pendingT) ([]byte, error) {
if pending.extra.seqNo.IsSet() {
fields[dl.FieldActionSeqNo] = pending.extra.seqNo
}

if pending.extra.availableRollbacks != nil {
if upgradeInfo, ok := fields[dl.FieldUpgradeInfo].(bulk.UpdateFields); ok {
upgradeInfo[dl.FieldAvailableRollbacks] = json.RawMessage(pending.extra.availableRollbacks)
} else {
fields[dl.FieldUpgradeInfo] = bulk.UpdateFields{dl.FieldAvailableRollbacks: json.RawMessage(pending.extra.availableRollbacks)}
}
}
}
return fields.Marshal()
}
Expand Down
20 changes: 11 additions & 9 deletions internal/pkg/dl/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,17 @@ const (
FiledType = "type"
FieldUnhealthyReason = "unhealthy_reason"

FieldActive = "active"
FieldNamespaces = "namespaces"
FieldTags = "tags"
FieldUpdatedAt = "updated_at"
FieldUnenrolledAt = "unenrolled_at"
FieldUpgradedAt = "upgraded_at"
FieldUpgradeStartedAt = "upgrade_started_at"
FieldUpgradeDetails = "upgrade_details"
FieldUpgradeAttempts = "upgrade_attempts"
FieldActive = "active"
FieldNamespaces = "namespaces"
FieldTags = "tags"
FieldUpdatedAt = "updated_at"
FieldUnenrolledAt = "unenrolled_at"
FieldUpgradedAt = "upgraded_at"
FieldUpgradeStartedAt = "upgrade_started_at"
FieldUpgradeDetails = "upgrade_details"
FieldUpgradeAttempts = "upgrade_attempts"
FieldUpgradeInfo = "upgrade"
FieldAvailableRollbacks = "rollbacks"

FieldAuditUnenrolledTime = "audit_unenrolled_time"
FieldAuditUnenrolledReason = "audit_unenrolled_reason"
Expand Down
16 changes: 16 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.

29 changes: 29 additions & 0 deletions model/openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,33 @@ 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
upgrade_information:
description: |
Container for upgrade information coming from agent
type: object
x-go-type: json.RawMessage
x-go-type-skip-optional-pointer: true
properties:
rollbacks:
$ref: "#/components/schemas/available_rollbacks"

checkinRequest:
type: object
required:
Expand Down Expand Up @@ -431,6 +458,8 @@ components:
The revision of the policy that the agent is currently running.
type: integer
format: int64
upgrade:
$ref: "#/components/schemas/upgrade_information"
actionSignature:
description: Optional action signing data.
type: object
Expand Down
27 changes: 27 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,19 @@
"replace_token": {
"description": "hash of token provided during enrollment that allows replacement by another enrollment with same ID",
"type": "string"
},
"upgrade" : {
"description": "Container for upgrade-related agent data",
"type": "object",
"properties": {
"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
Loading