Compare commits

...

15 Commits

Author SHA1 Message Date
Anthony Lapenna ecf52616e2 feat(edge): update deployment instructions 2019-06-17 11:37:09 +12:00
Anthony Lapenna 69bc599b5b feat(edge): refactor key creation 2019-06-17 09:18:17 +12:00
Anthony Lapenna e58b019ffa feat(edge): wip edge 2019-06-12 12:06:59 +12:00
Anthony Lapenna 1fc4e7bddb Merge branch 'develop' into edge 2019-06-05 09:24:22 +12:00
Anthony Lapenna 2cabfd574c Merge branch 'develop' into edge
# Conflicts:
#	api/portainer.go
2019-05-27 11:29:35 +12:00
Anthony Lapenna 3b946d84ac feat(endpoint-creation): update Edge agent deployment instructions 2019-05-24 18:01:50 +12:00
Anthony Lapenna 28abe55179 refactor(api): rename AgentIoTEnvironment to AgentEdgeEnvironment 2019-05-24 12:02:46 +12:00
Anthony Lapenna e31365c6a5 refactor(api): rename AgentIoTEnvironment to AgentEdgeEnvironment 2019-05-24 11:46:18 +12:00
Anthony Lapenna bedb4fc7f4 style(endpoint-creation): refactor IoT agent to Edge agent 2019-05-24 11:26:59 +12:00
Anthony Lapenna 8f05ba77b4 Merge branch 'develop' into edge 2019-05-24 11:11:57 +12:00
Anthony Lapenna d03e22e26e feat(intel): add -v /:/host and --name portainer_agent_iot to agent command 2019-04-24 20:15:05 +12:00
Anthony Lapenna ec667a19a0 feat(intel): add -e CAP_HOST_MANAGEMENT=1 to agent command 2019-04-24 20:05:28 +12:00
Anthony Lapenna 8afe1ac37b feat(intel): display agent features when connected to IoT endpoint 2019-04-24 19:35:26 +12:00
Anthony Lapenna 9dc3188cc0 feat(intel): fix webconsole and agent deployment command 2019-04-24 19:31:22 +12:00
Anthony Lapenna 9cf014adab feat(intel): POC Intel 2019-04-24 14:59:15 +12:00
20 changed files with 393 additions and 129 deletions
+43
View File
@@ -0,0 +1,43 @@
package chisel
import (
chserver "github.com/jpillora/chisel/server"
)
type Server struct {
address string
port string
fingerprint string
}
func NewServer(address string, port string) *Server {
return &Server{
address: address,
port: port,
}
}
// Start starts the reverse tunnel server
func (server *Server) Start() error {
// TODO: keyseed management (persistence)
// + auth management
// Consider multiple users for auth?
config := &chserver.Config{
Reverse: true,
KeySeed: "keyseedexample",
Auth: "agent@randomstring",
}
chiselServer, err := chserver.NewServer(config)
if err != nil {
return err
}
server.fingerprint = chiselServer.GetFingerprint()
return chiselServer.Start(server.address, server.port)
}
func (server *Server) GetFingerprint() string {
return server.fingerprint
}
+2
View File
@@ -33,6 +33,8 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
flags := &portainer.CLIFlags{ flags := &portainer.CLIFlags{
Addr: kingpin.Flag("bind", "Address and port to serve Portainer").Default(defaultBindAddress).Short('p').String(), Addr: kingpin.Flag("bind", "Address and port to serve Portainer").Default(defaultBindAddress).Short('p').String(),
TunnelAddr: kingpin.Flag("tunnel-addr", "Address to serve the tunnel server").Default(defaultTunnelServerAddress).String(),
TunnelPort: kingpin.Flag("tunnel-port", "Port to serve the tunnel server").Default(defaultTunnelServerPort).String(),
Assets: kingpin.Flag("assets", "Path to the assets").Default(defaultAssetsDirectory).Short('a').String(), Assets: kingpin.Flag("assets", "Path to the assets").Default(defaultAssetsDirectory).Short('a').String(),
Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(), Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(),
EndpointURL: kingpin.Flag("host", "Endpoint URL").Short('H').String(), EndpointURL: kingpin.Flag("host", "Endpoint URL").Short('H').String(),
+19 -17
View File
@@ -3,21 +3,23 @@
package cli package cli
const ( const (
defaultBindAddress = ":9000" defaultBindAddress = ":9000"
defaultDataDirectory = "/data" defaultTunnelServerAddress = "0.0.0.0"
defaultAssetsDirectory = "./" defaultTunnelServerPort = "8000"
defaultNoAuth = "false" defaultDataDirectory = "/data"
defaultNoAnalytics = "false" defaultAssetsDirectory = "./"
defaultTLS = "false" defaultNoAuth = "false"
defaultTLSSkipVerify = "false" defaultNoAnalytics = "false"
defaultTLSCACertPath = "/certs/ca.pem" defaultTLS = "false"
defaultTLSCertPath = "/certs/cert.pem" defaultTLSSkipVerify = "false"
defaultTLSKeyPath = "/certs/key.pem" defaultTLSCACertPath = "/certs/ca.pem"
defaultSSL = "false" defaultTLSCertPath = "/certs/cert.pem"
defaultSSLCertPath = "/certs/portainer.crt" defaultTLSKeyPath = "/certs/key.pem"
defaultSSLKeyPath = "/certs/portainer.key" defaultSSL = "false"
defaultSyncInterval = "60s" defaultSSLCertPath = "/certs/portainer.crt"
defaultSnapshot = "true" defaultSSLKeyPath = "/certs/portainer.key"
defaultSnapshotInterval = "5m" defaultSyncInterval = "60s"
defaultTemplateFile = "/templates.json" defaultSnapshot = "true"
defaultSnapshotInterval = "5m"
defaultTemplateFile = "/templates.json"
) )
+19 -17
View File
@@ -1,21 +1,23 @@
package cli package cli
const ( const (
defaultBindAddress = ":9000" defaultBindAddress = ":9000"
defaultDataDirectory = "C:\\data" defaultTunnelServerAddress = "0.0.0.0"
defaultAssetsDirectory = "./" defaultTunnelServerPort = "8000"
defaultNoAuth = "false" defaultDataDirectory = "C:\\data"
defaultNoAnalytics = "false" defaultAssetsDirectory = "./"
defaultTLS = "false" defaultNoAuth = "false"
defaultTLSSkipVerify = "false" defaultNoAnalytics = "false"
defaultTLSCACertPath = "C:\\certs\\ca.pem" defaultTLS = "false"
defaultTLSCertPath = "C:\\certs\\cert.pem" defaultTLSSkipVerify = "false"
defaultTLSKeyPath = "C:\\certs\\key.pem" defaultTLSCACertPath = "C:\\certs\\ca.pem"
defaultSSL = "false" defaultTLSCertPath = "C:\\certs\\cert.pem"
defaultSSLCertPath = "C:\\certs\\portainer.crt" defaultTLSKeyPath = "C:\\certs\\key.pem"
defaultSSLKeyPath = "C:\\certs\\portainer.key" defaultSSL = "false"
defaultSyncInterval = "60s" defaultSSLCertPath = "C:\\certs\\portainer.crt"
defaultSnapshot = "true" defaultSSLKeyPath = "C:\\certs\\portainer.key"
defaultSnapshotInterval = "5m" defaultSyncInterval = "60s"
defaultTemplateFile = "/templates.json" defaultSnapshot = "true"
defaultSnapshotInterval = "5m"
defaultTemplateFile = "/templates.json"
) )
+46 -39
View File
@@ -2,12 +2,14 @@ package main
import ( import (
"encoding/json" "encoding/json"
"log"
"os" "os"
"strings" "strings"
"time" "time"
"github.com/portainer/portainer/api" "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/bolt" "github.com/portainer/portainer/api/bolt"
"github.com/portainer/portainer/api/chisel"
"github.com/portainer/portainer/api/cli" "github.com/portainer/portainer/api/cli"
"github.com/portainer/portainer/api/cron" "github.com/portainer/portainer/api/cron"
"github.com/portainer/portainer/api/crypto" "github.com/portainer/portainer/api/crypto"
@@ -20,8 +22,6 @@ import (
"github.com/portainer/portainer/api/jwt" "github.com/portainer/portainer/api/jwt"
"github.com/portainer/portainer/api/ldap" "github.com/portainer/portainer/api/ldap"
"github.com/portainer/portainer/api/libcompose" "github.com/portainer/portainer/api/libcompose"
"log"
) )
func initCLI() *portainer.CLIFlags { func initCLI() *portainer.CLIFlags {
@@ -658,44 +658,51 @@ func main() {
go terminateIfNoAdminCreated(store.UserService) go terminateIfNoAdminCreated(store.UserService)
} }
var tunnelServer portainer.TunnelServer = chisel.NewServer(*flags.TunnelAddr, *flags.TunnelPort)
err = tunnelServer.Start()
if err != nil {
log.Fatal(err)
}
var server portainer.Server = &http.Server{ var server portainer.Server = &http.Server{
Status: applicationStatus, TunnelServerFingerprint: tunnelServer.GetFingerprint(),
BindAddress: *flags.Addr, Status: applicationStatus,
AssetsPath: *flags.Assets, BindAddress: *flags.Addr,
AuthDisabled: *flags.NoAuth, AssetsPath: *flags.Assets,
EndpointManagement: endpointManagement, AuthDisabled: *flags.NoAuth,
RoleService: store.RoleService, EndpointManagement: endpointManagement,
UserService: store.UserService, RoleService: store.RoleService,
TeamService: store.TeamService, UserService: store.UserService,
TeamMembershipService: store.TeamMembershipService, TeamService: store.TeamService,
EndpointService: store.EndpointService, TeamMembershipService: store.TeamMembershipService,
EndpointGroupService: store.EndpointGroupService, EndpointService: store.EndpointService,
ExtensionService: store.ExtensionService, EndpointGroupService: store.EndpointGroupService,
ResourceControlService: store.ResourceControlService, ExtensionService: store.ExtensionService,
SettingsService: store.SettingsService, ResourceControlService: store.ResourceControlService,
RegistryService: store.RegistryService, SettingsService: store.SettingsService,
DockerHubService: store.DockerHubService, RegistryService: store.RegistryService,
StackService: store.StackService, DockerHubService: store.DockerHubService,
ScheduleService: store.ScheduleService, StackService: store.StackService,
TagService: store.TagService, ScheduleService: store.ScheduleService,
TemplateService: store.TemplateService, TagService: store.TagService,
WebhookService: store.WebhookService, TemplateService: store.TemplateService,
SwarmStackManager: swarmStackManager, WebhookService: store.WebhookService,
ComposeStackManager: composeStackManager, SwarmStackManager: swarmStackManager,
ExtensionManager: extensionManager, ComposeStackManager: composeStackManager,
CryptoService: cryptoService, ExtensionManager: extensionManager,
JWTService: jwtService, CryptoService: cryptoService,
FileService: fileService, JWTService: jwtService,
LDAPService: ldapService, FileService: fileService,
GitService: gitService, LDAPService: ldapService,
SignatureService: digitalSignatureService, GitService: gitService,
JobScheduler: jobScheduler, SignatureService: digitalSignatureService,
Snapshotter: snapshotter, JobScheduler: jobScheduler,
SSL: *flags.SSL, Snapshotter: snapshotter,
SSLCert: *flags.SSLCert, SSL: *flags.SSL,
SSLKey: *flags.SSLKey, SSLCert: *flags.SSLCert,
DockerClientFactory: clientFactory, SSLKey: *flags.SSLKey,
JobService: jobService, DockerClientFactory: clientFactory,
JobService: jobService,
} }
log.Printf("Starting Portainer %s on %s", portainer.APIVersion, *flags.Addr) log.Printf("Starting Portainer %s on %s", portainer.APIVersion, *flags.Addr)
@@ -24,7 +24,7 @@ func (handler *Handler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
} }
if endpoint.Status == portainer.EndpointStatusDown { if endpoint.Type != 4 && endpoint.Status == portainer.EndpointStatusDown {
return &httperror.HandlerError{http.StatusServiceUnavailable, "Unable to query endpoint", errors.New("Endpoint is down")} return &httperror.HandlerError{http.StatusServiceUnavailable, "Unable to query endpoint", errors.New("Endpoint is down")}
} }
+63 -1
View File
@@ -1,10 +1,13 @@
package endpoints package endpoints
import ( import (
"encoding/base64"
"log" "log"
"math/rand"
"net/http" "net/http"
"runtime" "runtime"
"strconv" "strconv"
"strings"
httperror "github.com/portainer/libhttp/error" httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request" "github.com/portainer/libhttp/request"
@@ -41,7 +44,7 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error {
endpointType, err := request.RetrieveNumericMultiPartFormValue(r, "EndpointType", false) endpointType, err := request.RetrieveNumericMultiPartFormValue(r, "EndpointType", false)
if err != nil || endpointType == 0 { if err != nil || endpointType == 0 {
return portainer.Error("Invalid endpoint type value. Value must be one of: 1 (Docker environment), 2 (Agent environment) or 3 (Azure environment)") return portainer.Error("Invalid endpoint type value. Value must be one of: 1 (Docker environment), 2 (Agent environment), 3 (Azure environment) or 4 (Edge Agent environment)")
} }
payload.EndpointType = endpointType payload.EndpointType = endpointType
@@ -149,6 +152,8 @@ func (handler *Handler) endpointCreate(w http.ResponseWriter, r *http.Request) *
func (handler *Handler) createEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) { func (handler *Handler) createEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) {
if portainer.EndpointType(payload.EndpointType) == portainer.AzureEnvironment { if portainer.EndpointType(payload.EndpointType) == portainer.AzureEnvironment {
return handler.createAzureEndpoint(payload) return handler.createAzureEndpoint(payload)
} else if portainer.EndpointType(payload.EndpointType) == portainer.EdgeAgentEnvironment {
return handler.createEdgeAgentEndpoint(payload)
} }
if payload.TLS { if payload.TLS {
@@ -195,6 +200,63 @@ func (handler *Handler) createAzureEndpoint(payload *endpointCreatePayload) (*po
return endpoint, nil return endpoint, nil
} }
// TODO: relocate in a service
// must be unique (e.g. not used / referenced)
func randomInt(min, max int) int {
// should be randomize at service creation time?
// if not seeded, will always get same port order
// might not be a problem and maybe not required
//rand.Seed(time.Now().UnixNano())
return min + rand.Intn(max-min)
}
func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) {
endpointType := portainer.EdgeAgentEnvironment
endpointID := handler.EndpointService.GetNextIdentifier()
// get random port
// Dynamic ports (also called private ports) are 49152 to 65535.
// TODO: register this port somewhere
portnumber := randomInt(49152, 65535)
keyInformation := []string{
strings.TrimPrefix(payload.URL, "tcp://"),
"8000",
handler.TunnelServerFingerprint,
strconv.Itoa(portnumber),
"agent:randomstring",
}
key := strings.Join(keyInformation, "|")
encodedKey := base64.RawStdEncoding.EncodeToString([]byte(key))
endpoint := &portainer.Endpoint{
ID: portainer.EndpointID(endpointID),
Name: payload.Name,
URL: "tcp://localhost:" + strconv.Itoa(portnumber),
Type: endpointType,
GroupID: portainer.EndpointGroupID(payload.GroupID),
TLSConfig: portainer.TLSConfiguration{
TLS: false,
},
AuthorizedUsers: []portainer.UserID{},
AuthorizedTeams: []portainer.TeamID{},
Extensions: []portainer.EndpointExtension{},
Tags: payload.Tags,
Status: portainer.EndpointStatusUp,
Snapshots: []portainer.Snapshot{},
EdgeKey: string(encodedKey),
}
err := handler.EndpointService.CreateEndpoint(endpoint)
if err != nil {
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint inside the database", err}
}
return endpoint, nil
}
func (handler *Handler) createUnsecuredEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) { func (handler *Handler) createUnsecuredEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) {
endpointType := portainer.DockerEnvironment endpointType := portainer.DockerEnvironment
+2
View File
@@ -32,6 +32,8 @@ type Handler struct {
ProxyManager *proxy.Manager ProxyManager *proxy.Manager
Snapshotter portainer.Snapshotter Snapshotter portainer.Snapshotter
JobService portainer.JobService JobService portainer.JobService
// TODO: figure out a way to manage this (service?)
TunnelServerFingerprint string
} }
// NewHandler creates a handler to manage endpoint operations. // NewHandler creates a handler to manage endpoint operations.
+1 -1
View File
@@ -68,7 +68,7 @@ func (handler *Handler) websocketExec(w http.ResponseWriter, r *http.Request) *h
func (handler *Handler) handleExecRequest(w http.ResponseWriter, r *http.Request, params *webSocketRequestParams) error { func (handler *Handler) handleExecRequest(w http.ResponseWriter, r *http.Request, params *webSocketRequestParams) error {
r.Header.Del("Origin") r.Header.Del("Origin")
if params.nodeName != "" || params.endpoint.Type == portainer.AgentOnDockerEnvironment { if params.nodeName != "" || (params.endpoint.Type == portainer.AgentOnDockerEnvironment || params.endpoint.Type == portainer.EdgeAgentEnvironment) {
return handler.proxyWebsocketRequest(w, r, params) return handler.proxyWebsocketRequest(w, r, params)
} }
+2 -2
View File
@@ -3,10 +3,10 @@
package proxy package proxy
import ( import (
"github.com/Microsoft/go-winio"
"net" "net"
"net/http" "net/http"
"github.com/Microsoft/go-winio"
portainer "github.com/portainer/portainer/api" portainer "github.com/portainer/portainer/api"
) )
+40 -38
View File
@@ -39,44 +39,45 @@ import (
// Server implements the portainer.Server interface // Server implements the portainer.Server interface
type Server struct { type Server struct {
BindAddress string BindAddress string
AssetsPath string AssetsPath string
AuthDisabled bool TunnelServerFingerprint string
EndpointManagement bool AuthDisabled bool
Status *portainer.Status EndpointManagement bool
ExtensionManager portainer.ExtensionManager Status *portainer.Status
ComposeStackManager portainer.ComposeStackManager ExtensionManager portainer.ExtensionManager
CryptoService portainer.CryptoService ComposeStackManager portainer.ComposeStackManager
SignatureService portainer.DigitalSignatureService CryptoService portainer.CryptoService
JobScheduler portainer.JobScheduler SignatureService portainer.DigitalSignatureService
Snapshotter portainer.Snapshotter JobScheduler portainer.JobScheduler
RoleService portainer.RoleService Snapshotter portainer.Snapshotter
DockerHubService portainer.DockerHubService RoleService portainer.RoleService
EndpointService portainer.EndpointService DockerHubService portainer.DockerHubService
EndpointGroupService portainer.EndpointGroupService EndpointService portainer.EndpointService
FileService portainer.FileService EndpointGroupService portainer.EndpointGroupService
GitService portainer.GitService FileService portainer.FileService
JWTService portainer.JWTService GitService portainer.GitService
LDAPService portainer.LDAPService JWTService portainer.JWTService
ExtensionService portainer.ExtensionService LDAPService portainer.LDAPService
RegistryService portainer.RegistryService ExtensionService portainer.ExtensionService
ResourceControlService portainer.ResourceControlService RegistryService portainer.RegistryService
ScheduleService portainer.ScheduleService ResourceControlService portainer.ResourceControlService
SettingsService portainer.SettingsService ScheduleService portainer.ScheduleService
StackService portainer.StackService SettingsService portainer.SettingsService
SwarmStackManager portainer.SwarmStackManager StackService portainer.StackService
TagService portainer.TagService SwarmStackManager portainer.SwarmStackManager
TeamService portainer.TeamService TagService portainer.TagService
TeamMembershipService portainer.TeamMembershipService TeamService portainer.TeamService
TemplateService portainer.TemplateService TeamMembershipService portainer.TeamMembershipService
UserService portainer.UserService TemplateService portainer.TemplateService
WebhookService portainer.WebhookService UserService portainer.UserService
Handler *handler.Handler WebhookService portainer.WebhookService
SSL bool Handler *handler.Handler
SSLCert string SSL bool
SSLKey string SSLCert string
DockerClientFactory *docker.ClientFactory SSLKey string
JobService portainer.JobService DockerClientFactory *docker.ClientFactory
JobService portainer.JobService
} }
// Start starts the HTTP server // Start starts the HTTP server
@@ -132,6 +133,7 @@ func (server *Server) Start() error {
endpointHandler.ProxyManager = proxyManager endpointHandler.ProxyManager = proxyManager
endpointHandler.Snapshotter = server.Snapshotter endpointHandler.Snapshotter = server.Snapshotter
endpointHandler.JobService = server.JobService endpointHandler.JobService = server.JobService
endpointHandler.TunnelServerFingerprint = server.TunnelServerFingerprint
var endpointGroupHandler = endpointgroups.NewHandler(requestBouncer) var endpointGroupHandler = endpointgroups.NewHandler(requestBouncer)
endpointGroupHandler.EndpointGroupService = server.EndpointGroupService endpointGroupHandler.EndpointGroupService = server.EndpointGroupService
+12 -1
View File
@@ -10,6 +10,8 @@ type (
// CLIFlags represents the available flags on the CLI // CLIFlags represents the available flags on the CLI
CLIFlags struct { CLIFlags struct {
Addr *string Addr *string
TunnelAddr *string
TunnelPort *string
AdminPassword *string AdminPassword *string
AdminPasswordFile *string AdminPasswordFile *string
Assets *string Assets *string
@@ -250,7 +252,7 @@ type (
Snapshots []Snapshot `json:"Snapshots"` Snapshots []Snapshot `json:"Snapshots"`
UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"` UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"`
TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies"` TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies"`
EdgeKey string
// Deprecated fields // Deprecated fields
// Deprecated in DBVersion == 4 // Deprecated in DBVersion == 4
TLS bool `json:"TLS,omitempty"` TLS bool `json:"TLS,omitempty"`
@@ -594,6 +596,13 @@ type (
Start() error Start() error
} }
// Tunnel server defines the interface for the reverse tunneling server used
// with Edge agents.
TunnelServer interface {
Start() error
GetFingerprint() string
}
// UserService represents a service for managing user data // UserService represents a service for managing user data
UserService interface { UserService interface {
User(ID UserID) (*User, error) User(ID UserID) (*User, error)
@@ -951,6 +960,8 @@ const (
AgentOnDockerEnvironment AgentOnDockerEnvironment
// AzureEnvironment represents an endpoint connected to an Azure environment // AzureEnvironment represents an endpoint connected to an Azure environment
AzureEnvironment AzureEnvironment
// EdgeAgentEnvironment represents an endpoint connected to an Edge agent
EdgeAgentEnvironment
) )
const ( const (
+1 -1
View File
@@ -13,7 +13,7 @@ angular.module('portainer.docker')
agentProxy: false agentProxy: false
}; };
if (type === 2) { if (type === 2 || type === 4) {
mode.agentProxy = true; mode.agentProxy = true;
} }
@@ -74,7 +74,8 @@
{{ item.Type | endpointtypename }} {{ item.Type | endpointtypename }}
</span> </span>
</td> </td>
<td>{{ item.URL | stripprotocol }}</td> <td ng-if="item.Type !== 4">{{ item.URL | stripprotocol }}</td>
<td ng-if="item.Type === 4">-</td>
<td>{{ item.GroupName }}</td> <td>{{ item.GroupName }}</td>
<td> <td>
<a ui-sref="portainer.endpoints.endpoint.access({id: item.Id})" ng-if="$ctrl.accessManagement"> <a ui-sref="portainer.endpoints.endpoint.access({id: item.Id})" ng-if="$ctrl.accessManagement">
+4
View File
@@ -124,6 +124,8 @@ angular.module('portainer.app')
return 'Agent'; return 'Agent';
} else if (type === 3) { } else if (type === 3) {
return 'Azure ACI'; return 'Azure ACI';
} else if (type === 4) {
return 'Edge Agent';
} }
return ''; return '';
}; };
@@ -133,6 +135,8 @@ angular.module('portainer.app')
return function (type) { return function (type) {
if (type === 3) { if (type === 3) {
return 'fab fa-microsoft'; return 'fab fa-microsoft';
} else if (type === 4) {
return 'fa fa-cloud';
} }
return 'fab fa-docker'; return 'fab fa-docker';
}; };
@@ -56,6 +56,15 @@ function ($q, $scope, $state, $filter, clipboard, EndpointService, GroupService,
addEndpoint(name, 2, URL, publicURL, groupId, tags, true, true, true, null, null, null); addEndpoint(name, 2, URL, publicURL, groupId, tags, true, true, true, null, null, null);
}; };
$scope.addEdgeAgentEndpoint = function() {
var name = $scope.formValues.Name;
var groupId = $scope.formValues.GroupId;
var tags = $scope.formValues.Tags;
var URL = window.location.hostname;
addEndpoint(name, 4, URL, "", groupId, tags, false, false, false, null, null, null);
};
$scope.addAzureEndpoint = function() { $scope.addAzureEndpoint = function() {
var name = $scope.formValues.Name; var name = $scope.formValues.Name;
var applicationId = $scope.formValues.AzureApplicationId; var applicationId = $scope.formValues.AzureApplicationId;
@@ -85,9 +94,13 @@ function ($q, $scope, $state, $filter, clipboard, EndpointService, GroupService,
function addEndpoint(name, type, URL, PublicURL, groupId, tags, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) { function addEndpoint(name, type, URL, PublicURL, groupId, tags, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) {
$scope.state.actionInProgress = true; $scope.state.actionInProgress = true;
EndpointService.createRemoteEndpoint(name, type, URL, PublicURL, groupId, tags, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) EndpointService.createRemoteEndpoint(name, type, URL, PublicURL, groupId, tags, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile)
.then(function success() { .then(function success(data) {
Notifications.success('Endpoint created', name); Notifications.success('Endpoint created', name);
$state.go('portainer.endpoints', {}, {reload: true}); if (type === 4) {
$state.go('portainer.endpoints.endpoint', { id: data.Id });
} else {
$state.go('portainer.endpoints', {}, {reload: true});
}
}) })
.catch(function error(err) { .catch(function error(err) {
Notifications.error('Failure', err, 'Unable to create endpoint'); Notifications.error('Failure', err, 'Unable to create endpoint');
@@ -36,6 +36,16 @@
<p>Portainer agent</p> <p>Portainer agent</p>
</label> </label>
</div> </div>
<div>
<input type="radio" id="edge_agent_endpoint" ng-model="state.EnvironmentType" value="edge_agent">
<label for="edge_agent_endpoint">
<div class="boxselector_header">
<i class="fa fa-cloud" aria-hidden="true" style="margin-right: 2px;"></i>
Edge Agent
</div>
<p>Portainer Edge agent</p>
</label>
</div>
<div> <div>
<input type="radio" id="azure_endpoint" ng-model="state.EnvironmentType" value="azure"> <input type="radio" id="azure_endpoint" ng-model="state.EnvironmentType" value="azure">
<label for="azure_endpoint"> <label for="azure_endpoint">
@@ -77,6 +87,16 @@
</span> </span>
</div> </div>
</div> </div>
<div ng-if="state.EnvironmentType === 'edge_agent'">
<div class="col-sm-12 form-section-title" >
Information
</div>
<div class="form-group">
<span class="col-sm-12 text-muted small">
Allows you to create an endpoint that can be registered with an Edge agent. All the required information on how to connect an Edge agent to this endpoint will be available after endpoint creation.
</span>
</div>
</div>
<div ng-if="state.EnvironmentType === 'azure'"> <div ng-if="state.EnvironmentType === 'azure'">
<div class="col-sm-12 form-section-title" > <div class="col-sm-12 form-section-title" >
Information Information
@@ -238,6 +258,10 @@
<span ng-hide="state.actionInProgress"><i class="fa fa-plus" aria-hidden="true"></i> Add endpoint</span> <span ng-hide="state.actionInProgress"><i class="fa fa-plus" aria-hidden="true"></i> Add endpoint</span>
<span ng-show="state.actionInProgress">Creating endpoint...</span> <span ng-show="state.actionInProgress">Creating endpoint...</span>
</button> </button>
<button ng-if="state.EnvironmentType === 'edge_agent'" type="submit" class="btn btn-primary btn-sm" ng-disabled="state.actionInProgress || !endpointCreationForm.$valid" ng-click="addEdgeAgentEndpoint()" button-spinner="state.actionInProgress">
<span ng-hide="state.actionInProgress"><i class="fa fa-plus" aria-hidden="true"></i> Add endpoint</span>
<span ng-show="state.actionInProgress">Creating endpoint...</span>
</button>
<button ng-if="state.EnvironmentType === 'azure'" type="submit" class="btn btn-primary btn-sm" ng-disabled="state.actionInProgress || !endpointCreationForm.$valid" ng-click="addAzureEndpoint()" button-spinner="state.actionInProgress"> <button ng-if="state.EnvironmentType === 'azure'" type="submit" class="btn btn-primary btn-sm" ng-disabled="state.actionInProgress || !endpointCreationForm.$valid" ng-click="addAzureEndpoint()" button-spinner="state.actionInProgress">
<span ng-hide="state.actionInProgress"><i class="fa fa-plus" aria-hidden="true"></i> Add endpoint</span> <span ng-hide="state.actionInProgress"><i class="fa fa-plus" aria-hidden="true"></i> Add endpoint</span>
<span ng-show="state.actionInProgress">Creating endpoint...</span> <span ng-show="state.actionInProgress">Creating endpoint...</span>
@@ -5,6 +5,80 @@
</rd-header-content> </rd-header-content>
</rd-header> </rd-header>
<div class="row">
<information-panel ng-if="endpoint.Type === 4" title-text="Deploy an agent">
<span class="small text-muted">
<p>
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
Deploy the Edge agent on your remote Docker environment using the following command
</p>
<div style="margin-top: 10px;">
<uib-tabset active="state.deploymentTab">
<uib-tab index="0" heading="Standalone">
<code style=display:block;white-space:pre-wrap>
docker run -d -v /var/run/docker.sock:/var/run/docker.sock \
-v /var/lib/docker/volumes:/var/lib/docker/volumes \
-v /:/host \
--restart always \
-e EDGE=1 \
-e CAP_HOST_MANAGEMENT=1 \
-p 80:80 \
--name portainer_edge_agent \
portainer/pagent:edge
</code>
</uib-tab>
<uib-tab index="1" heading="Swarm">
<code style=display:block;white-space:pre-wrap>
docker network create \
--driver overlay \
--attachable \
portainer_agent_network;
docker service create \
--name portainer_edge_agent \
--network portainer_agent_network \
-e AGENT_CLUSTER_ADDR=tasks.portainer_edge_agent \
-e EDGE=1 \
-e CAP_HOST_MANAGEMENT=1 \
--mode global \
--publish mode=host,target=80,published=80 \
--constraint 'node.platform.os == linux' \
--mount type=bind,src=//var/run/docker.sock,dst=/var/run/docker.sock \
--mount type=bind,src=//var/lib/docker/volumes,dst=/var/lib/docker/volumes \
--mount type=bind,src=//,dst=/host \
portainer/pagent:edge
</code>
</uib-tab>
</uib-tabset>
<div style="margin-top: 10px;">
<span class="btn btn-primary btn-sm" ng-click="copyEdgeAgentDeploymentCommand()"><i class="fa fa-copy space-right" aria-hidden="true"></i>Copy command</span>
<span id="copyNotificationDeploymentCommand" style="margin-left: 7px; display: none; color: #23ae89;">
<i class="fa fa-check" aria-hidden="true" ></i> copied
</span>
</div>
</div>
<div class="col-sm-12 form-section-title" style="margin-top: 25px;">
Join token
</div>
<p>
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
Use the following join token to associate the Edge agent with this endpoint
</p>
<div style="margin-top: 10px;">
<code style=display:block;white-space:pre-wrap>
{{ endpoint.EdgeKey }}
</code>
<div style="margin-top: 10px;">
<span class="btn btn-primary btn-sm" ng-click="copyEdgeAgentKey()"><i class="fa fa-copy space-right" aria-hidden="true"></i>Copy token</span>
<span id="copyNotificationEdgeKey" style="margin-left: 7px; display: none; color: #23ae89;">
<i class="fa fa-check" aria-hidden="true" ></i> copied
</span>
</div>
</div>
</span>
</information-panel>
</div>
<div class="row"> <div class="row">
<div class="col-lg-12 col-md-12 col-xs-12"> <div class="col-lg-12 col-md-12 col-xs-12">
<rd-widget> <rd-widget>
@@ -22,7 +96,7 @@
</div> </div>
<!-- !name-input --> <!-- !name-input -->
<!-- endpoint-url-input --> <!-- endpoint-url-input -->
<div class="form-group"> <div class="form-group" ng-if="endpoint.Type !== 4">
<label for="endpoint_url" class="col-sm-3 col-lg-2 control-label text-left"> <label for="endpoint_url" class="col-sm-3 col-lg-2 control-label text-left">
Endpoint URL Endpoint URL
<portainer-tooltip position="bottom" message="URL or IP address of a Docker host. The Docker API must be exposed over a TCP port. Please refer to the Docker documentation to configure it."></portainer-tooltip> <portainer-tooltip position="bottom" message="URL or IP address of a Docker host. The Docker API must be exposed over a TCP port. Please refer to the Docker documentation to configure it."></portainer-tooltip>
@@ -70,7 +144,7 @@
</div> </div>
<!-- !tags --> <!-- !tags -->
<!-- endpoint-security --> <!-- endpoint-security -->
<div ng-if="endpointType === 'remote' && endpoint.Type !== 3"> <div ng-if="endpointType === 'remote' && endpoint.Type !== 3 && endpoint.Type !== 4">
<div class="col-sm-12 form-section-title"> <div class="col-sm-12 form-section-title">
Security Security
</div> </div>
@@ -1,8 +1,8 @@
import { EndpointSecurityFormData } from '../../../components/endpointSecurity/porEndpointSecurityModel'; import { EndpointSecurityFormData } from '../../../components/endpointSecurity/porEndpointSecurityModel';
angular.module('portainer.app') angular.module('portainer.app')
.controller('EndpointController', ['$q', '$scope', '$state', '$transition$', '$filter', 'EndpointService', 'GroupService', 'TagService', 'EndpointProvider', 'Notifications', .controller('EndpointController', ['$q', '$scope', '$state', '$transition$', '$filter', 'clipboard', 'EndpointService', 'GroupService', 'TagService', 'EndpointProvider', 'Notifications',
function ($q, $scope, $state, $transition$, $filter, EndpointService, GroupService, TagService, EndpointProvider, Notifications) { function ($q, $scope, $state, $transition$, $filter, clipboard, EndpointService, GroupService, TagService, EndpointProvider, Notifications) {
if (!$scope.applicationState.application.endpointManagement) { if (!$scope.applicationState.application.endpointManagement) {
$state.go('portainer.endpoints'); $state.go('portainer.endpoints');
@@ -10,13 +10,28 @@ function ($q, $scope, $state, $transition$, $filter, EndpointService, GroupServi
$scope.state = { $scope.state = {
uploadInProgress: false, uploadInProgress: false,
actionInProgress: false actionInProgress: false,
deploymentTab: 0
}; };
$scope.formValues = { $scope.formValues = {
SecurityFormData: new EndpointSecurityFormData() SecurityFormData: new EndpointSecurityFormData()
}; };
$scope.copyEdgeAgentDeploymentCommand = function() {
if ($scope.state.deploymentTab === 0) {
clipboard.copyText('docker run -d -v /var/run/docker.sock:/var/run/docker.sock -v /var/lib/docker/volumes:/var/lib/docker/volumes -v /:/host --restart always -e EDGE=1 -e CAP_HOST_MANAGEMENT=1 --name portainer_agent_iot portainer/pagent:edge');
} else {
clipboard.copyText('docker network create --driver overlay --attachable portainer_agent_network; docker service create --name portainer_edge_agent --network portainer_agent_network -e AGENT_CLUSTER_ADDR=tasks.portainer_edge_agent -e EDGE=1 -e CAP_HOST_MANAGEMENT=1 --mode global --publish mode=host,target=80,published=80 --constraint \'node.platform.os == linux\' --mount type=bind,src=//var/run/docker.sock,dst=/var/run/docker.sock --mount type=bind,src=//var/lib/docker/volumes,dst=/var/lib/docker/volume --mount type=bind,src=//,dst=/host portainer/pagent:edge');
}
$('#copyNotificationDeploymentCommand').show().fadeOut(2500);
};
$scope.copyEdgeAgentKey = function() {
clipboard.copyText($scope.endpoint.EdgeKey);
$('#copyNotificationEdgeKey').show().fadeOut(2500);
};
$scope.updateEndpoint = function() { $scope.updateEndpoint = function() {
var endpoint = $scope.endpoint; var endpoint = $scope.endpoint;
var securityData = $scope.formValues.SecurityFormData; var securityData = $scope.formValues.SecurityFormData;
+3 -3
View File
@@ -48,7 +48,7 @@ module.exports = function(grunt) {
]); ]);
}); });
grunt.task.registerTask('devopsbuild', 'devopsbuild:<platform>:<arch>', grunt.task.registerTask('devopsbuild', 'devopsbuild:<platform>:<arch>',
function(p, a) { function(p, a) {
grunt.task.run([ grunt.task.run([
'config:prod', 'config:prod',
@@ -146,7 +146,7 @@ gruntfile_cfg.copy = {
files: [ files: [
{ {
dest: '<%= root %>/', dest: '<%= root %>/',
src: 'templates.json', src: 'templates.json',
cwd: '' cwd: ''
} }
] ]
@@ -185,7 +185,7 @@ function shell_buildBinaryOnDevOps(p, a) {
function shell_run() { function shell_run() {
return [ return [
'docker rm -f portainer', 'docker rm -f portainer',
'docker run -d -p 9000:9000 -v $(pwd)/dist:/app -v /tmp/portainer:/data -v /var/run/docker.sock:/var/run/docker.sock:z --name portainer portainer/base /app/portainer --no-analytics --template-file /app/templates.json' 'docker run -d -p 9999:9999 -p 9000:9000 -v $(pwd)/dist:/app -v /tmp/portainer:/data -v /var/run/docker.sock:/var/run/docker.sock:z --name portainer portainer/base /app/portainer --no-analytics --template-file /app/templates.json'
].join(';'); ].join(';');
} }