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
103 changes: 82 additions & 21 deletions internal/controller/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,17 @@ var (
ErrAgentAlreadyExists = errors.New("agent already exists")
)

type Agent struct {
Hostname string `json:"hostname"`
Tags []string `json:"tags"`
LogSources []string `json:"log_sources"`
Metrics bool `json:"metrics"`
MetricsTargets []string `json:"metrics_targets"`
Profiles bool `json:"profiles"`
}

type ControllerAgent interface {
RegisterAgent(hostname string, tags, logSources []string, metrics bool, profiles bool) (string, error)
RegisterAgent(agent *Agent) (string, error)
DeregisterAgent(rid string) error
CreateAgentConfig(rid string) ([]byte, error)
ListAgents() ([]map[string]string, error)
Expand Down Expand Up @@ -186,6 +195,19 @@ prometheus.receive_http "default" {
}
forward_to = [prometheus.remote_write.default.receiver]
}

{{ if .MetricsTargets }}
{{ range $index, $source := .MetricsTargets }}
prometheus.scrape "custom_{{ $index }}" {
targets = [{"__address__" = "{{ $source.Address }}"}]
forward_to = [prometheus.remote_write.default.receiver]
scrape_interval = "15s"
{{ if $source.MetricsPath }}
metrics_path = "{{ $source.MetricsPath }}"
{{- end }}
}
{{ end -}}
{{ end -}}
{{ end -}}

