Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
164 changes: 164 additions & 0 deletions x-pack/agent/pkg/fleetapi/actions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License;
// you may not use this file except in compliance with the Elastic License.

package fleetapi

import (
"encoding/json"
"fmt"
"strings"
"time"

"github.com/pkg/errors"
)

// Action base interface for all the implemented action from the fleet API.
type Action interface {
fmt.Stringer
Type() string
ID() string
}

// BaseAction is the base of all actions to be executed.
type BaseAction struct {
ActionID string
ActionType string
}

// Type returns the action type.
func (a *BaseAction) Type() string {
return a.ActionType
}

// ID returns the action ID.
func (a *BaseAction) ID() string {
return a.ActionID
}

// UnknownAction is an action that is not know by the current version of the Agent and we don't want
// to return an error at parsing time but at execution time we can report or ignore.
//
// NOTE: We only keep the original type and the action id, the payload of the event is dropped, we
// do this to make sure we do not leak any unwanted information.
type UnknownAction struct {
*BaseAction
originalType string
}

// Type returns the type of the Action.
func (a *UnknownAction) Type() string {
return "UNKNOWN"
Comment thread
ph marked this conversation as resolved.
}

func (a *UnknownAction) String() string {
var s strings.Builder
s.WriteString("action_id: ")
s.WriteString(a.ID())
s.WriteString(", type: ")
s.WriteString(a.Type())
s.WriteString(" (original type: ")
s.WriteString(a.OriginalType())
s.WriteString(")")
return s.String()
}

// OriginalType returns the original type of the action as returned by the API.
func (a *UnknownAction) OriginalType() string {
return a.originalType
}

// PolicyChangeAction is a request to apply a new
type PolicyChangeAction struct {
*BaseAction
Policy map[string]interface{} `json:"policy"`
}

func (a *PolicyChangeAction) String() string {
var s strings.Builder
s.WriteString("action_id: ")
s.WriteString(a.ID())
s.WriteString(", type: ")
s.WriteString(a.Type())
return s.String()
}

// Actions is a list of Actions to executes and allow to unmarshal heterogenous action type.
type Actions []Action

// UnmarshalJSON takes every raw representation of an action and try to decode them.
func (a *Actions) UnmarshalJSON(data []byte) error {
type r struct {
ActionType string `json:"type"`
ActionID string `json:"id"`
Data json.RawMessage `json:"data"`
}

var responses []r

if err := json.Unmarshal(data, &responses); err != nil {
return errors.Wrap(err, "fail to decode actions")
}

actions := make([]Action, 0, len(responses))
var action Action

for _, response := range responses {
switch response.ActionType {
case "POLICY_CHANGE":
action = &PolicyChangeAction{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

i wonder if it's not possible keep it as []Action and make decisions based on ActionType. Lazy casting basically

@ph ph Dec 3, 2019

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I had mixed feeling went implementing this, I had two ideas:

  1. Make it generic, extract the minimal required fields for the action ( type / action ID) and puts everything else in a json.RawMessage field so it can be lazily extracted later one.
  2. Implement a base type + concrete type on top of it.

I've gone with the second option because I wanted to keep the serialization in a single place and theses are just data object no logic should be added to them. If new behavior is required, we either:

  1. Create a new type / type cast to a new type.
  2. Create a function that accepts the struct.

Now, I diverge on that with the Event, as you may have noticed I've created a SerializableEvent interface that implements a minimum public interface and expect the user to correctly serialize the type to the JSON. I did this because, an event from Agent doesn't have at the moment a finite number of possibilities.

BaseAction: &BaseAction{
ActionID: response.ActionID,
ActionType: response.ActionType,
},
}
if err := json.Unmarshal(response.Data, action); err != nil {
return errors.Wrap(err, "fail to decode POLICY_CHANGE action")
}
default:
action = &UnknownAction{
BaseAction: &BaseAction{ActionID: response.ActionID, ActionType: "UNKNOWN"},
originalType: response.ActionType,
}
}
actions = append(actions, action)
}

*a = actions
return nil
}

