betterNAS/apps/control-plane/cmd/control-plane/app.go

259 lines
6.1 KiB
Go

package main
import (
"sort"
"strings"
"time"
)
type appConfig struct {
version string
nextcloudBaseURL string
statePath string
dbPath string
sessionTTL time.Duration
nodeOfflineThreshold time.Duration
registrationEnabled bool
corsOrigin string
}
type app struct {
startedAt time.Time
now func() time.Time
config appConfig
store store
}
const defaultNodeOfflineThreshold = 2 * time.Minute
func newApp(config appConfig, startedAt time.Time) (*app, error) {
if config.nodeOfflineThreshold <= 0 {
config.nodeOfflineThreshold = defaultNodeOfflineThreshold
}
var s store
var err error
if config.dbPath != "" {
s, err = newSQLiteStore(config.dbPath)
} else {
s, err = newMemoryStore(config.statePath)
}
if err != nil {
return nil, err
}
return &app{
startedAt: startedAt,
now: time.Now,
config: config,
store: s,
}, nil
}
func (a *app) presentedNode(node nasNode) nasNode {
presented := copyNasNode(node)
if !nodeHeartbeatIsFresh(presented.LastSeenAt, a.now().UTC(), a.config.nodeOfflineThreshold) {
presented.Status = "offline"
}
return presented
}
func (a *app) listNodes(ownerID string) []nasNode {
nodes := a.store.listNodes(ownerID)
presented := make([]nasNode, 0, len(nodes))
for _, node := range nodes {
presented = append(presented, a.presentedNode(node))
}
sort.Slice(presented, func(i, j int) bool {
return presented[i].ID < presented[j].ID
})
return presented
}
func (a *app) listConnectedExports(ownerID string) []storageExport {
exports := a.store.listExports(ownerID)
connected := make([]storageExport, 0, len(exports))
for _, export := range exports {
context, ok := a.store.exportContext(export.ID, ownerID)
if !ok {
continue
}
if !nodeIsConnected(a.presentedNode(context.node)) {
continue
}
connected = append(connected, export)
}
return connected
}
func nodeHeartbeatIsFresh(lastSeenAt string, referenceTime time.Time, threshold time.Duration) bool {
lastSeenAt = strings.TrimSpace(lastSeenAt)
if threshold <= 0 || lastSeenAt == "" {
return false
}
parsedLastSeenAt, err := time.Parse(time.RFC3339, lastSeenAt)
if err != nil {
return false
}
referenceTime = referenceTime.UTC()
if parsedLastSeenAt.After(referenceTime) {
return true
}
return referenceTime.Sub(parsedLastSeenAt) <= threshold
}
func nodeIsConnected(node nasNode) bool {
return node.Status == "online" || node.Status == "degraded"
}
type nextcloudBackendStatus struct {
Configured bool `json:"configured"`
BaseURL string `json:"baseUrl"`
Provider string `json:"provider"`
}
type controlPlaneHealthResponse struct {
Service string `json:"service"`
Status string `json:"status"`
Timestamp string `json:"timestamp"`
UptimeSeconds int `json:"uptimeSeconds"`
Nextcloud nextcloudBackendStatus `json:"nextcloud"`
}
type controlPlaneVersionResponse struct {
Service string `json:"service"`
Version string `json:"version"`
APIVersion string `json:"apiVersion"`
}
type nasNode struct {
ID string `json:"id"`
MachineID string `json:"machineId"`
DisplayName string `json:"displayName"`
AgentVersion string `json:"agentVersion"`
Status string `json:"status"`
LastSeenAt string `json:"lastSeenAt"`
DirectAddress *string `json:"directAddress"`
RelayAddress *string `json:"relayAddress"`
OwnerID string `json:"-"`
}
type storageExport struct {
ID string `json:"id"`
NasNodeID string `json:"nasNodeId"`
Label string `json:"label"`
Path string `json:"path"`
MountPath string `json:"mountPath,omitempty"`
Protocols []string `json:"protocols"`
CapacityBytes *int64 `json:"capacityBytes"`
Tags []string `json:"tags"`
OwnerID string `json:"-"`
}
type mountProfile struct {
ID string `json:"id"`
ExportID string `json:"exportId"`
Protocol string `json:"protocol"`
DisplayName string `json:"displayName"`
MountURL string `json:"mountUrl"`
Readonly bool `json:"readonly"`
Credential mountCredential `json:"credential"`
}
type mountCredential struct {
Mode string `json:"mode"`
Username string `json:"username"`
Password string `json:"password"`
ExpiresAt string `json:"expiresAt"`
}
type cloudProfile struct {
ID string `json:"id"`
ExportID string `json:"exportId"`
Provider string `json:"provider"`
BaseURL string `json:"baseUrl"`
Path string `json:"path"`
}
type storageExportInput struct {
Label string `json:"label"`
Path string `json:"path"`
MountPath string `json:"mountPath,omitempty"`
Protocols []string `json:"protocols"`
CapacityBytes *int64 `json:"capacityBytes"`
Tags []string `json:"tags"`
}
type nodeRegistrationRequest struct {
MachineID string `json:"machineId"`
DisplayName string `json:"displayName"`
AgentVersion string `json:"agentVersion"`
DirectAddress *string `json:"directAddress"`
RelayAddress *string `json:"relayAddress"`
}
type nodeExportsRequest struct {
Exports []storageExportInput `json:"exports"`
}
type nodeHeartbeatRequest struct {
NodeID string `json:"nodeId"`
Status string `json:"status"`
LastSeenAt string `json:"lastSeenAt"`
}
type mountProfileRequest struct {
ExportID string `json:"exportId"`
}
type cloudProfileRequest struct {
UserID string `json:"userId"`
ExportID string `json:"exportId"`
Provider string `json:"provider"`
}
type exportContext struct {
export storageExport
node nasNode
}
type user struct {
ID string `json:"id"`
Username string `json:"username"`
CreatedAt string `json:"createdAt"`
}
func copyStringPointer(value *string) *string {
if value == nil {
return nil
}
copied := *value
return &copied
}
func copyInt64Pointer(value *int64) *int64 {
if value == nil {
return nil
}
copied := *value
return &copied
}
func copyStringSlice(values []string) []string {
if len(values) == 0 {
return []string{}
}
copied := make([]string, len(values))
copy(copied, values)
return copied
}