Compare commits

..

1 Commits

Author SHA1 Message Date
56558d7241 feat(agent): add status controller 2024-03-01 11:19:03 +01:00
12 changed files with 238 additions and 91 deletions

View File

@ -1,14 +0,0 @@
<html>
<head>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Emissary</h1>
<ul>
<li>Server URL: {{ .ServerURL }}</li>
<li>Claimed: {{if .Claimed}}true{{else}}false{{end}}</li>
<li>Connected: {{if .Connected}}true{{else}}false{{end}}</li>
<li>Thumbprint: <code>{{ .Thumbprint }}</code></li>
</ul>
</body>
</html>

View File

@ -1,13 +1,15 @@
package registration package status
import ( import (
"context" "context"
"fmt"
"net/http" "net/http"
"sync/atomic" "sync/atomic"
"forge.cadoles.com/Cadoles/emissary/internal/agent" "forge.cadoles.com/Cadoles/emissary/internal/agent"
"forge.cadoles.com/Cadoles/emissary/internal/datastore" "forge.cadoles.com/Cadoles/emissary/internal/datastore"
"github.com/pkg/errors" "github.com/pkg/errors"
"gitlab.com/wpetit/goweb/api"
"gitlab.com/wpetit/goweb/logger" "gitlab.com/wpetit/goweb/logger"
) )
@ -17,17 +19,23 @@ type Status struct {
Claimed bool Claimed bool
Thumbprint string Thumbprint string
ServerURL string ServerURL string
ClaimURL string
AgentURL string
AgentVersion string
} }
type Controller struct { type Controller struct {
status *atomic.Value status *atomic.Value
server *atomic.Value server *atomic.Value
addr string addr string
claimURL string
agentURL string
agentVersion string
} }
// Name implements node.Controller. // Name implements node.Controller.
func (c *Controller) Name() string { func (c *Controller) Name() string {
return "registration-controller" return "status-controller"
} }
// Reconcile implements node.Controller. // Reconcile implements node.Controller.
@ -36,16 +44,28 @@ func (c *Controller) Reconcile(ctx context.Context, state *agent.State) error {
thumbprint := agent.Thumbprint(ctx) thumbprint := agent.Thumbprint(ctx)
connected := true connected := true
agent, err := cl.GetAgent(
ctx, agent, err := cl.GetAgent(ctx, state.AgentID())
state.AgentID(),
)
if err != nil { if err != nil {
logger.Error(ctx, "could not get agent", logger.E(errors.WithStack(err))) logger.Error(ctx, "could not get agent", logger.E(errors.WithStack(err)))
var apiErr *api.Error
if errors.As(err, &apiErr) {
switch apiErr.Code {
case api.ErrCodeForbidden:
// Contact is ok but agent may be not claimed yet
default:
connected = false connected = false
} }
} else {
connected = false
}
}
claimed := agent != nil && agent.TenantID != nil claimed := agent != nil && agent.TenantID != nil
var agentID datastore.AgentID
if agent != nil {
agentID = agent.ID
}
c.status.Store(Status{ c.status.Store(Status{
Agent: agent, Agent: agent,
@ -53,27 +73,14 @@ func (c *Controller) Reconcile(ctx context.Context, state *agent.State) error {
Claimed: claimed, Claimed: claimed,
Thumbprint: thumbprint, Thumbprint: thumbprint,
ServerURL: cl.ServerURL(), ServerURL: cl.ServerURL(),
ClaimURL: fmt.Sprintf(c.claimURL, thumbprint),
AgentURL: fmt.Sprintf(c.agentURL, agentID),
AgentVersion: c.agentVersion,
}) })
if err := c.reconcileAgent(ctx, connected, claimed); err != nil {
return errors.WithStack(err)
}
return nil
}
func (c *Controller) reconcileAgent(ctx context.Context, connected bool, claimed bool) error {
shouldStart := !connected || !claimed
if shouldStart {
if err := c.startServer(ctx); err != nil { if err := c.startServer(ctx); err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
} else {
if err := c.stopServer(ctx); err != nil {
return errors.WithStack(err)
}
}
return nil return nil
} }
@ -104,21 +111,6 @@ func (c *Controller) startServer(ctx context.Context) error {
return nil return nil
} }
func (c *Controller) stopServer(ctx context.Context) error {
server := c.getServer()
if server == nil {
return nil
}
defer c.setServer(nil)
if err := server.Close(); err != nil {
return errors.WithStack(err)
}
return nil
}
func (c *Controller) setServer(s *http.Server) { func (c *Controller) setServer(s *http.Server) {
c.server.Store(s) c.server.Store(s)
} }
@ -132,9 +124,12 @@ func (c *Controller) getServer() *http.Server {
return server return server
} }
func NewController(addr string) *Controller { func NewController(addr string, claimURL string, agentURL string, agentVersion string) *Controller {
return &Controller{ return &Controller{
addr: addr, addr: addr,
claimURL: claimURL,
agentURL: agentURL,
agentVersion: agentVersion,
status: &atomic.Value{}, status: &atomic.Value{},
server: &atomic.Value{}, server: &atomic.Value{},
} }

View File

@ -1,4 +1,4 @@
package registration package status
import ( import (
"embed" "embed"

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,145 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="logo.png">
<title>Status | Emissary Agent</title>
<link rel="stylesheet" href="bulma-0.9.4.min.css">
<style>
body {
background-color: #f7f7f7f7;
}
.logo {
left: 50%;
position: absolute;
margin-left: -40px;
width: 100px;
margin-top: -120px
}
.card {
position:relative;
padding-top: 70px;
margin-top: 70px;
}
#qrcode {
display: flex;
flex-direction: row;
justify-content: center;
}
</style>
{{if or .Connected ( not .Claimed ) }}
<script type="text/javascript" src="qrcode.min.js"></script>
{{ end }}
</head>
<body>
<section class="section">
<div class="container">
<div class="column">
<div class="has-text-centered">
<h1 class="title is-size-1 ">Emissary</h1>
<h2 class="subtitle is-size-4">Agent Status</h2>
</div>
<div class="box card">
<img class="logo" src="logo.png" />
<div class="overflow:hidden">
<div class="level is-mobile" style="margin-top:-50px">
<div class="level-left">
<div class="level-item is-size-4-tablet is-size-7-mobile">
<strong class="mr-2">Connected:</strong>{{if .Connected }}<span class="has-text-success">✔</span>{{ else }}<span class="has-text-danger">✕</span>{{ end }}
</div>
</div>
<div class="level-right">
<div class="level-item is-size-4-tablet is-size-7-mobile">
<strong class="mr-2">Claimed:</strong>{{if .Claimed }}<span class="has-text-success">✔</span>{{ else }}<span class="has-text-warning">✕</span>{{ end }}
</div>
</div>
</div>
{{ if and .Connected ( not .Claimed ) }}
<h3 class="is-size-3 mt-4">Claim your agent</h3>
<p class="has-text-centered">
You can claim your agent by clicking the following link:<br />
<a class="button is-link is-medium mt-3" href="{{ .ClaimURL }}" target="_blank" rel="nofollow">Claim me</a><br />
</p>
<p class="has-text-centered mt-3">
You can also scan the following QRCode:
<div id="qrcode" class="mt-3" data-claim-url="{{ .ClaimURL }}"></div>
<script type="text/javascript">
(function() {
const qrCodeElement = document.getElementById("qrcode");
const claimUrl = qrCodeElement.dataset.claimUrl;
new QRCode(qrCodeElement, claimUrl);
}())
</script>
</p>
{{ end }}
{{ if and .Connected .Claimed }}
<h3 class="is-size-3 mt-4">Manage your agent</h3>
<p class="has-text-centered">
You can manage your agent by clicking the following link:<br />
<a class="button is-link is-medium mt-3" href="{{ .AgentURL }}" target="_blank" rel="nofollow">Manage me</a><br />
</p>
<p class="has-text-centered mt-3">
You can also scan the following QRCode:
<div id="qrcode" class="mt-3" data-agent-url="{{ .AgentURL }}"></div>
<script type="text/javascript">
(function() {
const qrCodeElement = document.getElementById("qrcode");
const agentUrl = qrCodeElement.dataset.agentUrl;
new QRCode(qrCodeElement, agentUrl);
}())
</script>
</p>
{{ end }}
<h3 class="is-size-3 mt-4">Informations</h3>
<div class="table-container">
<table class="table is-fullwidth">
<thead>
<tr>
<th>Attribute</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>Thumbprint</td>
<td><code>{{ .Thumbprint }}</code></td>
</tr>
<tr>
<td>Agent ID</td>
<td><code>{{ if .Agent }}{{ .Agent.ID }}{{ else }}unknown{{end}}</code></td>
</tr>
<tr>
<td>Agent Label</td>
<td><code>{{ with .Agent }}{{ if .Label }}{{ .Label }}{{ else }}empty{{end}}{{ else }}unknown{{end}}</code></td>
</tr>
<tr>
<td>Last server contact</td>
<td><code>{{ if .Agent }}{{ .Agent.ContactedAt }}{{ else }}unknown{{end}}</code></td>
</tr>
<tr>
<td>Server URL</td>
<td><code>{{ .ServerURL }}</code></td>
</tr>
<tr>
<td>Claim URL</td>
<td><code>{{ .ClaimURL }}</code></td>
</tr>
<tr>
<td>Agent URL</td>
<td><code>{{ if .Agent }}{{ .AgentURL }}{{ else }}unknown{{end}}</code></td>
</tr>
<tr>
<td>Agent version</td>
<td><code>{{ .AgentVersion }}</code></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</section>
</body>
</html>

View File

@ -9,8 +9,8 @@ import (
"forge.cadoles.com/Cadoles/emissary/internal/agent/controller/openwrt" "forge.cadoles.com/Cadoles/emissary/internal/agent/controller/openwrt"
"forge.cadoles.com/Cadoles/emissary/internal/agent/controller/persistence" "forge.cadoles.com/Cadoles/emissary/internal/agent/controller/persistence"
"forge.cadoles.com/Cadoles/emissary/internal/agent/controller/proxy" "forge.cadoles.com/Cadoles/emissary/internal/agent/controller/proxy"
"forge.cadoles.com/Cadoles/emissary/internal/agent/controller/registration"
"forge.cadoles.com/Cadoles/emissary/internal/agent/controller/spec" "forge.cadoles.com/Cadoles/emissary/internal/agent/controller/spec"
"forge.cadoles.com/Cadoles/emissary/internal/agent/controller/status"
"forge.cadoles.com/Cadoles/emissary/internal/agent/metadata" "forge.cadoles.com/Cadoles/emissary/internal/agent/metadata"
"forge.cadoles.com/Cadoles/emissary/internal/agent/metadata/collector/buildinfo" "forge.cadoles.com/Cadoles/emissary/internal/agent/metadata/collector/buildinfo"
"forge.cadoles.com/Cadoles/emissary/internal/agent/metadata/collector/shell" "forge.cadoles.com/Cadoles/emissary/internal/agent/metadata/collector/shell"
@ -95,9 +95,12 @@ func RunCommand() *cli.Command {
)) ))
} }
if ctrlConf.Registration.Enabled { if ctrlConf.Status.Enabled {
controllers = append(controllers, registration.NewController( controllers = append(controllers, status.NewController(
string(ctrlConf.Registration.Address), string(ctrlConf.Status.Address),
string(ctrlConf.Status.ClaimURL),
string(ctrlConf.Status.AgentURL),
string(ctx.String("projectVersion")),
)) ))
} }