// AckedAction represents a event to be send to the next checkin that will Ack an action.
type AckedAction struct {
EventType string `json:"type"`
ActionID string `json:"action_id"`
Ts time.Time `json:"timestamp"`
Msg string `json:"message,omitempty"`
}

// Type return the type of event.
func (a *AckedAction) Type() string {
return a.EventType
}

// Timestamp return when the event was created.
func (a *AckedAction) Timestamp() time.Time {
return a.Ts
}

// Message returns the human readable string describing the event.
func (a *AckedAction) Message() string {
return a.Msg
}

// Ack returns an event that represent an acked action.
func Ack(action Action) *AckedAction {
const t = "ACTION_ACKNOWLEDGED"

return &AckedAction{
EventType: t,
ActionID: action.ID(),
Msg: "Acknowledge action " + action.ID(),
Ts: time.Now(),
}
}
68 changes: 26 additions & 42 deletions x-pack/agent/pkg/fleetapi/checkin_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,67 +8,47 @@ import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"time"

"github.com/pkg/errors"
)

// CheckinRequest consists of multiple events reported to fleet ui.
//
// Example:
// POST /api/fleet/agents/a4937110-e53e-11e9-934f-47a8e38a522c/checkin
// {
// "events": [{
// "type": "STATE",
// "subtype": "STARTING",
// "message": "state changed from STOPPED to STARTING",
// "timestamp": "2019-10-01T13:42:54.323Z",
// "payload": {},
// "data": "{}"
// }]
// }
type CheckinRequest struct {
Events []Event `json:"events"`
Events []SerializableEvent `json:"events"`
}

// Event is a single event out of collection of reported events.
type Event struct {
EventType string `json:"type"`
Timestamp string `json:"timestamp"`
SubType string `json:"subtype"`
Message string `json:"message"`
Payload map[string]interface{} `json:"payload,omitempty"`
Data string `json:"data,omitempty"`
// SerializableEvent is a representation of the event to be send to the Fleet API via the checkin
// endpoint, we are liberal into what we accept to be send you only need a type and be able to be
// serialized into JSON.
type SerializableEvent interface {
// Type return the type of the event, this must be included in the serialized document.
Type() string

// Timestamp is used to keep track when the event was created in the system.
Timestamp() time.Time

// Message is a human readable string to explain what the event does, this would be displayed in
// the UI as a string of text.
Message() string
}

// Validate validates the enrollment request before sending it to the API.
func (e *CheckinRequest) Validate() error {
if len(e.Events) == 0 {
return errors.New("no events to report")
}

return nil
}

// CheckinResponse is a fleets response to checking API request.
//
// Example:
// {
// "action": "checkin",
// "success": true,
// "policy": {
// },
// "actions": []
// }
// CheckinResponse is the response send back from the server which contains all the action that
// need to be executed or proxy to running processes.
type CheckinResponse struct {
Action string `json:"action"`
Success bool `json:"success"`
Actions Actions `json:"actions"`
Success bool `json:"success"`
}

// Validate validates the response send from the server.
func (e *CheckinResponse) Validate() error {
var err error

return err
return nil
}

// CheckinCmd is a fleet API command.
Expand Down Expand Up @@ -100,10 +80,14 @@ func (e *CheckinCmd) Execute(r *CheckinRequest) (*CheckinResponse, error) {

resp, err := e.client.Send("POST", e.checkinPath, nil, nil, bytes.NewBuffer(b))
if err != nil {
return nil, err
return nil, errors.Wrap(err, "fail to checkin to fleet")
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return nil, extract(resp.Body)
}

checkinResponse := &CheckinResponse{}
decoder := json.NewDecoder(resp.Body)
if err := decoder.Decode(checkinResponse); err != nil {
Expand Down
Loading