{{ if .Profiles }}
Expand Down Expand Up @@ -233,15 +255,15 @@ http:
{{- end }}
`

func (c *controller) RegisterAgent(hostname string, tags, logSources []string, Metrics bool, Profiles bool) (string, error) {
slog.Debug("Register Agent", "hostname", hostname, "tags", tags, "logSources", logSources, "metrics", Metrics, "profiles", Profiles)
func (c *controller) RegisterAgent(data *Agent) (string, error) {
slog.Debug("Register Agent", "data", fmt.Sprintf("%+v", data))

agent, err := c.marshalAgent(hostname, tags, logSources, Metrics, Profiles)
agent, err := c.marshalAgent(data)
if err != nil {
return "", err
}

exists, err := c.model.GetAgent(&model.Agent{Hostname: hostname})
exists, err := c.model.GetAgent(&model.Agent{Hostname: data.Hostname})
if err != nil && !errors.Is(err, model.ErrAgentNotFound) {
return "", err
}
Expand Down Expand Up @@ -319,7 +341,11 @@ func (c *controller) CreateAgentConfig(rid string) ([]byte, error) {
Docker bool
Files []string
}
Metrics bool
Metrics bool
MetricsTargets []struct {
Address string
MetricsPath string
}
Profiles bool
}{
Hostname: agent.Hostname,
Expand All @@ -336,7 +362,11 @@ func (c *controller) CreateAgentConfig(rid string) ([]byte, error) {
Docker: false,
Files: make([]string, 0),
},
Metrics: agent.Metrics,
Metrics: agent.Metrics,
MetricsTargets: make([]struct {
Address string
MetricsPath string
}, 0),
Profiles: agent.Profiles,
}

Expand All @@ -353,12 +383,27 @@ func (c *controller) CreateAgentConfig(rid string) ([]byte, error) {
data.LogSources.Docker = true
case "file":
files = append(files, fmt.Sprintf("{__path__ = \"%s\"}", uri.Path))
data.LogSources.Files = files // fmt.Sprintf("[%s,]", strings.Join(files, ", "))
data.LogSources.Files = files
default:
continue
}
}

for _, source := range agent.MetricsTargets {
uri, err := url.Parse(source)
if err != nil {
continue
}
entry := struct {
Address string
MetricsPath string
}{
Address: uri.Host,
MetricsPath: uri.Path,
}
data.MetricsTargets = append(data.MetricsTargets, entry)
}

buf := new(bytes.Buffer)
if err := tmpl.Execute(buf, data); err != nil {
return nil, err
Expand Down Expand Up @@ -408,17 +453,17 @@ func (c *controller) GetAgent(rid string) (*model.Agent, error) {
return agent, nil
}

func (c *controller) marshalAgent(hostname string, tags, logSources []string, metrics bool, profiles bool) (*model.Agent, error) {
if hostname == "" {
func (c *controller) marshalAgent(data *Agent) (*model.Agent, error) {
if data.Hostname == "" {
return nil, fmt.Errorf("hostname must not be empty")
}

if len(logSources) == 0 {
if len(data.LogSources) == 0 {
return nil, fmt.Errorf("at least one log source must be specified")
}

var effectiveLogSources []string
for _, logSource := range logSources {
for _, logSource := range data.LogSources {
uri, err := url.Parse(logSource)
if err != nil {
continue
Expand All @@ -434,6 +479,21 @@ func (c *controller) marshalAgent(hostname string, tags, logSources []string, me
return nil, fmt.Errorf("no valid log source specified")
}

var effectiveMetricsTargets []string
for _, metricsTarget := range data.MetricsTargets {
uri, err := url.Parse(metricsTarget)
if err != nil {
continue
}
if !slices.Contains([]string{"http", "https"}, uri.Scheme) {
continue
}
if uri.Host == "" {
continue
}
effectiveMetricsTargets = append(effectiveMetricsTargets, uri.String())
}

password := rand.Text()
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
Expand All @@ -445,15 +505,16 @@ func (c *controller) marshalAgent(hostname string, tags, logSources []string, me
}

agent := &model.Agent{
Hostname: hostname,
LogSources: effectiveLogSources,
Metrics: metrics,
Profiles: profiles,
Tags: tags,
ResourceId: fmt.Sprintf("rid:finch:%s:agent:%s", c.config.Id(), uuid.New().String()),
Username: rand.Text(),
Password: password,
PasswordHash: string(hash),
Hostname: data.Hostname,
LogSources: effectiveLogSources,
Metrics: data.Metrics,
MetricsTargets: effectiveMetricsTargets,
Profiles: data.Profiles,
Tags: data.Tags,
ResourceId: fmt.Sprintf("rid:finch:%s:agent:%s", c.config.Id(), uuid.New().String()),
Username: rand.Text(),
Password: password,
PasswordHash: string(hash),
}

return agent, nil
Expand Down
94 changes: 84 additions & 10 deletions internal/controller/agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,15 +71,26 @@ func Test_RegisterAgentReturnsError_BadParameters(t *testing.T) {
ctrl := New(model, &mockedConfig)
assert.NotNil(t, ctrl, "create controller")

_, err := ctrl.RegisterAgent("", nil, nil, false, false)
data := Agent{
Hostname: "",
Tags: nil,
LogSources: nil,
Metrics: false,
MetricsTargets: nil,
Profiles: false,
}

_, err := ctrl.RegisterAgent(&data)
expected := "hostname must not be empty"
assert.EqualError(t, err, expected, "register agent with empty hostname")

_, err = ctrl.RegisterAgent("test-host", nil, nil, false, false)
data.Hostname = "test-host"
_, err = ctrl.RegisterAgent(&data)
expected = "at least one log source must be specified"
assert.EqualError(t, err, expected, "register agent with no log sources")

_, err = ctrl.RegisterAgent("test-host", nil, []string{"invalid://source"}, false, false)
data.LogSources = []string{"invalid://source"}
_, err = ctrl.RegisterAgent(&data)
expected = "no valid log source specified"
assert.EqualError(t, err, expected, "register agent with invalid log source")
}
Expand All @@ -92,7 +103,16 @@ func Test_RegisterAgentReturnsError_InvalidSecret(t *testing.T) {
ctrl := New(model, &config)
assert.NotNil(t, ctrl, "create controller")

_, err := ctrl.RegisterAgent("test-host", []string{"tag1"}, []string{"journal://"}, false, false)
data := Agent{
Hostname: "test-host",
Tags: []string{"tag1"},
LogSources: []string{"journal://"},
Metrics: false,
MetricsTargets: nil,
Profiles: false,
}

_, err := ctrl.RegisterAgent(&data)
assert.Error(t, err, "register agent with invalid config secret")
}

Expand All @@ -102,7 +122,16 @@ func Test_RegisterAgentReturnsResourceId(t *testing.T) {
ctrl := New(model, &mockedConfig)
assert.NotNil(t, ctrl, "create controller")

rid, err := ctrl.RegisterAgent("test-host", []string{"tag1", "tag2"}, []string{"file:///var/log/syslog"}, false, false)
data := Agent{
Hostname: "test-host",
Tags: []string{"tag1", "tag2"},
LogSources: []string{"file:///var/log/syslog"},
Metrics: false,
MetricsTargets: nil,
Profiles: false,
}

rid, err := ctrl.RegisterAgent(&data)
assert.NoError(t, err, "register agent with valid parameters")

assert.NotEmpty(t, rid, "resource ID not empty")
Expand Down Expand Up @@ -133,7 +162,16 @@ func Test_DeregisterAgentReturnsNil(t *testing.T) {
ctrl := New(model, &mockedConfig)
assert.NotNil(t, ctrl, "create controller")

rid, err := ctrl.RegisterAgent("test-host", []string{"tag1"}, []string{"file:///var/log/syslog"}, false, false)
data := Agent{
Hostname: "test-host",
Tags: []string{"tag1"},
LogSources: []string{"file:///var/log/syslog"},
Metrics: false,
MetricsTargets: nil,
Profiles: false,
}

rid, err := ctrl.RegisterAgent(&data)
assert.NoError(t, err, "register agent with valid parameters")

err = ctrl.DeregisterAgent(rid)
Expand All @@ -157,7 +195,16 @@ func Test_CreateAgentConfigReturnsConfig(t *testing.T) {
ctrl := New(model, &mockedConfig)
assert.NotNil(t, ctrl, "create controller")

rid, err := ctrl.RegisterAgent("test-host", []string{"tag1"}, []string{"file:///var/log/syslog"}, false, false)
data := Agent{
Hostname: "test-host",
Tags: []string{"tag1"},
LogSources: []string{"file:///var/log/syslog"},
Metrics: false,
MetricsTargets: nil,
Profiles: false,
}

rid, err := ctrl.RegisterAgent(&data)
assert.NoError(t, err, "register agent with valid parameters")

config, err := ctrl.CreateAgentConfig(rid)
Expand All @@ -182,7 +229,16 @@ func Test_GetAgentReturnsAgent(t *testing.T) {
ctrl := New(model, &mockedConfig)
assert.NotNil(t, ctrl, "create controller")

rid, err := ctrl.RegisterAgent("test-host", []string{"tag1"}, []string{"file:///var/log/syslog"}, false, false)
data := Agent{
Hostname: "test-host",
Tags: []string{"tag1"},
LogSources: []string{"file:///var/log/syslog"},
Metrics: false,
MetricsTargets: nil,
Profiles: false,
}

rid, err := ctrl.RegisterAgent(&data)
assert.NoError(t, err, "register agent with valid parameters")

agent, err := ctrl.GetAgent(rid)
Expand All @@ -207,10 +263,28 @@ func Test_ListAgentsReturnsAgents(t *testing.T) {
ctrl := New(model, &mockedConfig)
assert.NotNil(t, ctrl, "create controller")

_, err := ctrl.RegisterAgent("test-host-1", []string{"tag1"}, []string{"file:///var/log/syslog"}, false, false)
data := Agent{
Hostname: "test-host-1",
Tags: []string{"tag1"},
LogSources: []string{"file:///var/log/syslog"},
Metrics: false,
MetricsTargets: nil,
Profiles: false,
}

_, err := ctrl.RegisterAgent(&data)
assert.NoError(t, err, "register first agent")

_, err = ctrl.RegisterAgent("test-host-2", []string{"tag2"}, []string{"file:///var/log/syslog"}, false, false)
data = Agent{
Hostname: "test-host-2",
Tags: []string{"tag2"},
LogSources: []string{"file:///var/log/syslog"},
Metrics: false,
MetricsTargets: nil,
Profiles: false,
}

_, err = ctrl.RegisterAgent(&data)
assert.NoError(t, err, "register second agent")

agents, err := ctrl.ListAgents()
Expand Down
11 changes: 2 additions & 9 deletions internal/handler/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,22 +26,15 @@ func (h *handler) registerAgentHandlers() {
}

func (h *handler) CreateAgent(w http.ResponseWriter, r *http.Request) {
type payload struct {
Hostname string `json:"hostname"`
Tags []string `json:"tags"`
LogSources []string `json:"log_sources"`
Metrics bool `json:"metrics"`
Profiles bool `json:"profiles"`
}
var p payload
var p controller.Agent

if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
go h.makeLog(r, http.StatusBadRequest, slog.LevelError, "invalid request body")
h.makeError(w, http.StatusBadRequest, "invalid request body")
return
}

rid, err := h.controller.RegisterAgent(p.Hostname, p.Tags, p.LogSources, p.Metrics, p.Profiles)
rid, err := h.controller.RegisterAgent(&p)
if err != nil {
status := http.StatusInternalServerError
if errors.Is(err, controller.ErrAgentAlreadyExists) {
Expand Down
2 changes: 1 addition & 1 deletion internal/handler/agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ var mockedConfig = mockConfig{

type mockController struct{}

func (m *mockController) RegisterAgent(hostname string, tags, logSources []string, metrics bool, profiles bool) (string, error) {
func (m *mockController) RegisterAgent(data *controller.Agent) (string, error) {
return "rid:12345", nil
}
func (m *mockController) DeregisterAgent(rid string) error {
Expand Down
5 changes: 5 additions & 0 deletions internal/handler/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ components:
metrics:
type: boolean
example: true
metrics_targets:
type: array
items:
type: string
example: ["http://localhost:9100/metrics"]
profiles:
type: boolean
example: true
Expand Down
Loading