mirror of https://github.com/portainer/portainer
feat(edge): introduce support for Edge agent (#3031)
* feat(edge): fix webconsole and agent deployment command * feat(edge): display agent features when connected to IoT endpoint * feat(edge): add -e CAP_HOST_MANAGEMENT=1 to agent command * feat(edge): add -v /:/host and --name portainer_agent_iot to agent command * style(endpoint-creation): refactor IoT agent to Edge agent * refactor(api): rename AgentIoTEnvironment to AgentEdgeEnvironment * refactor(api): rename AgentIoTEnvironment to AgentEdgeEnvironment * feat(endpoint-creation): update Edge agent deployment instructions * feat(edge): wip edge * feat(edge): refactor key creation * feat(edge): update deployment instructions * feat(home): update Edge agent endpoint item * feat(edge): support dynamic ports * feat(edge): support sleep/wake and snapshots * feat(edge): support offline mode * feat(edge): host job support for Edge endpoints * feat(edge): introduce STANDBY state * feat(edge): update Edge agent deployment command * feat(edge): introduce EDGE_ID support * feat(edge): update default inactivity interval to 5min * feat(edge): reload Edge schedules after restart * fix(edge): fix execution of endpoint job against an Edge endpoint * fix(edge): fix minor issues with scheduling UI/UX * feat(edge): introduce EdgeSchedule version management * feat(edge): switch back to REQUIRED state from ACTIVE on error * refactor(edge): remove comment * feat(edge): updated tunnel status management * feat(edge): fix flickering UI when accessing Edge endpoint from home view * feat(edge): remove STANDBY status * fix(edge): fix an issue with console and Swarm endpoint * fix(edge): fix an issue with stack deployment * fix(edge): reset timer when applying active status * feat(edge): add background ping for Edge endpoints * fix(edge): fix infinite loading loop after Edge endpoint connection failure * fix(home): fix an issue with merge * feat(api): remove SnapshotRaw from EndpointList response * feat(api): add pagination for EndpointList operation * feat(api): rename last_id query parameter to start * feat(api): implement filter for EndpointList operation * fix(edge): prevent a pointer issue after removing an active Edge endpoint * feat(home): front - endpoint backend pagination (#2990) * feat(home): endpoint pagination with backend * feat(api): remove default limit value * fix(endpoints): fix a minor issue with column span * fix(endpointgroup-create): fix an issue with endpoint group creation * feat(app): minor loading optimizations * refactor(api): small refactor of EndpointList operation * fix(home): fix minor loading text display issue * refactor(api): document bolt services functions * feat(home): minor optimization * fix(api): replace seek with index scanning for EndpointPaginated * fix(api): fix invalid starting index issue * fix(api): first implementation of working filter * fix(home): endpoints list keeps backend pagination when it needs to * fix(api): endpoint pagination doesn't drop the first item on pages >=2 anymore * fix(home): UI flickering on page/filter load/change * feat(auth): login spinner * feat(api): support searching in associated endpoint group data * refactor(api): remove unused API endpoint * refactor(api): remove comment * refactor(api): refactor proxy manager * feat(api): declare EndpointList params as optional * feat(api): support groupID filter on endpoints route * feat(api): add new API operations endpointGroupAddEndpoint and endpointGroupDeleteEndpoint * feat(edge): new icon for Edge agent endpoint * fix(edge): fix missing exec quick action * fix(edge): add loading indicator when connecting to Edge endpoint * feat(edge): disable service webhooks for Edge endpoints * feat(endpoints): backend pagination for endpoints view (#3004) * feat(edge): dynamic loading for stack migration feature * feat(edge): wordwrap edge key * feat(endpoint-groups): backend pagination support for create and edit * feat(endpoint-groups): debounce on filter for create/edit views * feat(endpoint-groups): filter assigned on create view * (endpoint-groups): unassigned endpoints edit view * refactor(endpoint-groups): code clean * feat(endpoint-groups): remove message for Unassigned group * refactor(websocket): minor refactor associated to Edge agent * feat(endpoint-group): enable backend pagination (#3017) * feat(api): support groupID filter on endpoints route * feat(api): add new API operations endpointGroupAddEndpoint and endpointGroupDeleteEndpoint * feat(endpoint-groups): backend pagination support for create and edit * feat(endpoint-groups): debounce on filter for create/edit views * feat(endpoint-groups): filter assigned on create view * (endpoint-groups): unassigned endpoints edit view * refactor(endpoint-groups): code clean * feat(endpoint-groups): remove message for Unassigned group * refactor(api): endpoint group endpoint association refactor * refactor(api): rename files and remove comments * refactor(api): remove usage of utils * refactor(api): optional parameters * Merge branch 'feat-endpoint-backend-pagination' into edge # Conflicts: # api/bolt/endpoint/endpoint.go # api/http/handler/endpointgroups/endpointgroup_update.go # api/http/handler/endpointgroups/handler.go # api/http/handler/endpoints/endpoint_list.go # app/portainer/services/api/endpointService.js * fix(api): fix default tunnel server credentials * feat(api): update endpointListOperation behavior and parameters * fix(api): fix interface declaration * feat(edge): support configurable Edge agent checkin interval * feat(edge): support dynamic tunnel credentials * feat(edge): update Edge agent deployment commands * style(edge): update Edge agent settings text * refactor(edge): remove unused credentials management methods * feat(edge): associate a remote addr to tunnel credentials * style(edge): update Edge endpoint icon * feat(edge): support encrypted tunnel credentials * fix(edge): fix invalid pointer cast * feat(bolt): decode endpoints with jsoniter * feat(edge): persist reverse tunnel keyseed * refactor(edge): minor refactor * feat(edge): update chisel library usage * refactor(endpoint): use controller function * feat(api): database migration to DBVersion 19 * refactor(api): refactor AddSchedule function * refactor(schedules): remove comment * refactor(api): remove comment * refactor(api): remove comment * feat(api): tunnel manager now only manage Edge endpoints * refactor(api): clean-up and clarification of the Edge service * refactor(api): clean-up and clarification of the Edge service * fix(api): fix an issue with Edge agent snapshots * refactor(api): add missing comments * refactor(api): update constant description * style(home): remove loading text on error * feat(endpoint): remove 15s timeout for ping request * style(home): display information about associated Edge endpoints * feat(home): redirect to endpoint details on click on unassociated Edge endpoint * feat(settings): remove 60s Edge poll frequency optionpull/3052/head
parent
2252ab9da7
commit
12a512f01f
|
@ -5,6 +5,8 @@ import (
|
||||||
"path"
|
"path"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/portainer/portainer/api/bolt/tunnelserver"
|
||||||
|
|
||||||
"github.com/boltdb/bolt"
|
"github.com/boltdb/bolt"
|
||||||
"github.com/portainer/portainer/api"
|
"github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/bolt/dockerhub"
|
"github.com/portainer/portainer/api/bolt/dockerhub"
|
||||||
|
@ -51,6 +53,7 @@ type Store struct {
|
||||||
TeamMembershipService *teammembership.Service
|
TeamMembershipService *teammembership.Service
|
||||||
TeamService *team.Service
|
TeamService *team.Service
|
||||||
TemplateService *template.Service
|
TemplateService *template.Service
|
||||||
|
TunnelServerService *tunnelserver.Service
|
||||||
UserService *user.Service
|
UserService *user.Service
|
||||||
VersionService *version.Service
|
VersionService *version.Service
|
||||||
WebhookService *webhook.Service
|
WebhookService *webhook.Service
|
||||||
|
@ -220,6 +223,12 @@ func (store *Store) initServices() error {
|
||||||
}
|
}
|
||||||
store.TemplateService = templateService
|
store.TemplateService = templateService
|
||||||
|
|
||||||
|
tunnelServerService, err := tunnelserver.NewService(store.db)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
store.TunnelServerService = tunnelServerService
|
||||||
|
|
||||||
userService, err := user.NewService(store.db)
|
userService, err := user.NewService(store.db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -63,7 +63,7 @@ func (service *Service) Endpoints() ([]portainer.Endpoint, error) {
|
||||||
cursor := bucket.Cursor()
|
cursor := bucket.Cursor()
|
||||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||||
var endpoint portainer.Endpoint
|
var endpoint portainer.Endpoint
|
||||||
err := internal.UnmarshalObject(v, &endpoint)
|
err := internal.UnmarshalObjectWithJsoniter(v, &endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,8 @@ package internal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
||||||
|
jsoniter "github.com/json-iterator/go"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MarshalObject encodes an object to binary format
|
// MarshalObject encodes an object to binary format
|
||||||
|
@ -13,3 +15,11 @@ func MarshalObject(object interface{}) ([]byte, error) {
|
||||||
func UnmarshalObject(data []byte, object interface{}) error {
|
func UnmarshalObject(data []byte, object interface{}) error {
|
||||||
return json.Unmarshal(data, object)
|
return json.Unmarshal(data, object)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UnmarshalObjectWithJsoniter decodes an object from binary data
|
||||||
|
// using the jsoniter library. It is mainly used to accelerate endpoint
|
||||||
|
// decoding at the moment.
|
||||||
|
func UnmarshalObjectWithJsoniter(data []byte, object interface{}) error {
|
||||||
|
var jsoni = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||||
|
return jsoni.Unmarshal(data, &object)
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
package migrator
|
||||||
|
|
||||||
|
import portainer "github.com/portainer/portainer/api"
|
||||||
|
|
||||||
|
func (m *Migrator) updateSettingsToDBVersion19() error {
|
||||||
|
legacySettings, err := m.settingsService.Settings()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if legacySettings.EdgeAgentCheckinInterval == 0 {
|
||||||
|
legacySettings.EdgeAgentCheckinInterval = portainer.DefaultEdgeAgentCheckinIntervalInSeconds
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.settingsService.UpdateSettings(legacySettings)
|
||||||
|
}
|
|
@ -249,5 +249,13 @@ func (m *Migrator) Migrate() error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Portainer 1.21.1
|
||||||
|
if m.currentDBVersion < 19 {
|
||||||
|
err := m.updateSettingsToDBVersion19()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return m.versionService.StoreDBVersion(portainer.DBVersion)
|
return m.versionService.StoreDBVersion(portainer.DBVersion)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
package tunnelserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/bolt/internal"
|
||||||
|
|
||||||
|
"github.com/boltdb/bolt"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// BucketName represents the name of the bucket where this service stores data.
|
||||||
|
BucketName = "tunnel_server"
|
||||||
|
infoKey = "INFO"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Service represents a service for managing endpoint data.
|
||||||
|
type Service struct {
|
||||||
|
db *bolt.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewService creates a new instance of a service.
|
||||||
|
func NewService(db *bolt.DB) (*Service, error) {
|
||||||
|
err := internal.CreateBucket(db, BucketName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Service{
|
||||||
|
db: db,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info retrieve the TunnelServerInfo object.
|
||||||
|
func (service *Service) Info() (*portainer.TunnelServerInfo, error) {
|
||||||
|
var info portainer.TunnelServerInfo
|
||||||
|
|
||||||
|
err := internal.GetObject(service.db, BucketName, []byte(infoKey), &info)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &info, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateInfo persists a TunnelServerInfo object.
|
||||||
|
func (service *Service) UpdateInfo(settings *portainer.TunnelServerInfo) error {
|
||||||
|
return internal.UpdateObject(service.db, BucketName, []byte(infoKey), settings)
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package chisel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GenerateEdgeKey will generate a key that can be used by an Edge agent to register with a Portainer instance.
|
||||||
|
// The key represents the following data in this particular format:
|
||||||
|
// portainer_instance_url|tunnel_server_addr|tunnel_server_fingerprint|endpoint_ID
|
||||||
|
// The key returned by this function is a base64 encoded version of the data.
|
||||||
|
func (service *Service) GenerateEdgeKey(url, host string, endpointIdentifier int) string {
|
||||||
|
keyInformation := []string{
|
||||||
|
url,
|
||||||
|
fmt.Sprintf("%s:%s", host, service.serverPort),
|
||||||
|
service.serverFingerprint,
|
||||||
|
strconv.Itoa(endpointIdentifier),
|
||||||
|
}
|
||||||
|
|
||||||
|
key := strings.Join(keyInformation, "|")
|
||||||
|
return base64.RawStdEncoding.EncodeToString([]byte(key))
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
package chisel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AddSchedule register a schedule inside the tunnel details associated to an endpoint.
|
||||||
|
func (service *Service) AddSchedule(endpointID portainer.EndpointID, schedule *portainer.EdgeSchedule) {
|
||||||
|
tunnel := service.GetTunnelDetails(endpointID)
|
||||||
|
|
||||||
|
existingScheduleIndex := -1
|
||||||
|
for idx, existingSchedule := range tunnel.Schedules {
|
||||||
|
if existingSchedule.ID == schedule.ID {
|
||||||
|
existingScheduleIndex = idx
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if existingScheduleIndex == -1 {
|
||||||
|
tunnel.Schedules = append(tunnel.Schedules, *schedule)
|
||||||
|
} else {
|
||||||
|
tunnel.Schedules[existingScheduleIndex] = *schedule
|
||||||
|
}
|
||||||
|
|
||||||
|
key := strconv.Itoa(int(endpointID))
|
||||||
|
service.tunnelDetailsMap.Set(key, tunnel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveSchedule will remove the specified schedule from each tunnel it was registered with.
|
||||||
|
func (service *Service) RemoveSchedule(scheduleID portainer.ScheduleID) {
|
||||||
|
for item := range service.tunnelDetailsMap.IterBuffered() {
|
||||||
|
tunnelDetails := item.Val.(*portainer.TunnelDetails)
|
||||||
|
|
||||||
|
updatedSchedules := make([]portainer.EdgeSchedule, 0)
|
||||||
|
for _, schedule := range tunnelDetails.Schedules {
|
||||||
|
if schedule.ID == scheduleID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
updatedSchedules = append(updatedSchedules, schedule)
|
||||||
|
}
|
||||||
|
|
||||||
|
tunnelDetails.Schedules = updatedSchedules
|
||||||
|
service.tunnelDetailsMap.Set(item.Key, tunnelDetails)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,191 @@
|
||||||
|
package chisel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/dchest/uniuri"
|
||||||
|
|
||||||
|
cmap "github.com/orcaman/concurrent-map"
|
||||||
|
|
||||||
|
chserver "github.com/jpillora/chisel/server"
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
tunnelCleanupInterval = 10 * time.Second
|
||||||
|
requiredTimeout = 15 * time.Second
|
||||||
|
activeTimeout = 4*time.Minute + 30*time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
// Service represents a service to manage the state of multiple reverse tunnels.
|
||||||
|
// It is used to start a reverse tunnel server and to manage the connection status of each tunnel
|
||||||
|
// connected to the tunnel server.
|
||||||
|
type Service struct {
|
||||||
|
serverFingerprint string
|
||||||
|
serverPort string
|
||||||
|
tunnelDetailsMap cmap.ConcurrentMap
|
||||||
|
endpointService portainer.EndpointService
|
||||||
|
tunnelServerService portainer.TunnelServerService
|
||||||
|
snapshotter portainer.Snapshotter
|
||||||
|
chiselServer *chserver.Server
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewService returns a pointer to a new instance of Service
|
||||||
|
func NewService(endpointService portainer.EndpointService, tunnelServerService portainer.TunnelServerService) *Service {
|
||||||
|
return &Service{
|
||||||
|
tunnelDetailsMap: cmap.New(),
|
||||||
|
endpointService: endpointService,
|
||||||
|
tunnelServerService: tunnelServerService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartTunnelServer starts a tunnel server on the specified addr and port.
|
||||||
|
// It uses a seed to generate a new private/public key pair. If the seed cannot
|
||||||
|
// be found inside the database, it will generate a new one randomly and persist it.
|
||||||
|
// It starts the tunnel status verification process in the background.
|
||||||
|
// The snapshotter is used in the tunnel status verification process.
|
||||||
|
func (service *Service) StartTunnelServer(addr, port string, snapshotter portainer.Snapshotter) error {
|
||||||
|
keySeed, err := service.retrievePrivateKeySeed()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
config := &chserver.Config{
|
||||||
|
Reverse: true,
|
||||||
|
KeySeed: keySeed,
|
||||||
|
}
|
||||||
|
|
||||||
|
chiselServer, err := chserver.NewServer(config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
service.serverFingerprint = chiselServer.GetFingerprint()
|
||||||
|
service.serverPort = port
|
||||||
|
|
||||||
|
err = chiselServer.Start(addr, port)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
service.chiselServer = chiselServer
|
||||||
|
|
||||||
|
// TODO: work-around Chisel default behavior.
|
||||||
|
// By default, Chisel will allow anyone to connect if no user exists.
|
||||||
|
username, password := generateRandomCredentials()
|
||||||
|
err = service.chiselServer.AddUser(username, password, "127.0.0.1")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
service.snapshotter = snapshotter
|
||||||
|
go service.startTunnelVerificationLoop()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *Service) retrievePrivateKeySeed() (string, error) {
|
||||||
|
var serverInfo *portainer.TunnelServerInfo
|
||||||
|
|
||||||
|
serverInfo, err := service.tunnelServerService.Info()
|
||||||
|
if err == portainer.ErrObjectNotFound {
|
||||||
|
keySeed := uniuri.NewLen(16)
|
||||||
|
|
||||||
|
serverInfo = &portainer.TunnelServerInfo{
|
||||||
|
PrivateKeySeed: keySeed,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := service.tunnelServerService.UpdateInfo(serverInfo)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
} else if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return serverInfo.PrivateKeySeed, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *Service) startTunnelVerificationLoop() {
|
||||||
|
log.Printf("[DEBUG] [chisel, monitoring] [check_interval_seconds: %f] [message: starting tunnel management process]", tunnelCleanupInterval.Seconds())
|
||||||
|
ticker := time.NewTicker(tunnelCleanupInterval)
|
||||||
|
stopSignal := make(chan struct{})
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
service.checkTunnels()
|
||||||
|
case <-stopSignal:
|
||||||
|
ticker.Stop()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *Service) checkTunnels() {
|
||||||
|
for item := range service.tunnelDetailsMap.IterBuffered() {
|
||||||
|
tunnel := item.Val.(*portainer.TunnelDetails)
|
||||||
|
|
||||||
|
if tunnel.LastActivity.IsZero() || tunnel.Status == portainer.EdgeAgentIdle {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
elapsed := time.Since(tunnel.LastActivity)
|
||||||
|
log.Printf("[DEBUG] [chisel,monitoring] [endpoint_id: %s] [status: %s] [status_time_seconds: %f] [message: endpoint tunnel monitoring]", item.Key, tunnel.Status, elapsed.Seconds())
|
||||||
|
|
||||||
|
if tunnel.Status == portainer.EdgeAgentManagementRequired && elapsed.Seconds() < requiredTimeout.Seconds() {
|
||||||
|
continue
|
||||||
|
} else if tunnel.Status == portainer.EdgeAgentManagementRequired && elapsed.Seconds() > requiredTimeout.Seconds() {
|
||||||
|
log.Printf("[DEBUG] [chisel,monitoring] [endpoint_id: %s] [status: %s] [status_time_seconds: %f] [timeout_seconds: %f] [message: REQUIRED state timeout exceeded]", item.Key, tunnel.Status, elapsed.Seconds(), requiredTimeout.Seconds())
|
||||||
|
}
|
||||||
|
|
||||||
|
if tunnel.Status == portainer.EdgeAgentActive && elapsed.Seconds() < activeTimeout.Seconds() {
|
||||||
|
continue
|
||||||
|
} else if tunnel.Status == portainer.EdgeAgentActive && elapsed.Seconds() > activeTimeout.Seconds() {
|
||||||
|
log.Printf("[DEBUG] [chisel,monitoring] [endpoint_id: %s] [status: %s] [status_time_seconds: %f] [timeout_seconds: %f] [message: ACTIVE state timeout exceeded]", item.Key, tunnel.Status, elapsed.Seconds(), activeTimeout.Seconds())
|
||||||
|
|
||||||
|
endpointID, err := strconv.Atoi(item.Key)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ERROR] [chisel,snapshot,conversion] Invalid endpoint identifier (id: %s): %s", item.Key, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = service.snapshotEnvironment(portainer.EndpointID(endpointID), tunnel.Port)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ERROR] [snapshot] Unable to snapshot Edge endpoint (id: %s): %s", item.Key, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tunnel.Schedules) > 0 {
|
||||||
|
endpointID, err := strconv.Atoi(item.Key)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ERROR] [chisel,conversion] Invalid endpoint identifier (id: %s): %s", item.Key, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
service.SetTunnelStatusToIdle(portainer.EndpointID(endpointID))
|
||||||
|
} else {
|
||||||
|
service.tunnelDetailsMap.Remove(item.Key)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *Service) snapshotEnvironment(endpointID portainer.EndpointID, tunnelPort int) error {
|
||||||
|
endpoint, err := service.endpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
endpointURL := endpoint.URL
|
||||||
|
endpoint.URL = fmt.Sprintf("tcp://localhost:%d", tunnelPort)
|
||||||
|
snapshot, err := service.snapshotter.CreateSnapshot(endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint.Snapshots = []portainer.Snapshot{*snapshot}
|
||||||
|
endpoint.URL = endpointURL
|
||||||
|
return service.endpointService.UpdateEndpoint(endpoint.ID, endpoint)
|
||||||
|
}
|
|
@ -0,0 +1,144 @@
|
||||||
|
package chisel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/portainer/libcrypto"
|
||||||
|
|
||||||
|
"github.com/dchest/uniuri"
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
minAvailablePort = 49152
|
||||||
|
maxAvailablePort = 65535
|
||||||
|
)
|
||||||
|
|
||||||
|
// getUnusedPort is used to generate an unused random port in the dynamic port range.
|
||||||
|
// Dynamic ports (also called private ports) are 49152 to 65535.
|
||||||
|
func (service *Service) getUnusedPort() int {
|
||||||
|
port := randomInt(minAvailablePort, maxAvailablePort)
|
||||||
|
|
||||||
|
for item := range service.tunnelDetailsMap.IterBuffered() {
|
||||||
|
tunnel := item.Val.(*portainer.TunnelDetails)
|
||||||
|
if tunnel.Port == port {
|
||||||
|
return service.getUnusedPort()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return port
|
||||||
|
}
|
||||||
|
|
||||||
|
func randomInt(min, max int) int {
|
||||||
|
return min + rand.Intn(max-min)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTunnelDetails returns information about the tunnel associated to an endpoint.
|
||||||
|
func (service *Service) GetTunnelDetails(endpointID portainer.EndpointID) *portainer.TunnelDetails {
|
||||||
|
key := strconv.Itoa(int(endpointID))
|
||||||
|
|
||||||
|
if item, ok := service.tunnelDetailsMap.Get(key); ok {
|
||||||
|
tunnelDetails := item.(*portainer.TunnelDetails)
|
||||||
|
return tunnelDetails
|
||||||
|
}
|
||||||
|
|
||||||
|
schedules := make([]portainer.EdgeSchedule, 0)
|
||||||
|
return &portainer.TunnelDetails{
|
||||||
|
Status: portainer.EdgeAgentIdle,
|
||||||
|
Port: 0,
|
||||||
|
Schedules: schedules,
|
||||||
|
Credentials: "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTunnelStatusToActive update the status of the tunnel associated to the specified endpoint.
|
||||||
|
// It sets the status to ACTIVE.
|
||||||
|
func (service *Service) SetTunnelStatusToActive(endpointID portainer.EndpointID) {
|
||||||
|
tunnel := service.GetTunnelDetails(endpointID)
|
||||||
|
tunnel.Status = portainer.EdgeAgentActive
|
||||||
|
tunnel.Credentials = ""
|
||||||
|
tunnel.LastActivity = time.Now()
|
||||||
|
|
||||||
|
key := strconv.Itoa(int(endpointID))
|
||||||
|
service.tunnelDetailsMap.Set(key, tunnel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTunnelStatusToIdle update the status of the tunnel associated to the specified endpoint.
|
||||||
|
// It sets the status to IDLE.
|
||||||
|
// It removes any existing credentials associated to the tunnel.
|
||||||
|
func (service *Service) SetTunnelStatusToIdle(endpointID portainer.EndpointID) {
|
||||||
|
tunnel := service.GetTunnelDetails(endpointID)
|
||||||
|
|
||||||
|
tunnel.Status = portainer.EdgeAgentIdle
|
||||||
|
tunnel.Port = 0
|
||||||
|
tunnel.LastActivity = time.Now()
|
||||||
|
|
||||||
|
credentials := tunnel.Credentials
|
||||||
|
if credentials != "" {
|
||||||
|
tunnel.Credentials = ""
|
||||||
|
service.chiselServer.DeleteUser(strings.Split(credentials, ":")[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
key := strconv.Itoa(int(endpointID))
|
||||||
|
service.tunnelDetailsMap.Set(key, tunnel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTunnelStatusToRequired update the status of the tunnel associated to the specified endpoint.
|
||||||
|
// It sets the status to REQUIRED.
|
||||||
|
// If no port is currently associated to the tunnel, it will associate a random unused port to the tunnel
|
||||||
|
// and generate temporary credentials that can be used to establish a reverse tunnel on that port.
|
||||||
|
// Credentials are encrypted using the Edge ID associated to the endpoint.
|
||||||
|
func (service *Service) SetTunnelStatusToRequired(endpointID portainer.EndpointID) error {
|
||||||
|
tunnel := service.GetTunnelDetails(endpointID)
|
||||||
|
|
||||||
|
if tunnel.Port == 0 {
|
||||||
|
endpoint, err := service.endpointService.Endpoint(endpointID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
tunnel.Status = portainer.EdgeAgentManagementRequired
|
||||||
|
tunnel.Port = service.getUnusedPort()
|
||||||
|
tunnel.LastActivity = time.Now()
|
||||||
|
|
||||||
|
username, password := generateRandomCredentials()
|
||||||
|
authorizedRemote := fmt.Sprintf("^R:0.0.0.0:%d$", tunnel.Port)
|
||||||
|
err = service.chiselServer.AddUser(username, password, authorizedRemote)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
credentials, err := encryptCredentials(username, password, endpoint.EdgeID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
tunnel.Credentials = credentials
|
||||||
|
|
||||||
|
key := strconv.Itoa(int(endpointID))
|
||||||
|
service.tunnelDetailsMap.Set(key, tunnel)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateRandomCredentials() (string, string) {
|
||||||
|
username := uniuri.NewLen(8)
|
||||||
|
password := uniuri.NewLen(8)
|
||||||
|
return username, password
|
||||||
|
}
|
||||||
|
|
||||||
|
func encryptCredentials(username, password, key string) (string, error) {
|
||||||
|
credentials := fmt.Sprintf("%s:%s", username, password)
|
||||||
|
|
||||||
|
encryptedCredentials, err := libcrypto.Encrypt([]byte(credentials), []byte(key))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return base64.RawStdEncoding.EncodeToString(encryptedCredentials), nil
|
||||||
|
}
|
|
@ -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(),
|
||||||
|
|
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,10 +2,13 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/portainer/portainer/api/chisel"
|
||||||
|
|
||||||
"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/cli"
|
"github.com/portainer/portainer/api/cli"
|
||||||
|
@ -20,8 +23,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 {
|
||||||
|
@ -69,12 +70,12 @@ func initStore(dataStorePath string, fileService portainer.FileService) *bolt.St
|
||||||
return store
|
return store
|
||||||
}
|
}
|
||||||
|
|
||||||
func initComposeStackManager(dataStorePath string) portainer.ComposeStackManager {
|
func initComposeStackManager(dataStorePath string, reverseTunnelService portainer.ReverseTunnelService) portainer.ComposeStackManager {
|
||||||
return libcompose.NewComposeStackManager(dataStorePath)
|
return libcompose.NewComposeStackManager(dataStorePath, reverseTunnelService)
|
||||||
}
|
}
|
||||||
|
|
||||||
func initSwarmStackManager(assetsPath string, dataStorePath string, signatureService portainer.DigitalSignatureService, fileService portainer.FileService) (portainer.SwarmStackManager, error) {
|
func initSwarmStackManager(assetsPath string, dataStorePath string, signatureService portainer.DigitalSignatureService, fileService portainer.FileService, reverseTunnelService portainer.ReverseTunnelService) (portainer.SwarmStackManager, error) {
|
||||||
return exec.NewSwarmStackManager(assetsPath, dataStorePath, signatureService, fileService)
|
return exec.NewSwarmStackManager(assetsPath, dataStorePath, signatureService, fileService, reverseTunnelService)
|
||||||
}
|
}
|
||||||
|
|
||||||
func initJWTService(authenticationEnabled bool) portainer.JWTService {
|
func initJWTService(authenticationEnabled bool) portainer.JWTService {
|
||||||
|
@ -104,8 +105,8 @@ func initGitService() portainer.GitService {
|
||||||
return &git.Service{}
|
return &git.Service{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func initClientFactory(signatureService portainer.DigitalSignatureService) *docker.ClientFactory {
|
func initClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService) *docker.ClientFactory {
|
||||||
return docker.NewClientFactory(signatureService)
|
return docker.NewClientFactory(signatureService, reverseTunnelService)
|
||||||
}
|
}
|
||||||
|
|
||||||
func initSnapshotter(clientFactory *docker.ClientFactory) portainer.Snapshotter {
|
func initSnapshotter(clientFactory *docker.ClientFactory) portainer.Snapshotter {
|
||||||
|
@ -196,7 +197,7 @@ func loadEndpointSyncSystemSchedule(jobScheduler portainer.JobScheduler, schedul
|
||||||
return scheduleService.CreateSchedule(endpointSyncSchedule)
|
return scheduleService.CreateSchedule(endpointSyncSchedule)
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadSchedulesFromDatabase(jobScheduler portainer.JobScheduler, jobService portainer.JobService, scheduleService portainer.ScheduleService, endpointService portainer.EndpointService, fileService portainer.FileService) error {
|
func loadSchedulesFromDatabase(jobScheduler portainer.JobScheduler, jobService portainer.JobService, scheduleService portainer.ScheduleService, endpointService portainer.EndpointService, fileService portainer.FileService, reverseTunnelService portainer.ReverseTunnelService) error {
|
||||||
schedules, err := scheduleService.Schedules()
|
schedules, err := scheduleService.Schedules()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -213,6 +214,13 @@ func loadSchedulesFromDatabase(jobScheduler portainer.JobScheduler, jobService p
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if schedule.EdgeSchedule != nil {
|
||||||
|
for _, endpointID := range schedule.EdgeSchedule.Endpoints {
|
||||||
|
reverseTunnelService.AddSchedule(endpointID, schedule.EdgeSchedule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -265,6 +273,7 @@ func initSettings(settingsService portainer.SettingsService, flags *portainer.CL
|
||||||
AllowPrivilegedModeForRegularUsers: true,
|
AllowPrivilegedModeForRegularUsers: true,
|
||||||
EnableHostManagementFeatures: false,
|
EnableHostManagementFeatures: false,
|
||||||
SnapshotInterval: *flags.SnapshotInterval,
|
SnapshotInterval: *flags.SnapshotInterval,
|
||||||
|
EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds,
|
||||||
}
|
}
|
||||||
|
|
||||||
if *flags.Templates != "" {
|
if *flags.Templates != "" {
|
||||||
|
@ -540,7 +549,9 @@ func main() {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
clientFactory := initClientFactory(digitalSignatureService)
|
reverseTunnelService := chisel.NewService(store.EndpointService, store.TunnelServerService)
|
||||||
|
|
||||||
|
clientFactory := initClientFactory(digitalSignatureService, reverseTunnelService)
|
||||||
|
|
||||||
jobService := initJobService(clientFactory)
|
jobService := initJobService(clientFactory)
|
||||||
|
|
||||||
|
@ -551,12 +562,12 @@ func main() {
|
||||||
endpointManagement = false
|
endpointManagement = false
|
||||||
}
|
}
|
||||||
|
|
||||||
swarmStackManager, err := initSwarmStackManager(*flags.Assets, *flags.Data, digitalSignatureService, fileService)
|
swarmStackManager, err := initSwarmStackManager(*flags.Assets, *flags.Data, digitalSignatureService, fileService, reverseTunnelService)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
composeStackManager := initComposeStackManager(*flags.Data)
|
composeStackManager := initComposeStackManager(*flags.Data, reverseTunnelService)
|
||||||
|
|
||||||
err = initTemplates(store.TemplateService, fileService, *flags.Templates, *flags.TemplateFile)
|
err = initTemplates(store.TemplateService, fileService, *flags.Templates, *flags.TemplateFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -570,7 +581,7 @@ func main() {
|
||||||
|
|
||||||
jobScheduler := initJobScheduler()
|
jobScheduler := initJobScheduler()
|
||||||
|
|
||||||
err = loadSchedulesFromDatabase(jobScheduler, jobService, store.ScheduleService, store.EndpointService, fileService)
|
err = loadSchedulesFromDatabase(jobScheduler, jobService, store.ScheduleService, store.EndpointService, fileService, reverseTunnelService)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -658,7 +669,13 @@ func main() {
|
||||||
go terminateIfNoAdminCreated(store.UserService)
|
go terminateIfNoAdminCreated(store.UserService)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = reverseTunnelService.StartTunnelServer(*flags.TunnelAddr, *flags.TunnelPort, snapshotter)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
var server portainer.Server = &http.Server{
|
var server portainer.Server = &http.Server{
|
||||||
|
ReverseTunnelService: reverseTunnelService,
|
||||||
Status: applicationStatus,
|
Status: applicationStatus,
|
||||||
BindAddress: *flags.Addr,
|
BindAddress: *flags.Addr,
|
||||||
AssetsPath: *flags.Assets,
|
AssetsPath: *flags.Assets,
|
||||||
|
|
|
@ -53,7 +53,7 @@ func (runner *SnapshotJobRunner) Run() {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, endpoint := range endpoints {
|
for _, endpoint := range endpoints {
|
||||||
if endpoint.Type == portainer.AzureEnvironment {
|
if endpoint.Type == portainer.AzureEnvironment || endpoint.Type == portainer.EdgeAgentEnvironment {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,8 @@ import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"math/big"
|
"math/big"
|
||||||
|
|
||||||
|
"github.com/portainer/libcrypto"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -111,7 +113,7 @@ func (service *ECDSAService) CreateSignature(message string) (string, error) {
|
||||||
message = service.secret
|
message = service.secret
|
||||||
}
|
}
|
||||||
|
|
||||||
hash := HashFromBytes([]byte(message))
|
hash := libcrypto.HashFromBytes([]byte(message))
|
||||||
|
|
||||||
r := big.NewInt(0)
|
r := big.NewInt(0)
|
||||||
s := big.NewInt(0)
|
s := big.NewInt(0)
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
package crypto
|
|
||||||
|
|
||||||
import "crypto/md5"
|
|
||||||
|
|
||||||
// HashFromBytes returns the hash of the specified data
|
|
||||||
func HashFromBytes(data []byte) []byte {
|
|
||||||
digest := md5.New()
|
|
||||||
digest.Write(data)
|
|
||||||
return digest.Sum(nil)
|
|
||||||
}
|
|
|
@ -1,6 +1,7 @@
|
||||||
package docker
|
package docker
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
@ -17,13 +18,15 @@ const (
|
||||||
|
|
||||||
// ClientFactory is used to create Docker clients
|
// ClientFactory is used to create Docker clients
|
||||||
type ClientFactory struct {
|
type ClientFactory struct {
|
||||||
signatureService portainer.DigitalSignatureService
|
signatureService portainer.DigitalSignatureService
|
||||||
|
reverseTunnelService portainer.ReverseTunnelService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClientFactory returns a new instance of a ClientFactory
|
// NewClientFactory returns a new instance of a ClientFactory
|
||||||
func NewClientFactory(signatureService portainer.DigitalSignatureService) *ClientFactory {
|
func NewClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService) *ClientFactory {
|
||||||
return &ClientFactory{
|
return &ClientFactory{
|
||||||
signatureService: signatureService,
|
signatureService: signatureService,
|
||||||
|
reverseTunnelService: reverseTunnelService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,6 +38,8 @@ func (factory *ClientFactory) CreateClient(endpoint *portainer.Endpoint, nodeNam
|
||||||
return nil, unsupportedEnvironmentType
|
return nil, unsupportedEnvironmentType
|
||||||
} else if endpoint.Type == portainer.AgentOnDockerEnvironment {
|
} else if endpoint.Type == portainer.AgentOnDockerEnvironment {
|
||||||
return createAgentClient(endpoint, factory.signatureService, nodeName)
|
return createAgentClient(endpoint, factory.signatureService, nodeName)
|
||||||
|
} else if endpoint.Type == portainer.EdgeAgentEnvironment {
|
||||||
|
return createEdgeClient(endpoint, factory.reverseTunnelService, nodeName)
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(endpoint.URL, "unix://") || strings.HasPrefix(endpoint.URL, "npipe://") {
|
if strings.HasPrefix(endpoint.URL, "unix://") || strings.HasPrefix(endpoint.URL, "npipe://") {
|
||||||
|
@ -63,6 +68,28 @@ func createTCPClient(endpoint *portainer.Endpoint) (*client.Client, error) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createEdgeClient(endpoint *portainer.Endpoint, reverseTunnelService portainer.ReverseTunnelService, nodeName string) (*client.Client, error) {
|
||||||
|
httpCli, err := httpClient(endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
headers := map[string]string{}
|
||||||
|
if nodeName != "" {
|
||||||
|
headers[portainer.PortainerAgentTargetHeader] = nodeName
|
||||||
|
}
|
||||||
|
|
||||||
|
tunnel := reverseTunnelService.GetTunnelDetails(endpoint.ID)
|
||||||
|
endpointURL := fmt.Sprintf("http://localhost:%d", tunnel.Port)
|
||||||
|
|
||||||
|
return client.NewClientWithOpts(
|
||||||
|
client.WithHost(endpointURL),
|
||||||
|
client.WithVersion(portainer.SupportedDockerAPIVersion),
|
||||||
|
client.WithHTTPClient(httpCli),
|
||||||
|
client.WithHTTPHeaders(headers),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func createAgentClient(endpoint *portainer.Endpoint, signatureService portainer.DigitalSignatureService, nodeName string) (*client.Client, error) {
|
func createAgentClient(endpoint *portainer.Endpoint, signatureService portainer.DigitalSignatureService, nodeName string) (*client.Client, error) {
|
||||||
httpCli, err := httpClient(endpoint)
|
httpCli, err := httpClient(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -3,6 +3,7 @@ package exec
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path"
|
"path"
|
||||||
|
@ -13,20 +14,22 @@ import (
|
||||||
|
|
||||||
// SwarmStackManager represents a service for managing stacks.
|
// SwarmStackManager represents a service for managing stacks.
|
||||||
type SwarmStackManager struct {
|
type SwarmStackManager struct {
|
||||||
binaryPath string
|
binaryPath string
|
||||||
dataPath string
|
dataPath string
|
||||||
signatureService portainer.DigitalSignatureService
|
signatureService portainer.DigitalSignatureService
|
||||||
fileService portainer.FileService
|
fileService portainer.FileService
|
||||||
|
reverseTunnelService portainer.ReverseTunnelService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSwarmStackManager initializes a new SwarmStackManager service.
|
// NewSwarmStackManager initializes a new SwarmStackManager service.
|
||||||
// It also updates the configuration of the Docker CLI binary.
|
// It also updates the configuration of the Docker CLI binary.
|
||||||
func NewSwarmStackManager(binaryPath, dataPath string, signatureService portainer.DigitalSignatureService, fileService portainer.FileService) (*SwarmStackManager, error) {
|
func NewSwarmStackManager(binaryPath, dataPath string, signatureService portainer.DigitalSignatureService, fileService portainer.FileService, reverseTunnelService portainer.ReverseTunnelService) (*SwarmStackManager, error) {
|
||||||
manager := &SwarmStackManager{
|
manager := &SwarmStackManager{
|
||||||
binaryPath: binaryPath,
|
binaryPath: binaryPath,
|
||||||
dataPath: dataPath,
|
dataPath: dataPath,
|
||||||
signatureService: signatureService,
|
signatureService: signatureService,
|
||||||
fileService: fileService,
|
fileService: fileService,
|
||||||
|
reverseTunnelService: reverseTunnelService,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := manager.updateDockerCLIConfiguration(dataPath)
|
err := manager.updateDockerCLIConfiguration(dataPath)
|
||||||
|
@ -39,7 +42,7 @@ func NewSwarmStackManager(binaryPath, dataPath string, signatureService portaine
|
||||||
|
|
||||||
// Login executes the docker login command against a list of registries (including DockerHub).
|
// Login executes the docker login command against a list of registries (including DockerHub).
|
||||||
func (manager *SwarmStackManager) Login(dockerhub *portainer.DockerHub, registries []portainer.Registry, endpoint *portainer.Endpoint) {
|
func (manager *SwarmStackManager) Login(dockerhub *portainer.DockerHub, registries []portainer.Registry, endpoint *portainer.Endpoint) {
|
||||||
command, args := prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint)
|
command, args := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint)
|
||||||
for _, registry := range registries {
|
for _, registry := range registries {
|
||||||
if registry.Authentication {
|
if registry.Authentication {
|
||||||
registryArgs := append(args, "login", "--username", registry.Username, "--password", registry.Password, registry.URL)
|
registryArgs := append(args, "login", "--username", registry.Username, "--password", registry.Password, registry.URL)
|
||||||
|
@ -55,7 +58,7 @@ func (manager *SwarmStackManager) Login(dockerhub *portainer.DockerHub, registri
|
||||||
|
|
||||||
// Logout executes the docker logout command.
|
// Logout executes the docker logout command.
|
||||||
func (manager *SwarmStackManager) Logout(endpoint *portainer.Endpoint) error {
|
func (manager *SwarmStackManager) Logout(endpoint *portainer.Endpoint) error {
|
||||||
command, args := prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint)
|
command, args := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint)
|
||||||
args = append(args, "logout")
|
args = append(args, "logout")
|
||||||
return runCommandAndCaptureStdErr(command, args, nil, "")
|
return runCommandAndCaptureStdErr(command, args, nil, "")
|
||||||
}
|
}
|
||||||
|
@ -63,7 +66,7 @@ func (manager *SwarmStackManager) Logout(endpoint *portainer.Endpoint) error {
|
||||||
// Deploy executes the docker stack deploy command.
|
// Deploy executes the docker stack deploy command.
|
||||||
func (manager *SwarmStackManager) Deploy(stack *portainer.Stack, prune bool, endpoint *portainer.Endpoint) error {
|
func (manager *SwarmStackManager) Deploy(stack *portainer.Stack, prune bool, endpoint *portainer.Endpoint) error {
|
||||||
stackFilePath := path.Join(stack.ProjectPath, stack.EntryPoint)
|
stackFilePath := path.Join(stack.ProjectPath, stack.EntryPoint)
|
||||||
command, args := prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint)
|
command, args := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint)
|
||||||
|
|
||||||
if prune {
|
if prune {
|
||||||
args = append(args, "stack", "deploy", "--prune", "--with-registry-auth", "--compose-file", stackFilePath, stack.Name)
|
args = append(args, "stack", "deploy", "--prune", "--with-registry-auth", "--compose-file", stackFilePath, stack.Name)
|
||||||
|
@ -82,7 +85,7 @@ func (manager *SwarmStackManager) Deploy(stack *portainer.Stack, prune bool, end
|
||||||
|
|
||||||
// Remove executes the docker stack rm command.
|
// Remove executes the docker stack rm command.
|
||||||
func (manager *SwarmStackManager) Remove(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
func (manager *SwarmStackManager) Remove(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||||
command, args := prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint)
|
command, args := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint)
|
||||||
args = append(args, "stack", "rm", stack.Name)
|
args = append(args, "stack", "rm", stack.Name)
|
||||||
return runCommandAndCaptureStdErr(command, args, nil, "")
|
return runCommandAndCaptureStdErr(command, args, nil, "")
|
||||||
}
|
}
|
||||||
|
@ -106,7 +109,7 @@ func runCommandAndCaptureStdErr(command string, args []string, env []string, wor
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func prepareDockerCommandAndArgs(binaryPath, dataPath string, endpoint *portainer.Endpoint) (string, []string) {
|
func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, dataPath string, endpoint *portainer.Endpoint) (string, []string) {
|
||||||
// Assume Linux as a default
|
// Assume Linux as a default
|
||||||
command := path.Join(binaryPath, "docker")
|
command := path.Join(binaryPath, "docker")
|
||||||
|
|
||||||
|
@ -116,7 +119,14 @@ func prepareDockerCommandAndArgs(binaryPath, dataPath string, endpoint *portaine
|
||||||
|
|
||||||
args := make([]string, 0)
|
args := make([]string, 0)
|
||||||
args = append(args, "--config", dataPath)
|
args = append(args, "--config", dataPath)
|
||||||
args = append(args, "-H", endpoint.URL)
|
|
||||||
|
endpointURL := endpoint.URL
|
||||||
|
if endpoint.Type == portainer.EdgeAgentEnvironment {
|
||||||
|
tunnel := manager.reverseTunnelService.GetTunnelDetails(endpoint.ID)
|
||||||
|
endpointURL = fmt.Sprintf("tcp://localhost:%d", tunnel.Port)
|
||||||
|
}
|
||||||
|
|
||||||
|
args = append(args, "-H", endpointURL)
|
||||||
|
|
||||||
if endpoint.TLSConfig.TLS {
|
if endpoint.TLSConfig.TLS {
|
||||||
args = append(args, "--tls")
|
args = append(args, "--tls")
|
||||||
|
|
|
@ -11,9 +11,11 @@ import (
|
||||||
// Handler is the HTTP handler used to proxy requests to external APIs.
|
// Handler is the HTTP handler used to proxy requests to external APIs.
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
*mux.Router
|
*mux.Router
|
||||||
requestBouncer *security.RequestBouncer
|
requestBouncer *security.RequestBouncer
|
||||||
EndpointService portainer.EndpointService
|
EndpointService portainer.EndpointService
|
||||||
ProxyManager *proxy.Manager
|
SettingsService portainer.SettingsService
|
||||||
|
ProxyManager *proxy.Manager
|
||||||
|
ReverseTunnelService portainer.ReverseTunnelService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler creates a handler to proxy requests to external APIs.
|
// NewHandler creates a handler to proxy requests to external APIs.
|
||||||
|
|
|
@ -29,7 +29,7 @@ func (handler *Handler) proxyRequestsToAzureAPI(w http.ResponseWriter, r *http.R
|
||||||
}
|
}
|
||||||
|
|
||||||
var proxy http.Handler
|
var proxy http.Handler
|
||||||
proxy = handler.ProxyManager.GetProxy(string(endpointID))
|
proxy = handler.ProxyManager.GetProxy(endpoint)
|
||||||
if proxy == nil {
|
if proxy == nil {
|
||||||
proxy, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint)
|
proxy, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -3,6 +3,7 @@ package endpointproxy
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
"github.com/portainer/libhttp/request"
|
"github.com/portainer/libhttp/request"
|
||||||
|
@ -24,7 +25,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 != portainer.EdgeAgentEnvironment && 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")}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,8 +34,32 @@ func (handler *Handler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if endpoint.Type == portainer.EdgeAgentEnvironment {
|
||||||
|
if endpoint.EdgeID == "" {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "No Edge agent registered with the endpoint", errors.New("No agent available")}
|
||||||
|
}
|
||||||
|
|
||||||
|
tunnel := handler.ReverseTunnelService.GetTunnelDetails(endpoint.ID)
|
||||||
|
if tunnel.Status == portainer.EdgeAgentIdle {
|
||||||
|
handler.ProxyManager.DeleteProxy(endpoint)
|
||||||
|
|
||||||
|
err := handler.ReverseTunnelService.SetTunnelStatusToRequired(endpoint.ID)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update tunnel status", err}
|
||||||
|
}
|
||||||
|
|
||||||
|
settings, err := handler.SettingsService.Settings()
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
|
||||||
|
}
|
||||||
|
|
||||||
|
waitForAgentToConnect := time.Duration(settings.EdgeAgentCheckinInterval) * time.Second
|
||||||
|
time.Sleep(waitForAgentToConnect * 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var proxy http.Handler
|
var proxy http.Handler
|
||||||
proxy = handler.ProxyManager.GetProxy(string(endpointID))
|
proxy = handler.ProxyManager.GetProxy(endpoint)
|
||||||
if proxy == nil {
|
if proxy == nil {
|
||||||
proxy, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint)
|
proxy, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
package endpoints
|
package endpoints
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"log"
|
"log"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
@ -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,52 @@ func (handler *Handler) createAzureEndpoint(payload *endpointCreatePayload) (*po
|
||||||
return endpoint, nil
|
return endpoint, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) {
|
||||||
|
endpointType := portainer.EdgeAgentEnvironment
|
||||||
|
endpointID := handler.EndpointService.GetNextIdentifier()
|
||||||
|
|
||||||
|
portainerURL, err := url.Parse(payload.URL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint URL", err}
|
||||||
|
}
|
||||||
|
|
||||||
|
portainerHost, _, err := net.SplitHostPort(portainerURL.Host)
|
||||||
|
if err != nil {
|
||||||
|
portainerHost = portainerURL.Host
|
||||||
|
}
|
||||||
|
|
||||||
|
if portainerHost == "localhost" {
|
||||||
|
return nil, &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint URL", errors.New("cannot use localhost as endpoint URL")}
|
||||||
|
}
|
||||||
|
|
||||||
|
edgeKey := handler.ReverseTunnelService.GenerateEdgeKey(payload.URL, portainerHost, endpointID)
|
||||||
|
|
||||||
|
endpoint := &portainer.Endpoint{
|
||||||
|
ID: portainer.EndpointID(endpointID),
|
||||||
|
Name: payload.Name,
|
||||||
|
URL: portainerHost,
|
||||||
|
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: edgeKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
|
@ -41,7 +41,7 @@ func (handler *Handler) endpointDelete(w http.ResponseWriter, r *http.Request) *
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove endpoint from the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove endpoint from the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
handler.ProxyManager.DeleteProxy(string(endpointID))
|
handler.ProxyManager.DeleteProxy(endpoint)
|
||||||
|
|
||||||
return response.Empty(w)
|
return response.Empty(w)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
package endpoints
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
httperror "github.com/portainer/libhttp/error"
|
||||||
|
"github.com/portainer/libhttp/request"
|
||||||
|
"github.com/portainer/libhttp/response"
|
||||||
|
"github.com/portainer/portainer/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
type endpointStatusInspectResponse struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Port int `json:"port"`
|
||||||
|
Schedules []portainer.EdgeSchedule `json:"schedules"`
|
||||||
|
CheckinInterval int `json:"checkin"`
|
||||||
|
Credentials string `json:"credentials"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET request on /api/endpoints/:id/status
|
||||||
|
func (handler *Handler) endpointStatusInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||||
|
if err == portainer.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}
|
||||||
|
}
|
||||||
|
|
||||||
|
if endpoint.Type != portainer.EdgeAgentEnvironment {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "Status unavailable for non Edge agent endpoints", errors.New("Status unavailable")}
|
||||||
|
}
|
||||||
|
|
||||||
|
edgeIdentifier := r.Header.Get(portainer.PortainerAgentEdgeIDHeader)
|
||||||
|
if edgeIdentifier == "" {
|
||||||
|
return &httperror.HandlerError{http.StatusForbidden, "Missing Edge identifier", errors.New("missing Edge identifier")}
|
||||||
|
}
|
||||||
|
|
||||||
|
if endpoint.EdgeID != "" && endpoint.EdgeID != edgeIdentifier {
|
||||||
|
return &httperror.HandlerError{http.StatusForbidden, "Invalid Edge identifier", errors.New("invalid Edge identifier")}
|
||||||
|
}
|
||||||
|
|
||||||
|
if endpoint.EdgeID == "" {
|
||||||
|
endpoint.EdgeID = edgeIdentifier
|
||||||
|
|
||||||
|
err := handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to Unable to persist endpoint changes inside the database", err}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
settings, err := handler.SettingsService.Settings()
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
|
||||||
|
}
|
||||||
|
|
||||||
|
tunnel := handler.ReverseTunnelService.GetTunnelDetails(endpoint.ID)
|
||||||
|
|
||||||
|
statusResponse := endpointStatusInspectResponse{
|
||||||
|
Status: tunnel.Status,
|
||||||
|
Port: tunnel.Port,
|
||||||
|
Schedules: tunnel.Schedules,
|
||||||
|
CheckinInterval: settings.EdgeAgentCheckinInterval,
|
||||||
|
Credentials: tunnel.Credentials,
|
||||||
|
}
|
||||||
|
|
||||||
|
if tunnel.Status == portainer.EdgeAgentManagementRequired {
|
||||||
|
handler.ReverseTunnelService.SetTunnelStatusToActive(endpoint.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.JSON(w, statusResponse)
|
||||||
|
}
|
|
@ -35,6 +35,8 @@ type Handler struct {
|
||||||
ProxyManager *proxy.Manager
|
ProxyManager *proxy.Manager
|
||||||
Snapshotter portainer.Snapshotter
|
Snapshotter portainer.Snapshotter
|
||||||
JobService portainer.JobService
|
JobService portainer.JobService
|
||||||
|
ReverseTunnelService portainer.ReverseTunnelService
|
||||||
|
SettingsService portainer.SettingsService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler creates a handler to manage endpoint operations.
|
// NewHandler creates a handler to manage endpoint operations.
|
||||||
|
@ -65,5 +67,8 @@ func NewHandler(bouncer *security.RequestBouncer, authorizeEndpointManagement bo
|
||||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointJob))).Methods(http.MethodPost)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointJob))).Methods(http.MethodPost)
|
||||||
h.Handle("/endpoints/{id}/snapshot",
|
h.Handle("/endpoints/{id}/snapshot",
|
||||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointSnapshot))).Methods(http.MethodPost)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointSnapshot))).Methods(http.MethodPost)
|
||||||
|
h.Handle("/endpoints/{id}/status",
|
||||||
|
bouncer.PublicAccess(httperror.LoggerHandler(h.endpointStatusInspect))).Methods(http.MethodGet)
|
||||||
|
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,9 +5,9 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/portainer/libcrypto"
|
||||||
"github.com/portainer/libhttp/response"
|
"github.com/portainer/libhttp/response"
|
||||||
"github.com/portainer/portainer/api"
|
"github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/crypto"
|
|
||||||
"github.com/portainer/portainer/api/http/client"
|
"github.com/portainer/portainer/api/http/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ func (handler *Handler) motd(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
message := strings.Join(data.Message, "\n")
|
message := strings.Join(data.Message, "\n")
|
||||||
|
|
||||||
hash := crypto.HashFromBytes([]byte(message))
|
hash := libcrypto.HashFromBytes([]byte(message))
|
||||||
resp := motdResponse{
|
resp := motdResponse{
|
||||||
Title: data.Title,
|
Title: data.Title,
|
||||||
Message: message,
|
Message: message,
|
||||||
|
|
|
@ -12,12 +12,13 @@ import (
|
||||||
// Handler is the HTTP handler used to handle schedule operations.
|
// Handler is the HTTP handler used to handle schedule operations.
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
*mux.Router
|
*mux.Router
|
||||||
ScheduleService portainer.ScheduleService
|
ScheduleService portainer.ScheduleService
|
||||||
EndpointService portainer.EndpointService
|
EndpointService portainer.EndpointService
|
||||||
SettingsService portainer.SettingsService
|
SettingsService portainer.SettingsService
|
||||||
FileService portainer.FileService
|
FileService portainer.FileService
|
||||||
JobService portainer.JobService
|
JobService portainer.JobService
|
||||||
JobScheduler portainer.JobScheduler
|
JobScheduler portainer.JobScheduler
|
||||||
|
ReverseTunnelService portainer.ReverseTunnelService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler creates a handler to manage schedule operations.
|
// NewHandler creates a handler to manage schedule operations.
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
package schedules
|
package schedules
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/asaskevich/govalidator"
|
"github.com/asaskevich/govalidator"
|
||||||
|
@ -113,7 +115,7 @@ func (payload *scheduleCreateFromFileContentPayload) Validate(r *http.Request) e
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /api/schedules?method=file/string
|
// POST /api/schedules?method=file|string
|
||||||
func (handler *Handler) scheduleCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
func (handler *Handler) scheduleCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
settings, err := handler.SettingsService.Settings()
|
settings, err := handler.SettingsService.Settings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -219,6 +221,46 @@ func (handler *Handler) createScheduleObjectFromFileContentPayload(payload *sche
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *Handler) addAndPersistSchedule(schedule *portainer.Schedule, file []byte) error {
|
func (handler *Handler) addAndPersistSchedule(schedule *portainer.Schedule, file []byte) error {
|
||||||
|
nonEdgeEndpointIDs := make([]portainer.EndpointID, 0)
|
||||||
|
edgeEndpointIDs := make([]portainer.EndpointID, 0)
|
||||||
|
|
||||||
|
edgeCronExpression := strings.Split(schedule.CronExpression, " ")
|
||||||
|
if len(edgeCronExpression) == 6 {
|
||||||
|
edgeCronExpression = edgeCronExpression[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ID := range schedule.ScriptExecutionJob.Endpoints {
|
||||||
|
|
||||||
|
endpoint, err := handler.EndpointService.Endpoint(ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if endpoint.Type != portainer.EdgeAgentEnvironment {
|
||||||
|
nonEdgeEndpointIDs = append(nonEdgeEndpointIDs, endpoint.ID)
|
||||||
|
} else {
|
||||||
|
edgeEndpointIDs = append(edgeEndpointIDs, endpoint.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(edgeEndpointIDs) > 0 {
|
||||||
|
edgeSchedule := &portainer.EdgeSchedule{
|
||||||
|
ID: schedule.ID,
|
||||||
|
CronExpression: strings.Join(edgeCronExpression, " "),
|
||||||
|
Script: base64.RawStdEncoding.EncodeToString(file),
|
||||||
|
Endpoints: edgeEndpointIDs,
|
||||||
|
Version: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, endpointID := range edgeEndpointIDs {
|
||||||
|
handler.ReverseTunnelService.AddSchedule(endpointID, edgeSchedule)
|
||||||
|
}
|
||||||
|
|
||||||
|
schedule.EdgeSchedule = edgeSchedule
|
||||||
|
}
|
||||||
|
|
||||||
|
schedule.ScriptExecutionJob.Endpoints = nonEdgeEndpointIDs
|
||||||
|
|
||||||
scriptPath, err := handler.FileService.StoreScheduledJobFileFromBytes(strconv.Itoa(int(schedule.ID)), file)
|
scriptPath, err := handler.FileService.StoreScheduledJobFileFromBytes(strconv.Itoa(int(schedule.ID)), file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -42,6 +42,8 @@ func (handler *Handler) scheduleDelete(w http.ResponseWriter, r *http.Request) *
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the files associated to the schedule on the filesystem", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the files associated to the schedule on the filesystem", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handler.ReverseTunnelService.RemoveSchedule(schedule.ID)
|
||||||
|
|
||||||
handler.JobScheduler.UnscheduleJob(schedule.ID)
|
handler.JobScheduler.UnscheduleJob(schedule.ID)
|
||||||
|
|
||||||
err = handler.ScheduleService.DeleteSchedule(portainer.ScheduleID(scheduleID))
|
err = handler.ScheduleService.DeleteSchedule(portainer.ScheduleID(scheduleID))
|
||||||
|
|
|
@ -3,6 +3,7 @@ package schedules
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
@ -18,6 +19,7 @@ type taskContainer struct {
|
||||||
Status string `json:"Status"`
|
Status string `json:"Status"`
|
||||||
Created float64 `json:"Created"`
|
Created float64 `json:"Created"`
|
||||||
Labels map[string]string `json:"Labels"`
|
Labels map[string]string `json:"Labels"`
|
||||||
|
Edge bool `json:"Edge"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET request on /api/schedules/:id/tasks
|
// GET request on /api/schedules/:id/tasks
|
||||||
|
@ -64,6 +66,22 @@ func (handler *Handler) scheduleTasks(w http.ResponseWriter, r *http.Request) *h
|
||||||
tasks = append(tasks, endpointTasks...)
|
tasks = append(tasks, endpointTasks...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if schedule.EdgeSchedule != nil {
|
||||||
|
for _, endpointID := range schedule.EdgeSchedule.Endpoints {
|
||||||
|
|
||||||
|
cronTask := taskContainer{
|
||||||
|
ID: fmt.Sprintf("schedule_%d", schedule.EdgeSchedule.ID),
|
||||||
|
EndpointID: endpointID,
|
||||||
|
Edge: true,
|
||||||
|
Status: "",
|
||||||
|
Created: 0,
|
||||||
|
Labels: map[string]string{},
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks = append(tasks, cronTask)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return response.JSON(w, tasks)
|
return response.JSON(w, tasks)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,6 +105,7 @@ func extractTasksFromContainerSnasphot(endpoint *portainer.Endpoint, scheduleID
|
||||||
for _, container := range containers {
|
for _, container := range containers {
|
||||||
if container.Labels["io.portainer.schedule.id"] == strconv.Itoa(int(scheduleID)) {
|
if container.Labels["io.portainer.schedule.id"] == strconv.Itoa(int(scheduleID)) {
|
||||||
container.EndpointID = endpoint.ID
|
container.EndpointID = endpoint.ID
|
||||||
|
container.Edge = false
|
||||||
endpointTasks = append(endpointTasks, container)
|
endpointTasks = append(endpointTasks, container)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package schedules
|
package schedules
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -58,7 +59,15 @@ func (handler *Handler) scheduleUpdate(w http.ResponseWriter, r *http.Request) *
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a schedule with the specified identifier inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a schedule with the specified identifier inside the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateJobSchedule := updateSchedule(schedule, &payload)
|
updateJobSchedule := false
|
||||||
|
if schedule.EdgeSchedule != nil {
|
||||||
|
err := handler.updateEdgeSchedule(schedule, &payload)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update Edge schedule", err}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
updateJobSchedule = updateSchedule(schedule, &payload)
|
||||||
|
}
|
||||||
|
|
||||||
if payload.FileContent != nil {
|
if payload.FileContent != nil {
|
||||||
_, err := handler.FileService.StoreScheduledJobFileFromBytes(strconv.Itoa(scheduleID), []byte(*payload.FileContent))
|
_, err := handler.FileService.StoreScheduledJobFileFromBytes(strconv.Itoa(scheduleID), []byte(*payload.FileContent))
|
||||||
|
@ -85,6 +94,46 @@ func (handler *Handler) scheduleUpdate(w http.ResponseWriter, r *http.Request) *
|
||||||
return response.JSON(w, schedule)
|
return response.JSON(w, schedule)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (handler *Handler) updateEdgeSchedule(schedule *portainer.Schedule, payload *scheduleUpdatePayload) error {
|
||||||
|
if payload.Name != nil {
|
||||||
|
schedule.Name = *payload.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.Endpoints != nil {
|
||||||
|
|
||||||
|
edgeEndpointIDs := make([]portainer.EndpointID, 0)
|
||||||
|
|
||||||
|
for _, ID := range payload.Endpoints {
|
||||||
|
endpoint, err := handler.EndpointService.Endpoint(ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if endpoint.Type == portainer.EdgeAgentEnvironment {
|
||||||
|
edgeEndpointIDs = append(edgeEndpointIDs, endpoint.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
schedule.EdgeSchedule.Endpoints = edgeEndpointIDs
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.CronExpression != nil {
|
||||||
|
schedule.EdgeSchedule.CronExpression = *payload.CronExpression
|
||||||
|
schedule.EdgeSchedule.Version++
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.FileContent != nil {
|
||||||
|
schedule.EdgeSchedule.Script = base64.RawStdEncoding.EncodeToString([]byte(*payload.FileContent))
|
||||||
|
schedule.EdgeSchedule.Version++
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, endpointID := range schedule.EdgeSchedule.Endpoints {
|
||||||
|
handler.ReverseTunnelService.AddSchedule(endpointID, schedule.EdgeSchedule)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func updateSchedule(schedule *portainer.Schedule, payload *scheduleUpdatePayload) bool {
|
func updateSchedule(schedule *portainer.Schedule, payload *scheduleUpdatePayload) bool {
|
||||||
updateJobSchedule := false
|
updateJobSchedule := false
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,7 @@ type settingsUpdatePayload struct {
|
||||||
EnableHostManagementFeatures *bool
|
EnableHostManagementFeatures *bool
|
||||||
SnapshotInterval *string
|
SnapshotInterval *string
|
||||||
TemplatesURL *string
|
TemplatesURL *string
|
||||||
|
EdgeAgentCheckinInterval *int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (payload *settingsUpdatePayload) Validate(r *http.Request) error {
|
func (payload *settingsUpdatePayload) Validate(r *http.Request) error {
|
||||||
|
@ -103,6 +104,10 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if payload.EdgeAgentCheckinInterval != nil {
|
||||||
|
settings.EdgeAgentCheckinInterval = *payload.EdgeAgentCheckinInterval
|
||||||
|
}
|
||||||
|
|
||||||
tlsError := handler.updateTLS(settings)
|
tlsError := handler.updateTLS(settings)
|
||||||
if tlsError != nil {
|
if tlsError != nil {
|
||||||
return tlsError
|
return tlsError
|
||||||
|
|
|
@ -62,8 +62,10 @@ func (handler *Handler) handleAttachRequest(w http.ResponseWriter, r *http.Reque
|
||||||
|
|
||||||
r.Header.Del("Origin")
|
r.Header.Del("Origin")
|
||||||
|
|
||||||
if params.nodeName != "" || params.endpoint.Type == portainer.AgentOnDockerEnvironment {
|
if params.endpoint.Type == portainer.AgentOnDockerEnvironment {
|
||||||
return handler.proxyWebsocketRequest(w, r, params)
|
return handler.proxyAgentWebsocketRequest(w, r, params)
|
||||||
|
} else if params.endpoint.Type == portainer.EdgeAgentEnvironment {
|
||||||
|
return handler.proxyEdgeAgentWebsocketRequest(w, r, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
websocketConn, err := handler.connectionUpgrader.Upgrade(w, r, nil)
|
websocketConn, err := handler.connectionUpgrader.Upgrade(w, r, nil)
|
||||||
|
|
|
@ -68,8 +68,10 @@ 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.endpoint.Type == portainer.AgentOnDockerEnvironment {
|
||||||
return handler.proxyWebsocketRequest(w, r, params)
|
return handler.proxyAgentWebsocketRequest(w, r, params)
|
||||||
|
} else if params.endpoint.Type == portainer.EdgeAgentEnvironment {
|
||||||
|
return handler.proxyEdgeAgentWebsocketRequest(w, r, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
websocketConn, err := handler.connectionUpgrader.Upgrade(w, r, nil)
|
websocketConn, err := handler.connectionUpgrader.Upgrade(w, r, nil)
|
||||||
|
|
|
@ -11,10 +11,11 @@ import (
|
||||||
// Handler is the HTTP handler used to handle websocket operations.
|
// Handler is the HTTP handler used to handle websocket operations.
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
*mux.Router
|
*mux.Router
|
||||||
EndpointService portainer.EndpointService
|
EndpointService portainer.EndpointService
|
||||||
SignatureService portainer.DigitalSignatureService
|
SignatureService portainer.DigitalSignatureService
|
||||||
requestBouncer *security.RequestBouncer
|
ReverseTunnelService portainer.ReverseTunnelService
|
||||||
connectionUpgrader websocket.Upgrader
|
requestBouncer *security.RequestBouncer
|
||||||
|
connectionUpgrader websocket.Upgrader
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler creates a handler to manage websocket operations.
|
// NewHandler creates a handler to manage websocket operations.
|
||||||
|
|
|
@ -2,14 +2,37 @@ package websocket
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/koding/websocketproxy"
|
"github.com/koding/websocketproxy"
|
||||||
"github.com/portainer/portainer/api"
|
"github.com/portainer/portainer/api"
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (handler *Handler) proxyWebsocketRequest(w http.ResponseWriter, r *http.Request, params *webSocketRequestParams) error {
|
func (handler *Handler) proxyEdgeAgentWebsocketRequest(w http.ResponseWriter, r *http.Request, params *webSocketRequestParams) error {
|
||||||
|
tunnel := handler.ReverseTunnelService.GetTunnelDetails(params.endpoint.ID)
|
||||||
|
|
||||||
|
endpointURL, err := url.Parse(fmt.Sprintf("http://localhost:%d", tunnel.Port))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
endpointURL.Scheme = "ws"
|
||||||
|
proxy := websocketproxy.NewProxy(endpointURL)
|
||||||
|
|
||||||
|
proxy.Director = func(incoming *http.Request, out http.Header) {
|
||||||
|
out.Set(portainer.PortainerAgentTargetHeader, params.nodeName)
|
||||||
|
}
|
||||||
|
|
||||||
|
handler.ReverseTunnelService.SetTunnelStatusToActive(params.endpoint.ID)
|
||||||
|
proxy.ServeHTTP(w, r)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler *Handler) proxyAgentWebsocketRequest(w http.ResponseWriter, r *http.Request, params *webSocketRequestParams) error {
|
||||||
agentURL, err := url.Parse(params.endpoint.URL)
|
agentURL, err := url.Parse(params.endpoint.URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -24,7 +24,9 @@ type (
|
||||||
DockerHubService portainer.DockerHubService
|
DockerHubService portainer.DockerHubService
|
||||||
SettingsService portainer.SettingsService
|
SettingsService portainer.SettingsService
|
||||||
SignatureService portainer.DigitalSignatureService
|
SignatureService portainer.DigitalSignatureService
|
||||||
|
ReverseTunnelService portainer.ReverseTunnelService
|
||||||
endpointIdentifier portainer.EndpointID
|
endpointIdentifier portainer.EndpointID
|
||||||
|
endpointType portainer.EndpointType
|
||||||
}
|
}
|
||||||
restrictedDockerOperationContext struct {
|
restrictedDockerOperationContext struct {
|
||||||
isAdmin bool
|
isAdmin bool
|
||||||
|
@ -58,7 +60,19 @@ func (p *proxyTransport) RoundTrip(request *http.Request) (*http.Response, error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *proxyTransport) executeDockerRequest(request *http.Request) (*http.Response, error) {
|
func (p *proxyTransport) executeDockerRequest(request *http.Request) (*http.Response, error) {
|
||||||
return p.dockerTransport.RoundTrip(request)
|
response, err := p.dockerTransport.RoundTrip(request)
|
||||||
|
|
||||||
|
if p.endpointType != portainer.EdgeAgentEnvironment {
|
||||||
|
return response, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
p.ReverseTunnelService.SetTunnelStatusToActive(p.endpointIdentifier)
|
||||||
|
} else {
|
||||||
|
p.ReverseTunnelService.SetTunnelStatusToIdle(p.endpointIdentifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *proxyTransport) proxyDockerRequest(request *http.Request) (*http.Response, error) {
|
func (p *proxyTransport) proxyDockerRequest(request *http.Request) (*http.Response, error) {
|
||||||
|
|
|
@ -21,6 +21,7 @@ type proxyFactory struct {
|
||||||
RegistryService portainer.RegistryService
|
RegistryService portainer.RegistryService
|
||||||
DockerHubService portainer.DockerHubService
|
DockerHubService portainer.DockerHubService
|
||||||
SignatureService portainer.DigitalSignatureService
|
SignatureService portainer.DigitalSignatureService
|
||||||
|
ReverseTunnelService portainer.ReverseTunnelService
|
||||||
}
|
}
|
||||||
|
|
||||||
func (factory *proxyFactory) newHTTPProxy(u *url.URL) http.Handler {
|
func (factory *proxyFactory) newHTTPProxy(u *url.URL) http.Handler {
|
||||||
|
@ -29,21 +30,21 @@ func (factory *proxyFactory) newHTTPProxy(u *url.URL) http.Handler {
|
||||||
}
|
}
|
||||||
|
|
||||||
func newAzureProxy(credentials *portainer.AzureCredentials) (http.Handler, error) {
|
func newAzureProxy(credentials *portainer.AzureCredentials) (http.Handler, error) {
|
||||||
url, err := url.Parse(AzureAPIBaseURL)
|
remoteURL, err := url.Parse(AzureAPIBaseURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
proxy := newSingleHostReverseProxyWithHostHeader(url)
|
proxy := newSingleHostReverseProxyWithHostHeader(remoteURL)
|
||||||
proxy.Transport = NewAzureTransport(credentials)
|
proxy.Transport = NewAzureTransport(credentials)
|
||||||
|
|
||||||
return proxy, nil
|
return proxy, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (factory *proxyFactory) newDockerHTTPSProxy(u *url.URL, tlsConfig *portainer.TLSConfiguration, enableSignature bool, endpointID portainer.EndpointID) (http.Handler, error) {
|
func (factory *proxyFactory) newDockerHTTPSProxy(u *url.URL, tlsConfig *portainer.TLSConfiguration, endpoint *portainer.Endpoint) (http.Handler, error) {
|
||||||
u.Scheme = "https"
|
u.Scheme = "https"
|
||||||
|
|
||||||
proxy := factory.createDockerReverseProxy(u, enableSignature, endpointID)
|
proxy := factory.createDockerReverseProxy(u, endpoint)
|
||||||
config, err := crypto.CreateTLSConfigurationFromDisk(tlsConfig.TLSCACertPath, tlsConfig.TLSCertPath, tlsConfig.TLSKeyPath, tlsConfig.TLSSkipVerify)
|
config, err := crypto.CreateTLSConfigurationFromDisk(tlsConfig.TLSCACertPath, tlsConfig.TLSCertPath, tlsConfig.TLSKeyPath, tlsConfig.TLSSkipVerify)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -53,13 +54,19 @@ func (factory *proxyFactory) newDockerHTTPSProxy(u *url.URL, tlsConfig *portaine
|
||||||
return proxy, nil
|
return proxy, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (factory *proxyFactory) newDockerHTTPProxy(u *url.URL, enableSignature bool, endpointID portainer.EndpointID) http.Handler {
|
func (factory *proxyFactory) newDockerHTTPProxy(u *url.URL, endpoint *portainer.Endpoint) http.Handler {
|
||||||
u.Scheme = "http"
|
u.Scheme = "http"
|
||||||
return factory.createDockerReverseProxy(u, enableSignature, endpointID)
|
return factory.createDockerReverseProxy(u, endpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (factory *proxyFactory) createDockerReverseProxy(u *url.URL, enableSignature bool, endpointID portainer.EndpointID) *httputil.ReverseProxy {
|
func (factory *proxyFactory) createDockerReverseProxy(u *url.URL, endpoint *portainer.Endpoint) *httputil.ReverseProxy {
|
||||||
proxy := newSingleHostReverseProxyWithHostHeader(u)
|
proxy := newSingleHostReverseProxyWithHostHeader(u)
|
||||||
|
|
||||||
|
enableSignature := false
|
||||||
|
if endpoint.Type == portainer.AgentOnDockerEnvironment {
|
||||||
|
enableSignature = true
|
||||||
|
}
|
||||||
|
|
||||||
transport := &proxyTransport{
|
transport := &proxyTransport{
|
||||||
enableSignature: enableSignature,
|
enableSignature: enableSignature,
|
||||||
ResourceControlService: factory.ResourceControlService,
|
ResourceControlService: factory.ResourceControlService,
|
||||||
|
@ -67,8 +74,10 @@ func (factory *proxyFactory) createDockerReverseProxy(u *url.URL, enableSignatur
|
||||||
SettingsService: factory.SettingsService,
|
SettingsService: factory.SettingsService,
|
||||||
RegistryService: factory.RegistryService,
|
RegistryService: factory.RegistryService,
|
||||||
DockerHubService: factory.DockerHubService,
|
DockerHubService: factory.DockerHubService,
|
||||||
|
ReverseTunnelService: factory.ReverseTunnelService,
|
||||||
dockerTransport: &http.Transport{},
|
dockerTransport: &http.Transport{},
|
||||||
endpointIdentifier: endpointID,
|
endpointIdentifier: endpoint.ID,
|
||||||
|
endpointType: endpoint.Type,
|
||||||
}
|
}
|
||||||
|
|
||||||
if enableSignature {
|
if enableSignature {
|
||||||
|
|
|
@ -8,7 +8,7 @@ import (
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (factory *proxyFactory) newLocalProxy(path string, endpointID portainer.EndpointID) http.Handler {
|
func (factory *proxyFactory) newLocalProxy(path string, endpoint *portainer.Endpoint) http.Handler {
|
||||||
proxy := &localProxy{}
|
proxy := &localProxy{}
|
||||||
transport := &proxyTransport{
|
transport := &proxyTransport{
|
||||||
enableSignature: false,
|
enableSignature: false,
|
||||||
|
@ -18,7 +18,9 @@ func (factory *proxyFactory) newLocalProxy(path string, endpointID portainer.End
|
||||||
RegistryService: factory.RegistryService,
|
RegistryService: factory.RegistryService,
|
||||||
DockerHubService: factory.DockerHubService,
|
DockerHubService: factory.DockerHubService,
|
||||||
dockerTransport: newSocketTransport(path),
|
dockerTransport: newSocketTransport(path),
|
||||||
endpointIdentifier: endpointID,
|
ReverseTunnelService: factory.ReverseTunnelService,
|
||||||
|
endpointIdentifier: endpoint.ID,
|
||||||
|
endpointType: endpoint.Type,
|
||||||
}
|
}
|
||||||
proxy.Transport = transport
|
proxy.Transport = transport
|
||||||
return proxy
|
return proxy
|
||||||
|
|
|
@ -5,12 +5,11 @@ package proxy
|
||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"github.com/Microsoft/go-winio"
|
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (factory *proxyFactory) newLocalProxy(path string, endpointID portainer.EndpointID) http.Handler {
|
func (factory *proxyFactory) newLocalProxy(path string, endpoint *portainer.Endpoint) http.Handler {
|
||||||
proxy := &localProxy{}
|
proxy := &localProxy{}
|
||||||
transport := &proxyTransport{
|
transport := &proxyTransport{
|
||||||
enableSignature: false,
|
enableSignature: false,
|
||||||
|
@ -19,8 +18,10 @@ func (factory *proxyFactory) newLocalProxy(path string, endpointID portainer.End
|
||||||
SettingsService: factory.SettingsService,
|
SettingsService: factory.SettingsService,
|
||||||
RegistryService: factory.RegistryService,
|
RegistryService: factory.RegistryService,
|
||||||
DockerHubService: factory.DockerHubService,
|
DockerHubService: factory.DockerHubService,
|
||||||
|
ReverseTunnelService: factory.ReverseTunnelService,
|
||||||
dockerTransport: newNamedPipeTransport(path),
|
dockerTransport: newNamedPipeTransport(path),
|
||||||
endpointIdentifier: endpointID,
|
endpointIdentifier: endpoint.ID,
|
||||||
|
endpointType: endpoint.Type,
|
||||||
}
|
}
|
||||||
proxy.Transport = transport
|
proxy.Transport = transport
|
||||||
return proxy
|
return proxy
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package proxy
|
package proxy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -21,6 +22,7 @@ type (
|
||||||
// Manager represents a service used to manage Docker proxies.
|
// Manager represents a service used to manage Docker proxies.
|
||||||
Manager struct {
|
Manager struct {
|
||||||
proxyFactory *proxyFactory
|
proxyFactory *proxyFactory
|
||||||
|
reverseTunnelService portainer.ReverseTunnelService
|
||||||
proxies cmap.ConcurrentMap
|
proxies cmap.ConcurrentMap
|
||||||
extensionProxies cmap.ConcurrentMap
|
extensionProxies cmap.ConcurrentMap
|
||||||
legacyExtensionProxies cmap.ConcurrentMap
|
legacyExtensionProxies cmap.ConcurrentMap
|
||||||
|
@ -34,6 +36,7 @@ type (
|
||||||
RegistryService portainer.RegistryService
|
RegistryService portainer.RegistryService
|
||||||
DockerHubService portainer.DockerHubService
|
DockerHubService portainer.DockerHubService
|
||||||
SignatureService portainer.DigitalSignatureService
|
SignatureService portainer.DigitalSignatureService
|
||||||
|
ReverseTunnelService portainer.ReverseTunnelService
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -50,13 +53,15 @@ func NewManager(parameters *ManagerParams) *Manager {
|
||||||
RegistryService: parameters.RegistryService,
|
RegistryService: parameters.RegistryService,
|
||||||
DockerHubService: parameters.DockerHubService,
|
DockerHubService: parameters.DockerHubService,
|
||||||
SignatureService: parameters.SignatureService,
|
SignatureService: parameters.SignatureService,
|
||||||
|
ReverseTunnelService: parameters.ReverseTunnelService,
|
||||||
},
|
},
|
||||||
|
reverseTunnelService: parameters.ReverseTunnelService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetProxy returns the proxy associated to a key
|
// GetProxy returns the proxy associated to a key
|
||||||
func (manager *Manager) GetProxy(key string) http.Handler {
|
func (manager *Manager) GetProxy(endpoint *portainer.Endpoint) http.Handler {
|
||||||
proxy, ok := manager.proxies.Get(key)
|
proxy, ok := manager.proxies.Get(string(endpoint.ID))
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -76,8 +81,8 @@ func (manager *Manager) CreateAndRegisterProxy(endpoint *portainer.Endpoint) (ht
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteProxy deletes the proxy associated to a key
|
// DeleteProxy deletes the proxy associated to a key
|
||||||
func (manager *Manager) DeleteProxy(key string) {
|
func (manager *Manager) DeleteProxy(endpoint *portainer.Endpoint) {
|
||||||
manager.proxies.Remove(key)
|
manager.proxies.Remove(string(endpoint.ID))
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetExtensionProxy returns an extension proxy associated to an extension identifier
|
// GetExtensionProxy returns an extension proxy associated to an extension identifier
|
||||||
|
@ -136,28 +141,40 @@ func (manager *Manager) CreateLegacyExtensionProxy(key, extensionAPIURL string)
|
||||||
return proxy, nil
|
return proxy, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (manager *Manager) createDockerProxy(endpointURL *url.URL, tlsConfig *portainer.TLSConfiguration, endpointID portainer.EndpointID) (http.Handler, error) {
|
func (manager *Manager) createDockerProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
|
||||||
if endpointURL.Scheme == "tcp" {
|
baseURL := endpoint.URL
|
||||||
if tlsConfig.TLS || tlsConfig.TLSSkipVerify {
|
if endpoint.Type == portainer.EdgeAgentEnvironment {
|
||||||
return manager.proxyFactory.newDockerHTTPSProxy(endpointURL, tlsConfig, false, endpointID)
|
tunnel := manager.reverseTunnelService.GetTunnelDetails(endpoint.ID)
|
||||||
}
|
baseURL = fmt.Sprintf("http://localhost:%d", tunnel.Port)
|
||||||
return manager.proxyFactory.newDockerHTTPProxy(endpointURL, false, endpointID), nil
|
|
||||||
}
|
}
|
||||||
return manager.proxyFactory.newLocalProxy(endpointURL.Path, endpointID), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (manager *Manager) createProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
|
endpointURL, err := url.Parse(baseURL)
|
||||||
endpointURL, err := url.Parse(endpoint.URL)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
switch endpoint.Type {
|
switch endpoint.Type {
|
||||||
case portainer.AgentOnDockerEnvironment:
|
case portainer.AgentOnDockerEnvironment:
|
||||||
return manager.proxyFactory.newDockerHTTPSProxy(endpointURL, &endpoint.TLSConfig, true, endpoint.ID)
|
return manager.proxyFactory.newDockerHTTPSProxy(endpointURL, &endpoint.TLSConfig, endpoint)
|
||||||
case portainer.AzureEnvironment:
|
case portainer.EdgeAgentEnvironment:
|
||||||
return newAzureProxy(&endpoint.AzureCredentials)
|
return manager.proxyFactory.newDockerHTTPProxy(endpointURL, endpoint), nil
|
||||||
default:
|
|
||||||
return manager.createDockerProxy(endpointURL, &endpoint.TLSConfig, endpoint.ID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if endpointURL.Scheme == "tcp" {
|
||||||
|
if endpoint.TLSConfig.TLS || endpoint.TLSConfig.TLSSkipVerify {
|
||||||
|
return manager.proxyFactory.newDockerHTTPSProxy(endpointURL, &endpoint.TLSConfig, endpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
return manager.proxyFactory.newDockerHTTPProxy(endpointURL, endpoint), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return manager.proxyFactory.newLocalProxy(endpointURL.Path, endpoint), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (manager *Manager) createProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
|
||||||
|
if endpoint.Type == portainer.AzureEnvironment {
|
||||||
|
return newAzureProxy(&endpoint.AzureCredentials)
|
||||||
|
}
|
||||||
|
|
||||||
|
return manager.createDockerProxy(endpoint)
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,6 +44,7 @@ type Server struct {
|
||||||
AuthDisabled bool
|
AuthDisabled bool
|
||||||
EndpointManagement bool
|
EndpointManagement bool
|
||||||
Status *portainer.Status
|
Status *portainer.Status
|
||||||
|
ReverseTunnelService portainer.ReverseTunnelService
|
||||||
ExtensionManager portainer.ExtensionManager
|
ExtensionManager portainer.ExtensionManager
|
||||||
ComposeStackManager portainer.ComposeStackManager
|
ComposeStackManager portainer.ComposeStackManager
|
||||||
CryptoService portainer.CryptoService
|
CryptoService portainer.CryptoService
|
||||||
|
@ -88,6 +89,7 @@ func (server *Server) Start() error {
|
||||||
RegistryService: server.RegistryService,
|
RegistryService: server.RegistryService,
|
||||||
DockerHubService: server.DockerHubService,
|
DockerHubService: server.DockerHubService,
|
||||||
SignatureService: server.SignatureService,
|
SignatureService: server.SignatureService,
|
||||||
|
ReverseTunnelService: server.ReverseTunnelService,
|
||||||
}
|
}
|
||||||
proxyManager := proxy.NewManager(proxyManagerParameters)
|
proxyManager := proxy.NewManager(proxyManagerParameters)
|
||||||
|
|
||||||
|
@ -132,6 +134,8 @@ 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.ReverseTunnelService = server.ReverseTunnelService
|
||||||
|
endpointHandler.SettingsService = server.SettingsService
|
||||||
|
|
||||||
var endpointGroupHandler = endpointgroups.NewHandler(requestBouncer)
|
var endpointGroupHandler = endpointgroups.NewHandler(requestBouncer)
|
||||||
endpointGroupHandler.EndpointGroupService = server.EndpointGroupService
|
endpointGroupHandler.EndpointGroupService = server.EndpointGroupService
|
||||||
|
@ -140,6 +144,8 @@ func (server *Server) Start() error {
|
||||||
var endpointProxyHandler = endpointproxy.NewHandler(requestBouncer)
|
var endpointProxyHandler = endpointproxy.NewHandler(requestBouncer)
|
||||||
endpointProxyHandler.EndpointService = server.EndpointService
|
endpointProxyHandler.EndpointService = server.EndpointService
|
||||||
endpointProxyHandler.ProxyManager = proxyManager
|
endpointProxyHandler.ProxyManager = proxyManager
|
||||||
|
endpointProxyHandler.SettingsService = server.SettingsService
|
||||||
|
endpointProxyHandler.ReverseTunnelService = server.ReverseTunnelService
|
||||||
|
|
||||||
var fileHandler = file.NewHandler(filepath.Join(server.AssetsPath, "public"))
|
var fileHandler = file.NewHandler(filepath.Join(server.AssetsPath, "public"))
|
||||||
|
|
||||||
|
@ -168,6 +174,7 @@ func (server *Server) Start() error {
|
||||||
schedulesHandler.JobService = server.JobService
|
schedulesHandler.JobService = server.JobService
|
||||||
schedulesHandler.JobScheduler = server.JobScheduler
|
schedulesHandler.JobScheduler = server.JobScheduler
|
||||||
schedulesHandler.SettingsService = server.SettingsService
|
schedulesHandler.SettingsService = server.SettingsService
|
||||||
|
schedulesHandler.ReverseTunnelService = server.ReverseTunnelService
|
||||||
|
|
||||||
var settingsHandler = settings.NewHandler(requestBouncer)
|
var settingsHandler = settings.NewHandler(requestBouncer)
|
||||||
settingsHandler.SettingsService = server.SettingsService
|
settingsHandler.SettingsService = server.SettingsService
|
||||||
|
@ -216,6 +223,7 @@ func (server *Server) Start() error {
|
||||||
var websocketHandler = websocket.NewHandler(requestBouncer)
|
var websocketHandler = websocket.NewHandler(requestBouncer)
|
||||||
websocketHandler.EndpointService = server.EndpointService
|
websocketHandler.EndpointService = server.EndpointService
|
||||||
websocketHandler.SignatureService = server.SignatureService
|
websocketHandler.SignatureService = server.SignatureService
|
||||||
|
websocketHandler.ReverseTunnelService = server.ReverseTunnelService
|
||||||
|
|
||||||
var webhookHandler = webhooks.NewHandler(requestBouncer)
|
var webhookHandler = webhooks.NewHandler(requestBouncer)
|
||||||
webhookHandler.WebhookService = server.WebhookService
|
webhookHandler.WebhookService = server.WebhookService
|
||||||
|
|
|
@ -2,6 +2,7 @@ package libcompose
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
@ -17,19 +18,28 @@ import (
|
||||||
|
|
||||||
// ComposeStackManager represents a service for managing compose stacks.
|
// ComposeStackManager represents a service for managing compose stacks.
|
||||||
type ComposeStackManager struct {
|
type ComposeStackManager struct {
|
||||||
dataPath string
|
dataPath string
|
||||||
|
reverseTunnelService portainer.ReverseTunnelService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewComposeStackManager initializes a new ComposeStackManager service.
|
// NewComposeStackManager initializes a new ComposeStackManager service.
|
||||||
func NewComposeStackManager(dataPath string) *ComposeStackManager {
|
func NewComposeStackManager(dataPath string, reverseTunnelService portainer.ReverseTunnelService) *ComposeStackManager {
|
||||||
return &ComposeStackManager{
|
return &ComposeStackManager{
|
||||||
dataPath: dataPath,
|
dataPath: dataPath,
|
||||||
|
reverseTunnelService: reverseTunnelService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func createClient(endpoint *portainer.Endpoint) (client.Factory, error) {
|
func (manager *ComposeStackManager) createClient(endpoint *portainer.Endpoint) (client.Factory, error) {
|
||||||
|
|
||||||
|
endpointURL := endpoint.URL
|
||||||
|
if endpoint.Type == portainer.EdgeAgentEnvironment {
|
||||||
|
tunnel := manager.reverseTunnelService.GetTunnelDetails(endpoint.ID)
|
||||||
|
endpointURL = fmt.Sprintf("tcp://localhost:%d", tunnel.Port)
|
||||||
|
}
|
||||||
|
|
||||||
clientOpts := client.Options{
|
clientOpts := client.Options{
|
||||||
Host: endpoint.URL,
|
Host: endpointURL,
|
||||||
APIVersion: portainer.SupportedDockerAPIVersion,
|
APIVersion: portainer.SupportedDockerAPIVersion,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,7 +57,7 @@ func createClient(endpoint *portainer.Endpoint) (client.Factory, error) {
|
||||||
// Up will deploy a compose stack (equivalent of docker-compose up)
|
// Up will deploy a compose stack (equivalent of docker-compose up)
|
||||||
func (manager *ComposeStackManager) Up(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
func (manager *ComposeStackManager) Up(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||||
|
|
||||||
clientFactory, err := createClient(endpoint)
|
clientFactory, err := manager.createClient(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -85,7 +95,7 @@ func (manager *ComposeStackManager) Up(stack *portainer.Stack, endpoint *portain
|
||||||
|
|
||||||
// Down will shutdown a compose stack (equivalent of docker-compose down)
|
// Down will shutdown a compose stack (equivalent of docker-compose down)
|
||||||
func (manager *ComposeStackManager) Down(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
func (manager *ComposeStackManager) Down(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||||
clientFactory, err := createClient(endpoint)
|
clientFactory, err := manager.createClient(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package portainer
|
package portainer
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
type (
|
type (
|
||||||
// Pair defines a key/value string pair
|
// Pair defines a key/value string pair
|
||||||
Pair struct {
|
Pair struct {
|
||||||
|
@ -10,6 +12,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
|
||||||
|
@ -105,6 +109,7 @@ type (
|
||||||
SnapshotInterval string `json:"SnapshotInterval"`
|
SnapshotInterval string `json:"SnapshotInterval"`
|
||||||
TemplatesURL string `json:"TemplatesURL"`
|
TemplatesURL string `json:"TemplatesURL"`
|
||||||
EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"`
|
EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"`
|
||||||
|
EdgeAgentCheckinInterval int `json:"EdgeAgentCheckinInterval"`
|
||||||
|
|
||||||
// Deprecated fields
|
// Deprecated fields
|
||||||
DisplayDonationHeader bool
|
DisplayDonationHeader bool
|
||||||
|
@ -250,7 +255,8 @@ 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"`
|
||||||
|
EdgeID string `json:"EdgeID,omitempty"`
|
||||||
|
EdgeKey string `json:"EdgeKey"`
|
||||||
// Deprecated fields
|
// Deprecated fields
|
||||||
// Deprecated in DBVersion == 4
|
// Deprecated in DBVersion == 4
|
||||||
TLS bool `json:"TLS,omitempty"`
|
TLS bool `json:"TLS,omitempty"`
|
||||||
|
@ -333,11 +339,21 @@ type (
|
||||||
Recurring bool
|
Recurring bool
|
||||||
Created int64
|
Created int64
|
||||||
JobType JobType
|
JobType JobType
|
||||||
|
EdgeSchedule *EdgeSchedule
|
||||||
ScriptExecutionJob *ScriptExecutionJob
|
ScriptExecutionJob *ScriptExecutionJob
|
||||||
SnapshotJob *SnapshotJob
|
SnapshotJob *SnapshotJob
|
||||||
EndpointSyncJob *EndpointSyncJob
|
EndpointSyncJob *EndpointSyncJob
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EdgeSchedule represents a scheduled job that can run on Edge environments.
|
||||||
|
EdgeSchedule struct {
|
||||||
|
ID ScheduleID `json:"Id"`
|
||||||
|
CronExpression string `json:"CronExpression"`
|
||||||
|
Script string `json:"Script"`
|
||||||
|
Version int `json:"Version"`
|
||||||
|
Endpoints []EndpointID `json:"Endpoints"`
|
||||||
|
}
|
||||||
|
|
||||||
// WebhookID represents a webhook identifier.
|
// WebhookID represents a webhook identifier.
|
||||||
WebhookID int
|
WebhookID int
|
||||||
|
|
||||||
|
@ -575,6 +591,20 @@ type (
|
||||||
Valid bool `json:"Valid,omitempty"`
|
Valid bool `json:"Valid,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TunnelDetails represents information associated to a tunnel
|
||||||
|
TunnelDetails struct {
|
||||||
|
Status string
|
||||||
|
LastActivity time.Time
|
||||||
|
Port int
|
||||||
|
Schedules []EdgeSchedule
|
||||||
|
Credentials string
|
||||||
|
}
|
||||||
|
|
||||||
|
// TunnelServerInfo represents information associated to the tunnel server
|
||||||
|
TunnelServerInfo struct {
|
||||||
|
PrivateKeySeed string `json:"PrivateKeySeed"`
|
||||||
|
}
|
||||||
|
|
||||||
// CLIService represents a service for managing CLI
|
// CLIService represents a service for managing CLI
|
||||||
CLIService interface {
|
CLIService interface {
|
||||||
ParseFlags(version string) (*CLIFlags, error)
|
ParseFlags(version string) (*CLIFlags, error)
|
||||||
|
@ -692,6 +722,12 @@ type (
|
||||||
StoreDBVersion(version int) error
|
StoreDBVersion(version int) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TunnelServerService represents a service for managing data associated to the tunnel server
|
||||||
|
TunnelServerService interface {
|
||||||
|
Info() (*TunnelServerInfo, error)
|
||||||
|
UpdateInfo(info *TunnelServerInfo) error
|
||||||
|
}
|
||||||
|
|
||||||
// WebhookService represents a service for managing webhook data.
|
// WebhookService represents a service for managing webhook data.
|
||||||
WebhookService interface {
|
WebhookService interface {
|
||||||
Webhooks() ([]Webhook, error)
|
Webhooks() ([]Webhook, error)
|
||||||
|
@ -850,13 +886,25 @@ type (
|
||||||
DisableExtension(extension *Extension) error
|
DisableExtension(extension *Extension) error
|
||||||
UpdateExtension(extension *Extension, version string) error
|
UpdateExtension(extension *Extension, version string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReverseTunnelService represensts a service used to manage reverse tunnel connections.
|
||||||
|
ReverseTunnelService interface {
|
||||||
|
StartTunnelServer(addr, port string, snapshotter Snapshotter) error
|
||||||
|
GenerateEdgeKey(url, host string, endpointIdentifier int) string
|
||||||
|
SetTunnelStatusToActive(endpointID EndpointID)
|
||||||
|
SetTunnelStatusToRequired(endpointID EndpointID) error
|
||||||
|
SetTunnelStatusToIdle(endpointID EndpointID)
|
||||||
|
GetTunnelDetails(endpointID EndpointID) *TunnelDetails
|
||||||
|
AddSchedule(endpointID EndpointID, schedule *EdgeSchedule)
|
||||||
|
RemoveSchedule(scheduleID ScheduleID)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// APIVersion is the version number of the Portainer API
|
// APIVersion is the version number of the Portainer API
|
||||||
APIVersion = "1.21.0"
|
APIVersion = "1.21.0"
|
||||||
// DBVersion is the version number of the Portainer database
|
// DBVersion is the version number of the Portainer database
|
||||||
DBVersion = 18
|
DBVersion = 19
|
||||||
// AssetsServerURL represents the URL of the Portainer asset server
|
// AssetsServerURL represents the URL of the Portainer asset server
|
||||||
AssetsServerURL = "https://portainer-io-assets.sfo2.digitaloceanspaces.com"
|
AssetsServerURL = "https://portainer-io-assets.sfo2.digitaloceanspaces.com"
|
||||||
// MessageOfTheDayURL represents the URL where Portainer MOTD message can be retrieved
|
// MessageOfTheDayURL represents the URL where Portainer MOTD message can be retrieved
|
||||||
|
@ -865,6 +913,8 @@ const (
|
||||||
ExtensionDefinitionsURL = AssetsServerURL + "/extensions-1.21.0.json"
|
ExtensionDefinitionsURL = AssetsServerURL + "/extensions-1.21.0.json"
|
||||||
// PortainerAgentHeader represents the name of the header available in any agent response
|
// PortainerAgentHeader represents the name of the header available in any agent response
|
||||||
PortainerAgentHeader = "Portainer-Agent"
|
PortainerAgentHeader = "Portainer-Agent"
|
||||||
|
// PortainerAgentEdgeIDHeader represent the name of the header containing the Edge ID associated to an agent/agent cluster
|
||||||
|
PortainerAgentEdgeIDHeader = "X-PortainerAgent-EdgeID"
|
||||||
// PortainerAgentTargetHeader represent the name of the header containing the target node name
|
// PortainerAgentTargetHeader represent the name of the header containing the target node name
|
||||||
PortainerAgentTargetHeader = "X-PortainerAgent-Target"
|
PortainerAgentTargetHeader = "X-PortainerAgent-Target"
|
||||||
// PortainerAgentSignatureHeader represent the name of the header containing the digital signature
|
// PortainerAgentSignatureHeader represent the name of the header containing the digital signature
|
||||||
|
@ -878,6 +928,8 @@ const (
|
||||||
SupportedDockerAPIVersion = "1.24"
|
SupportedDockerAPIVersion = "1.24"
|
||||||
// ExtensionServer represents the server used by Portainer to communicate with extensions
|
// ExtensionServer represents the server used by Portainer to communicate with extensions
|
||||||
ExtensionServer = "localhost"
|
ExtensionServer = "localhost"
|
||||||
|
// DefaultEdgeAgentCheckinIntervalInSeconds represents the default interval (in seconds) used by Edge agents to checkin with the Portainer instance
|
||||||
|
DefaultEdgeAgentCheckinIntervalInSeconds = 5
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -953,6 +1005,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 (
|
||||||
|
@ -1019,6 +1073,15 @@ const (
|
||||||
CustomRegistry
|
CustomRegistry
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// EdgeAgentIdle represents an idle state for a tunnel connected to an Edge endpoint.
|
||||||
|
EdgeAgentIdle string = "IDLE"
|
||||||
|
// EdgeAgentManagementRequired represents a required state for a tunnel connected to an Edge endpoint
|
||||||
|
EdgeAgentManagementRequired string = "REQUIRED"
|
||||||
|
// EdgeAgentActive represents an active state for a tunnel connected to an Edge endpoint
|
||||||
|
EdgeAgentActive string = "ACTIVE"
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
OperationDockerContainerArchiveInfo Authorization = "DockerContainerArchiveInfo"
|
OperationDockerContainerArchiveInfo Authorization = "DockerContainerArchiveInfo"
|
||||||
OperationDockerContainerList Authorization = "DockerContainerList"
|
OperationDockerContainerList Authorization = "DockerContainerList"
|
||||||
|
|
16
app/app.js
16
app/app.js
|
@ -1,8 +1,8 @@
|
||||||
import _ from 'lodash-es';
|
import _ from 'lodash-es';
|
||||||
|
|
||||||
angular.module('portainer')
|
angular.module('portainer')
|
||||||
.run(['$rootScope', '$state', 'Authentication', 'authManager', 'StateManager', 'EndpointProvider', 'Notifications', 'Analytics', 'cfpLoadingBar', '$transitions', 'HttpRequestHelper',
|
.run(['$rootScope', '$state', '$interval', 'Authentication', 'authManager', 'StateManager', 'EndpointProvider', 'Notifications', 'Analytics', 'SystemService', 'cfpLoadingBar', '$transitions', 'HttpRequestHelper',
|
||||||
function ($rootScope, $state, Authentication, authManager, StateManager, EndpointProvider, Notifications, Analytics, cfpLoadingBar, $transitions, HttpRequestHelper) {
|
function ($rootScope, $state, $interval, Authentication, authManager, StateManager, EndpointProvider, Notifications, Analytics, SystemService, cfpLoadingBar, $transitions, HttpRequestHelper) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
EndpointProvider.initialize();
|
EndpointProvider.initialize();
|
||||||
|
@ -34,8 +34,20 @@ function ($rootScope, $state, Authentication, authManager, StateManager, Endpoin
|
||||||
$transitions.onBefore({}, function() {
|
$transitions.onBefore({}, function() {
|
||||||
HttpRequestHelper.resetAgentHeaders();
|
HttpRequestHelper.resetAgentHeaders();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Keep-alive Edge endpoints by sending a ping request every minute
|
||||||
|
$interval(function() {
|
||||||
|
ping(EndpointProvider, SystemService);
|
||||||
|
}, 60 * 1000)
|
||||||
|
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
|
function ping(EndpointProvider, SystemService) {
|
||||||
|
let endpoint = EndpointProvider.currentEndpoint();
|
||||||
|
if (endpoint !== undefined && endpoint.Type === 4) {
|
||||||
|
SystemService.ping(endpoint.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function initAuthentication(authManager, Authentication, $rootScope, $state) {
|
function initAuthentication(authManager, Authentication, $rootScope, $state) {
|
||||||
authManager.checkAuthOnRefresh();
|
authManager.checkAuthOnRefresh();
|
||||||
|
|
|
@ -13,6 +13,7 @@ angular.module('portainer.docker')
|
||||||
showQuickActionLogs: true,
|
showQuickActionLogs: true,
|
||||||
showQuickActionConsole: true,
|
showQuickActionConsole: true,
|
||||||
showQuickActionInspect: true,
|
showQuickActionInspect: true,
|
||||||
|
showQuickActionExec: true,
|
||||||
showQuickActionAttach: false
|
showQuickActionAttach: false
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { logsHandler, genericHandler } from "./response/handlers";
|
import {genericHandler, logsHandler} from './response/handlers';
|
||||||
|
|
||||||
angular.module('portainer.docker')
|
angular.module('portainer.docker')
|
||||||
.factory('Container', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', 'ContainersInterceptor',
|
.factory('Container', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', 'ContainersInterceptor',
|
||||||
|
@ -11,7 +11,7 @@ function ContainerFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, C
|
||||||
{
|
{
|
||||||
query: {
|
query: {
|
||||||
method: 'GET', params: { all: 0, action: 'json', filters: '@filters' },
|
method: 'GET', params: { all: 0, action: 'json', filters: '@filters' },
|
||||||
isArray: true, interceptor: ContainersInterceptor, timeout: 10000
|
isArray: true, interceptor: ContainersInterceptor, timeout: 15000
|
||||||
},
|
},
|
||||||
get: {
|
get: {
|
||||||
method: 'GET', params: { action: 'json' }
|
method: 'GET', params: { action: 'json' }
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { jsonObjectsToArrayHandler, deleteImageHandler } from './response/handlers';
|
import {deleteImageHandler, jsonObjectsToArrayHandler} from './response/handlers';
|
||||||
import { imageGetResponse } from './response/image';
|
import {imageGetResponse} from './response/image';
|
||||||
|
|
||||||
angular.module('portainer.docker')
|
angular.module('portainer.docker')
|
||||||
.factory('Image', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', 'HttpRequestHelper', 'ImagesInterceptor',
|
.factory('Image', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', 'HttpRequestHelper', 'ImagesInterceptor',
|
||||||
|
@ -10,7 +10,7 @@ function ImageFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, HttpR
|
||||||
endpointId: EndpointProvider.endpointID
|
endpointId: EndpointProvider.endpointID
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
query: {method: 'GET', params: {all: 0, action: 'json'}, isArray: true, interceptor: ImagesInterceptor, timeout: 10000},
|
query: {method: 'GET', params: {all: 0, action: 'json'}, isArray: true, interceptor: ImagesInterceptor, timeout: 15000},
|
||||||
get: {method: 'GET', params: {action: 'json'}},
|
get: {method: 'GET', params: {action: 'json'}},
|
||||||
search: {method: 'GET', params: {action: 'search'}},
|
search: {method: 'GET', params: {action: 'search'}},
|
||||||
history: {method: 'GET', params: {action: 'history'}, isArray: true},
|
history: {method: 'GET', params: {action: 'history'}, isArray: true},
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { genericHandler } from './response/handlers';
|
import {genericHandler} from './response/handlers';
|
||||||
|
|
||||||
angular.module('portainer.docker')
|
angular.module('portainer.docker')
|
||||||
.factory('Network', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', 'NetworksInterceptor',
|
.factory('Network', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', 'NetworksInterceptor',
|
||||||
|
@ -10,7 +10,7 @@ function NetworkFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, Net
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
query: {
|
query: {
|
||||||
method: 'GET', isArray: true, interceptor: NetworksInterceptor, timeout: 10000
|
method: 'GET', isArray: true, interceptor: NetworksInterceptor, timeout: 15000
|
||||||
},
|
},
|
||||||
get: {
|
get: {
|
||||||
method: 'GET'
|
method: 'GET'
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { jsonObjectsToArrayHandler } from './response/handlers';
|
import {jsonObjectsToArrayHandler} from './response/handlers';
|
||||||
|
|
||||||
angular.module('portainer.docker')
|
angular.module('portainer.docker')
|
||||||
.factory('System', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', 'InfoInterceptor', 'VersionInterceptor',
|
.factory('System', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', 'InfoInterceptor', 'VersionInterceptor',
|
||||||
|
@ -10,7 +10,7 @@ angular.module('portainer.docker')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
info: {
|
info: {
|
||||||
method: 'GET', params: { action: 'info' }, timeout: 10000, interceptor: InfoInterceptor
|
method: 'GET', params: { action: 'info' }, timeout: 15000, interceptor: InfoInterceptor
|
||||||
},
|
},
|
||||||
version: { method: 'GET', params: { action: 'version' }, timeout: 4500, interceptor: VersionInterceptor },
|
version: { method: 'GET', params: { action: 'version' }, timeout: 4500, interceptor: VersionInterceptor },
|
||||||
events: {
|
events: {
|
||||||
|
|
|
@ -7,7 +7,7 @@ angular.module('portainer.docker')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ping: {
|
ping: {
|
||||||
method: 'GET', params: { action: '_ping', endpointId: '@endpointId' }, timeout: 10000
|
method: 'GET', params: { action: '_ping', endpointId: '@endpointId' }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { genericHandler } from './response/handlers';
|
import {genericHandler} from './response/handlers';
|
||||||
|
|
||||||
angular.module('portainer.docker')
|
angular.module('portainer.docker')
|
||||||
.factory('Volume', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', 'VolumesInterceptor',
|
.factory('Volume', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', 'VolumesInterceptor',
|
||||||
|
@ -9,7 +9,7 @@ angular.module('portainer.docker')
|
||||||
endpointId: EndpointProvider.endpointID
|
endpointId: EndpointProvider.endpointID
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
query: { method: 'GET', interceptor: VolumesInterceptor, timeout: 10000},
|
query: { method: 'GET', interceptor: VolumesInterceptor, timeout: 15000},
|
||||||
get: { method: 'GET', params: {id: '@id'} },
|
get: { method: 'GET', params: {id: '@id'} },
|
||||||
create: {method: 'POST', params: {action: 'create'}, transformResponse: genericHandler, ignoreLoadingBar: true},
|
create: {method: 'POST', params: {action: 'create'}, transformResponse: genericHandler, ignoreLoadingBar: true},
|
||||||
remove: {
|
remove: {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { EventViewModel } from '../models/event';
|
import {EventViewModel} from '../models/event';
|
||||||
|
|
||||||
angular.module('portainer.docker')
|
angular.module('portainer.docker')
|
||||||
.factory('SystemService', ['$q', 'System', 'SystemEndpoint', function SystemServiceFactory($q, System, SystemEndpoint) {
|
.factory('SystemService', ['$q', 'System', 'SystemEndpoint', function SystemServiceFactory($q, System, SystemEndpoint) {
|
||||||
|
|
|
@ -71,7 +71,7 @@
|
||||||
ng-model="service.Image" ng-change="updateServiceAttribute(service, 'Image')" id="image_name" disable-authorization="DockerServiceUpdate">
|
ng-model="service.Image" ng-change="updateServiceAttribute(service, 'Image')" id="image_name" disable-authorization="DockerServiceUpdate">
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr ng-if="applicationState.endpoint.type !== 4">
|
||||||
<td colspan="{{webhookURL ? '1' : '2'}}">
|
<td colspan="{{webhookURL ? '1' : '2'}}">
|
||||||
Service webhook
|
Service webhook
|
||||||
<portainer-tooltip position="top" message="Webhook (or callback URI) used to automate the update of this service. Sending a POST request to this callback URI (without requiring any authentication) will pull the most up-to-date version of the associated image and re-deploy this service."></portainer-tooltip>
|
<portainer-tooltip position="top" message="Webhook (or callback URI) used to automate the update of this service. Sending a POST request to this callback URI (without requiring any authentication) will pull the most up-to-date version of the associated image and re-deploy this service."></portainer-tooltip>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import _ from "lodash-es";
|
import _ from 'lodash-es';
|
||||||
import angular from "angular";
|
import angular from 'angular';
|
||||||
|
|
||||||
import AccessViewerPolicyModel from '../../models/access'
|
import AccessViewerPolicyModel from '../../models/access';
|
||||||
|
|
||||||
class AccessViewerController {
|
class AccessViewerController {
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
|
|
|
@ -51,15 +51,19 @@
|
||||||
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter: $ctrl.applyFilters | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))">
|
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter: $ctrl.applyFilters | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))">
|
||||||
<td>
|
<td>
|
||||||
{{ item.Endpoint.Name }}
|
{{ item.Endpoint.Name }}
|
||||||
|
<a ng-if="item.Edge" ng-click="$ctrl.getEdgeTaskLogs(item.EndpointId, item.Id)"><i class="fa fa-download" aria-hidden="true" style="margin-left: 5px; margin-right: 2px;"></i> Download logs</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a ng-click="$ctrl.goToContainerLogs(item.EndpointId, item.Id)">{{ item.Id | truncate: 32 }}</a>
|
<a ng-if="!item.Edge" ng-click="$ctrl.goToContainerLogs(item.EndpointId, item.Id)">{{ item.Id | truncate: 32 }}</a>
|
||||||
|
<span ng-if="item.Edge">-</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="label label-{{ item.Status|containerstatusbadge }}">{{ item.Status }}</span>
|
<span ng-if="!item.Edge" class="label label-{{ item.Status|containerstatusbadge }}">{{ item.Status }}</span>
|
||||||
|
<span ng-if="item.Edge">-</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ item.Created | getisodatefromtimestamp}}
|
<span ng-if="!item.Edge">{{ item.Created | getisodatefromtimestamp}}</span>
|
||||||
|
<span ng-if="item.Edge">-</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr ng-if="!$ctrl.dataset">
|
<tr ng-if="!$ctrl.dataset">
|
||||||
|
|
|
@ -8,6 +8,7 @@ angular.module('portainer.docker').component('scheduleTasksDatatable', {
|
||||||
tableKey: '@',
|
tableKey: '@',
|
||||||
orderBy: '@',
|
orderBy: '@',
|
||||||
reverseOrder: '<',
|
reverseOrder: '<',
|
||||||
goToContainerLogs: '<'
|
goToContainerLogs: '<',
|
||||||
|
getEdgeTaskLogs: '<'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
<div class="blocklist-item" ng-click="$ctrl.onSelect($ctrl.model)">
|
<div class="blocklist-item" ng-click="$ctrl.onSelect($ctrl.model)">
|
||||||
<div class="blocklist-item-box">
|
<div class="blocklist-item-box">
|
||||||
<span ng-class="['blocklist-item-logo', 'endpoint-item', { azure: $ctrl.model.Type === 3 }]">
|
<span ng-class="['blocklist-item-logo', 'endpoint-item', { azure: $ctrl.model.Type === 3 }]">
|
||||||
<i ng-class="$ctrl.model.Type | endpointtypeicon" class="fa-4x blue-icon" aria-hidden="true"></i>
|
<i ng-if="$ctrl.model.Type !== 4" ng-class="$ctrl.model.Type | endpointtypeicon" class="fa-4x blue-icon" aria-hidden="true"></i>
|
||||||
|
<img ng-if="$ctrl.model.Type === 4" src="../../../../../assets/images/edge_endpoint.png" />
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="col-sm-12">
|
<span class="col-sm-12">
|
||||||
|
@ -12,13 +13,16 @@
|
||||||
{{ $ctrl.model.Name }}
|
{{ $ctrl.model.Name }}
|
||||||
</span>
|
</span>
|
||||||
<span class="space-left blocklist-item-subtitle">
|
<span class="space-left blocklist-item-subtitle">
|
||||||
<span class="label label-{{ $ctrl.model.Status|endpointstatusbadge }}">
|
<span ng-if="$ctrl.model.Type === 4" class="small text-muted">
|
||||||
|
<span ng-if="$ctrl.model.EdgeID"><i class="fas fa-link"></i> associated</span>
|
||||||
|
<span ng-if="!$ctrl.model.EdgeID"><i class="fas fa-unlink"></i> <s>associated</s></span>
|
||||||
|
</span>
|
||||||
|
<span class="label label-{{ $ctrl.model.Status|endpointstatusbadge }}" ng-if="$ctrl.model.Type !== 4">
|
||||||
{{ $ctrl.model.Status === 1 ? 'up' : 'down' }}
|
{{ $ctrl.model.Status === 1 ? 'up' : 'down' }}
|
||||||
</span>
|
</span>
|
||||||
<span class="space-left small text-muted" ng-if="$ctrl.model.Snapshots[0]">
|
<span class="space-left small text-muted" ng-if="$ctrl.model.Snapshots[0]">
|
||||||
{{ $ctrl.model.Snapshots[0].Time | getisodatefromtimestamp }}
|
{{ $ctrl.model.Snapshots[0].Time | getisodatefromtimestamp }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
|
@ -64,6 +68,12 @@
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="blocklist-item-line endpoint-item" ng-if="!$ctrl.model.Snapshots[0]">
|
||||||
|
<span class="blocklist-item-desc">
|
||||||
|
No snapshot available
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="blocklist-item-line endpoint-item">
|
<div class="blocklist-item-line endpoint-item">
|
||||||
<span class="small text-muted">
|
<span class="small text-muted">
|
||||||
<span ng-if="$ctrl.model.Type === 1">
|
<span ng-if="$ctrl.model.Type === 1">
|
||||||
|
@ -82,7 +92,7 @@
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="small text-muted">
|
<span class="small text-muted" ng-if="$ctrl.model.Type !== 4">
|
||||||
{{ $ctrl.model.URL | stripprotocol }}
|
{{ $ctrl.model.URL | stripprotocol }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -89,7 +89,6 @@ angular.module('portainer.app').controller('EndpointListController', ['Datatable
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$onInit = function() {
|
this.$onInit = function() {
|
||||||
this.state.loading = true;
|
|
||||||
var textFilter = DatatableService.getDataTableTextFilters(this.tableKey);
|
var textFilter = DatatableService.getDataTableTextFilters(this.tableKey);
|
||||||
this.state.paginatedItemLimit = PaginationService.getPaginationLimit(this.tableKey);
|
this.state.paginatedItemLimit = PaginationService.getPaginationLimit(this.tableKey);
|
||||||
if (textFilter !== null) {
|
if (textFilter !== null) {
|
||||||
|
|
|
@ -25,7 +25,7 @@ angular.module('portainer.app').component('scheduleForm', {
|
||||||
ctrl.formValues = {
|
ctrl.formValues = {
|
||||||
datetime: ctrl.model.CronExpression ? cronToDatetime(ctrl.model.CronExpression) : moment(),
|
datetime: ctrl.model.CronExpression ? cronToDatetime(ctrl.model.CronExpression) : moment(),
|
||||||
scheduleValue: ctrl.scheduleValues[0],
|
scheduleValue: ctrl.scheduleValues[0],
|
||||||
cronMethod: 'basic'
|
cronMethod: ctrl.model.Recurring ? 'advanced' : 'basic'
|
||||||
};
|
};
|
||||||
|
|
||||||
function cronToDatetime(cron) {
|
function cronToDatetime(cron) {
|
||||||
|
@ -38,7 +38,7 @@ angular.module('portainer.app').component('scheduleForm', {
|
||||||
|
|
||||||
function datetimeToCron(datetime) {
|
function datetimeToCron(datetime) {
|
||||||
var date = moment(datetime);
|
var date = moment(datetime);
|
||||||
return '0 '.concat(date.minutes(), ' ', date.hours(), ' ', date.date(), ' ', (date.month() + 1));
|
return '0 '.concat(date.minutes(), ' ', date.hours(), ' ', date.date(), ' ', (date.month() + 1), ' *');
|
||||||
}
|
}
|
||||||
|
|
||||||
this.action = function() {
|
this.action = function() {
|
||||||
|
|
|
@ -1,4 +1,15 @@
|
||||||
<form class="form-horizontal" name="scheduleForm">
|
<form class="form-horizontal" name="scheduleForm">
|
||||||
|
<div class="col-sm-12 form-section-title">
|
||||||
|
Information
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<span class="col-sm-12 text-muted small">
|
||||||
|
<p>
|
||||||
|
<i class="fa fa-exclamation-triangle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i> Due to how schedules behave differently on Edge endpoints and other endpoints it is recommended to create specific schedules that will only target one
|
||||||
|
type of endpoint.
|
||||||
|
</p>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<div class="col-sm-12 form-section-title">
|
<div class="col-sm-12 form-section-title">
|
||||||
Schedule configuration
|
Schedule configuration
|
||||||
</div>
|
</div>
|
||||||
|
@ -114,7 +125,12 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<span class="col-sm-12 text-muted small">
|
<span class="col-sm-12 text-muted small">
|
||||||
You can refer to the <a href="https://godoc.org/github.com/robfig/cron#hdr-CRON_Expression_Format" target="_blank">following documentation</a> to get more information about the supported cron expression format.
|
<p>
|
||||||
|
You can refer to the <a href="https://godoc.org/github.com/robfig/cron#hdr-CRON_Expression_Format" target="_blank">following documentation</a> to get more information about the supported cron expression format.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<i class="fa fa-exclamation-triangle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i> Edge endpoint schedules are managed by <code>cron</code> on the underlying host. You need to use a valid cron expression that is different from the documentation above.
|
||||||
|
</p>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -123,6 +139,13 @@
|
||||||
<div class="col-sm-12 form-section-title">
|
<div class="col-sm-12 form-section-title">
|
||||||
Job configuration
|
Job configuration
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<span class="col-sm-12 text-muted small">
|
||||||
|
<p>
|
||||||
|
<i class="fa fa-exclamation-triangle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i> This configuration will be ignored for Edge endpoint schedules.
|
||||||
|
</p>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<!-- image-input -->
|
<!-- image-input -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="schedule_image" class="col-sm-2 control-label text-left">Image</label>
|
<label for="schedule_image" class="col-sm-2 control-label text-left">Image</label>
|
||||||
|
@ -195,8 +218,13 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<span class="col-sm-12 text-muted small">
|
<span class="col-sm-12 text-muted small">
|
||||||
This schedule will be executed via a privileged container on the target hosts. You can access the host filesystem under the
|
<p>
|
||||||
<code>/host</code> folder.
|
This schedule will be executed via a privileged container on the target hosts. You can access the host filesystem under the
|
||||||
|
<code>/host</code> folder.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<i class="fa fa-exclamation-triangle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i> Edge endpoint schedules are managed by <code>cron</code> on the underlying host. You have full access to the filesystem without having to use the <code>/host</code> folder.
|
||||||
|
</p>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|
|
@ -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';
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { createStatus } from '../../docker/models/container';
|
import _ from 'lodash-es';
|
||||||
|
import {createStatus} from '../../docker/models/container';
|
||||||
|
|
||||||
export function ScheduleDefaultModel() {
|
export function ScheduleDefaultModel() {
|
||||||
this.Name = '';
|
this.Name = '';
|
||||||
|
@ -9,7 +10,7 @@ export function ScheduleDefaultModel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function ScriptExecutionDefaultJobModel() {
|
function ScriptExecutionDefaultJobModel() {
|
||||||
this.Image = '';
|
this.Image = 'ubuntu:latest';
|
||||||
this.Endpoints = [];
|
this.Endpoints = [];
|
||||||
this.FileContent = '';
|
this.FileContent = '';
|
||||||
this.File = null;
|
this.File = null;
|
||||||
|
@ -23,14 +24,20 @@ export function ScheduleModel(data) {
|
||||||
this.JobType = data.JobType;
|
this.JobType = data.JobType;
|
||||||
this.CronExpression = data.CronExpression;
|
this.CronExpression = data.CronExpression;
|
||||||
this.Created = data.Created;
|
this.Created = data.Created;
|
||||||
|
this.EdgeSchedule = data.EdgeSchedule;
|
||||||
if (this.JobType === 1) {
|
if (this.JobType === 1) {
|
||||||
this.Job = new ScriptExecutionJobModel(data.ScriptExecutionJob);
|
this.Job = new ScriptExecutionJobModel(data.ScriptExecutionJob, data.EdgeSchedule);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ScriptExecutionJobModel(data) {
|
function ScriptExecutionJobModel(data, edgeSchedule) {
|
||||||
this.Image = data.Image;
|
this.Image = data.Image;
|
||||||
this.Endpoints = data.Endpoints;
|
this.Endpoints = data.Endpoints;
|
||||||
|
|
||||||
|
if (edgeSchedule !== null) {
|
||||||
|
this.Endpoints = _.concat(data.Endpoints, edgeSchedule.Endpoints);
|
||||||
|
}
|
||||||
|
|
||||||
this.FileContent = '';
|
this.FileContent = '';
|
||||||
this.Method = 'editor';
|
this.Method = 'editor';
|
||||||
this.RetryCount = data.RetryCount;
|
this.RetryCount = data.RetryCount;
|
||||||
|
@ -42,6 +49,7 @@ export function ScriptExecutionTaskModel(data) {
|
||||||
this.EndpointId = data.EndpointId;
|
this.EndpointId = data.EndpointId;
|
||||||
this.Status = createStatus(data.Status);
|
this.Status = createStatus(data.Status);
|
||||||
this.Created = data.Created;
|
this.Created = data.Created;
|
||||||
|
this.Edge = data.Edge;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ScheduleCreateRequest(model) {
|
export function ScheduleCreateRequest(model) {
|
||||||
|
|
|
@ -10,6 +10,7 @@ export function SettingsViewModel(data) {
|
||||||
this.TemplatesURL = data.TemplatesURL;
|
this.TemplatesURL = data.TemplatesURL;
|
||||||
this.ExternalTemplates = data.ExternalTemplates;
|
this.ExternalTemplates = data.ExternalTemplates;
|
||||||
this.EnableHostManagementFeatures = data.EnableHostManagementFeatures;
|
this.EnableHostManagementFeatures = data.EnableHostManagementFeatures;
|
||||||
|
this.EdgeAgentCheckinInterval = data.EdgeAgentCheckinInterval;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PublicSettingsViewModel(settings) {
|
export function PublicSettingsViewModel(settings) {
|
||||||
|
|
|
@ -13,8 +13,9 @@ angular.module('portainer.app')
|
||||||
update: { method: 'PUT', params: { id: '@id' } },
|
update: { method: 'PUT', params: { id: '@id' } },
|
||||||
updateAccess: { method: 'PUT', params: { id: '@id', action: 'access' } },
|
updateAccess: { method: 'PUT', params: { id: '@id', action: 'access' } },
|
||||||
remove: { method: 'DELETE', params: { id: '@id'} },
|
remove: { method: 'DELETE', params: { id: '@id'} },
|
||||||
snapshots: { method: 'POST', params: { action: 'snapshot' }},
|
snapshots: { method: 'POST', params: { action: 'snapshot' } },
|
||||||
snapshot: { method: 'POST', params: { id: '@id', action: 'snapshot' }},
|
snapshot: { method: 'POST', params: { id: '@id', action: 'snapshot' } },
|
||||||
executeJob: { method: 'POST', ignoreLoadingBar: true, params: { id: '@id', action: 'job' } }
|
executeJob: { method: 'POST', ignoreLoadingBar: true, params: { id: '@id', action: 'job' } },
|
||||||
|
status: { method: 'GET', params: { id: '@id', action: 'status' } }
|
||||||
});
|
});
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -66,7 +66,12 @@ function EndpointServiceFactory($q, Endpoints, FileUploadService) {
|
||||||
service.createRemoteEndpoint = function(name, type, URL, PublicURL, groupID, tags, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) {
|
service.createRemoteEndpoint = function(name, type, URL, PublicURL, groupID, tags, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) {
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
|
|
||||||
FileUploadService.createEndpoint(name, type, 'tcp://' + URL, PublicURL, groupID, tags, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile)
|
var endpointURL = URL;
|
||||||
|
if (type !== 4) {
|
||||||
|
endpointURL = 'tcp://' + URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
FileUploadService.createEndpoint(name, type, endpointURL, PublicURL, groupID, tags, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile)
|
||||||
.then(function success(response) {
|
.then(function success(response) {
|
||||||
deferred.resolve(response.data);
|
deferred.resolve(response.data);
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,8 +1,4 @@
|
||||||
import {
|
import {EndpointGroupCreateRequest, EndpointGroupModel, EndpointGroupUpdateRequest} from '../../models/group';
|
||||||
EndpointGroupModel,
|
|
||||||
EndpointGroupCreateRequest,
|
|
||||||
EndpointGroupUpdateRequest
|
|
||||||
} from '../../models/group';
|
|
||||||
|
|
||||||
angular.module('portainer.app')
|
angular.module('portainer.app')
|
||||||
.factory('GroupService', ['$q', 'EndpointGroups',
|
.factory('GroupService', ['$q', 'EndpointGroups',
|
||||||
|
|
|
@ -171,6 +171,7 @@ function StateManagerFactory($q, SystemService, InfoHelper, LocalStorage, Settin
|
||||||
var endpointAPIVersion = parseFloat(data.version.ApiVersion);
|
var endpointAPIVersion = parseFloat(data.version.ApiVersion);
|
||||||
state.endpoint.mode = endpointMode;
|
state.endpoint.mode = endpointMode;
|
||||||
state.endpoint.name = endpoint.Name;
|
state.endpoint.name = endpoint.Name;
|
||||||
|
state.endpoint.type = endpoint.Type;
|
||||||
state.endpoint.apiVersion = endpointAPIVersion;
|
state.endpoint.apiVersion = endpointAPIVersion;
|
||||||
state.endpoint.extensions = assignExtensions(extensions);
|
state.endpoint.extensions = assignExtensions(extensions);
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { EndpointSecurityFormData } from '../../../components/endpointSecurity/porEndpointSecurityModel';
|
import {EndpointSecurityFormData} from '../../../components/endpointSecurity/porEndpointSecurityModel';
|
||||||
|
|
||||||
angular.module('portainer.app')
|
angular.module('portainer.app')
|
||||||
.controller('CreateEndpointController', ['$q', '$scope', '$state', '$filter', 'clipboard', 'EndpointService', 'GroupService', 'TagService', 'Notifications',
|
.controller('CreateEndpointController', ['$q', '$scope', '$state', '$filter', 'clipboard', 'EndpointService', 'GroupService', 'TagService', 'Notifications',
|
||||||
|
@ -27,6 +27,14 @@ function ($q, $scope, $state, $filter, clipboard, EndpointService, GroupService,
|
||||||
$('#copyNotification').fadeOut(2000);
|
$('#copyNotification').fadeOut(2000);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.setDefaultPortainerInstanceURL = function() {
|
||||||
|
$scope.formValues.URL = window.location.origin;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.resetEndpointURL = function() {
|
||||||
|
$scope.formValues.URL = '';
|
||||||
|
};
|
||||||
|
|
||||||
$scope.addDockerEndpoint = function() {
|
$scope.addDockerEndpoint = function() {
|
||||||
var name = $scope.formValues.Name;
|
var name = $scope.formValues.Name;
|
||||||
var URL = $filter('stripprotocol')($scope.formValues.URL);
|
var URL = $filter('stripprotocol')($scope.formValues.URL);
|
||||||
|
@ -56,6 +64,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 = $scope.formValues.URL;
|
||||||
|
|
||||||
|
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 +102,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');
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
<div class="form-group"></div>
|
<div class="form-group"></div>
|
||||||
<div class="form-group" style="margin-bottom: 0">
|
<div class="form-group" style="margin-bottom: 0">
|
||||||
<div class="boxselector_wrapper">
|
<div class="boxselector_wrapper">
|
||||||
<div>
|
<div ng-click="resetEndpointURL()">
|
||||||
<input type="radio" id="docker_endpoint" ng-model="state.EnvironmentType" value="docker">
|
<input type="radio" id="docker_endpoint" ng-model="state.EnvironmentType" value="docker">
|
||||||
<label for="docker_endpoint">
|
<label for="docker_endpoint">
|
||||||
<div class="boxselector_header">
|
<div class="boxselector_header">
|
||||||
|
@ -26,7 +26,7 @@
|
||||||
<p>Docker environment</p>
|
<p>Docker environment</p>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div ng-click="resetEndpointURL()">
|
||||||
<input type="radio" id="agent_endpoint" ng-model="state.EnvironmentType" value="agent">
|
<input type="radio" id="agent_endpoint" ng-model="state.EnvironmentType" value="agent">
|
||||||
<label for="agent_endpoint">
|
<label for="agent_endpoint">
|
||||||
<div class="boxselector_header">
|
<div class="boxselector_header">
|
||||||
|
@ -36,6 +36,16 @@
|
||||||
<p>Portainer agent</p>
|
<p>Portainer agent</p>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div ng-click="setDefaultPortainerInstanceURL()">
|
||||||
|
<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. The Edge agent will initiate the communications with the Portainer instance. 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
|
||||||
|
@ -138,6 +158,26 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- !endpoint-url-input -->
|
<!-- !endpoint-url-input -->
|
||||||
|
<!-- portainer-instance-input -->
|
||||||
|
<div ng-if="state.EnvironmentType === 'edge_agent'">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="endpoint_url" class="col-sm-3 col-lg-2 control-label text-left">
|
||||||
|
Portainer server URL
|
||||||
|
<portainer-tooltip position="bottom" message="URL of the Portainer instance that the agent will use to initiate the communications."></portainer-tooltip>
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-9 col-lg-10">
|
||||||
|
<input type="text" class="form-control" name="endpoint_url" ng-model="formValues.URL" placeholder="e.g. 10.0.0.10:9000 or portainer.mydomain.com" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" ng-show="endpointCreationForm.endpoint_url.$invalid">
|
||||||
|
<div class="col-sm-12 small text-warning">
|
||||||
|
<div ng-messages="endpointCreationForm.endpoint_url.$error">
|
||||||
|
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !portainer-instance-input -->
|
||||||
<!-- endpoint-public-url-input -->
|
<!-- endpoint-public-url-input -->
|
||||||
<div ng-if="state.EnvironmentType === 'docker' || state.EnvironmentType === 'agent'">
|
<div ng-if="state.EnvironmentType === 'docker' || state.EnvironmentType === 'agent'">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
@ -238,6 +278,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>
|
||||||
|
|
|
@ -1,10 +1,108 @@
|
||||||
<rd-header>
|
<rd-header>
|
||||||
<rd-header-title title-text="Endpoint details"></rd-header-title>
|
<rd-header-title title-text="Endpoint details">
|
||||||
|
<a data-toggle="tooltip" title="Refresh" ui-sref="portainer.endpoints.endpoint({id: endpoint.Id})" ui-sref-opts="{reload: true}">
|
||||||
|
<i class="fa fa-sync" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
|
</rd-header-title>
|
||||||
<rd-header-content>
|
<rd-header-content>
|
||||||
<a ui-sref="portainer.endpoints">Endpoints</a> > <a ui-sref="portainer.endpoints.endpoint({id: endpoint.Id})">{{ endpoint.Name }}</a>
|
<a ui-sref="portainer.endpoints">Endpoints</a> > <a ui-sref="portainer.endpoints.endpoint({id: endpoint.Id})">{{ endpoint.Name }}</a>
|
||||||
</rd-header-content>
|
</rd-header-content>
|
||||||
</rd-header>
|
</rd-header>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<information-panel ng-if="endpoint.Type === 4 && endpoint.EdgeID" title-text="Edge information">
|
||||||
|
<span class="small text-muted">
|
||||||
|
<p>
|
||||||
|
<i class="fa fa-info-circle blue-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
|
This Edge endpoint is associated to an Edge environment.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Edge key: <code>{{ endpoint.EdgeKey }}</code>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Edge identifier: <code>{{ endpoint.EdgeID }}</code>
|
||||||
|
</p>
|
||||||
|
</span>
|
||||||
|
</information-panel>
|
||||||
|
<information-panel ng-if="endpoint.Type === 4 && !endpoint.EdgeID" 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 EDGE_ID={{ randomEdgeID }} \
|
||||||
|
-e CAP_HOST_MANAGEMENT=1 \
|
||||||
|
-p 8000:80 \
|
||||||
|
-v portainer_agent_data:/data \
|
||||||
|
--name portainer_edge_agent \
|
||||||
|
portainer/agent
|
||||||
|
</code>
|
||||||
|
</uib-tab>
|
||||||
|
<uib-tab index="1" heading="Swarm">
|
||||||
|
<code style=display:block;white-space:pre-wrap>
|
||||||
|
docker network create \
|
||||||
|
--driver overlay \
|
||||||
|
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 EDGE_ID={{ randomEdgeID }} \
|
||||||
|
-e CAP_HOST_MANAGEMENT=1 \
|
||||||
|
--mode global \
|
||||||
|
--publish mode=host,published=8000,target=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 \
|
||||||
|
--mount type=volume,src=portainer_agent_data,dst=/data \
|
||||||
|
portainer/agent
|
||||||
|
</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>
|
||||||
|
<p>
|
||||||
|
The agent will communicate with Portainer via <u>{{ edgeKeyDetails.instanceURL }}</u> and <u>tcp://{{ edgeKeyDetails.tunnelServerAddr }}</u>
|
||||||
|
</p>
|
||||||
|
<div style="margin-top: 10px; overflow-wrap: break-word;">
|
||||||
|
<code>
|
||||||
|
{{ 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 +120,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 +168,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,10 @@
|
||||||
import { EndpointSecurityFormData } from '../../../components/endpointSecurity/porEndpointSecurityModel';
|
import _ from 'lodash-es';
|
||||||
|
import uuidv4 from 'uuid/v4';
|
||||||
|
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 +12,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 EDGE_ID=' + $scope.randomEdgeID +' -e CAP_HOST_MANAGEMENT=1 -p 8000:80 -v portainer_agent_data:/data --name portainer_edge_agent portainer/agent');
|
||||||
|
} else {
|
||||||
|
clipboard.copyText('docker network create --driver overlay 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 EDGE_ID=' + $scope.randomEdgeID +' -e CAP_HOST_MANAGEMENT=1 --mode global --publish mode=host,published=8000,target=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 --mount type=volume,src=portainer_agent_data,dst=/data portainer/agent');
|
||||||
|
}
|
||||||
|
$('#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;
|
||||||
|
@ -61,6 +78,20 @@ function ($q, $scope, $state, $transition$, $filter, EndpointService, GroupServi
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function decodeEdgeKey(key) {
|
||||||
|
let keyInformation = {};
|
||||||
|
|
||||||
|
if (key === "") {
|
||||||
|
return keyInformation;
|
||||||
|
}
|
||||||
|
|
||||||
|
let decodedKey = _.split(atob(key), "|");
|
||||||
|
keyInformation.instanceURL = decodedKey[0];
|
||||||
|
keyInformation.tunnelServerAddr = decodedKey[1];
|
||||||
|
|
||||||
|
return keyInformation;
|
||||||
|
}
|
||||||
|
|
||||||
function initView() {
|
function initView() {
|
||||||
$q.all({
|
$q.all({
|
||||||
endpoint: EndpointService.endpoint($transition$.params().id),
|
endpoint: EndpointService.endpoint($transition$.params().id),
|
||||||
|
@ -75,6 +106,10 @@ function ($q, $scope, $state, $transition$, $filter, EndpointService, GroupServi
|
||||||
$scope.endpointType = 'remote';
|
$scope.endpointType = 'remote';
|
||||||
}
|
}
|
||||||
endpoint.URL = $filter('stripprotocol')(endpoint.URL);
|
endpoint.URL = $filter('stripprotocol')(endpoint.URL);
|
||||||
|
if (endpoint.Type === 4) {
|
||||||
|
$scope.edgeKeyDetails = decodeEdgeKey(endpoint.EdgeKey);
|
||||||
|
$scope.randomEdgeID = uuidv4();
|
||||||
|
}
|
||||||
$scope.endpoint = endpoint;
|
$scope.endpoint = endpoint;
|
||||||
$scope.groups = data.groups;
|
$scope.groups = data.groups;
|
||||||
$scope.availableTags = data.tags;
|
$scope.availableTags = data.tags;
|
||||||
|
|
|
@ -24,7 +24,12 @@
|
||||||
</span>
|
</span>
|
||||||
</information-panel>
|
</information-panel>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row" style="width:100%; height:100%; text-align: center; display: flex; flex-direction: column; align-items: center; justify-content: center;" ng-if="state.connectingToEdgeEndpoint">
|
||||||
|
Connecting to the Edge endpoint...
|
||||||
|
<i class="fa fa-cog fa-spin" style="margin-left: 5px"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row" ng-if="!state.connectingToEdgeEndpoint">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<endpoint-list
|
<endpoint-list
|
||||||
title-text="Endpoints" title-icon="fa-plug"
|
title-text="Endpoints" title-icon="fa-plug"
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
angular.module('portainer.app')
|
angular.module('portainer.app')
|
||||||
.controller('HomeController', ['$q', '$scope', '$state', 'Authentication', 'EndpointService', 'EndpointHelper', 'GroupService', 'Notifications', 'EndpointProvider', 'StateManager', 'LegacyExtensionManager', 'ModalService', 'MotdService', 'SystemService',
|
.controller('HomeController', ['$q', '$scope', '$state', '$interval', 'Authentication', 'EndpointService', 'EndpointHelper', 'GroupService', 'Notifications', 'EndpointProvider', 'StateManager', 'LegacyExtensionManager', 'ModalService', 'MotdService', 'SystemService',
|
||||||
function($q, $scope, $state, Authentication, EndpointService, EndpointHelper, GroupService, Notifications, EndpointProvider, StateManager, LegacyExtensionManager, ModalService, MotdService, SystemService) {
|
function($q, $scope, $state, $interval, Authentication, EndpointService, EndpointHelper, GroupService, Notifications, EndpointProvider, StateManager, LegacyExtensionManager, ModalService, MotdService, SystemService) {
|
||||||
|
|
||||||
|
$scope.state = {
|
||||||
|
connectingToEdgeEndpoint: false,
|
||||||
|
};
|
||||||
|
|
||||||
$scope.goToEdit = function(id) {
|
$scope.goToEdit = function(id) {
|
||||||
$state.go('portainer.endpoints.endpoint', { id: id });
|
$state.go('portainer.endpoints.endpoint', { id: id });
|
||||||
|
@ -9,10 +13,12 @@ angular.module('portainer.app')
|
||||||
$scope.goToDashboard = function(endpoint) {
|
$scope.goToDashboard = function(endpoint) {
|
||||||
if (endpoint.Type === 3) {
|
if (endpoint.Type === 3) {
|
||||||
return switchToAzureEndpoint(endpoint);
|
return switchToAzureEndpoint(endpoint);
|
||||||
|
} else if (endpoint.Type === 4) {
|
||||||
|
return switchToEdgeEndpoint(endpoint);
|
||||||
}
|
}
|
||||||
|
|
||||||
checkEndpointStatus(endpoint)
|
checkEndpointStatus(endpoint)
|
||||||
.then(function sucess() {
|
.then(function success() {
|
||||||
return switchToDockerEndpoint(endpoint);
|
return switchToDockerEndpoint(endpoint);
|
||||||
}).catch(function error(err) {
|
}).catch(function error(err) {
|
||||||
Notifications.error('Failure', err, 'Unable to verify endpoint status');
|
Notifications.error('Failure', err, 'Unable to verify endpoint status');
|
||||||
|
@ -41,7 +47,7 @@ angular.module('portainer.app')
|
||||||
|
|
||||||
var status = 1;
|
var status = 1;
|
||||||
SystemService.ping(endpoint.Id)
|
SystemService.ping(endpoint.Id)
|
||||||
.then(function sucess() {
|
.then(function success() {
|
||||||
status = 1;
|
status = 1;
|
||||||
}).catch(function error() {
|
}).catch(function error() {
|
||||||
status = 2;
|
status = 2;
|
||||||
|
@ -52,7 +58,7 @@ angular.module('portainer.app')
|
||||||
}
|
}
|
||||||
|
|
||||||
EndpointService.updateEndpoint(endpoint.Id, { Status: status })
|
EndpointService.updateEndpoint(endpoint.Id, { Status: status })
|
||||||
.then(function sucess() {
|
.then(function success() {
|
||||||
deferred.resolve(endpoint);
|
deferred.resolve(endpoint);
|
||||||
}).catch(function error(err) {
|
}).catch(function error(err) {
|
||||||
deferred.reject({ msg: 'Unable to update endpoint status', err: err });
|
deferred.reject({ msg: 'Unable to update endpoint status', err: err });
|
||||||
|
@ -75,11 +81,33 @@ angular.module('portainer.app')
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function switchToEdgeEndpoint(endpoint) {
|
||||||
|
if (!endpoint.EdgeID) {
|
||||||
|
$state.go('portainer.endpoints.endpoint', { id: endpoint.Id });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.state.connectingToEdgeEndpoint = true;
|
||||||
|
SystemService.ping(endpoint.Id)
|
||||||
|
.then(function success() {
|
||||||
|
endpoint.Status = 1;
|
||||||
|
})
|
||||||
|
.catch(function error() {
|
||||||
|
endpoint.Status = 2;
|
||||||
|
})
|
||||||
|
.finally(function final() {
|
||||||
|
switchToDockerEndpoint(endpoint);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function switchToDockerEndpoint(endpoint) {
|
function switchToDockerEndpoint(endpoint) {
|
||||||
if (endpoint.Status === 2 && endpoint.Snapshots[0] && endpoint.Snapshots[0].Swarm === true) {
|
if (endpoint.Status === 2 && endpoint.Snapshots[0] && endpoint.Snapshots[0].Swarm === true) {
|
||||||
|
$scope.state.connectingToEdgeEndpoint = false;
|
||||||
Notifications.error('Failure', '', 'Endpoint is unreachable. Connect to another swarm manager.');
|
Notifications.error('Failure', '', 'Endpoint is unreachable. Connect to another swarm manager.');
|
||||||
return;
|
return;
|
||||||
} else if (endpoint.Status === 2 && !endpoint.Snapshots[0]) {
|
} else if (endpoint.Status === 2 && !endpoint.Snapshots[0]) {
|
||||||
|
$scope.state.connectingToEdgeEndpoint = false;
|
||||||
Notifications.error('Failure', '', 'Endpoint is unreachable and there is no snapshot available for offline browsing.');
|
Notifications.error('Failure', '', 'Endpoint is unreachable and there is no snapshot available for offline browsing.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -97,6 +125,9 @@ angular.module('portainer.app')
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
Notifications.error('Failure', err, 'Unable to connect to the Docker endpoint');
|
Notifications.error('Failure', err, 'Unable to connect to the Docker endpoint');
|
||||||
|
})
|
||||||
|
.finally(function final() {
|
||||||
|
$scope.state.connectingToEdgeEndpoint = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { ScheduleDefaultModel } from '../../../models/schedule';
|
import {ScheduleDefaultModel} from '../../../models/schedule';
|
||||||
|
|
||||||
angular.module('portainer.app')
|
angular.module('portainer.app')
|
||||||
.controller('CreateScheduleController', ['$q', '$scope', '$state', 'Notifications', 'EndpointService', 'GroupService', 'ScheduleService',
|
.controller('CreateScheduleController', ['$q', '$scope', '$state', 'Notifications', 'EndpointService', 'GroupService', 'ScheduleService',
|
||||||
|
|
|
@ -56,6 +56,7 @@
|
||||||
table-key="schedule-tasks"
|
table-key="schedule-tasks"
|
||||||
order-by="Status" reverse-order="true"
|
order-by="Status" reverse-order="true"
|
||||||
go-to-container-logs="goToContainerLogs"
|
go-to-container-logs="goToContainerLogs"
|
||||||
|
get-edge-task-logs="getEdgeTaskLogs"
|
||||||
></schedule-tasks-datatable>
|
></schedule-tasks-datatable>
|
||||||
</uib-tab>
|
</uib-tab>
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
angular.module('portainer.app')
|
angular.module('portainer.app')
|
||||||
.controller('ScheduleController', ['$q', '$scope', '$transition$', '$state', 'Notifications', 'EndpointService', 'GroupService', 'ScheduleService', 'EndpointProvider',
|
.controller('ScheduleController', ['$q', '$scope', '$transition$', '$state', 'Notifications', 'EndpointService', 'GroupService', 'ScheduleService', 'EndpointProvider', 'HostBrowserService', 'FileSaver',
|
||||||
function ($q, $scope, $transition$, $state, Notifications, EndpointService, GroupService, ScheduleService, EndpointProvider) {
|
function ($q, $scope, $transition$, $state, Notifications, EndpointService, GroupService, ScheduleService, EndpointProvider, HostBrowserService, FileSaver) {
|
||||||
|
|
||||||
$scope.state = {
|
$scope.state = {
|
||||||
actionInProgress: false
|
actionInProgress: false
|
||||||
|
@ -8,6 +8,7 @@ function ($q, $scope, $transition$, $state, Notifications, EndpointService, Grou
|
||||||
|
|
||||||
$scope.update = update;
|
$scope.update = update;
|
||||||
$scope.goToContainerLogs = goToContainerLogs;
|
$scope.goToContainerLogs = goToContainerLogs;
|
||||||
|
$scope.getEdgeTaskLogs = getEdgeTaskLogs;
|
||||||
|
|
||||||
function update() {
|
function update() {
|
||||||
var model = $scope.schedule;
|
var model = $scope.schedule;
|
||||||
|
@ -31,6 +32,26 @@ function ($q, $scope, $transition$, $state, Notifications, EndpointService, Grou
|
||||||
$state.go('docker.containers.container.logs', { id: containerId });
|
$state.go('docker.containers.container.logs', { id: containerId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getEdgeTaskLogs(endpointId, scheduleId) {
|
||||||
|
var currentId = EndpointProvider.endpointID();
|
||||||
|
EndpointProvider.setEndpointID(endpointId);
|
||||||
|
|
||||||
|
var filePath = '/host/opt/portainer/scripts/' + scheduleId + '.log';
|
||||||
|
HostBrowserService.get(filePath)
|
||||||
|
.then(function onFileReceived(data) {
|
||||||
|
var downloadData = new Blob([data.file], {
|
||||||
|
type: 'text/plain;charset=utf-8'
|
||||||
|
});
|
||||||
|
FileSaver.saveAs(downloadData, scheduleId + '.log');
|
||||||
|
})
|
||||||
|
.catch(function notifyOnError(err) {
|
||||||
|
Notifications.error('Failure', err, 'Unable to download file');
|
||||||
|
})
|
||||||
|
.finally(function final() {
|
||||||
|
EndpointProvider.setEndpointID(currentId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function associateEndpointsToTasks(tasks, endpoints) {
|
function associateEndpointsToTasks(tasks, endpoints) {
|
||||||
for (var i = 0; i < tasks.length; i++) {
|
for (var i = 0; i < tasks.length; i++) {
|
||||||
var task = tasks[i];
|
var task = tasks[i];
|
||||||
|
|
|
@ -112,7 +112,23 @@
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- security -->
|
<!-- !security -->
|
||||||
|
<!-- edge -->
|
||||||
|
<div class="col-sm-12 form-section-title">
|
||||||
|
Edge
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<label for="edge_checkin" class="col-sm-3 control-label text-left">
|
||||||
|
Edge agent poll frequency
|
||||||
|
<portainer-tooltip position="bottom" message="Specify the interval used by each Edge agent to checkin with the Portainer instance"></portainer-tooltip>
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<select id="edge_checkin" class="form-control" ng-model="settings.EdgeAgentCheckinInterval" ng-options="+(opt.value) as opt.key for opt in state.availableEdgeAgentCheckinOptions"></select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !edge -->
|
||||||
<!-- actions -->
|
<!-- actions -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
|
|
|
@ -3,7 +3,21 @@ angular.module('portainer.app')
|
||||||
function ($scope, $state, Notifications, SettingsService, StateManager) {
|
function ($scope, $state, Notifications, SettingsService, StateManager) {
|
||||||
|
|
||||||
$scope.state = {
|
$scope.state = {
|
||||||
actionInProgress: false
|
actionInProgress: false,
|
||||||
|
availableEdgeAgentCheckinOptions: [
|
||||||
|
{
|
||||||
|
key: '5 seconds',
|
||||||
|
value: 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '10 seconds',
|
||||||
|
value: 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '30 seconds',
|
||||||
|
value: 30
|
||||||
|
},
|
||||||
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.formValues = {
|
$scope.formValues = {
|
||||||
|
|
|
@ -158,14 +158,20 @@ function ($q, $scope, $state, $transition$, StackService, NodeService, ServiceSe
|
||||||
function loadStack(id) {
|
function loadStack(id) {
|
||||||
var agentProxy = $scope.applicationState.endpoint.mode.agentProxy;
|
var agentProxy = $scope.applicationState.endpoint.mode.agentProxy;
|
||||||
|
|
||||||
|
EndpointService.endpoints()
|
||||||
|
.then(function success(data) {
|
||||||
|
$scope.endpoints = data.value;
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
Notifications.error('Failure', err, 'Unable to retrieve endpoints');
|
||||||
|
});
|
||||||
|
|
||||||
$q.all({
|
$q.all({
|
||||||
stack: StackService.stack(id),
|
stack: StackService.stack(id),
|
||||||
endpoints: EndpointService.endpoints(),
|
|
||||||
groups: GroupService.groups()
|
groups: GroupService.groups()
|
||||||
})
|
})
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
var stack = data.stack;
|
var stack = data.stack;
|
||||||
$scope.endpoints = data.endpoints.value;
|
|
||||||
$scope.groups = data.groups;
|
$scope.groups = data.groups;
|
||||||
$scope.stack = stack;
|
$scope.stack = stack;
|
||||||
|
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
|
@ -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 8000:8000 -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(';');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -69,6 +69,7 @@
|
||||||
"splitargs": "github:deviantony/splitargs#semver:~0.2.0",
|
"splitargs": "github:deviantony/splitargs#semver:~0.2.0",
|
||||||
"toastr": "github:CodeSeven/toastr#semver:~2.1.3",
|
"toastr": "github:CodeSeven/toastr#semver:~2.1.3",
|
||||||
"ui-select": "^0.19.8",
|
"ui-select": "^0.19.8",
|
||||||
|
"uuid": "^3.3.2",
|
||||||
"xterm": "^3.8.0"
|
"xterm": "^3.8.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
Loading…
Reference in New Issue