2022-04-07 12:17:36 +00:00
package endpointedge
2019-07-25 22:38:07 +00:00
import (
2023-01-06 19:25:41 +00:00
"bytes"
2024-08-22 13:54:34 +00:00
"cmp"
2020-06-25 03:25:51 +00:00
"encoding/base64"
2020-08-04 00:44:17 +00:00
"errors"
2022-04-07 12:17:36 +00:00
"fmt"
2023-01-06 19:25:41 +00:00
"hash/fnv"
"io"
2019-07-25 22:38:07 +00:00
"net/http"
2023-01-06 19:25:41 +00:00
"net/http/httptest"
2020-08-04 00:44:17 +00:00
"strconv"
2023-01-06 19:25:41 +00:00
"strings"
2021-03-01 00:43:47 +00:00
"time"
2019-07-25 22:38:07 +00:00
2021-02-23 03:21:39 +00:00
portainer "github.com/portainer/portainer/api"
2023-05-18 17:58:33 +00:00
"github.com/portainer/portainer/api/dataservices"
2024-05-28 19:42:56 +00:00
"github.com/portainer/portainer/api/internal/edge"
2023-01-06 19:25:41 +00:00
"github.com/portainer/portainer/api/internal/edge/cache"
2023-09-01 22:27:02 +00:00
httperror "github.com/portainer/portainer/pkg/libhttp/error"
"github.com/portainer/portainer/pkg/libhttp/request"
"github.com/portainer/portainer/pkg/libhttp/response"
2024-07-09 18:18:13 +00:00
"github.com/rs/zerolog/log"
2019-07-25 22:38:07 +00:00
)
2020-05-14 02:14:28 +00:00
type stackStatusResponse struct {
2021-02-23 03:21:39 +00:00
// EdgeStack Identifier
ID portainer . EdgeStackID ` example:"1" `
// Version of this stack
Version int ` example:"3" `
2020-05-14 02:14:28 +00:00
}
2020-06-25 03:25:51 +00:00
type edgeJobResponse struct {
2021-02-23 03:21:39 +00:00
// EdgeJob Identifier
ID portainer . EdgeJobID ` json:"Id" example:"2" `
// Whether to collect logs
CollectLogs bool ` json:"CollectLogs" example:"true" `
// A cron expression to schedule this job
CronExpression string ` json:"CronExpression" example:"* * * * *" `
// Script to run
Script string ` json:"Script" example:"echo hello" `
// Version of this EdgeJob
Version int ` json:"Version" example:"2" `
2020-06-25 03:25:51 +00:00
}
2022-04-07 12:17:36 +00:00
type endpointEdgeStatusInspectResponse struct {
2021-09-20 00:14:22 +00:00
// Status represents the environment(endpoint) status
2021-02-23 03:21:39 +00:00
Status string ` json:"status" example:"REQUIRED" `
// The tunnel port
Port int ` json:"port" example:"8732" `
2021-09-20 00:14:22 +00:00
// List of requests for jobs to run on the environment(endpoint)
2021-02-23 03:21:39 +00:00
Schedules [ ] edgeJobResponse ` json:"schedules" `
// The current value of CheckinInterval
CheckinInterval int ` json:"checkin" example:"5" `
//
2022-04-07 12:17:36 +00:00
Credentials string ` json:"credentials" `
2021-09-20 00:14:22 +00:00
// List of stacks to be deployed on the environments(endpoints)
2021-02-23 03:21:39 +00:00
Stacks [ ] stackStatusResponse ` json:"stacks" `
2019-07-25 22:38:07 +00:00
}
2022-04-07 12:17:36 +00:00
// @id EndpointEdgeStatusInspect
2021-09-20 00:14:22 +00:00
// @summary Get environment(endpoint) status
2022-04-07 12:17:36 +00:00
// @description environment(endpoint) for edge agent to check status of environment(endpoint)
2021-09-20 00:14:22 +00:00
// @description **Access policy**: restricted only to Edge environments(endpoints)
2021-02-23 03:21:39 +00:00
// @tags endpoints
2021-11-30 02:31:16 +00:00
// @security ApiKeyAuth
2021-02-23 03:21:39 +00:00
// @security jwt
2021-09-20 00:14:22 +00:00
// @param id path int true "Environment(Endpoint) identifier"
2022-04-07 12:17:36 +00:00
// @success 200 {object} endpointEdgeStatusInspectResponse "Success"
2021-02-23 03:21:39 +00:00
// @failure 400 "Invalid request"
2021-09-20 00:14:22 +00:00
// @failure 403 "Permission denied to access environment(endpoint)"
// @failure 404 "Environment(Endpoint) not found"
2021-02-23 03:21:39 +00:00
// @failure 500 "Server error"
2022-04-07 12:17:36 +00:00
// @router /endpoints/{id}/edge/status [get]
func ( handler * Handler ) endpointEdgeStatusInspect ( w http . ResponseWriter , r * http . Request ) * httperror . HandlerError {
2023-01-06 19:25:41 +00:00
endpointID , err := request . RetrieveNumericRouteVariableValue ( r , "id" )
2019-07-25 22:38:07 +00:00
if err != nil {
2023-01-06 19:25:41 +00:00
return httperror . BadRequest ( "Invalid environment identifier route variable" , err )
}
2024-08-22 13:54:34 +00:00
if cachedResp := handler . respondFromCache ( w , r , portainer . EndpointID ( endpointID ) ) ; cachedResp {
2023-01-06 19:25:41 +00:00
return nil
}
if _ , ok := handler . DataStore . Endpoint ( ) . Heartbeat ( portainer . EndpointID ( endpointID ) ) ; ! ok {
2023-07-05 20:26:52 +00:00
// EE-5190
2024-10-08 06:17:09 +00:00
return httperror . Forbidden ( "Permission denied to access environment. The device has not been trusted yet" , fmt . Errorf ( "unable to retrieve endpoint heartbeat. Environment ID: %d" , endpointID ) )
2023-01-06 19:25:41 +00:00
}
endpoint , err := handler . DataStore . Endpoint ( ) . Endpoint ( portainer . EndpointID ( endpointID ) )
if err != nil {
2023-07-05 20:26:52 +00:00
// EE-5190
2024-10-08 06:17:09 +00:00
return httperror . Forbidden ( "Permission denied to access environment. The device has not been trusted yet" , fmt . Errorf ( "unable to retrieve endpoint from database: %w. Environment ID: %d" , err , endpointID ) )
2019-07-25 22:38:07 +00:00
}
2024-07-09 18:18:13 +00:00
firstConn := endpoint . LastCheckInDate == 0
2024-08-22 13:54:34 +00:00
if err := handler . requestBouncer . AuthorizedEdgeEndpointOperation ( r , endpoint ) ; err != nil {
2024-10-08 06:17:09 +00:00
return httperror . Forbidden ( "Permission denied to access environment. The device has not been trusted yet" , fmt . Errorf ( "unauthorized Edge endpoint operation: %w. Environment name: %s" , err , endpoint . Name ) )
2019-07-25 22:38:07 +00:00
}
2023-07-07 21:00:20 +00:00
handler . DataStore . Endpoint ( ) . UpdateHeartbeat ( endpoint . ID )
2024-08-22 13:54:34 +00:00
if err := handler . requestBouncer . TrustedEdgeEnvironmentAccess ( handler . DataStore , endpoint ) ; err != nil {
2024-10-08 06:17:09 +00:00
return httperror . Forbidden ( "Permission denied to access environment. The device has not been trusted yet" , fmt . Errorf ( "untrusted Edge environment access: %w. Environment name: %s" , err , endpoint . Name ) )
2023-07-07 21:00:20 +00:00
}
2023-05-18 17:58:33 +00:00
var statusResponse * endpointEdgeStatusInspectResponse
2024-08-22 13:54:34 +00:00
if err := handler . DataStore . UpdateTx ( func ( tx dataservices . DataStoreTx ) error {
2024-07-09 18:18:13 +00:00
statusResponse , err = handler . inspectStatus ( tx , r , portainer . EndpointID ( endpointID ) , firstConn )
2023-09-05 23:27:20 +00:00
return err
2024-08-22 13:54:34 +00:00
} ) ; err != nil {
2023-05-18 17:58:33 +00:00
var httpErr * httperror . HandlerError
if errors . As ( err , & httpErr ) {
2024-10-08 06:17:09 +00:00
httpErr . Err = fmt . Errorf ( "edge polling error: %w. Environment name: %s" , httpErr . Err , endpoint . Name )
2023-05-18 17:58:33 +00:00
return httpErr
}
2024-10-08 06:17:09 +00:00
return httperror . InternalServerError ( "Unexpected error" , fmt . Errorf ( "edge polling error: %w. Environment name: %s" , err , endpoint . Name ) )
2023-05-18 17:58:33 +00:00
}
return cacheResponse ( w , endpoint . ID , * statusResponse )
}
2024-08-22 13:54:34 +00:00
func ( handler * Handler ) parseHeaders ( r * http . Request , endpoint * portainer . Endpoint ) error {
endpoint . EdgeID = cmp . Or ( endpoint . EdgeID , r . Header . Get ( portainer . PortainerAgentEdgeIDHeader ) )
agentPlatform , agentPlatformErr := parseAgentPlatform ( r )
if agentPlatformErr != nil {
return httperror . BadRequest ( "agent platform header is not valid" , agentPlatformErr )
}
endpoint . Type = agentPlatform
version := r . Header . Get ( portainer . PortainerAgentHeader )
endpoint . Agent . Version = version
return nil
}
2024-07-09 18:18:13 +00:00
func ( handler * Handler ) inspectStatus ( tx dataservices . DataStoreTx , r * http . Request , endpointID portainer . EndpointID , firstConn bool ) ( * endpointEdgeStatusInspectResponse , error ) {
2023-05-18 17:58:33 +00:00
endpoint , err := tx . Endpoint ( ) . Endpoint ( endpointID )
if err != nil {
return nil , err
}
2024-08-22 13:54:34 +00:00
if err := handler . parseHeaders ( r , endpoint ) ; err != nil {
return nil , err
2022-08-12 01:21:56 +00:00
}
2019-07-25 22:38:07 +00:00
2023-07-14 15:34:50 +00:00
// Take an initial snapshot
2024-07-09 18:18:13 +00:00
if firstConn {
if err := handler . ReverseTunnelService . Open ( endpoint ) ; err != nil {
log . Error ( ) . Err ( err ) . Msg ( "could not open the tunnel" )
}
2023-07-14 15:34:50 +00:00
}
2022-04-07 12:17:36 +00:00
endpoint . LastCheckInDate = time . Now ( ) . Unix ( )
2020-08-04 00:44:17 +00:00
2024-08-22 13:54:34 +00:00
if err := tx . Endpoint ( ) . UpdateEndpoint ( endpoint . ID , endpoint ) ; err != nil {
2023-07-05 20:26:52 +00:00
return nil , httperror . InternalServerError ( "Unable to persist environment changes inside the database" , err )
2023-04-27 02:22:05 +00:00
}
2024-05-28 19:42:56 +00:00
tunnel := handler . ReverseTunnelService . Config ( endpoint . ID )
2022-04-07 12:17:36 +00:00
statusResponse := endpointEdgeStatusInspectResponse {
Status : tunnel . Status ,
Port : tunnel . Port ,
2024-05-28 19:42:56 +00:00
CheckinInterval : edge . EffectiveCheckinInterval ( tx , endpoint ) ,
2022-04-07 12:17:36 +00:00
Credentials : tunnel . Credentials ,
}
2022-01-17 22:25:29 +00:00
2024-10-14 13:37:13 +00:00
schedules , handlerErr := handler . buildSchedules ( tx , endpoint . ID )
2022-04-07 12:17:36 +00:00
if handlerErr != nil {
2023-05-18 17:58:33 +00:00
return nil , handlerErr
2022-01-17 22:25:29 +00:00
}
2022-04-07 12:17:36 +00:00
statusResponse . Schedules = schedules
2022-01-17 22:25:29 +00:00
2023-05-18 17:58:33 +00:00
edgeStacksStatus , handlerErr := handler . buildEdgeStacks ( tx , endpoint . ID )
2022-04-07 12:17:36 +00:00
if handlerErr != nil {
2023-05-18 17:58:33 +00:00
return nil , handlerErr
2022-04-07 12:17:36 +00:00
}
statusResponse . Stacks = edgeStacksStatus
2023-05-18 17:58:33 +00:00
return & statusResponse , nil
2022-04-07 12:17:36 +00:00
}
func parseAgentPlatform ( r * http . Request ) ( portainer . EndpointType , error ) {
agentPlatformHeader := r . Header . Get ( portainer . HTTPResponseAgentPlatform )
if agentPlatformHeader == "" {
return 0 , errors . New ( "agent platform header is missing" )
}
agentPlatformNumber , err := strconv . Atoi ( agentPlatformHeader )
2021-03-01 00:43:47 +00:00
if err != nil {
2022-04-07 12:17:36 +00:00
return 0 , err
2019-07-25 22:38:07 +00:00
}
2022-04-07 12:17:36 +00:00
agentPlatform := portainer . AgentPlatform ( agentPlatformNumber )
2019-07-25 22:38:07 +00:00
2022-04-07 12:17:36 +00:00
switch agentPlatform {
case portainer . AgentPlatformDocker :
return portainer . EdgeAgentOnDockerEnvironment , nil
case portainer . AgentPlatformKubernetes :
return portainer . EdgeAgentOnKubernetesEnvironment , nil
default :
return 0 , fmt . Errorf ( "agent platform %v is not valid" , agentPlatform )
}
}
2024-10-14 13:37:13 +00:00
func ( handler * Handler ) buildSchedules ( tx dataservices . DataStoreTx , endpointID portainer . EndpointID ) ( [ ] edgeJobResponse , * httperror . HandlerError ) {
2020-06-25 03:25:51 +00:00
schedules := [ ] edgeJobResponse { }
2024-10-14 13:37:13 +00:00
edgeJobs , err := tx . EdgeJob ( ) . ReadAll ( )
if err != nil {
return nil , httperror . InternalServerError ( "Unable to retrieve Edge Jobs" , err )
}
for _ , job := range edgeJobs {
_ , endpointHasJob := job . Endpoints [ endpointID ]
if ! endpointHasJob {
for _ , edgeGroupID := range job . EdgeGroups {
member , _ , err := edge . EndpointInEdgeGroup ( tx , endpointID , edgeGroupID )
if err != nil {
return nil , httperror . InternalServerError ( "Unable to retrieve relations" , err )
} else if member {
endpointHasJob = true
break
}
}
}
if ! endpointHasJob {
continue
}
2022-12-19 21:54:51 +00:00
var collectLogs bool
if _ , ok := job . GroupLogsCollection [ endpointID ] ; ok {
collectLogs = job . GroupLogsCollection [ endpointID ] . CollectLogs
} else {
collectLogs = job . Endpoints [ endpointID ] . CollectLogs
}
2020-06-25 03:25:51 +00:00
schedule := edgeJobResponse {
ID : job . ID ,
CronExpression : job . CronExpression ,
2022-12-19 21:54:51 +00:00
CollectLogs : collectLogs ,
2020-06-25 03:25:51 +00:00
Version : job . Version ,
}
2022-03-28 15:02:09 +00:00
file , err := handler . FileService . GetFileContent ( job . ScriptPath , "" )
2020-06-25 03:25:51 +00:00
if err != nil {
2022-09-14 23:42:39 +00:00
return nil , httperror . InternalServerError ( "Unable to retrieve Edge job script file" , err )
2020-06-25 03:25:51 +00:00
}
schedule . Script = base64 . RawStdEncoding . EncodeToString ( file )
schedules = append ( schedules , schedule )
}
2023-07-05 20:26:52 +00:00
2022-04-07 12:17:36 +00:00
return schedules , nil
}
2020-06-25 03:25:51 +00:00
2023-05-18 17:58:33 +00:00
func ( handler * Handler ) buildEdgeStacks ( tx dataservices . DataStoreTx , endpointID portainer . EndpointID ) ( [ ] stackStatusResponse , * httperror . HandlerError ) {
relation , err := tx . EndpointRelation ( ) . EndpointRelation ( endpointID )
2020-05-14 02:14:28 +00:00
if err != nil {
2022-09-14 23:42:39 +00:00
return nil , httperror . InternalServerError ( "Unable to retrieve relation object from the database" , err )
2020-05-14 02:14:28 +00:00
}
edgeStacksStatus := [ ] stackStatusResponse { }
for stackID := range relation . EdgeStacks {
2023-05-18 17:58:33 +00:00
version , ok := tx . EdgeStack ( ) . EdgeStackVersion ( stackID )
2023-01-06 19:25:41 +00:00
if ! ok {
2022-09-14 23:42:39 +00:00
return nil , httperror . InternalServerError ( "Unable to retrieve edge stack from the database" , err )
2020-05-14 02:14:28 +00:00
}
stackStatus := stackStatusResponse {
2023-01-06 19:25:41 +00:00
ID : stackID ,
Version : version ,
2020-05-14 02:14:28 +00:00
}
edgeStacksStatus = append ( edgeStacksStatus , stackStatus )
}
2023-01-06 19:25:41 +00:00
2022-04-07 12:17:36 +00:00
return edgeStacksStatus , nil
2019-07-25 22:38:07 +00:00
}
2023-01-06 19:25:41 +00:00
func cacheResponse ( w http . ResponseWriter , endpointID portainer . EndpointID , statusResponse endpointEdgeStatusInspectResponse ) * httperror . HandlerError {
rr := httptest . NewRecorder ( )
2024-08-22 13:54:34 +00:00
if err := response . JSON ( rr , statusResponse ) ; err != nil {
return err
2023-01-06 19:25:41 +00:00
}
h := fnv . New32a ( )
h . Write ( rr . Body . Bytes ( ) )
etag := strconv . FormatUint ( uint64 ( h . Sum32 ( ) ) , 16 )
cache . Set ( endpointID , [ ] byte ( etag ) )
resp := rr . Result ( )
for k , vs := range resp . Header {
for _ , v := range vs {
w . Header ( ) . Add ( k , v )
}
}
w . Header ( ) . Set ( "ETag" , etag )
io . Copy ( w , resp . Body )
return nil
}
func ( handler * Handler ) respondFromCache ( w http . ResponseWriter , r * http . Request , endpointID portainer . EndpointID ) bool {
inmHeader := r . Header . Get ( "If-None-Match" )
etags := strings . Split ( inmHeader , "," )
if len ( inmHeader ) == 0 || etags [ 0 ] == "" {
return false
}
cachedETag , ok := cache . Get ( endpointID )
if ! ok {
return false
}
for _ , etag := range etags {
if ! bytes . Equal ( [ ] byte ( etag ) , cachedETag ) {
continue
}
handler . DataStore . Endpoint ( ) . UpdateHeartbeat ( endpointID )
w . Header ( ) . Set ( "ETag" , etag )
w . WriteHeader ( http . StatusNotModified )
return true
}
return false
}