View File

@ -24,7 +24,7 @@ type ControllersConfig struct {
App AppControllerConfig `yaml:"app"` App AppControllerConfig `yaml:"app"`
SysUpgrade SysUpgradeControllerConfig `yaml:"sysupgrade"` SysUpgrade SysUpgradeControllerConfig `yaml:"sysupgrade"`
MDNS MDNSControllerConfig `yaml:"mdns"` MDNS MDNSControllerConfig `yaml:"mdns"`
Registration RegistrationControllerConfig `yaml:"registration"` Status StatusControllerConfig `yaml:"status"`
} }
type PersistenceControllerConfig struct { type PersistenceControllerConfig struct {
@ -61,9 +61,11 @@ type MDNSControllerConfig struct {
Enabled InterpolatedBool `yaml:"enabled"` Enabled InterpolatedBool `yaml:"enabled"`
} }
type RegistrationControllerConfig struct { type StatusControllerConfig struct {
Enabled InterpolatedBool `yaml:"enabled"` Enabled InterpolatedBool `yaml:"enabled"`
Address InterpolatedString `yaml:"address"` Address InterpolatedString `yaml:"address"`
ClaimURL InterpolatedString `yaml:"claimURL"`
AgentURL InterpolatedString `yaml:"agentURL"`
} }
func NewDefaultAgentConfig() AgentConfig { func NewDefaultAgentConfig() AgentConfig {
@ -100,9 +102,11 @@ func NewDefaultAgentConfig() AgentConfig {
MDNS: MDNSControllerConfig{ MDNS: MDNSControllerConfig{
Enabled: true, Enabled: true,
}, },
Registration: RegistrationControllerConfig{ Status: StatusControllerConfig{
Enabled: true, Enabled: true,
Address: ":42521", Address: ":42521",
ClaimURL: "http://localhost:3001/claim/%s",
AgentURL: "http://localhost:3001/agents/%v",
}, },
}, },
Collectors: []ShellCollectorConfig{ Collectors: []ShellCollectorConfig{

View File

@ -63,6 +63,16 @@ agent:
- -c - -c
- source /etc/openwrt_release && echo "$DISTRIB_ID-$DISTRIB_RELEASE-$DISTRIB_REVISION" - source /etc/openwrt_release && echo "$DISTRIB_ID-$DISTRIB_RELEASE-$DISTRIB_REVISION"
# Status controller configuration
status:
enabled: true
# Status page listening address
address: :42521
# Agent claim URL template (see Emissary HQ)
claimURL: http://127.0.0.1:3001/claim/%v
# Agent URL template (see Emissary HQ)
agentURL: http://127.0.0.1:3001/agents/%v
# Collectors configuration # Collectors configuration
collectors: collectors:
- name: uname - name: uname

View File

@ -1,5 +1,6 @@
**/*.go **/*.go
internal/**/*.json internal/**/*.json
**/*.gotpl
modd.conf modd.conf
tmp/config.yml tmp/config.yml
.env { .env {