diff --git a/cmd/agent/main.go b/cmd/agent/main.go index 1327abd6b..c67aeff23 100644 --- a/cmd/agent/main.go +++ b/cmd/agent/main.go @@ -13,12 +13,10 @@ import ( "github.com/portainer/agent" "github.com/portainer/agent/crypto" "github.com/portainer/agent/docker" + "github.com/portainer/agent/edge" "github.com/portainer/agent/exec" - "github.com/portainer/agent/filesystem" "github.com/portainer/agent/ghw" "github.com/portainer/agent/http" - "github.com/portainer/agent/http/client" - "github.com/portainer/agent/internal/edge" "github.com/portainer/agent/kubernetes" "github.com/portainer/agent/logutils" "github.com/portainer/agent/net" @@ -179,17 +177,18 @@ func main() { // !Security // Edge - edgeManagerParameters := &edge.ManagerParameters{ - Options: options, - AdvertiseAddr: advertiseAddr, - ClusterService: clusterService, - DockerInfoService: dockerInfoService, - ContainerPlatform: containerPlatform, - } - edgeManager := edge.NewManager(edgeManagerParameters) - + var edgeManager *edge.Manager if options.EdgeMode { - edgeKey, err := retrieveEdgeKey(options.EdgeKey, clusterService) + edgeManagerParameters := &edge.ManagerParameters{ + Options: options, + AdvertiseAddr: advertiseAddr, + ClusterService: clusterService, + DockerInfoService: dockerInfoService, + ContainerPlatform: containerPlatform, + } + edgeManager = edge.NewManager(edgeManagerParameters) + + edgeKey, err := edge.RetrieveEdgeKey(options.EdgeKey, clusterService) if err != nil { log.Printf("[ERROR] [main,edge] [message: Unable to retrieve Edge key] [error: %s]", err) } @@ -232,11 +231,11 @@ func main() { ContainerPlatform: containerPlatform, } - if edgeManager.IsEdgeModeEnabled() { + if options.EdgeMode { config.Addr = advertiseAddr } - err = startAPIServer(config) + err = startAPIServer(config, options.EdgeMode) if err != nil && !errors.Is(err, gohttp.ErrServerClosed) { log.Fatalf("[ERROR] [main,http] [message: Unable to start Agent API server] [error: %s]", err) } @@ -250,14 +249,14 @@ func main() { fmt.Printf("[DEBUG] [main] [message: shutting down] [signal: %s]", s) } -func startAPIServer(config *http.APIServerConfig) error { +func startAPIServer(config *http.APIServerConfig, edgeMode bool) error { server := http.NewAPIServer(config) - if config.EdgeManager.IsEdgeModeEnabled() { - return server.StartUnsecured() + if edgeMode { + return server.StartUnsecured(edgeMode) } - return server.StartSecured() + return server.StartSecured(edgeMode) } func parseOptions() (*agent.Options, error) { @@ -289,71 +288,3 @@ func serveEdgeUI(edgeManager *edge.Manager, serverAddr, serverPort string) { } }() } - -func retrieveEdgeKey(edgeKey string, clusterService agent.ClusterService) (string, error) { - - if edgeKey != "" { - log.Println("[INFO] [main,edge] [message: Edge key loaded from options]") - return edgeKey, nil - } - - var keyRetrievalError error - - edgeKey, keyRetrievalError = retrieveEdgeKeyFromFilesystem() - if keyRetrievalError != nil { - return "", keyRetrievalError - } - - if edgeKey == "" && clusterService != nil { - edgeKey, keyRetrievalError = retrieveEdgeKeyFromCluster(clusterService) - if keyRetrievalError != nil { - return "", keyRetrievalError - } - } - - return edgeKey, nil -} - -func retrieveEdgeKeyFromFilesystem() (string, error) { - var edgeKey string - - edgeKeyFilePath := fmt.Sprintf("%s/%s", agent.DataDirectory, agent.EdgeKeyFile) - - keyFileExists, err := filesystem.FileExists(edgeKeyFilePath) - if err != nil { - return "", err - } - - if keyFileExists { - filesystemKey, err := filesystem.ReadFromFile(edgeKeyFilePath) - if err != nil { - return "", err - } - - log.Println("[INFO] [main,edge] [message: Edge key loaded from the filesystem]") - edgeKey = string(filesystemKey) - } - - return edgeKey, nil -} - -func retrieveEdgeKeyFromCluster(clusterService agent.ClusterService) (string, error) { - var edgeKey string - - member := clusterService.GetMemberWithEdgeKeySet() - if member != nil { - httpCli := client.NewAPIClient() - - memberAddr := fmt.Sprintf("%s:%s", member.IPAddress, member.Port) - memberKey, err := httpCli.GetEdgeKey(memberAddr) - if err != nil { - log.Printf("[ERROR] [main,edge,http,cluster] [message: Unable to retrieve Edge key from cluster member] [error: %s]", err) - return "", err - } - - log.Println("[INFO] [main,edge] [message: Edge key loaded from cluster]") - edgeKey = memberKey - } - - return edgeKey, nil -} diff --git a/internal/edge/edge.go b/edge/edge.go similarity index 80% rename from internal/edge/edge.go rename to edge/edge.go index 3f6e9914d..b52dc4af7 100644 --- a/internal/edge/edge.go +++ b/edge/edge.go @@ -7,6 +7,8 @@ import ( "time" "github.com/portainer/agent" + "github.com/portainer/agent/edge/scheduler" + "github.com/portainer/agent/edge/stack" ) type ( @@ -18,10 +20,9 @@ type ( clusterService agent.ClusterService dockerInfoService agent.DockerInfoService key *edgeKey - logsManager *logsManager + logsManager *scheduler.LogsManager pollService *PollService - pollServiceConfig *pollServiceConfig - stackManager *StackManager + stackManager *stack.StackManager } // ManagerParameters represents an object used to create a Manager @@ -48,7 +49,7 @@ func NewManager(parameters *ManagerParameters) *Manager { // Start starts the manager func (manager *Manager) Start() error { if !manager.IsKeySet() { - return errors.New("Unable to start Edge manager without key") + return errors.New("unable to start Edge manager without key") } apiServerAddr := fmt.Sprintf("%s:%s", manager.advertiseAddr, manager.agentOptions.AgentServerPort) @@ -59,7 +60,7 @@ func (manager *Manager) Start() error { PollFrequency: agent.DefaultEdgePollInterval, InactivityTimeout: manager.agentOptions.EdgeInactivityTimeout, InsecurePoll: manager.agentOptions.EdgeInsecurePoll, - Tunnel: manager.agentOptions.EdgeTunnel, + TunnelCapability: manager.agentOptions.EdgeTunnel, PortainerURL: manager.key.PortainerInstanceURL, EndpointID: manager.key.EndpointID, TunnelServerAddr: manager.key.TunnelServerAddr, @@ -67,16 +68,16 @@ func (manager *Manager) Start() error { ContainerPlatform: manager.containerPlatform, } - log.Printf("[DEBUG] [internal,edge] [api_addr: %s] [edge_id: %s] [poll_frequency: %s] [inactivity_timeout: %s] [insecure_poll: %t] [tunnel: %t]", pollServiceConfig.APIServerAddr, pollServiceConfig.EdgeID, pollServiceConfig.PollFrequency, pollServiceConfig.InactivityTimeout, pollServiceConfig.InsecurePoll, pollServiceConfig.Tunnel) + log.Printf("[DEBUG] [internal,edge] [api_addr: %s] [edge_id: %s] [poll_frequency: %s] [inactivity_timeout: %s] [insecure_poll: %t] [tunnel_capability: %t]", pollServiceConfig.APIServerAddr, pollServiceConfig.EdgeID, pollServiceConfig.PollFrequency, pollServiceConfig.InactivityTimeout, pollServiceConfig.InsecurePoll, manager.agentOptions.EdgeTunnel) - stackManager, err := newStackManager(manager.key.PortainerInstanceURL, manager.key.EndpointID, manager.agentOptions.EdgeID, pollServiceConfig.InsecurePoll, pollServiceConfig.Tunnel) + stackManager, err := stack.NewStackManager(manager.key.PortainerInstanceURL, manager.key.EndpointID, manager.agentOptions.EdgeID, pollServiceConfig.InsecurePoll) if err != nil { return err } manager.stackManager = stackManager - manager.logsManager = newLogsManager(manager.key.PortainerInstanceURL, manager.key.EndpointID, manager.agentOptions.EdgeID, pollServiceConfig.InsecurePoll, pollServiceConfig.Tunnel) - manager.logsManager.start() + manager.logsManager = scheduler.NewLogsManager(manager.key.PortainerInstanceURL, manager.key.EndpointID, manager.agentOptions.EdgeID, pollServiceConfig.InsecurePoll) + manager.logsManager.Start() pollService, err := newPollService(manager.stackManager, manager.logsManager, pollServiceConfig) if err != nil { @@ -92,11 +93,6 @@ func (manager *Manager) Start() error { return nil } -// IsEdgeModeEnabled returns true if edge mode is enabled -func (manager *Manager) IsEdgeModeEnabled() bool { - return manager.agentOptions.EdgeMode -} - // ResetActivityTimer resets the activity timer func (manager *Manager) ResetActivityTimer() { manager.pollService.resetActivityTimer() @@ -143,13 +139,13 @@ func (manager *Manager) startEdgeBackgroundProcessOnKubernetes(runtimeCheckFrequ return } - err = manager.stackManager.setEngineStatus(engineTypeKubernetes) + err = manager.stackManager.SetEngineStatus(stack.EngineTypeKubernetes) if err != nil { log.Printf("[ERROR] [internal,edge,runtime] [message: unable to set engine status] [error: %s]", err) return } - err = manager.stackManager.start() + err = manager.stackManager.Start() if err != nil { log.Printf("[ERROR] [internal,edge,runtime] [message: unable to start stack manager] [error: %s]", err) return @@ -190,9 +186,9 @@ func (manager *Manager) checkDockerRuntimeConfig() error { log.Printf("[DEBUG] [internal,edge,runtime,docker] [message: Docker runtime configuration check] [engine_status: %d] [leader_node: %t]", runtimeConfiguration.DockerConfiguration.EngineStatus, agentRunsOnLeaderNode) if !agentRunsOnSwarm || agentRunsOnLeaderNode { - engineStatus := engineTypeDockerStandalone + engineStatus := stack.EngineTypeDockerStandalone if agentRunsOnSwarm { - engineStatus = engineTypeDockerSwarm + engineStatus = stack.EngineTypeDockerSwarm } err = manager.pollService.start() @@ -200,12 +196,12 @@ func (manager *Manager) checkDockerRuntimeConfig() error { return err } - err = manager.stackManager.setEngineStatus(engineStatus) + err = manager.stackManager.SetEngineStatus(engineStatus) if err != nil { return err } - err = manager.stackManager.start() + err = manager.stackManager.Start() if err != nil { return err } @@ -216,7 +212,7 @@ func (manager *Manager) checkDockerRuntimeConfig() error { return err } - err = manager.stackManager.stop() + err = manager.stackManager.Stop() if err != nil { return err } diff --git a/internal/edge/key.go b/edge/key.go similarity index 61% rename from internal/edge/key.go rename to edge/key.go index 898230500..7fb8c7e44 100644 --- a/internal/edge/key.go +++ b/edge/key.go @@ -4,6 +4,7 @@ import ( "encoding/base64" "errors" "fmt" + "log" "strings" "github.com/portainer/agent" @@ -19,7 +20,7 @@ type edgeKey struct { } // SetKey parses and associates an Edge key to the agent. -// If the agent is running inside a Swarm cluster, it will also set the "set" flag to specify that a key is set on this agent in the cluster. +// If the agent is running inside a cluster, it will also set the "set" flag to specify that a key is set on this agent in the cluster. func (manager *Manager) SetKey(key string) error { edgeKey, err := parseEdgeKey(key) if err != nil { @@ -58,10 +59,7 @@ func (manager *Manager) GetKey() string { // IsKeySet returns true if an Edge key is associated to the agent func (manager *Manager) IsKeySet() bool { - if manager.key == nil { - return false - } - return true + return manager.key != nil } // PropagateKeyInCluster propagates the Edge key associated to the agent to all the other agents inside the cluster @@ -94,7 +92,6 @@ func (manager *Manager) PropagateKeyInCluster() error { // parseEdgeKey decodes a base64 encoded key and extract the decoded information from the following // format: ||| -// are expected in the user:password format func parseEdgeKey(key string) (*edgeKey, error) { decodedKey, err := base64.RawStdEncoding.DecodeString(key) if err != nil { @@ -122,3 +119,66 @@ func encodeKey(edgeKey *edgeKey) string { encodedKey := base64.RawStdEncoding.EncodeToString([]byte(keyInfo)) return encodedKey } + +func RetrieveEdgeKey(edgeKey string, clusterService agent.ClusterService) (string, error) { + if edgeKey != "" { + log.Println("[INFO] [main,edge] [message: Edge key loaded from options]") + return edgeKey, nil + } + + var keyRetrievalError error + + edgeKey, keyRetrievalError = retrieveEdgeKeyFromFilesystem() + if keyRetrievalError != nil { + return "", keyRetrievalError + } + + if edgeKey == "" && clusterService != nil { + edgeKey, keyRetrievalError = retrieveEdgeKeyFromCluster(clusterService) + if keyRetrievalError != nil { + return "", keyRetrievalError + } + } + + return edgeKey, nil +} + +func retrieveEdgeKeyFromFilesystem() (string, error) { + edgeKeyFilePath := fmt.Sprintf("%s/%s", agent.DataDirectory, agent.EdgeKeyFile) + + keyFileExists, err := filesystem.FileExists(edgeKeyFilePath) + if err != nil { + return "", err + } + + if !keyFileExists { + return "", nil + } + + filesystemKey, err := filesystem.ReadFromFile(edgeKeyFilePath) + if err != nil { + return "", err + } + + log.Println("[INFO] [main,edge] [message: Edge key loaded from the filesystem]") + return string(filesystemKey), nil +} + +func retrieveEdgeKeyFromCluster(clusterService agent.ClusterService) (string, error) { + member := clusterService.GetMemberWithEdgeKeySet() + if member == nil { + return "", nil + } + + httpCli := client.NewAPIClient() + + memberAddr := fmt.Sprintf("%s:%s", member.IPAddress, member.Port) + memberKey, err := httpCli.GetEdgeKey(memberAddr) + if err != nil { + log.Printf("[ERROR] [main,edge,http,cluster] [message: Unable to retrieve Edge key from cluster member] [error: %s]", err) + return "", err + } + + log.Println("[INFO] [main,edge] [message: Edge key loaded from cluster]") + return memberKey, nil +} diff --git a/internal/edge/poll.go b/edge/poll.go similarity index 87% rename from internal/edge/poll.go rename to edge/poll.go index acd374489..356e8eea6 100644 --- a/internal/edge/poll.go +++ b/edge/poll.go @@ -13,7 +13,8 @@ import ( "github.com/portainer/agent" "github.com/portainer/agent/chisel" - "github.com/portainer/agent/filesystem" + "github.com/portainer/agent/edge/scheduler" + "github.com/portainer/agent/edge/stack" "github.com/portainer/libcrypto" ) @@ -26,7 +27,6 @@ type PollService struct { apiServerAddr string pollIntervalInSeconds float64 insecurePoll bool - tunnel bool inactivityTimeout time.Duration edgeID string httpClient *http.Client @@ -34,12 +34,12 @@ type PollService struct { scheduleManager agent.Scheduler lastActivity time.Time refreshSignal chan struct{} - edgeStackManager *StackManager + edgeStackManager *stack.StackManager portainerURL string endpointID string tunnelServerAddr string tunnelServerFingerprint string - logsManager *logsManager + logsManager *scheduler.LogsManager containerPlatform agent.ContainerPlatform } @@ -49,7 +49,7 @@ type pollServiceConfig struct { InactivityTimeout string PollFrequency string InsecurePoll bool - Tunnel bool + TunnelCapability bool PortainerURL string EndpointID string TunnelServerAddr string @@ -58,7 +58,8 @@ type pollServiceConfig struct { } // newPollService returns a pointer to a new instance of PollService -func newPollService(edgeStackManager *StackManager, logsManager *logsManager, config *pollServiceConfig) (*PollService, error) { +// If TunneCapability is disabled, it will only poll for Edge stacks and schedule without managing reverse tunnels. +func newPollService(edgeStackManager *stack.StackManager, logsManager *scheduler.LogsManager, config *pollServiceConfig) (*PollService, error) { pollFrequency, err := time.ParseDuration(config.PollFrequency) if err != nil { return nil, err @@ -69,15 +70,13 @@ func newPollService(edgeStackManager *StackManager, logsManager *logsManager, co return nil, err } - return &PollService{ + pollService := &PollService{ apiServerAddr: config.APIServerAddr, edgeID: config.EdgeID, pollIntervalInSeconds: pollFrequency.Seconds(), insecurePoll: config.InsecurePoll, - tunnel: config.Tunnel, inactivityTimeout: inactivityTimeout, - tunnelClient: chisel.NewClient(), - scheduleManager: filesystem.NewCronManager(), + scheduleManager: scheduler.NewCronManager(), refreshSignal: nil, edgeStackManager: edgeStackManager, portainerURL: config.PortainerURL, @@ -86,15 +85,17 @@ func newPollService(edgeStackManager *StackManager, logsManager *logsManager, co tunnelServerFingerprint: config.TunnelServerFingerprint, logsManager: logsManager, containerPlatform: config.ContainerPlatform, - }, nil -} + } -func (service *PollService) closeTunnel() error { - return service.tunnelClient.CloseTunnel() + if config.TunnelCapability { + pollService.tunnelClient = chisel.NewClient() + } + + return pollService, nil } func (service *PollService) resetActivityTimer() { - if service.tunnelClient.IsTunnelOpen() { + if service.tunnelClient != nil && service.tunnelClient.IsTunnelOpen() { service.lastActivity = time.Now() } } @@ -172,7 +173,7 @@ func (service *PollService) startActivityMonitoringLoop() { elapsed := time.Since(service.lastActivity) log.Printf("[DEBUG] [internal,edge,monitoring] [tunnel_last_activity_seconds: %f] [message: tunnel activity monitoring]", elapsed.Seconds()) - if service.tunnelClient.IsTunnelOpen() && elapsed.Seconds() > service.inactivityTimeout.Seconds() { + if service.tunnelClient != nil && service.tunnelClient.IsTunnelOpen() && elapsed.Seconds() > service.inactivityTimeout.Seconds() { log.Printf("[INFO] [internal,edge,monitoring] [tunnel_last_activity_seconds: %f] [message: shutting down tunnel after inactivity period]", elapsed.Seconds()) @@ -265,19 +266,19 @@ func (service *PollService) poll() error { log.Printf("[DEBUG] [internal,edge,poll] [status: %s] [port: %d] [schedule_count: %d] [checkin_interval_seconds: %f]", responseData.Status, responseData.Port, len(responseData.Schedules), responseData.CheckinInterval) - if responseData.Status == "IDLE" && service.tunnelClient.IsTunnelOpen() { - log.Printf("[DEBUG] [internal,edge,poll] [status: %s] [message: Idle status detected, shutting down tunnel]", responseData.Status) + if service.tunnelClient != nil { + if responseData.Status == "IDLE" && service.tunnelClient.IsTunnelOpen() { + log.Printf("[DEBUG] [internal,edge,poll] [status: %s] [message: Idle status detected, shutting down tunnel]", responseData.Status) - err := service.tunnelClient.CloseTunnel() - if err != nil { - log.Printf("[ERROR] [internal,edge,poll] [message: Unable to shutdown tunnel] [error: %s]", err) + err := service.tunnelClient.CloseTunnel() + if err != nil { + log.Printf("[ERROR] [internal,edge,poll] [message: Unable to shutdown tunnel] [error: %s]", err) + } } - } - if service.tunnel == true { - if responseData.Status == "REQUIRED" && !service.tunnelClient.IsTunnelOpen(){ + if responseData.Status == "REQUIRED" && !service.tunnelClient.IsTunnelOpen() { log.Println("[DEBUG] [internal,edge,poll] [message: Required status detected, creating reverse tunnel]") - + err := service.createTunnel(responseData.Credentials, responseData.Port) if err != nil { log.Printf("[ERROR] [internal,edge,poll] [message: Unable to create tunnel] [error: %s]", err) @@ -298,7 +299,7 @@ func (service *PollService) poll() error { } } - service.logsManager.handleReceivedLogsRequests(logsToCollect) + service.logsManager.HandleReceivedLogsRequests(logsToCollect) if responseData.CheckinInterval != service.pollIntervalInSeconds { log.Printf("[DEBUG] [internal,edge,poll] [old_interval: %f] [new_interval: %f] [message: updating poll interval]", service.pollIntervalInSeconds, responseData.CheckinInterval) @@ -313,7 +314,7 @@ func (service *PollService) poll() error { stacks[stack.ID] = stack.Version } - err := service.edgeStackManager.updateStacksStatus(stacks) + err := service.edgeStackManager.UpdateStacksStatus(stacks) if err != nil { log.Printf("[ERROR] [internal,edge,stack] [message: an error occurred during stack management] [error: %s]", err) return err diff --git a/internal/edge/logs.go b/edge/scheduler/logs.go similarity index 87% rename from internal/edge/logs.go rename to edge/scheduler/logs.go index f38d5c94a..31b8f4326 100644 --- a/internal/edge/logs.go +++ b/edge/scheduler/logs.go @@ -1,4 +1,4 @@ -package edge +package scheduler import ( "fmt" @@ -10,7 +10,7 @@ import ( "github.com/portainer/agent/http/client" ) -type logsManager struct { +type LogsManager struct { httpClient *client.PortainerClient stopSignal chan struct{} jobs map[int]logStatus @@ -25,17 +25,17 @@ const ( logFailed ) -func newLogsManager(portainerURL, endpointID, edgeID string, insecurePoll bool, tunnel bool) *logsManager { - cli := client.NewPortainerClient(portainerURL, endpointID, edgeID, insecurePoll, tunnel) +func NewLogsManager(portainerURL, endpointID, edgeID string, insecurePoll bool) *LogsManager { + cli := client.NewPortainerClient(portainerURL, endpointID, edgeID, insecurePoll) - return &logsManager{ + return &LogsManager{ httpClient: cli, stopSignal: nil, jobs: map[int]logStatus{}, } } -func (manager *logsManager) start() error { +func (manager *LogsManager) Start() error { if manager.stopSignal != nil { return nil } @@ -100,7 +100,7 @@ func (manager *logsManager) start() error { return nil } -func (manager *logsManager) stop() { +func (manager *LogsManager) stop() { if manager.stopSignal != nil { log.Printf("[DEBUG] [internal,edge,logs] [message: logs manager stopped]") close(manager.stopSignal) @@ -108,7 +108,7 @@ func (manager *logsManager) stop() { } } -func (manager *logsManager) handleReceivedLogsRequests(jobs []int) { +func (manager *LogsManager) HandleReceivedLogsRequests(jobs []int) { for _, jobID := range jobs { if _, ok := manager.jobs[jobID]; !ok { log.Printf("[DEBUG] [internal,edge,logs] [job_identifier: %d] [message: added job to queue]", jobID) @@ -117,7 +117,7 @@ func (manager *logsManager) handleReceivedLogsRequests(jobs []int) { } } -func (manager *logsManager) next() int { +func (manager *LogsManager) next() int { for jobID, status := range manager.jobs { if status == logPending { return jobID diff --git a/filesystem/scheduler.go b/edge/scheduler/scheduler.go similarity index 88% rename from filesystem/scheduler.go rename to edge/scheduler/scheduler.go index 3613842fa..01a101ba4 100644 --- a/filesystem/scheduler.go +++ b/edge/scheduler/scheduler.go @@ -1,6 +1,7 @@ +//go:build !windows // +build !windows -package filesystem +package scheduler import ( "encoding/base64" @@ -9,6 +10,7 @@ import ( "strings" "github.com/portainer/agent" + "github.com/portainer/agent/filesystem" ) const ( @@ -42,7 +44,7 @@ func (manager *CronManager) Schedule(schedules []agent.Schedule) error { if manager.cronFileExists { log.Println("[DEBUG] [filesystem,cron] [message: no schedules available, removing cron file]") manager.cronFileExists = false - return RemoveFile(fmt.Sprintf("%s%s/%s", agent.HostRoot, cronDirectory, cronFile)) + return filesystem.RemoveFile(fmt.Sprintf("%s%s/%s", agent.HostRoot, cronDirectory, cronFile)) } return nil } @@ -81,7 +83,7 @@ func createCronEntry(schedule *agent.Schedule) (string, error) { return "", err } - err = WriteFile(fmt.Sprintf("%s%s", agent.HostRoot, agent.ScheduleScriptDirectory), fmt.Sprintf("schedule_%d", schedule.ID), []byte(decodedScript), 0744) + err = filesystem.WriteFile(fmt.Sprintf("%s%s", agent.HostRoot, agent.ScheduleScriptDirectory), fmt.Sprintf("schedule_%d", schedule.ID), []byte(decodedScript), 0744) if err != nil { return "", err } @@ -118,7 +120,7 @@ func (manager *CronManager) flushEntries() error { cronEntries = append(cronEntries, "") cronFileContent := strings.Join(cronEntries, "\n") - err := WriteFile(fmt.Sprintf("%s%s", agent.HostRoot, cronDirectory), cronFile, []byte(cronFileContent), 0644) + err := filesystem.WriteFile(fmt.Sprintf("%s%s", agent.HostRoot, cronDirectory), cronFile, []byte(cronFileContent), 0644) if err != nil { return err } diff --git a/filesystem/scheduler_windows.go b/edge/scheduler/scheduler_windows.go similarity index 86% rename from filesystem/scheduler_windows.go rename to edge/scheduler/scheduler_windows.go index 350bba18b..01c889769 100644 --- a/filesystem/scheduler_windows.go +++ b/edge/scheduler/scheduler_windows.go @@ -1,6 +1,7 @@ +//go:build windows // +build windows -package filesystem +package scheduler import "github.com/portainer/agent" diff --git a/internal/edge/stack.go b/edge/stack/stack.go similarity index 87% rename from internal/edge/stack.go rename to edge/stack/stack.go index de5c1eb66..698d33130 100644 --- a/internal/edge/stack.go +++ b/edge/stack/stack.go @@ -1,4 +1,4 @@ -package edge +package stack import ( "context" @@ -55,27 +55,27 @@ const ( type engineType int const ( + // TODO: consider defining this in agent.go or re-use/enhance some of the existing constants + // that are declared in agent.go _ engineType = iota - engineTypeDockerStandalone - engineTypeDockerSwarm - engineTypeKubernetes + EngineTypeDockerStandalone + EngineTypeDockerSwarm + EngineTypeKubernetes ) // StackManager represents a service for managing Edge stacks type StackManager struct { - engineType engineType - stacks map[edgeStackID]*edgeStack - stopSignal chan struct{} - deployer agent.Deployer - portainerURL string - endpointID string - isEnabled bool - httpClient *client.PortainerClient + engineType engineType + stacks map[edgeStackID]*edgeStack + stopSignal chan struct{} + deployer agent.Deployer + isEnabled bool + httpClient *client.PortainerClient } -// newStackManager returns a pointer to a new instance of StackManager -func newStackManager(portainerURL, endpointID, edgeID string, insecurePoll bool, tunnel bool) (*StackManager, error) { - cli := client.NewPortainerClient(portainerURL, endpointID, edgeID, insecurePoll, tunnel) +// NewStackManager returns a pointer to a new instance of StackManager +func NewStackManager(portainerURL, endpointID, edgeID string, insecurePoll bool) (*StackManager, error) { + cli := client.NewPortainerClient(portainerURL, endpointID, edgeID, insecurePoll) stackManager := &StackManager{ stacks: map[edgeStackID]*edgeStack{}, @@ -86,7 +86,7 @@ func newStackManager(portainerURL, endpointID, edgeID string, insecurePoll bool, return stackManager, nil } -func (manager *StackManager) updateStacksStatus(stacks map[int]int) error { +func (manager *StackManager) UpdateStacksStatus(stacks map[int]int) error { if !manager.isEnabled { return nil } @@ -122,7 +122,7 @@ func (manager *StackManager) updateStacksStatus(stacks map[int]int) error { folder := fmt.Sprintf("%s/%d", agent.EdgeStackFilesPath, stackID) fileName := "docker-compose.yml" - if manager.engineType == engineTypeKubernetes { + if manager.engineType == EngineTypeKubernetes { fileName = fmt.Sprintf("%s.yml", stack.Name) } @@ -155,7 +155,7 @@ func (manager *StackManager) updateStacksStatus(stacks map[int]int) error { return nil } -func (manager *StackManager) stop() error { +func (manager *StackManager) Stop() error { if manager.stopSignal != nil { close(manager.stopSignal) manager.stopSignal = nil @@ -165,7 +165,7 @@ func (manager *StackManager) stop() error { return nil } -func (manager *StackManager) start() error { +func (manager *StackManager) Start() error { if manager.stopSignal != nil { return nil } @@ -218,14 +218,14 @@ func (manager *StackManager) next() *edgeStack { return nil } -func (manager *StackManager) setEngineStatus(engineStatus engineType) error { +func (manager *StackManager) SetEngineStatus(engineStatus engineType) error { if engineStatus == manager.engineType { return nil } manager.engineType = engineStatus - err := manager.stop() + err := manager.Stop() if err != nil { return err } @@ -283,11 +283,11 @@ func (manager *StackManager) deleteStack(ctx context.Context, stack *edgeStack, func buildDeployerService(engineStatus engineType) (agent.Deployer, error) { switch engineStatus { - case engineTypeDockerStandalone: + case EngineTypeDockerStandalone: return exec.NewDockerComposeStackService(agent.DockerBinaryPath) - case engineTypeDockerSwarm: + case EngineTypeDockerSwarm: return exec.NewDockerSwarmStackService(agent.DockerBinaryPath) - case engineTypeKubernetes: + case EngineTypeKubernetes: return exec.NewKubernetesDeployer(agent.DockerBinaryPath), nil } diff --git a/http/client/portainer_client.go b/http/client/portainer_client.go index 726fff788..658ee7827 100644 --- a/http/client/portainer_client.go +++ b/http/client/portainer_client.go @@ -23,7 +23,7 @@ type PortainerClient struct { } // NewPortainerClient returns a pointer to a new PortainerClient instance -func NewPortainerClient(serverAddress, endpointID, edgeID string, insecurePoll bool, tunnel bool) *PortainerClient { +func NewPortainerClient(serverAddress, endpointID, edgeID string, insecurePoll bool) *PortainerClient { httpCli := &http.Client{ Timeout: 10 * time.Second, } diff --git a/http/edge.go b/http/edge.go index 59200742b..312b56d4d 100644 --- a/http/edge.go +++ b/http/edge.go @@ -6,7 +6,7 @@ import ( "net/http" "time" - "github.com/portainer/agent/internal/edge" + "github.com/portainer/agent/edge" "github.com/gorilla/mux" ) diff --git a/http/handler/handler.go b/http/handler/handler.go index 22ff7c5e3..75d5b5192 100644 --- a/http/handler/handler.go +++ b/http/handler/handler.go @@ -1,13 +1,13 @@ package handler import ( - "errors" "net/http" "regexp" "strconv" "strings" "github.com/portainer/agent" + "github.com/portainer/agent/edge" "github.com/portainer/agent/exec" httpagenthandler "github.com/portainer/agent/http/handler/agent" "github.com/portainer/agent/http/handler/browse" @@ -21,9 +21,7 @@ import ( "github.com/portainer/agent/http/handler/websocket" "github.com/portainer/agent/http/proxy" "github.com/portainer/agent/http/security" - "github.com/portainer/agent/internal/edge" kubecli "github.com/portainer/agent/kubernetes" - httperror "github.com/portainer/libhttp/error" ) // Handler is the main handler of the application. @@ -40,8 +38,6 @@ type Handler struct { webSocketHandler *websocket.Handler hostHandler *host.Handler pingHandler *ping.Handler - securedProtocol bool - edgeManager *edge.Manager containerPlatform agent.ContainerPlatform } @@ -79,8 +75,6 @@ func NewHandler(config *Config) *Handler { webSocketHandler: websocket.NewHandler(config.ClusterService, config.RuntimeConfiguration, notaryService, config.KubeClient), hostHandler: host.NewHandler(config.SystemService, agentProxy, notaryService), pingHandler: ping.NewHandler(), - securedProtocol: config.Secured, - edgeManager: config.EdgeManager, containerPlatform: config.ContainerPlatform, } } @@ -91,15 +85,6 @@ func (h *Handler) ServeHTTP(rw http.ResponseWriter, request *http.Request) { return } - if !h.securedProtocol && !h.edgeManager.IsKeySet() { - httperror.WriteError(rw, http.StatusForbidden, "Unable to use the unsecured agent API without Edge key", errors.New("edge key not set")) - return - } - - if h.edgeManager.IsEdgeModeEnabled() { - h.edgeManager.ResetActivityTimer() - } - request.URL.Path = dockerAPIVersionRegexp.ReplaceAllString(request.URL.Path, "") rw.Header().Set(agent.HTTPResponseAgentHeaderName, agent.Version) rw.Header().Set(agent.HTTPResponseAgentApiVersion, agent.APIVersion) diff --git a/http/handler/key/handler.go b/http/handler/key/handler.go index 45b1d525b..e41d5bb47 100644 --- a/http/handler/key/handler.go +++ b/http/handler/key/handler.go @@ -4,8 +4,8 @@ import ( "net/http" "github.com/gorilla/mux" + "github.com/portainer/agent/edge" "github.com/portainer/agent/http/security" - "github.com/portainer/agent/internal/edge" httperror "github.com/portainer/libhttp/error" ) diff --git a/http/handler/key/key_create.go b/http/handler/key/key_create.go index 372a73b98..17a00b8f3 100644 --- a/http/handler/key/key_create.go +++ b/http/handler/key/key_create.go @@ -23,7 +23,7 @@ func (payload *keyCreatePayload) Validate(r *http.Request) error { } func (handler *Handler) keyCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - if !handler.edgeManager.IsEdgeModeEnabled() { + if handler.edgeManager == nil { return &httperror.HandlerError{http.StatusServiceUnavailable, "Edge key management is disabled on non Edge agent", errors.New("Edge key management is disabled")} } diff --git a/http/handler/key/key_inspect.go b/http/handler/key/key_inspect.go index 0b9bd25ff..2987e9485 100644 --- a/http/handler/key/key_inspect.go +++ b/http/handler/key/key_inspect.go @@ -13,7 +13,7 @@ type keyInspectResponse struct { } func (handler *Handler) keyInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - if !handler.edgeManager.IsEdgeModeEnabled() { + if handler.edgeManager == nil { return &httperror.HandlerError{http.StatusServiceUnavailable, "Edge key management is disabled on non Edge agent", errors.New("Edge key management is disabled")} } diff --git a/http/server.go b/http/server.go index be29c80e4..2066e6a8e 100644 --- a/http/server.go +++ b/http/server.go @@ -3,15 +3,17 @@ package http import ( "context" "crypto/tls" + "errors" "log" "net/http" "time" "github.com/portainer/agent" + "github.com/portainer/agent/edge" "github.com/portainer/agent/exec" "github.com/portainer/agent/http/handler" - "github.com/portainer/agent/internal/edge" "github.com/portainer/agent/kubernetes" + httperror "github.com/portainer/libhttp/error" ) // APIServer is the web server exposing the API of an agent. @@ -62,8 +64,21 @@ func NewAPIServer(config *APIServerConfig) *APIServer { } } +func (server *APIServer) enhanceAPIForEdgeMode(next http.Handler, isSecure bool) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !isSecure && !server.edgeManager.IsKeySet() { + httperror.WriteError(w, http.StatusForbidden, "Unable to use the unsecured agent API without Edge key", errors.New("edge key not set")) + return + } + + server.edgeManager.ResetActivityTimer() + + next.ServeHTTP(w, r) + }) +} + // Start starts a new web server by listening on the specified listenAddr. -func (server *APIServer) StartUnsecured() error { +func (server *APIServer) StartUnsecured(edgeMode bool) error { config := &handler.Config{ SystemService: server.systemService, ClusterService: server.clusterService, @@ -71,15 +86,19 @@ func (server *APIServer) StartUnsecured() error { RuntimeConfiguration: server.agentTags, AgentOptions: server.agentOptions, EdgeManager: server.edgeManager, - Secured: false, KubeClient: server.kubeClient, KubernetesDeployer: server.kubernetesDeployer, + Secured: false, ContainerPlatform: server.containerPlatform, } - h := handler.NewHandler(config) + var h http.Handler = handler.NewHandler(config) listenAddr := server.addr + ":" + server.port + if edgeMode { + h = server.enhanceAPIForEdgeMode(h, false) + } + log.Printf("[INFO] [http] [server_addr: %s] [server_port: %s] [secured: %t] [api_version: %s] [message: Starting Agent API server]", server.addr, server.port, config.Secured, agent.Version) httpServer := &http.Server{ @@ -93,7 +112,7 @@ func (server *APIServer) StartUnsecured() error { } // Start starts a new web server by listening on the specified listenAddr. -func (server *APIServer) StartSecured() error { +func (server *APIServer) StartSecured(edgeMode bool) error { config := &handler.Config{ SystemService: server.systemService, ClusterService: server.clusterService, @@ -101,15 +120,19 @@ func (server *APIServer) StartSecured() error { RuntimeConfiguration: server.agentTags, AgentOptions: server.agentOptions, EdgeManager: server.edgeManager, - Secured: true, KubeClient: server.kubeClient, KubernetesDeployer: server.kubernetesDeployer, + Secured: true, ContainerPlatform: server.containerPlatform, } - h := handler.NewHandler(config) + var h http.Handler = handler.NewHandler(config) listenAddr := server.addr + ":" + server.port + if edgeMode { + h = server.enhanceAPIForEdgeMode(h, true) + } + log.Printf("[INFO] [http] [server_addr: %s] [server_port: %s] [secured: %t] [api_version: %s] [message: Starting Agent API server]", server.addr, server.port, config.Secured, agent.Version) tlsConfig := &tls.Config{