diff --git a/api/http/handler/endpoints/endpoint_create.go b/api/http/handler/endpoints/endpoint_create.go index 210941fbc..f16e177c1 100644 --- a/api/http/handler/endpoints/endpoint_create.go +++ b/api/http/handler/endpoints/endpoint_create.go @@ -2,17 +2,20 @@ package endpoints import ( "errors" + "fmt" "net" "net/http" "net/url" "runtime" "strconv" "strings" + "time" httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/crypto" "github.com/portainer/portainer/api/http/client" "github.com/portainer/portainer/api/internal/edge" ) @@ -20,7 +23,7 @@ import ( type endpointCreatePayload struct { Name string URL string - EndpointType int + EndpointCreationType endpointCreationEnum PublicURL string GroupID int TLS bool @@ -36,6 +39,17 @@ type endpointCreatePayload struct { EdgeCheckinInterval int } +type endpointCreationEnum int + +const ( + _ endpointCreationEnum = iota + localDockerEnvironment + agentEnvironment + azureEnvironment + edgeAgentEnvironment + localKubernetesEnvironment +) + func (payload *endpointCreatePayload) Validate(r *http.Request) error { name, err := request.RetrieveMultiPartFormValue(r, "Name", false) if err != nil { @@ -43,11 +57,11 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error { } payload.Name = name - endpointType, err := request.RetrieveNumericMultiPartFormValue(r, "EndpointType", false) - if err != nil || endpointType == 0 { - return errors.New("Invalid endpoint type value. Value must be one of: 1 (Docker environment), 2 (Agent environment), 3 (Azure environment) or 4 (Edge Agent environment)") + endpointCreationType, err := request.RetrieveNumericMultiPartFormValue(r, "EndpointCreationType", false) + if err != nil || endpointCreationType == 0 { + return errors.New("Invalid endpoint type value. Value must be one of: 1 (Docker environment), 2 (Agent environment), 3 (Azure environment), 4 (Edge Agent environment) or 5 (Local Kubernetes environment)") } - payload.EndpointType = endpointType + payload.EndpointCreationType = endpointCreationEnum(endpointCreationType) groupID, _ := request.RetrieveNumericMultiPartFormValue(r, "GroupID", true) if groupID == 0 { @@ -97,8 +111,8 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error { } } - switch portainer.EndpointType(payload.EndpointType) { - case portainer.AzureEnvironment: + switch payload.EndpointCreationType { + case azureEnvironment: azureApplicationID, err := request.RetrieveMultiPartFormValue(r, "AzureApplicationID", false) if err != nil { return errors.New("Invalid Azure application ID") @@ -182,22 +196,34 @@ func (handler *Handler) endpointCreate(w http.ResponseWriter, r *http.Request) * } func (handler *Handler) createEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) { - switch portainer.EndpointType(payload.EndpointType) { - case portainer.AzureEnvironment: + switch payload.EndpointCreationType { + case azureEnvironment: return handler.createAzureEndpoint(payload) - case portainer.EdgeAgentOnDockerEnvironment: - return handler.createEdgeAgentEndpoint(payload, portainer.EdgeAgentOnDockerEnvironment) + case edgeAgentEnvironment: + return handler.createEdgeAgentEndpoint(payload) - case portainer.KubernetesLocalEnvironment: + case localKubernetesEnvironment: return handler.createKubernetesEndpoint(payload) + } - case portainer.EdgeAgentOnKubernetesEnvironment: - return handler.createEdgeAgentEndpoint(payload, portainer.EdgeAgentOnKubernetesEnvironment) + endpointType := portainer.DockerEnvironment + if payload.EndpointCreationType == agentEnvironment { + agentPlatform, err := handler.pingAndCheckPlatform(payload) + if err != nil { + return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to get endpoint type", err} + } + + if agentPlatform == portainer.AgentPlatformDocker { + endpointType = portainer.AgentOnDockerEnvironment + } else if agentPlatform == portainer.AgentPlatformKubernetes { + endpointType = portainer.AgentOnKubernetesEnvironment + payload.URL = strings.TrimPrefix(payload.URL, "tcp://") + } } if payload.TLS { - return handler.createTLSSecuredEndpoint(payload, portainer.EndpointType(payload.EndpointType)) + return handler.createTLSSecuredEndpoint(payload, endpointType) } return handler.createUnsecuredEndpoint(payload) } @@ -241,7 +267,7 @@ func (handler *Handler) createAzureEndpoint(payload *endpointCreatePayload) (*po return endpoint, nil } -func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload, endpointType portainer.EndpointType) (*portainer.Endpoint, *httperror.HandlerError) { +func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) { endpointID := handler.DataStore.Endpoint().GetNextIdentifier() portainerURL, err := url.Parse(payload.URL) @@ -264,7 +290,7 @@ func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload, ID: portainer.EndpointID(endpointID), Name: payload.Name, URL: portainerHost, - Type: endpointType, + Type: portainer.EdgeAgentOnDockerEnvironment, GroupID: portainer.EndpointGroupID(payload.GroupID), TLSConfig: portainer.TLSConfiguration{ TLS: false, @@ -472,3 +498,58 @@ func (handler *Handler) storeTLSFiles(endpoint *portainer.Endpoint, payload *end return nil } + +func (handler *Handler) pingAndCheckPlatform(payload *endpointCreatePayload) (portainer.AgentPlatform, error) { + httpCli := &http.Client{ + Timeout: 3 * time.Second, + } + + if payload.TLS { + tlsConfig, err := crypto.CreateTLSConfigurationFromBytes(payload.TLSCACertFile, payload.TLSCertFile, payload.TLSKeyFile, payload.TLSSkipVerify, payload.TLSSkipClientVerify) + if err != nil { + return 0, err + } + + httpCli.Transport = &http.Transport{ + TLSClientConfig: tlsConfig, + } + } + + url, err := url.Parse(fmt.Sprintf("%s/ping", payload.URL)) + if err != nil { + return 0, err + } + + url.Scheme = "https" + + req, err := http.NewRequest(http.MethodGet, url.String(), nil) + if err != nil { + return 0, err + } + + resp, err := httpCli.Do(req) + if err != nil { + return 0, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusNoContent { + return 0, fmt.Errorf("Failed request with status %d", resp.StatusCode) + } + + agentPlatformHeader := resp.Header.Get(portainer.HTTPResponseAgentPlatform) + if agentPlatformHeader == "" { + return 0, errors.New("Agent Platform Header is missing") + } + + agentPlatformNumber, err := strconv.Atoi(agentPlatformHeader) + if err != nil { + return 0, err + } + + if agentPlatformNumber == 0 { + return 0, errors.New("Agent platform is invalid") + } + + return portainer.AgentPlatform(agentPlatformNumber), nil +} diff --git a/api/http/handler/endpoints/endpoint_status_inspect.go b/api/http/handler/endpoints/endpoint_status_inspect.go index 8c1dacd62..787af6788 100644 --- a/api/http/handler/endpoints/endpoint_status_inspect.go +++ b/api/http/handler/endpoints/endpoint_status_inspect.go @@ -2,13 +2,15 @@ package endpoints import ( "encoding/base64" + "errors" "net/http" + "strconv" httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer/api" - "github.com/portainer/portainer/api/bolt/errors" + bolterrors "github.com/portainer/portainer/api/bolt/errors" ) type stackStatusResponse struct { @@ -41,7 +43,7 @@ func (handler *Handler) endpointStatusInspect(w http.ResponseWriter, r *http.Req } endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID)) - if err == errors.ErrObjectNotFound { + if err == bolterrors.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} @@ -54,10 +56,27 @@ func (handler *Handler) endpointStatusInspect(w http.ResponseWriter, r *http.Req if endpoint.EdgeID == "" { edgeIdentifier := r.Header.Get(portainer.PortainerAgentEdgeIDHeader) - endpoint.EdgeID = edgeIdentifier - err := handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint) + agentPlatformHeader := r.Header.Get(portainer.HTTPResponseAgentPlatform) + if agentPlatformHeader == "" { + return &httperror.HandlerError{http.StatusInternalServerError, "Agent Platform Header is missing", errors.New("Agent Platform Header is missing")} + } + + agentPlatformNumber, err := strconv.Atoi(agentPlatformHeader) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to parse agent platform header", err} + } + + agentPlatform := portainer.AgentPlatform(agentPlatformNumber) + + if agentPlatform == portainer.AgentPlatformDocker { + endpoint.Type = portainer.EdgeAgentOnDockerEnvironment + } else if agentPlatform == portainer.AgentPlatformKubernetes { + endpoint.Type = portainer.EdgeAgentOnKubernetesEnvironment + } + + err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to Unable to persist endpoint changes inside the database", err} } diff --git a/api/portainer.go b/api/portainer.go index dcbf25af3..fab4fdd46 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -11,6 +11,9 @@ type ( RoleID RoleID `json:"RoleId"` } + // AgentPlatform represents a platform type for an Agent + AgentPlatform int + // APIOperationAuthorizationRequest represent an request for the authorization to execute an API operation APIOperationAuthorizationRequest struct { Path string @@ -1141,6 +1144,8 @@ const ( PortainerAgentHeader = "Portainer-Agent" // PortainerAgentEdgeIDHeader represent the name of the header containing the Edge ID associated to an agent/agent cluster PortainerAgentEdgeIDHeader = "X-PortainerAgent-EdgeID" + // HTTPResponseAgentPlatform represents the name of the header containing the Agent platform + HTTPResponseAgentPlatform = "Portainer-Agent-Platform" // PortainerAgentTargetHeader represent the name of the header containing the target node name PortainerAgentTargetHeader = "X-PortainerAgent-Target" // PortainerAgentSignatureHeader represent the name of the header containing the digital signature @@ -1174,6 +1179,14 @@ const ( AuthenticationOAuth ) +const ( + _ AgentPlatform = iota + // AgentPlatformDocker represent the Docker platform (Standalone/Swarm) + AgentPlatformDocker + // AgentPlatformKubernetes represent the Kubernetes platform + AgentPlatformKubernetes +) + const ( _ EdgeJobLogsStatus = iota // EdgeJobLogsStatusIdle represents an idle log collection job diff --git a/app/portainer/models/endpoint/models.js b/app/portainer/models/endpoint/models.js index feae2b9ab..cdeb9a716 100644 --- a/app/portainer/models/endpoint/models.js +++ b/app/portainer/models/endpoint/models.js @@ -18,6 +18,17 @@ export const PortainerEndpointTypes = Object.freeze({ EdgeAgentOnKubernetesEnvironment: 7, }); +/** + * JS reference of endpoint_create.go#EndpointCreationType iota + */ +export const PortainerEndpointCreationTypes = Object.freeze({ + LocalDockerEnvironment: 1, + AgentEnvironment: 2, + AzureEnvironment: 3, + EdgeAgentEnvironment: 4, + LocalKubernetesEnvironment: 5, +}); + export const PortainerEndpointConnectionTypes = Object.freeze({ DOCKER_LOCAL: 1, KUBERNETES_LOCAL: 2, diff --git a/app/portainer/services/api/endpointService.js b/app/portainer/services/api/endpointService.js index 3bbdf567f..19b05260a 100644 --- a/app/portainer/services/api/endpointService.js +++ b/app/portainer/services/api/endpointService.js @@ -1,4 +1,4 @@ -import { PortainerEndpointTypes } from 'Portainer/models/endpoint/models'; +import { PortainerEndpointCreationTypes } from 'Portainer/models/endpoint/models'; angular.module('portainer.app').factory('EndpointService', [ '$q', @@ -59,7 +59,7 @@ angular.module('portainer.app').factory('EndpointService', [ service.createLocalEndpoint = function () { var deferred = $q.defer(); - FileUploadService.createEndpoint('local', PortainerEndpointTypes.DockerEnvironment, '', '', 1, [], false) + FileUploadService.createEndpoint('local', PortainerEndpointCreationTypes.LocalDockerEnvironment, '', '', 1, [], false) .then(function success(response) { deferred.resolve(response.data); }) @@ -72,7 +72,7 @@ angular.module('portainer.app').factory('EndpointService', [ service.createRemoteEndpoint = function ( name, - type, + creationType, URL, PublicURL, groupID, @@ -88,17 +88,13 @@ angular.module('portainer.app').factory('EndpointService', [ var deferred = $q.defer(); var endpointURL = URL; - if ( - type !== PortainerEndpointTypes.EdgeAgentOnDockerEnvironment && - type !== PortainerEndpointTypes.AgentOnKubernetesEnvironment && - type !== PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment - ) { + if (creationType !== PortainerEndpointCreationTypes.EdgeAgentEnvironment) { endpointURL = 'tcp://' + URL; } FileUploadService.createEndpoint( name, - type, + creationType, endpointURL, PublicURL, groupID, @@ -124,7 +120,7 @@ angular.module('portainer.app').factory('EndpointService', [ service.createLocalKubernetesEndpoint = function () { var deferred = $q.defer(); - FileUploadService.createEndpoint('local', 5, '', '', 1, [], true, true, true) + FileUploadService.createEndpoint('local', PortainerEndpointCreationTypes.LocalKubernetesEnvironment, '', '', 1, [], true, true, true) .then(function success(response) { deferred.resolve(response.data); }) diff --git a/app/portainer/services/fileUpload.js b/app/portainer/services/fileUpload.js index 69ebc7720..fe581645a 100644 --- a/app/portainer/services/fileUpload.js +++ b/app/portainer/services/fileUpload.js @@ -1,3 +1,4 @@ +import { PortainerEndpointCreationTypes } from 'Portainer/models/endpoint/models'; import { genericHandler, jsonObjectsToArrayHandler } from '../../docker/rest/response/handlers'; angular.module('portainer.app').factory('FileUploadService', [ @@ -112,12 +113,26 @@ angular.module('portainer.app').factory('FileUploadService', [ }); }; - service.createEndpoint = function (name, type, URL, PublicURL, groupID, tagIds, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile, checkinInterval) { + service.createEndpoint = function ( + name, + creationType, + URL, + PublicURL, + groupID, + tagIds, + TLS, + TLSSkipVerify, + TLSSkipClientVerify, + TLSCAFile, + TLSCertFile, + TLSKeyFile, + checkinInterval + ) { return Upload.upload({ url: 'api/endpoints', data: { Name: name, - EndpointType: type, + EndpointCreationType: creationType, URL: URL, PublicURL: PublicURL, GroupID: groupID, @@ -139,7 +154,7 @@ angular.module('portainer.app').factory('FileUploadService', [ url: 'api/endpoints', data: { Name: name, - EndpointType: 3, + EndpointCreationType: PortainerEndpointCreationTypes.Azure, GroupID: groupId, TagIds: Upload.json(tagIds), AzureApplicationID: applicationId, diff --git a/app/portainer/views/endpoints/create/createEndpointController.js b/app/portainer/views/endpoints/create/createEndpointController.js index 21b39af9d..c108f1725 100644 --- a/app/portainer/views/endpoints/create/createEndpointController.js +++ b/app/portainer/views/endpoints/create/createEndpointController.js @@ -1,4 +1,4 @@ -import { PortainerEndpointTypes } from 'Portainer/models/endpoint/models'; +import { PortainerEndpointCreationTypes, PortainerEndpointTypes } from 'Portainer/models/endpoint/models'; import { EndpointSecurityFormData } from '../../../components/endpointSecurity/porEndpointSecurityModel'; angular @@ -56,7 +56,7 @@ angular }; $scope.copyAgentCommand = function () { - if ($scope.state.deploymentTab === 0) { + if ($scope.state.deploymentTab === 1) { clipboard.copyText('curl -L https://downloads.portainer.io/agent-stack.yml -o agent-stack.yml && docker stack deploy --compose-file=agent-stack.yml portainer-agent'); } else { clipboard.copyText('curl -L https://downloads.portainer.io/portainer-agent-k8s.yaml -o portainer-agent-k8s.yaml; kubectl apply -f portainer-agent-k8s.yaml'); @@ -102,19 +102,31 @@ angular var TLSCertFile = TLSSkipClientVerify ? null : securityData.TLSCert; var TLSKeyFile = TLSSkipClientVerify ? null : securityData.TLSKey; - addEndpoint(name, PortainerEndpointTypes.DockerEnvironment, URL, publicURL, groupId, tagIds, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile); + addEndpoint( + name, + PortainerEndpointCreationTypes.LocalDockerEnvironment, + URL, + publicURL, + groupId, + tagIds, + TLS, + TLSSkipVerify, + TLSSkipClientVerify, + TLSCAFile, + TLSCertFile, + TLSKeyFile + ); }; $scope.addAgentEndpoint = function () { var name = $scope.formValues.Name; - var URL = $filter('stripprotocol')($scope.formValues.URL); + // var URL = $filter('stripprotocol')($scope.formValues.URL); + var URL = $scope.formValues.URL; var publicURL = $scope.formValues.PublicURL === '' ? URL.split(':')[0] : $scope.formValues.PublicURL; var groupId = $scope.formValues.GroupId; var tagIds = $scope.formValues.TagIds; - addEndpoint(name, PortainerEndpointTypes.AgentOnDockerEnvironment, URL, publicURL, groupId, tagIds, true, true, true, null, null, null); - // TODO: k8s merge - temporarily updated to AgentOnKubernetesEnvironment, breaking Docker agent support - // addEndpoint(name, PortainerEndpointTypes.AgentOnKubernetesEnvironment, URL, publicURL, groupId, tags, true, true, true, null, null, null); + addEndpoint(name, PortainerEndpointCreationTypes.AgentEnvironment, URL, publicURL, groupId, tagIds, true, true, true, null, null, null); }; $scope.addEdgeAgentEndpoint = function () { @@ -123,9 +135,7 @@ angular var tagIds = $scope.formValues.TagIds; var URL = $scope.formValues.URL; - addEndpoint(name, PortainerEndpointTypes.EdgeAgentOnDockerEnvironment, URL, '', groupId, tagIds, false, false, false, null, null, null, $scope.formValues.CheckinInterval); - // TODO: k8s merge - temporarily updated to EdgeAgentOnKubernetesEnvironment, breaking Docker Edge agent support - // addEndpoint(name, PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment, URL, "", groupId, tags, false, false, false, null, null, null); + addEndpoint(name, PortainerEndpointCreationTypes.EdgeAgentEnvironment, URL, '', groupId, tagIds, false, false, false, null, null, null, $scope.formValues.CheckinInterval); }; $scope.addAzureEndpoint = function () { @@ -154,11 +164,11 @@ angular }); } - function addEndpoint(name, type, URL, PublicURL, groupId, tagIds, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile, CheckinInterval) { + function addEndpoint(name, creationType, URL, PublicURL, groupId, tagIds, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile, CheckinInterval) { $scope.state.actionInProgress = true; EndpointService.createRemoteEndpoint( name, - type, + creationType, URL, PublicURL, groupId, @@ -171,14 +181,19 @@ angular TLSKeyFile, CheckinInterval ) - .then(function success(data) { + .then(function success(endpoint) { Notifications.success('Endpoint created', name); - if (type === PortainerEndpointTypes.EdgeAgentOnDockerEnvironment || type === PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment) { - $state.go('portainer.endpoints.endpoint', { id: data.Id }); - } else if (type === PortainerEndpointTypes.AgentOnKubernetesEnvironment) { - $state.go('portainer.endpoints.endpoint.kubernetesConfig', { id: data.Id }); - } else { - $state.go('portainer.endpoints', {}, { reload: true }); + switch (endpoint.Type) { + case PortainerEndpointTypes.EdgeAgentOnDockerEnvironment: + case PortainerEndpointTypes.EdgeAgentOnKubernetesEnvironment: + $state.go('portainer.endpoints.endpoint', { id: endpoint.Id }); + break; + case PortainerEndpointTypes.AgentOnKubernetesEnvironment: + $state.go('portainer.endpoints.endpoint.kubernetesConfig', { id: endpoint.Id }); + break; + default: + $state.go('portainer.endpoints', {}, { reload: true }); + break; } }) .catch(function error(err) { diff --git a/app/portainer/views/endpoints/create/createendpoint.html b/app/portainer/views/endpoints/create/createendpoint.html index 06a1c62dd..ee9e6e25a 100644 --- a/app/portainer/views/endpoints/create/createendpoint.html +++ b/app/portainer/views/endpoints/create/createendpoint.html @@ -73,19 +73,19 @@
- curl -L https://downloads.portainer.io/portainer-agent-k8s.yaml -o portainer-agent-k8s.yaml; kubectl apply -f portainer-agent-k8s.yaml
-
+ curl -L https://downloads.portainer.io/portainer-agent-k8s.yaml -o portainer-agent-k8s.yaml; kubectl apply -f portainer-agent-k8s.yaml
- curl -L https://downloads.portainer.io/agent-stack.yml -o agent-stack.yml && docker stack deploy --compose-file=agent-stack.yml portainer-agent
-
+ curl -L https://downloads.portainer.io/agent-stack.yml -o agent-stack.yml && docker stack deploy --compose-file=agent-stack.yml portainer-agent
- Deploy the Edge agent on your remote Docker/Kubernetes environment using the following command(s) + Refer to the platform related command below to deploy the Edge agent in your remote cluster.
The agent will communicate with Portainer via {{ edgeKeyDetails.instanceURL }} and tcp://{{ edgeKeyDetails.tunnelServerAddr }}
{{ dockerCommands.standalone }}
+ curl https://downloads.portainer.io/portainer-edge-agent-setup.sh | sudo bash -s -- {{ randomEdgeID }} {{ endpoint.EdgeKey }}
{{ dockerCommands.swarm }}
- curl https://downloads.portainer.io/portainer-edge-agent-setup.sh | sudo bash -s -- {{ randomEdgeID }} {{ endpoint.EdgeKey }}
-
+ {{ dockerCommands.standalone }}