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"
|
||||
"time"
|
||||
|
||||
"github.com/portainer/portainer/api/bolt/tunnelserver"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/dockerhub"
|
||||
|
@ -51,6 +53,7 @@ type Store struct {
|
|||
TeamMembershipService *teammembership.Service
|
||||
TeamService *team.Service
|
||||
TemplateService *template.Service
|
||||
TunnelServerService *tunnelserver.Service
|
||||
UserService *user.Service
|
||||
VersionService *version.Service
|
||||
WebhookService *webhook.Service
|
||||
|
@ -220,6 +223,12 @@ func (store *Store) initServices() error {
|
|||
}
|
||||
store.TemplateService = templateService
|
||||
|
||||
tunnelServerService, err := tunnelserver.NewService(store.db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
store.TunnelServerService = tunnelServerService
|
||||
|
||||
userService, err := user.NewService(store.db)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
@ -63,7 +63,7 @@ func (service *Service) Endpoints() ([]portainer.Endpoint, error) {
|
|||
cursor := bucket.Cursor()
|
||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||
var endpoint portainer.Endpoint
|
||||
err := internal.UnmarshalObject(v, &endpoint)
|
||||
err := internal.UnmarshalObjectWithJsoniter(v, &endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@ package internal
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
// MarshalObject encodes an object to binary format
|
||||
|
@ -13,3 +15,11 @@ func MarshalObject(object interface{}) ([]byte, error) {
|
|||
func UnmarshalObject(data []byte, object interface{}) error {
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -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{
|
||||
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(),
|
||||
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(),
|
||||
|
|
|
@ -3,21 +3,23 @@
|
|||
package cli
|
||||
|
||||
const (
|
||||
defaultBindAddress = ":9000"
|
||||
defaultDataDirectory = "/data"
|
||||
defaultAssetsDirectory = "./"
|
||||
defaultNoAuth = "false"
|
||||
defaultNoAnalytics = "false"
|
||||
defaultTLS = "false"
|
||||
defaultTLSSkipVerify = "false"
|
||||
defaultTLSCACertPath = "/certs/ca.pem"
|
||||
defaultTLSCertPath = "/certs/cert.pem"
|
||||
defaultTLSKeyPath = "/certs/key.pem"
|
||||
defaultSSL = "false"
|
||||
defaultSSLCertPath = "/certs/portainer.crt"
|
||||
defaultSSLKeyPath = "/certs/portainer.key"
|
||||
defaultSyncInterval = "60s"
|
||||
defaultSnapshot = "true"
|
||||
defaultSnapshotInterval = "5m"
|
||||
defaultTemplateFile = "/templates.json"
|
||||
defaultBindAddress = ":9000"
|
||||
defaultTunnelServerAddress = "0.0.0.0"
|
||||
defaultTunnelServerPort = "8000"
|
||||
defaultDataDirectory = "/data"
|
||||
defaultAssetsDirectory = "./"
|
||||
defaultNoAuth = "false"
|
||||
defaultNoAnalytics = "false"
|
||||
defaultTLS = "false"
|
||||
defaultTLSSkipVerify = "false"
|
||||
defaultTLSCACertPath = "/certs/ca.pem"
|
||||
defaultTLSCertPath = "/certs/cert.pem"
|
||||
defaultTLSKeyPath = "/certs/key.pem"
|
||||
defaultSSL = "false"
|
||||
defaultSSLCertPath = "/certs/portainer.crt"
|
||||
defaultSSLKeyPath = "/certs/portainer.key"
|
||||
defaultSyncInterval = "60s"
|
||||
defaultSnapshot = "true"
|
||||
defaultSnapshotInterval = "5m"
|
||||
defaultTemplateFile = "/templates.json"
|
||||
)
|
||||
|
|
|
@ -1,21 +1,23 @@
|
|||
package cli
|
||||
|
||||
const (
|
||||
defaultBindAddress = ":9000"
|
||||
defaultDataDirectory = "C:\\data"
|
||||
defaultAssetsDirectory = "./"
|
||||
defaultNoAuth = "false"
|
||||
defaultNoAnalytics = "false"
|
||||
defaultTLS = "false"
|
||||
defaultTLSSkipVerify = "false"
|
||||
defaultTLSCACertPath = "C:\\certs\\ca.pem"
|
||||
defaultTLSCertPath = "C:\\certs\\cert.pem"
|
||||
defaultTLSKeyPath = "C:\\certs\\key.pem"
|
||||
defaultSSL = "false"
|
||||
defaultSSLCertPath = "C:\\certs\\portainer.crt"
|
||||
defaultSSLKeyPath = "C:\\certs\\portainer.key"
|
||||
defaultSyncInterval = "60s"
|
||||
defaultSnapshot = "true"
|
||||
defaultSnapshotInterval = "5m"
|
||||
defaultTemplateFile = "/templates.json"
|
||||
defaultBindAddress = ":9000"
|
||||
defaultTunnelServerAddress = "0.0.0.0"
|
||||
defaultTunnelServerPort = "8000"
|
||||
defaultDataDirectory = "C:\\data"
|
||||
defaultAssetsDirectory = "./"
|
||||
defaultNoAuth = "false"
|
||||
defaultNoAnalytics = "false"
|
||||
defaultTLS = "false"
|
||||
defaultTLSSkipVerify = "false"
|
||||
defaultTLSCACertPath = "C:\\certs\\ca.pem"
|
||||
defaultTLSCertPath = "C:\\certs\\cert.pem"
|
||||
defaultTLSKeyPath = "C:\\certs\\key.pem"
|
||||
defaultSSL = "false"
|
||||
defaultSSLCertPath = "C:\\certs\\portainer.crt"
|
||||
defaultSSLKeyPath = "C:\\certs\\portainer.key"
|
||||
defaultSyncInterval = "60s"
|
||||
defaultSnapshot = "true"
|
||||
defaultSnapshotInterval = "5m"
|
||||
defaultTemplateFile = "/templates.json"
|
||||
)
|
||||
|
|
|
@ -2,10 +2,13 @@ package main
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/portainer/portainer/api/chisel"
|
||||
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt"
|
||||
"github.com/portainer/portainer/api/cli"
|
||||
|
@ -20,8 +23,6 @@ import (
|
|||
"github.com/portainer/portainer/api/jwt"
|
||||
"github.com/portainer/portainer/api/ldap"
|
||||
"github.com/portainer/portainer/api/libcompose"
|
||||
|
||||
"log"
|
||||
)
|
||||
|
||||
func initCLI() *portainer.CLIFlags {
|
||||
|
@ -69,12 +70,12 @@ func initStore(dataStorePath string, fileService portainer.FileService) *bolt.St
|
|||
return store
|
||||
}
|
||||
|
||||
func initComposeStackManager(dataStorePath string) portainer.ComposeStackManager {
|
||||
return libcompose.NewComposeStackManager(dataStorePath)
|
||||
func initComposeStackManager(dataStorePath string, reverseTunnelService portainer.ReverseTunnelService) portainer.ComposeStackManager {
|
||||
return libcompose.NewComposeStackManager(dataStorePath, reverseTunnelService)
|
||||
}
|
||||
|
||||
func initSwarmStackManager(assetsPath string, dataStorePath string, signatureService portainer.DigitalSignatureService, fileService portainer.FileService) (portainer.SwarmStackManager, error) {
|
||||
return exec.NewSwarmStackManager(assetsPath, dataStorePath, signatureService, fileService)
|
||||
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, reverseTunnelService)
|
||||
}
|
||||
|
||||
func initJWTService(authenticationEnabled bool) portainer.JWTService {
|
||||
|
@ -104,8 +105,8 @@ func initGitService() portainer.GitService {
|
|||
return &git.Service{}
|
||||
}
|
||||
|
||||
func initClientFactory(signatureService portainer.DigitalSignatureService) *docker.ClientFactory {
|
||||
return docker.NewClientFactory(signatureService)
|
||||
func initClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService) *docker.ClientFactory {
|
||||
return docker.NewClientFactory(signatureService, reverseTunnelService)
|
||||
}
|
||||
|
||||
func initSnapshotter(clientFactory *docker.ClientFactory) portainer.Snapshotter {
|
||||
|
@ -196,7 +197,7 @@ func loadEndpointSyncSystemSchedule(jobScheduler portainer.JobScheduler, schedul
|
|||
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()
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -213,6 +214,13 @@ func loadSchedulesFromDatabase(jobScheduler portainer.JobScheduler, jobService p
|
|||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if schedule.EdgeSchedule != nil {
|
||||
for _, endpointID := range schedule.EdgeSchedule.Endpoints {
|
||||
reverseTunnelService.AddSchedule(endpointID, schedule.EdgeSchedule)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -265,6 +273,7 @@ func initSettings(settingsService portainer.SettingsService, flags *portainer.CL
|
|||
AllowPrivilegedModeForRegularUsers: true,
|
||||
EnableHostManagementFeatures: false,
|
||||
SnapshotInterval: *flags.SnapshotInterval,
|
||||
EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds,
|
||||
}
|
||||
|
||||
if *flags.Templates != "" {
|
||||
|
@ -540,7 +549,9 @@ func main() {
|
|||
log.Fatal(err)
|
||||
}
|
||||
|
||||
clientFactory := initClientFactory(digitalSignatureService)
|
||||
reverseTunnelService := chisel.NewService(store.EndpointService, store.TunnelServerService)
|
||||
|
||||
clientFactory := initClientFactory(digitalSignatureService, reverseTunnelService)
|
||||
|
||||
jobService := initJobService(clientFactory)
|
||||
|
||||
|
@ -551,12 +562,12 @@ func main() {
|
|||
endpointManagement = false
|
||||
}
|
||||
|
||||
swarmStackManager, err := initSwarmStackManager(*flags.Assets, *flags.Data, digitalSignatureService, fileService)
|
||||
swarmStackManager, err := initSwarmStackManager(*flags.Assets, *flags.Data, digitalSignatureService, fileService, reverseTunnelService)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
composeStackManager := initComposeStackManager(*flags.Data)
|
||||
composeStackManager := initComposeStackManager(*flags.Data, reverseTunnelService)
|
||||
|
||||
err = initTemplates(store.TemplateService, fileService, *flags.Templates, *flags.TemplateFile)
|
||||
if err != nil {
|
||||
|
@ -570,7 +581,7 @@ func main() {
|
|||
|
||||
jobScheduler := initJobScheduler()
|
||||
|
||||
err = loadSchedulesFromDatabase(jobScheduler, jobService, store.ScheduleService, store.EndpointService, fileService)
|
||||
err = loadSchedulesFromDatabase(jobScheduler, jobService, store.ScheduleService, store.EndpointService, fileService, reverseTunnelService)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
@ -658,7 +669,13 @@ func main() {
|
|||
go terminateIfNoAdminCreated(store.UserService)
|
||||
}
|
||||
|
||||
err = reverseTunnelService.StartTunnelServer(*flags.TunnelAddr, *flags.TunnelPort, snapshotter)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var server portainer.Server = &http.Server{
|
||||
ReverseTunnelService: reverseTunnelService,
|
||||
Status: applicationStatus,
|
||||
BindAddress: *flags.Addr,
|
||||
AssetsPath: *flags.Assets,
|
||||
|
|
|
@ -53,7 +53,7 @@ func (runner *SnapshotJobRunner) Run() {
|
|||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if endpoint.Type == portainer.AzureEnvironment {
|
||||
if endpoint.Type == portainer.AzureEnvironment || endpoint.Type == portainer.EdgeAgentEnvironment {
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,8 @@ import (
|
|||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"math/big"
|
||||
|
||||
"github.com/portainer/libcrypto"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -111,7 +113,7 @@ func (service *ECDSAService) CreateSignature(message string) (string, error) {
|
|||
message = service.secret
|
||||
}
|
||||
|
||||
hash := HashFromBytes([]byte(message))
|
||||
hash := libcrypto.HashFromBytes([]byte(message))
|
||||
|
||||
r := 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
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
@ -17,13 +18,15 @@ const (
|
|||
|
||||
// ClientFactory is used to create Docker clients
|
||||
type ClientFactory struct {
|
||||
signatureService portainer.DigitalSignatureService
|
||||
signatureService portainer.DigitalSignatureService
|
||||
reverseTunnelService portainer.ReverseTunnelService
|
||||
}
|
||||
|
||||
// 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{
|
||||
signatureService: signatureService,
|
||||
signatureService: signatureService,
|
||||
reverseTunnelService: reverseTunnelService,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -35,6 +38,8 @@ func (factory *ClientFactory) CreateClient(endpoint *portainer.Endpoint, nodeNam
|
|||
return nil, unsupportedEnvironmentType
|
||||
} else if endpoint.Type == portainer.AgentOnDockerEnvironment {
|
||||
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://") {
|
||||
|
@ -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) {
|
||||
httpCli, err := httpClient(endpoint)
|
||||
if err != nil {
|
||||
|
|
|
@ -3,6 +3,7 @@ package exec
|
|||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
|
@ -13,20 +14,22 @@ import (
|
|||
|
||||
// SwarmStackManager represents a service for managing stacks.
|
||||
type SwarmStackManager struct {
|
||||
binaryPath string
|
||||
dataPath string
|
||||
signatureService portainer.DigitalSignatureService
|
||||
fileService portainer.FileService
|
||||
binaryPath string
|
||||
dataPath string
|
||||
signatureService portainer.DigitalSignatureService
|
||||
fileService portainer.FileService
|
||||
reverseTunnelService portainer.ReverseTunnelService
|
||||
}
|
||||
|
||||
// NewSwarmStackManager initializes a new SwarmStackManager service.
|
||||
// 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{
|
||||
binaryPath: binaryPath,
|
||||
dataPath: dataPath,
|
||||
signatureService: signatureService,
|
||||
fileService: fileService,
|
||||
binaryPath: binaryPath,
|
||||
dataPath: dataPath,
|
||||
signatureService: signatureService,
|
||||
fileService: fileService,
|
||||
reverseTunnelService: reverseTunnelService,
|
||||
}
|
||||
|
||||
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).
|
||||
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 {
|
||||
if registry.Authentication {
|
||||
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.
|
||||
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")
|
||||
return runCommandAndCaptureStdErr(command, args, nil, "")
|
||||
}
|
||||
|
@ -63,7 +66,7 @@ func (manager *SwarmStackManager) Logout(endpoint *portainer.Endpoint) error {
|
|||
// Deploy executes the docker stack deploy command.
|
||||
func (manager *SwarmStackManager) Deploy(stack *portainer.Stack, prune bool, endpoint *portainer.Endpoint) error {
|
||||
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 {
|
||||
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.
|
||||
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)
|
||||
return runCommandAndCaptureStdErr(command, args, nil, "")
|
||||
}
|
||||
|
@ -106,7 +109,7 @@ func runCommandAndCaptureStdErr(command string, args []string, env []string, wor
|
|||
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
|
||||
command := path.Join(binaryPath, "docker")
|
||||
|
||||
|
@ -116,7 +119,14 @@ func prepareDockerCommandAndArgs(binaryPath, dataPath string, endpoint *portaine
|
|||
|
||||
args := make([]string, 0)
|
||||
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 {
|
||||
args = append(args, "--tls")
|
||||
|
|
|
@ -11,9 +11,11 @@ import (
|
|||
// Handler is the HTTP handler used to proxy requests to external APIs.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
requestBouncer *security.RequestBouncer
|
||||
EndpointService portainer.EndpointService
|
||||
ProxyManager *proxy.Manager
|
||||
requestBouncer *security.RequestBouncer
|
||||
EndpointService portainer.EndpointService
|
||||
SettingsService portainer.SettingsService
|
||||
ProxyManager *proxy.Manager
|
||||
ReverseTunnelService portainer.ReverseTunnelService
|
||||
}
|
||||
|
||||
// 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
|
||||
proxy = handler.ProxyManager.GetProxy(string(endpointID))
|
||||
proxy = handler.ProxyManager.GetProxy(endpoint)
|
||||
if proxy == nil {
|
||||
proxy, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint)
|
||||
if err != nil {
|
||||
|
|
|
@ -3,6 +3,7 @@ package endpointproxy
|
|||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"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}
|
||||
}
|
||||
|
||||
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")}
|
||||
}
|
||||
|
||||
|
@ -33,8 +34,32 @@ func (handler *Handler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.
|
|||
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
|
||||
proxy = handler.ProxyManager.GetProxy(string(endpointID))
|
||||
proxy = handler.ProxyManager.GetProxy(endpoint)
|
||||
if proxy == nil {
|
||||
proxy, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint)
|
||||
if err != nil {
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
package endpoints
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"runtime"
|
||||
"strconv"
|
||||
|
||||
|
@ -41,7 +44,7 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error {
|
|||
|
||||
endpointType, err := request.RetrieveNumericMultiPartFormValue(r, "EndpointType", false)
|
||||
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
|
||||
|
||||
|
@ -149,6 +152,8 @@ func (handler *Handler) endpointCreate(w http.ResponseWriter, r *http.Request) *
|
|||
func (handler *Handler) createEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) {
|
||||
if portainer.EndpointType(payload.EndpointType) == portainer.AzureEnvironment {
|
||||
return handler.createAzureEndpoint(payload)
|
||||
} else if portainer.EndpointType(payload.EndpointType) == portainer.EdgeAgentEnvironment {
|
||||
return handler.createEdgeAgentEndpoint(payload)
|
||||
}
|
||||
|
||||
if payload.TLS {
|
||||
|
@ -195,6 +200,52 @@ func (handler *Handler) createAzureEndpoint(payload *endpointCreatePayload) (*po
|
|||
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) {
|
||||
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}
|
||||
}
|
||||
|
||||
handler.ProxyManager.DeleteProxy(string(endpointID))
|
||||
handler.ProxyManager.DeleteProxy(endpoint)
|
||||
|
||||
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
|
||||
Snapshotter portainer.Snapshotter
|
||||
JobService portainer.JobService
|
||||
ReverseTunnelService portainer.ReverseTunnelService
|
||||
SettingsService portainer.SettingsService
|
||||
}
|
||||
|
||||
// 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)
|
||||
h.Handle("/endpoints/{id}/snapshot",
|
||||
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
|
||||
}
|
||||
|
|
|
@ -5,9 +5,9 @@ import (
|
|||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/portainer/libcrypto"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/crypto"
|
||||
"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")
|
||||
|
||||
hash := crypto.HashFromBytes([]byte(message))
|
||||
hash := libcrypto.HashFromBytes([]byte(message))
|
||||
resp := motdResponse{
|
||||
Title: data.Title,
|
||||
Message: message,
|
||||
|
|
|
@ -12,12 +12,13 @@ import (
|
|||
// Handler is the HTTP handler used to handle schedule operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
ScheduleService portainer.ScheduleService
|
||||
EndpointService portainer.EndpointService
|
||||
SettingsService portainer.SettingsService
|
||||
FileService portainer.FileService
|
||||
JobService portainer.JobService
|
||||
JobScheduler portainer.JobScheduler
|
||||
ScheduleService portainer.ScheduleService
|
||||
EndpointService portainer.EndpointService
|
||||
SettingsService portainer.SettingsService
|
||||
FileService portainer.FileService
|
||||
JobService portainer.JobService
|
||||
JobScheduler portainer.JobScheduler
|
||||
ReverseTunnelService portainer.ReverseTunnelService
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage schedule operations.
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
package schedules
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
|
@ -113,7 +115,7 @@ func (payload *scheduleCreateFromFileContentPayload) Validate(r *http.Request) e
|
|||
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 {
|
||||
settings, err := handler.SettingsService.Settings()
|
||||
if err != nil {
|
||||
|
@ -219,6 +221,46 @@ func (handler *Handler) createScheduleObjectFromFileContentPayload(payload *sche
|
|||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
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}
|
||||
}
|
||||
|
||||
handler.ReverseTunnelService.RemoveSchedule(schedule.ID)
|
||||
|
||||
handler.JobScheduler.UnscheduleJob(schedule.ID)
|
||||
|
||||
err = handler.ScheduleService.DeleteSchedule(portainer.ScheduleID(scheduleID))
|
||||
|
|
|
@ -3,6 +3,7 @@ package schedules
|
|||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
|
@ -18,6 +19,7 @@ type taskContainer struct {
|
|||
Status string `json:"Status"`
|
||||
Created float64 `json:"Created"`
|
||||
Labels map[string]string `json:"Labels"`
|
||||
Edge bool `json:"Edge"`
|
||||
}
|
||||
|
||||
// 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...)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -87,6 +105,7 @@ func extractTasksFromContainerSnasphot(endpoint *portainer.Endpoint, scheduleID
|
|||
for _, container := range containers {
|
||||
if container.Labels["io.portainer.schedule.id"] == strconv.Itoa(int(scheduleID)) {
|
||||
container.EndpointID = endpoint.ID
|
||||
container.Edge = false
|
||||
endpointTasks = append(endpointTasks, container)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package schedules
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"net/http"
|
||||
"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}
|
||||
}
|
||||
|
||||
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 {
|
||||
_, 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)
|
||||
}
|
||||
|
||||
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 {
|
||||
updateJobSchedule := false
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ type settingsUpdatePayload struct {
|
|||
EnableHostManagementFeatures *bool
|
||||
SnapshotInterval *string
|
||||
TemplatesURL *string
|
||||
EdgeAgentCheckinInterval *int
|
||||
}
|
||||
|
||||
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)
|
||||
if tlsError != nil {
|
||||
return tlsError
|
||||
|
|
|
@ -62,8 +62,10 @@ func (handler *Handler) handleAttachRequest(w http.ResponseWriter, r *http.Reque
|
|||
|
||||
r.Header.Del("Origin")
|
||||
|
||||
if params.nodeName != "" || params.endpoint.Type == portainer.AgentOnDockerEnvironment {
|
||||
return handler.proxyWebsocketRequest(w, r, params)
|
||||
if params.endpoint.Type == portainer.AgentOnDockerEnvironment {
|
||||
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)
|
||||
|
|
|
@ -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 {
|
||||
r.Header.Del("Origin")
|
||||
|
||||
if params.nodeName != "" || params.endpoint.Type == portainer.AgentOnDockerEnvironment {
|
||||
return handler.proxyWebsocketRequest(w, r, params)
|
||||
if params.endpoint.Type == portainer.AgentOnDockerEnvironment {
|
||||
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)
|
||||
|
|
|
@ -11,10 +11,11 @@ import (
|
|||
// Handler is the HTTP handler used to handle websocket operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
EndpointService portainer.EndpointService
|
||||
SignatureService portainer.DigitalSignatureService
|
||||
requestBouncer *security.RequestBouncer
|
||||
connectionUpgrader websocket.Upgrader
|
||||
EndpointService portainer.EndpointService
|
||||
SignatureService portainer.DigitalSignatureService
|
||||
ReverseTunnelService portainer.ReverseTunnelService
|
||||
requestBouncer *security.RequestBouncer
|
||||
connectionUpgrader websocket.Upgrader
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage websocket operations.
|
||||
|
|
|
@ -2,14 +2,37 @@ package websocket
|
|||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/koding/websocketproxy"
|
||||
"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)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
@ -24,7 +24,9 @@ type (
|
|||
DockerHubService portainer.DockerHubService
|
||||
SettingsService portainer.SettingsService
|
||||
SignatureService portainer.DigitalSignatureService
|
||||
ReverseTunnelService portainer.ReverseTunnelService
|
||||
endpointIdentifier portainer.EndpointID
|
||||
endpointType portainer.EndpointType
|
||||
}
|
||||
restrictedDockerOperationContext struct {
|
||||
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) {
|
||||
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) {
|
||||
|
|
|
@ -21,6 +21,7 @@ type proxyFactory struct {
|
|||
RegistryService portainer.RegistryService
|
||||
DockerHubService portainer.DockerHubService
|
||||
SignatureService portainer.DigitalSignatureService
|
||||
ReverseTunnelService portainer.ReverseTunnelService
|
||||
}
|
||||
|
||||
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) {
|
||||
url, err := url.Parse(AzureAPIBaseURL)
|
||||
remoteURL, err := url.Parse(AzureAPIBaseURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
proxy := newSingleHostReverseProxyWithHostHeader(url)
|
||||
proxy := newSingleHostReverseProxyWithHostHeader(remoteURL)
|
||||
proxy.Transport = NewAzureTransport(credentials)
|
||||
|
||||
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"
|
||||
|
||||
proxy := factory.createDockerReverseProxy(u, enableSignature, endpointID)
|
||||
proxy := factory.createDockerReverseProxy(u, endpoint)
|
||||
config, err := crypto.CreateTLSConfigurationFromDisk(tlsConfig.TLSCACertPath, tlsConfig.TLSCertPath, tlsConfig.TLSKeyPath, tlsConfig.TLSSkipVerify)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -53,13 +54,19 @@ func (factory *proxyFactory) newDockerHTTPSProxy(u *url.URL, tlsConfig *portaine
|
|||
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"
|
||||
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)
|
||||
|
||||
enableSignature := false
|
||||
if endpoint.Type == portainer.AgentOnDockerEnvironment {
|
||||
enableSignature = true
|
||||
}
|
||||
|
||||
transport := &proxyTransport{
|
||||
enableSignature: enableSignature,
|
||||
ResourceControlService: factory.ResourceControlService,
|
||||
|
@ -67,8 +74,10 @@ func (factory *proxyFactory) createDockerReverseProxy(u *url.URL, enableSignatur
|
|||
SettingsService: factory.SettingsService,
|
||||
RegistryService: factory.RegistryService,
|
||||
DockerHubService: factory.DockerHubService,
|
||||
ReverseTunnelService: factory.ReverseTunnelService,
|
||||
dockerTransport: &http.Transport{},
|
||||
endpointIdentifier: endpointID,
|
||||
endpointIdentifier: endpoint.ID,
|
||||
endpointType: endpoint.Type,
|
||||
}
|
||||
|
||||
if enableSignature {
|
||||
|
|
|
@ -8,7 +8,7 @@ import (
|
|||
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{}
|
||||
transport := &proxyTransport{
|
||||
enableSignature: false,
|
||||
|
@ -18,7 +18,9 @@ func (factory *proxyFactory) newLocalProxy(path string, endpointID portainer.End
|
|||
RegistryService: factory.RegistryService,
|
||||
DockerHubService: factory.DockerHubService,
|
||||
dockerTransport: newSocketTransport(path),
|
||||
endpointIdentifier: endpointID,
|
||||
ReverseTunnelService: factory.ReverseTunnelService,
|
||||
endpointIdentifier: endpoint.ID,
|
||||
endpointType: endpoint.Type,
|
||||
}
|
||||
proxy.Transport = transport
|
||||
return proxy
|
||||
|
|
|
@ -5,12 +5,11 @@ package proxy
|
|||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"github.com/Microsoft/go-winio"
|
||||
|
||||
|
||||
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{}
|
||||
transport := &proxyTransport{
|
||||
enableSignature: false,
|
||||
|
@ -19,8 +18,10 @@ func (factory *proxyFactory) newLocalProxy(path string, endpointID portainer.End
|
|||
SettingsService: factory.SettingsService,
|
||||
RegistryService: factory.RegistryService,
|
||||
DockerHubService: factory.DockerHubService,
|
||||
ReverseTunnelService: factory.ReverseTunnelService,
|
||||
dockerTransport: newNamedPipeTransport(path),
|
||||
endpointIdentifier: endpointID,
|
||||
endpointIdentifier: endpoint.ID,
|
||||
endpointType: endpoint.Type,
|
||||
}
|
||||
proxy.Transport = transport
|
||||
return proxy
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package proxy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
@ -21,6 +22,7 @@ type (
|
|||
// Manager represents a service used to manage Docker proxies.
|
||||
Manager struct {
|
||||
proxyFactory *proxyFactory
|
||||
reverseTunnelService portainer.ReverseTunnelService
|
||||
proxies cmap.ConcurrentMap
|
||||
extensionProxies cmap.ConcurrentMap
|
||||
legacyExtensionProxies cmap.ConcurrentMap
|
||||
|
@ -34,6 +36,7 @@ type (
|
|||
RegistryService portainer.RegistryService
|
||||
DockerHubService portainer.DockerHubService
|
||||
SignatureService portainer.DigitalSignatureService
|
||||
ReverseTunnelService portainer.ReverseTunnelService
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -50,13 +53,15 @@ func NewManager(parameters *ManagerParams) *Manager {
|
|||
RegistryService: parameters.RegistryService,
|
||||
DockerHubService: parameters.DockerHubService,
|
||||
SignatureService: parameters.SignatureService,
|
||||
ReverseTunnelService: parameters.ReverseTunnelService,
|
||||
},
|
||||
reverseTunnelService: parameters.ReverseTunnelService,
|
||||
}
|
||||
}
|
||||
|
||||
// GetProxy returns the proxy associated to a key
|
||||
func (manager *Manager) GetProxy(key string) http.Handler {
|
||||
proxy, ok := manager.proxies.Get(key)
|
||||
func (manager *Manager) GetProxy(endpoint *portainer.Endpoint) http.Handler {
|
||||
proxy, ok := manager.proxies.Get(string(endpoint.ID))
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
@ -76,8 +81,8 @@ func (manager *Manager) CreateAndRegisterProxy(endpoint *portainer.Endpoint) (ht
|
|||
}
|
||||
|
||||
// DeleteProxy deletes the proxy associated to a key
|
||||
func (manager *Manager) DeleteProxy(key string) {
|
||||
manager.proxies.Remove(key)
|
||||
func (manager *Manager) DeleteProxy(endpoint *portainer.Endpoint) {
|
||||
manager.proxies.Remove(string(endpoint.ID))
|
||||
}
|
||||
|
||||
// GetExtensionProxy returns an extension proxy associated to an extension identifier
|
||||
|
@ -136,28 +141,40 @@ func (manager *Manager) CreateLegacyExtensionProxy(key, extensionAPIURL string)
|
|||
return proxy, nil
|
||||
}
|
||||
|
||||
func (manager *Manager) createDockerProxy(endpointURL *url.URL, tlsConfig *portainer.TLSConfiguration, endpointID portainer.EndpointID) (http.Handler, error) {
|
||||
if endpointURL.Scheme == "tcp" {
|
||||
if tlsConfig.TLS || tlsConfig.TLSSkipVerify {
|
||||
return manager.proxyFactory.newDockerHTTPSProxy(endpointURL, tlsConfig, false, endpointID)
|
||||
}
|
||||
return manager.proxyFactory.newDockerHTTPProxy(endpointURL, false, endpointID), nil
|
||||
func (manager *Manager) createDockerProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
|
||||
baseURL := endpoint.URL
|
||||
if endpoint.Type == portainer.EdgeAgentEnvironment {
|
||||
tunnel := manager.reverseTunnelService.GetTunnelDetails(endpoint.ID)
|
||||
baseURL = fmt.Sprintf("http://localhost:%d", tunnel.Port)
|
||||
}
|
||||
return manager.proxyFactory.newLocalProxy(endpointURL.Path, endpointID), nil
|
||||
}
|
||||
|
||||
func (manager *Manager) createProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
|
||||
endpointURL, err := url.Parse(endpoint.URL)
|
||||
endpointURL, err := url.Parse(baseURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch endpoint.Type {
|
||||
case portainer.AgentOnDockerEnvironment:
|
||||
return manager.proxyFactory.newDockerHTTPSProxy(endpointURL, &endpoint.TLSConfig, true, endpoint.ID)
|
||||
case portainer.AzureEnvironment:
|
||||
return newAzureProxy(&endpoint.AzureCredentials)
|
||||
default:
|
||||
return manager.createDockerProxy(endpointURL, &endpoint.TLSConfig, endpoint.ID)
|
||||
return manager.proxyFactory.newDockerHTTPSProxy(endpointURL, &endpoint.TLSConfig, endpoint)
|
||||
case portainer.EdgeAgentEnvironment:
|
||||
return manager.proxyFactory.newDockerHTTPProxy(endpointURL, endpoint), nil
|
||||
}
|
||||
|
||||
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
|
||||
EndpointManagement bool
|
||||
Status *portainer.Status
|
||||
ReverseTunnelService portainer.ReverseTunnelService
|
||||
ExtensionManager portainer.ExtensionManager
|
||||
ComposeStackManager portainer.ComposeStackManager
|
||||
CryptoService portainer.CryptoService
|
||||
|
@ -88,6 +89,7 @@ func (server *Server) Start() error {
|
|||
RegistryService: server.RegistryService,
|
||||
DockerHubService: server.DockerHubService,
|
||||
SignatureService: server.SignatureService,
|
||||
ReverseTunnelService: server.ReverseTunnelService,
|
||||
}
|
||||
proxyManager := proxy.NewManager(proxyManagerParameters)
|
||||
|
||||
|
@ -132,6 +134,8 @@ func (server *Server) Start() error {
|
|||
endpointHandler.ProxyManager = proxyManager
|
||||
endpointHandler.Snapshotter = server.Snapshotter
|
||||
endpointHandler.JobService = server.JobService
|
||||
endpointHandler.ReverseTunnelService = server.ReverseTunnelService
|
||||
endpointHandler.SettingsService = server.SettingsService
|
||||
|
||||
var endpointGroupHandler = endpointgroups.NewHandler(requestBouncer)
|
||||
endpointGroupHandler.EndpointGroupService = server.EndpointGroupService
|
||||
|
@ -140,6 +144,8 @@ func (server *Server) Start() error {
|
|||
var endpointProxyHandler = endpointproxy.NewHandler(requestBouncer)
|
||||
endpointProxyHandler.EndpointService = server.EndpointService
|
||||
endpointProxyHandler.ProxyManager = proxyManager
|
||||
endpointProxyHandler.SettingsService = server.SettingsService
|
||||
endpointProxyHandler.ReverseTunnelService = server.ReverseTunnelService
|
||||
|
||||
var fileHandler = file.NewHandler(filepath.Join(server.AssetsPath, "public"))
|
||||
|
||||
|
@ -168,6 +174,7 @@ func (server *Server) Start() error {
|
|||
schedulesHandler.JobService = server.JobService
|
||||
schedulesHandler.JobScheduler = server.JobScheduler
|
||||
schedulesHandler.SettingsService = server.SettingsService
|
||||
schedulesHandler.ReverseTunnelService = server.ReverseTunnelService
|
||||
|
||||
var settingsHandler = settings.NewHandler(requestBouncer)
|
||||
settingsHandler.SettingsService = server.SettingsService
|
||||
|
@ -216,6 +223,7 @@ func (server *Server) Start() error {
|
|||
var websocketHandler = websocket.NewHandler(requestBouncer)
|
||||
websocketHandler.EndpointService = server.EndpointService
|
||||
websocketHandler.SignatureService = server.SignatureService
|
||||
websocketHandler.ReverseTunnelService = server.ReverseTunnelService
|
||||
|
||||
var webhookHandler = webhooks.NewHandler(requestBouncer)
|
||||
webhookHandler.WebhookService = server.WebhookService
|
||||
|
|
|
@ -2,6 +2,7 @@ package libcompose
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
|
@ -17,19 +18,28 @@ import (
|
|||
|
||||
// ComposeStackManager represents a service for managing compose stacks.
|
||||
type ComposeStackManager struct {
|
||||
dataPath string
|
||||
dataPath string
|
||||
reverseTunnelService portainer.ReverseTunnelService
|
||||
}
|
||||
|
||||
// NewComposeStackManager initializes a new ComposeStackManager service.
|
||||
func NewComposeStackManager(dataPath string) *ComposeStackManager {
|
||||
func NewComposeStackManager(dataPath string, reverseTunnelService portainer.ReverseTunnelService) *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{
|
||||
Host: endpoint.URL,
|
||||
Host: endpointURL,
|
||||
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)
|
||||
func (manager *ComposeStackManager) Up(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||
|
||||
clientFactory, err := createClient(endpoint)
|
||||
clientFactory, err := manager.createClient(endpoint)
|
||||
if err != nil {
|
||||
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)
|
||||
func (manager *ComposeStackManager) Down(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||
clientFactory, err := createClient(endpoint)
|
||||
clientFactory, err := manager.createClient(endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package portainer
|
||||
|
||||
import "time"
|
||||
|
||||
type (
|
||||
// Pair defines a key/value string pair
|
||||
Pair struct {
|
||||
|
@ -10,6 +12,8 @@ type (
|
|||
// CLIFlags represents the available flags on the CLI
|
||||
CLIFlags struct {
|
||||
Addr *string
|
||||
TunnelAddr *string
|
||||
TunnelPort *string
|
||||
AdminPassword *string
|
||||
AdminPasswordFile *string
|
||||
Assets *string
|
||||
|
@ -105,6 +109,7 @@ type (
|
|||
SnapshotInterval string `json:"SnapshotInterval"`
|
||||
TemplatesURL string `json:"TemplatesURL"`
|
||||
EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"`
|
||||
EdgeAgentCheckinInterval int `json:"EdgeAgentCheckinInterval"`
|
||||
|
||||
// Deprecated fields
|
||||
DisplayDonationHeader bool
|
||||
|
@ -250,7 +255,8 @@ type (
|
|||
Snapshots []Snapshot `json:"Snapshots"`
|
||||
UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"`
|
||||
TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies"`
|
||||
|
||||
EdgeID string `json:"EdgeID,omitempty"`
|
||||
EdgeKey string `json:"EdgeKey"`
|
||||
// Deprecated fields
|
||||
// Deprecated in DBVersion == 4
|
||||
TLS bool `json:"TLS,omitempty"`
|
||||
|
@ -333,11 +339,21 @@ type (
|
|||
Recurring bool
|
||||
Created int64
|
||||
JobType JobType
|
||||
EdgeSchedule *EdgeSchedule
|
||||
ScriptExecutionJob *ScriptExecutionJob
|
||||
SnapshotJob *SnapshotJob
|
||||
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 int
|
||||
|
||||
|
@ -575,6 +591,20 @@ type (
|
|||
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 interface {
|
||||
ParseFlags(version string) (*CLIFlags, error)
|
||||
|
@ -692,6 +722,12 @@ type (
|
|||
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 interface {
|
||||
Webhooks() ([]Webhook, error)
|
||||
|
@ -850,13 +886,25 @@ type (
|
|||
DisableExtension(extension *Extension) 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 (
|
||||
// APIVersion is the version number of the Portainer API
|
||||
APIVersion = "1.21.0"
|
||||
// DBVersion is the version number of the Portainer database
|
||||
DBVersion = 18
|
||||
DBVersion = 19
|
||||
// AssetsServerURL represents the URL of the Portainer asset server
|
||||
AssetsServerURL = "https://portainer-io-assets.sfo2.digitaloceanspaces.com"
|
||||
// MessageOfTheDayURL represents the URL where Portainer MOTD message can be retrieved
|
||||
|
@ -865,6 +913,8 @@ const (
|
|||
ExtensionDefinitionsURL = AssetsServerURL + "/extensions-1.21.0.json"
|
||||
// PortainerAgentHeader represents the name of the header available in any agent response
|
||||
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 = "X-PortainerAgent-Target"
|
||||
// PortainerAgentSignatureHeader represent the name of the header containing the digital signature
|
||||
|
@ -878,6 +928,8 @@ const (
|
|||
SupportedDockerAPIVersion = "1.24"
|
||||
// ExtensionServer represents the server used by Portainer to communicate with extensions
|
||||
ExtensionServer = "localhost"
|
||||
// DefaultEdgeAgentCheckinIntervalInSeconds represents the default interval (in seconds) used by Edge agents to checkin with the Portainer instance
|
||||
DefaultEdgeAgentCheckinIntervalInSeconds = 5
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -953,6 +1005,8 @@ const (
|
|||
AgentOnDockerEnvironment
|
||||
// AzureEnvironment represents an endpoint connected to an Azure environment
|
||||
AzureEnvironment
|
||||
// EdgeAgentEnvironment represents an endpoint connected to an Edge agent
|
||||
EdgeAgentEnvironment
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -1019,6 +1073,15 @@ const (
|
|||
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 (
|
||||
OperationDockerContainerArchiveInfo Authorization = "DockerContainerArchiveInfo"
|
||||
OperationDockerContainerList Authorization = "DockerContainerList"
|
||||
|
|
16
app/app.js
16
app/app.js
|
@ -1,8 +1,8 @@
|
|||
import _ from 'lodash-es';
|
||||
|
||||
angular.module('portainer')
|
||||
.run(['$rootScope', '$state', 'Authentication', 'authManager', 'StateManager', 'EndpointProvider', 'Notifications', 'Analytics', 'cfpLoadingBar', '$transitions', 'HttpRequestHelper',
|
||||
function ($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, $interval, Authentication, authManager, StateManager, EndpointProvider, Notifications, Analytics, SystemService, cfpLoadingBar, $transitions, HttpRequestHelper) {
|
||||
'use strict';
|
||||
|
||||
EndpointProvider.initialize();
|
||||
|
@ -34,8 +34,20 @@ function ($rootScope, $state, Authentication, authManager, StateManager, Endpoin
|
|||
$transitions.onBefore({}, function() {
|
||||
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) {
|
||||
authManager.checkAuthOnRefresh();
|
||||
|
|
|
@ -13,6 +13,7 @@ angular.module('portainer.docker')
|
|||
showQuickActionLogs: true,
|
||||
showQuickActionConsole: true,
|
||||
showQuickActionInspect: true,
|
||||
showQuickActionExec: true,
|
||||
showQuickActionAttach: false
|
||||
});
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ angular.module('portainer.docker')
|
|||
agentProxy: false
|
||||
};
|
||||
|
||||
if (type === 2) {
|
||||
if (type === 2 || type === 4) {
|
||||
mode.agentProxy = true;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { logsHandler, genericHandler } from "./response/handlers";
|
||||
import {genericHandler, logsHandler} from './response/handlers';
|
||||
|
||||
angular.module('portainer.docker')
|
||||
.factory('Container', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', 'ContainersInterceptor',
|
||||
|
@ -11,7 +11,7 @@ function ContainerFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, C
|
|||
{
|
||||
query: {
|
||||
method: 'GET', params: { all: 0, action: 'json', filters: '@filters' },
|
||||
isArray: true, interceptor: ContainersInterceptor, timeout: 10000
|
||||
isArray: true, interceptor: ContainersInterceptor, timeout: 15000
|
||||
},
|
||||
get: {
|
||||
method: 'GET', params: { action: 'json' }
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { jsonObjectsToArrayHandler, deleteImageHandler } from './response/handlers';
|
||||
import { imageGetResponse } from './response/image';
|
||||
import {deleteImageHandler, jsonObjectsToArrayHandler} from './response/handlers';
|
||||
import {imageGetResponse} from './response/image';
|
||||
|
||||
angular.module('portainer.docker')
|
||||
.factory('Image', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', 'HttpRequestHelper', 'ImagesInterceptor',
|
||||
|
@ -10,7 +10,7 @@ function ImageFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, HttpR
|
|||
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'}},
|
||||
search: {method: 'GET', params: {action: 'search'}},
|
||||
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')
|
||||
.factory('Network', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', 'NetworksInterceptor',
|
||||
|
@ -10,7 +10,7 @@ function NetworkFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, Net
|
|||
},
|
||||
{
|
||||
query: {
|
||||
method: 'GET', isArray: true, interceptor: NetworksInterceptor, timeout: 10000
|
||||
method: 'GET', isArray: true, interceptor: NetworksInterceptor, timeout: 15000
|
||||
},
|
||||
get: {
|
||||
method: 'GET'
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { jsonObjectsToArrayHandler } from './response/handlers';
|
||||
import {jsonObjectsToArrayHandler} from './response/handlers';
|
||||
|
||||
angular.module('portainer.docker')
|
||||
.factory('System', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', 'InfoInterceptor', 'VersionInterceptor',
|
||||
|
@ -10,7 +10,7 @@ angular.module('portainer.docker')
|
|||
},
|
||||
{
|
||||
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 },
|
||||
events: {
|
||||
|
|
|
@ -7,7 +7,7 @@ angular.module('portainer.docker')
|
|||
},
|
||||
{
|
||||
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')
|
||||
.factory('Volume', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', 'VolumesInterceptor',
|
||||
|
@ -9,7 +9,7 @@ angular.module('portainer.docker')
|
|||
endpointId: EndpointProvider.endpointID
|
||||
},
|
||||
{
|
||||
query: { method: 'GET', interceptor: VolumesInterceptor, timeout: 10000},
|
||||
query: { method: 'GET', interceptor: VolumesInterceptor, timeout: 15000},
|
||||
get: { method: 'GET', params: {id: '@id'} },
|
||||
create: {method: 'POST', params: {action: 'create'}, transformResponse: genericHandler, ignoreLoadingBar: true},
|
||||
remove: {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { EventViewModel } from '../models/event';
|
||||
import {EventViewModel} from '../models/event';
|
||||
|
||||
angular.module('portainer.docker')
|
||||
.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">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<tr ng-if="applicationState.endpoint.type !== 4">
|
||||
<td colspan="{{webhookURL ? '1' : '2'}}">
|
||||
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>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import _ from "lodash-es";
|
||||
import angular from "angular";
|
||||
import _ from 'lodash-es';
|
||||
import angular from 'angular';
|
||||
|
||||
import AccessViewerPolicyModel from '../../models/access'
|
||||
import AccessViewerPolicyModel from '../../models/access';
|
||||
|
||||
class AccessViewerController {
|
||||
/* @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))">
|
||||
<td>
|
||||
{{ 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>
|
||||
<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>
|
||||
<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>
|
||||
{{ item.Created | getisodatefromtimestamp}}
|
||||
<span ng-if="!item.Edge">{{ item.Created | getisodatefromtimestamp}}</span>
|
||||
<span ng-if="item.Edge">-</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="!$ctrl.dataset">
|
||||
|
|
|
@ -8,6 +8,7 @@ angular.module('portainer.docker').component('scheduleTasksDatatable', {
|
|||
tableKey: '@',
|
||||
orderBy: '@',
|
||||
reverseOrder: '<',
|
||||
goToContainerLogs: '<'
|
||||
goToContainerLogs: '<',
|
||||
getEdgeTaskLogs: '<'
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
<div class="blocklist-item" ng-click="$ctrl.onSelect($ctrl.model)">
|
||||
<div class="blocklist-item-box">
|
||||
<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 class="col-sm-12">
|
||||
|
@ -12,13 +13,16 @@
|
|||
{{ $ctrl.model.Name }}
|
||||
</span>
|
||||
<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' }}
|
||||
</span>
|
||||
<span class="space-left small text-muted" ng-if="$ctrl.model.Snapshots[0]">
|
||||
{{ $ctrl.model.Snapshots[0].Time | getisodatefromtimestamp }}
|
||||
</span>
|
||||
|
||||
</span>
|
||||
</span>
|
||||
<span>
|
||||
|
@ -64,6 +68,12 @@
|
|||
</span>
|
||||
</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">
|
||||
<span class="small text-muted">
|
||||
<span ng-if="$ctrl.model.Type === 1">
|
||||
|
@ -82,7 +92,7 @@
|
|||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<span class="small text-muted">
|
||||
<span class="small text-muted" ng-if="$ctrl.model.Type !== 4">
|
||||
{{ $ctrl.model.URL | stripprotocol }}
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
@ -89,7 +89,6 @@ angular.module('portainer.app').controller('EndpointListController', ['Datatable
|
|||
}
|
||||
|
||||
this.$onInit = function() {
|
||||
this.state.loading = true;
|
||||
var textFilter = DatatableService.getDataTableTextFilters(this.tableKey);
|
||||
this.state.paginatedItemLimit = PaginationService.getPaginationLimit(this.tableKey);
|
||||
if (textFilter !== null) {
|
||||
|
|
|
@ -25,7 +25,7 @@ angular.module('portainer.app').component('scheduleForm', {
|
|||
ctrl.formValues = {
|
||||
datetime: ctrl.model.CronExpression ? cronToDatetime(ctrl.model.CronExpression) : moment(),
|
||||
scheduleValue: ctrl.scheduleValues[0],
|
||||
cronMethod: 'basic'
|
||||
cronMethod: ctrl.model.Recurring ? 'advanced' : 'basic'
|
||||
};
|
||||
|
||||
function cronToDatetime(cron) {
|
||||
|
@ -38,7 +38,7 @@ angular.module('portainer.app').component('scheduleForm', {
|
|||
|
||||
function datetimeToCron(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() {
|
||||
|
|
|
@ -1,4 +1,15 @@
|
|||
<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">
|
||||
Schedule configuration
|
||||
</div>
|
||||
|
@ -114,7 +125,12 @@
|
|||
</div>
|
||||
<div class="form-group">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -123,6 +139,13 @@
|
|||
<div class="col-sm-12 form-section-title">
|
||||
Job configuration
|
||||
</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 -->
|
||||
<div class="form-group">
|
||||
<label for="schedule_image" class="col-sm-2 control-label text-left">Image</label>
|
||||
|
@ -195,8 +218,13 @@
|
|||
</div>
|
||||
<div class="form-group">
|
||||
<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
|
||||
<code>/host</code> folder.
|
||||
<p>
|
||||
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>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
|
|
|
@ -124,6 +124,8 @@ angular.module('portainer.app')
|
|||
return 'Agent';
|
||||
} else if (type === 3) {
|
||||
return 'Azure ACI';
|
||||
} else if (type === 4) {
|
||||
return 'Edge Agent';
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
@ -133,6 +135,8 @@ angular.module('portainer.app')
|
|||
return function (type) {
|
||||
if (type === 3) {
|
||||
return 'fab fa-microsoft';
|
||||
} else if (type === 4) {
|
||||
return 'fa fa-cloud';
|
||||
}
|
||||
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() {
|
||||
this.Name = '';
|
||||
|
@ -9,7 +10,7 @@ export function ScheduleDefaultModel() {
|
|||
}
|
||||
|
||||
function ScriptExecutionDefaultJobModel() {
|
||||
this.Image = '';
|
||||
this.Image = 'ubuntu:latest';
|
||||
this.Endpoints = [];
|
||||
this.FileContent = '';
|
||||
this.File = null;
|
||||
|
@ -23,14 +24,20 @@ export function ScheduleModel(data) {
|
|||
this.JobType = data.JobType;
|
||||
this.CronExpression = data.CronExpression;
|
||||
this.Created = data.Created;
|
||||
this.EdgeSchedule = data.EdgeSchedule;
|
||||
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.Endpoints = data.Endpoints;
|
||||
|
||||
if (edgeSchedule !== null) {
|
||||
this.Endpoints = _.concat(data.Endpoints, edgeSchedule.Endpoints);
|
||||
}
|
||||
|
||||
this.FileContent = '';
|
||||
this.Method = 'editor';
|
||||
this.RetryCount = data.RetryCount;
|
||||
|
@ -42,6 +49,7 @@ export function ScriptExecutionTaskModel(data) {
|
|||
this.EndpointId = data.EndpointId;
|
||||
this.Status = createStatus(data.Status);
|
||||
this.Created = data.Created;
|
||||
this.Edge = data.Edge;
|
||||
}
|
||||
|
||||
export function ScheduleCreateRequest(model) {
|
||||
|
|
|
@ -10,6 +10,7 @@ export function SettingsViewModel(data) {
|
|||
this.TemplatesURL = data.TemplatesURL;
|
||||
this.ExternalTemplates = data.ExternalTemplates;
|
||||
this.EnableHostManagementFeatures = data.EnableHostManagementFeatures;
|
||||
this.EdgeAgentCheckinInterval = data.EdgeAgentCheckinInterval;
|
||||
}
|
||||
|
||||
export function PublicSettingsViewModel(settings) {
|
||||
|
|
|
@ -13,8 +13,9 @@ angular.module('portainer.app')
|
|||
update: { method: 'PUT', params: { id: '@id' } },
|
||||
updateAccess: { method: 'PUT', params: { id: '@id', action: 'access' } },
|
||||
remove: { method: 'DELETE', params: { id: '@id'} },
|
||||
snapshots: { method: 'POST', params: { action: 'snapshot' }},
|
||||
snapshot: { method: 'POST', params: { id: '@id', action: 'snapshot' }},
|
||||
executeJob: { method: 'POST', ignoreLoadingBar: true, params: { id: '@id', action: 'job' } }
|
||||
snapshots: { method: 'POST', params: { action: 'snapshot' } },
|
||||
snapshot: { method: 'POST', params: { id: '@id', action: 'snapshot' } },
|
||||
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) {
|
||||
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) {
|
||||
deferred.resolve(response.data);
|
||||
})
|
||||
|
|
|
@ -1,8 +1,4 @@
|
|||
import {
|
||||
EndpointGroupModel,
|
||||
EndpointGroupCreateRequest,
|
||||
EndpointGroupUpdateRequest
|
||||
} from '../../models/group';
|
||||
import {EndpointGroupCreateRequest, EndpointGroupModel, EndpointGroupUpdateRequest} from '../../models/group';
|
||||
|
||||
angular.module('portainer.app')
|
||||
.factory('GroupService', ['$q', 'EndpointGroups',
|
||||
|
|
|
@ -171,6 +171,7 @@ function StateManagerFactory($q, SystemService, InfoHelper, LocalStorage, Settin
|
|||
var endpointAPIVersion = parseFloat(data.version.ApiVersion);
|
||||
state.endpoint.mode = endpointMode;
|
||||
state.endpoint.name = endpoint.Name;
|
||||
state.endpoint.type = endpoint.Type;
|
||||
state.endpoint.apiVersion = endpointAPIVersion;
|
||||
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')
|
||||
.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);
|
||||
};
|
||||
|
||||
$scope.setDefaultPortainerInstanceURL = function() {
|
||||
$scope.formValues.URL = window.location.origin;
|
||||
};
|
||||
|
||||
$scope.resetEndpointURL = function() {
|
||||
$scope.formValues.URL = '';
|
||||
};
|
||||
|
||||
$scope.addDockerEndpoint = function() {
|
||||
var name = $scope.formValues.Name;
|
||||
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);
|
||||
};
|
||||
|
||||
$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() {
|
||||
var name = $scope.formValues.Name;
|
||||
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) {
|
||||
$scope.state.actionInProgress = true;
|
||||
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);
|
||||
$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) {
|
||||
Notifications.error('Failure', err, 'Unable to create endpoint');
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
<div class="form-group"></div>
|
||||
<div class="form-group" style="margin-bottom: 0">
|
||||
<div class="boxselector_wrapper">
|
||||
<div>
|
||||
<div ng-click="resetEndpointURL()">
|
||||
<input type="radio" id="docker_endpoint" ng-model="state.EnvironmentType" value="docker">
|
||||
<label for="docker_endpoint">
|
||||
<div class="boxselector_header">
|
||||
|
@ -26,7 +26,7 @@
|
|||
<p>Docker environment</p>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<div ng-click="resetEndpointURL()">
|
||||
<input type="radio" id="agent_endpoint" ng-model="state.EnvironmentType" value="agent">
|
||||
<label for="agent_endpoint">
|
||||
<div class="boxselector_header">
|
||||
|
@ -36,6 +36,16 @@
|
|||
<p>Portainer agent</p>
|
||||
</label>
|
||||
</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>
|
||||
<input type="radio" id="azure_endpoint" ng-model="state.EnvironmentType" value="azure">
|
||||
<label for="azure_endpoint">
|
||||
|
@ -77,6 +87,16 @@
|
|||
</span>
|
||||
</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 class="col-sm-12 form-section-title" >
|
||||
Information
|
||||
|
@ -138,6 +158,26 @@
|
|||
</div>
|
||||
</div>
|
||||
<!-- !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 -->
|
||||
<div ng-if="state.EnvironmentType === 'docker' || state.EnvironmentType === 'agent'">
|
||||
<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-show="state.actionInProgress">Creating endpoint...</span>
|
||||
</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">
|
||||
<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>
|
||||
|
|
|
@ -1,10 +1,108 @@
|
|||
<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>
|
||||
<a ui-sref="portainer.endpoints">Endpoints</a> > <a ui-sref="portainer.endpoints.endpoint({id: endpoint.Id})">{{ endpoint.Name }}</a>
|
||||
</rd-header-content>
|
||||
</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="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
|
@ -22,7 +120,7 @@
|
|||
</div>
|
||||
<!-- !name-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">
|
||||
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>
|
||||
|
@ -70,7 +168,7 @@
|
|||
</div>
|
||||
<!-- !tags -->
|
||||
<!-- 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">
|
||||
Security
|
||||
</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')
|
||||
.controller('EndpointController', ['$q', '$scope', '$state', '$transition$', '$filter', 'EndpointService', 'GroupService', 'TagService', 'EndpointProvider', 'Notifications',
|
||||
function ($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, clipboard, EndpointService, GroupService, TagService, EndpointProvider, Notifications) {
|
||||
|
||||
if (!$scope.applicationState.application.endpointManagement) {
|
||||
$state.go('portainer.endpoints');
|
||||
|
@ -10,13 +12,28 @@ function ($q, $scope, $state, $transition$, $filter, EndpointService, GroupServi
|
|||
|
||||
$scope.state = {
|
||||
uploadInProgress: false,
|
||||
actionInProgress: false
|
||||
actionInProgress: false,
|
||||
deploymentTab: 0
|
||||
};
|
||||
|
||||
$scope.formValues = {
|
||||
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() {
|
||||
var endpoint = $scope.endpoint;
|
||||
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() {
|
||||
$q.all({
|
||||
endpoint: EndpointService.endpoint($transition$.params().id),
|
||||
|
@ -75,6 +106,10 @@ function ($q, $scope, $state, $transition$, $filter, EndpointService, GroupServi
|
|||
$scope.endpointType = 'remote';
|
||||
}
|
||||
endpoint.URL = $filter('stripprotocol')(endpoint.URL);
|
||||
if (endpoint.Type === 4) {
|
||||
$scope.edgeKeyDetails = decodeEdgeKey(endpoint.EdgeKey);
|
||||
$scope.randomEdgeID = uuidv4();
|
||||
}
|
||||
$scope.endpoint = endpoint;
|
||||
$scope.groups = data.groups;
|
||||
$scope.availableTags = data.tags;
|
||||
|
|
|
@ -24,7 +24,12 @@
|
|||
</span>
|
||||
</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">
|
||||
<endpoint-list
|
||||
title-text="Endpoints" title-icon="fa-plug"
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
angular.module('portainer.app')
|
||||
.controller('HomeController', ['$q', '$scope', '$state', '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) {
|
||||
.controller('HomeController', ['$q', '$scope', '$state', '$interval', '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) {
|
||||
$state.go('portainer.endpoints.endpoint', { id: id });
|
||||
|
@ -9,10 +13,12 @@ angular.module('portainer.app')
|
|||
$scope.goToDashboard = function(endpoint) {
|
||||
if (endpoint.Type === 3) {
|
||||
return switchToAzureEndpoint(endpoint);
|
||||
} else if (endpoint.Type === 4) {
|
||||
return switchToEdgeEndpoint(endpoint);
|
||||
}
|
||||
|
||||
checkEndpointStatus(endpoint)
|
||||
.then(function sucess() {
|
||||
.then(function success() {
|
||||
return switchToDockerEndpoint(endpoint);
|
||||
}).catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to verify endpoint status');
|
||||
|
@ -41,7 +47,7 @@ angular.module('portainer.app')
|
|||
|
||||
var status = 1;
|
||||
SystemService.ping(endpoint.Id)
|
||||
.then(function sucess() {
|
||||
.then(function success() {
|
||||
status = 1;
|
||||
}).catch(function error() {
|
||||
status = 2;
|
||||
|
@ -52,7 +58,7 @@ angular.module('portainer.app')
|
|||
}
|
||||
|
||||
EndpointService.updateEndpoint(endpoint.Id, { Status: status })
|
||||
.then(function sucess() {
|
||||
.then(function success() {
|
||||
deferred.resolve(endpoint);
|
||||
}).catch(function error(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) {
|
||||
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.');
|
||||
return;
|
||||
} 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.');
|
||||
return;
|
||||
}
|
||||
|
@ -97,6 +125,9 @@ angular.module('portainer.app')
|
|||
})
|
||||
.catch(function error(err) {
|
||||
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')
|
||||
.controller('CreateScheduleController', ['$q', '$scope', '$state', 'Notifications', 'EndpointService', 'GroupService', 'ScheduleService',
|
||||
|
|
|
@ -56,6 +56,7 @@
|
|||
table-key="schedule-tasks"
|
||||
order-by="Status" reverse-order="true"
|
||||
go-to-container-logs="goToContainerLogs"
|
||||
get-edge-task-logs="getEdgeTaskLogs"
|
||||
></schedule-tasks-datatable>
|
||||
</uib-tab>
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
angular.module('portainer.app')
|
||||
.controller('ScheduleController', ['$q', '$scope', '$transition$', '$state', 'Notifications', 'EndpointService', 'GroupService', 'ScheduleService', 'EndpointProvider',
|
||||
function ($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, HostBrowserService, FileSaver) {
|
||||
|
||||
$scope.state = {
|
||||
actionInProgress: false
|
||||
|
@ -8,6 +8,7 @@ function ($q, $scope, $transition$, $state, Notifications, EndpointService, Grou
|
|||
|
||||
$scope.update = update;
|
||||
$scope.goToContainerLogs = goToContainerLogs;
|
||||
$scope.getEdgeTaskLogs = getEdgeTaskLogs;
|
||||
|
||||
function update() {
|
||||
var model = $scope.schedule;
|
||||
|
@ -31,6 +32,26 @@ function ($q, $scope, $transition$, $state, Notifications, EndpointService, Grou
|
|||
$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) {
|
||||
for (var i = 0; i < tasks.length; i++) {
|
||||
var task = tasks[i];
|
||||
|
|
|
@ -112,7 +112,23 @@
|
|||
</label>
|
||||
</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 -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
|
|
|
@ -3,7 +3,21 @@ angular.module('portainer.app')
|
|||
function ($scope, $state, Notifications, SettingsService, StateManager) {
|
||||
|
||||
$scope.state = {
|
||||
actionInProgress: false
|
||||
actionInProgress: false,
|
||||
availableEdgeAgentCheckinOptions: [
|
||||
{
|
||||
key: '5 seconds',
|
||||
value: 5
|
||||
},
|
||||
{
|
||||
key: '10 seconds',
|
||||
value: 10
|
||||
},
|
||||
{
|
||||
key: '30 seconds',
|
||||
value: 30
|
||||
},
|
||||
]
|
||||
};
|
||||
|
||||
$scope.formValues = {
|
||||
|
|
|
@ -158,14 +158,20 @@ function ($q, $scope, $state, $transition$, StackService, NodeService, ServiceSe
|
|||
function loadStack(id) {
|
||||
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({
|
||||
stack: StackService.stack(id),
|
||||
endpoints: EndpointService.endpoints(),
|
||||
groups: GroupService.groups()
|
||||
})
|
||||
.then(function success(data) {
|
||||
var stack = data.stack;
|
||||
$scope.endpoints = data.endpoints.value;
|
||||
$scope.groups = data.groups;
|
||||
$scope.stack = stack;
|
||||
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
|
@ -48,7 +48,7 @@ module.exports = function(grunt) {
|
|||
]);
|
||||
});
|
||||
|
||||
grunt.task.registerTask('devopsbuild', 'devopsbuild:<platform>:<arch>',
|
||||
grunt.task.registerTask('devopsbuild', 'devopsbuild:<platform>:<arch>',
|
||||
function(p, a) {
|
||||
grunt.task.run([
|
||||
'config:prod',
|
||||
|
@ -146,7 +146,7 @@ gruntfile_cfg.copy = {
|
|||
files: [
|
||||
{
|
||||
dest: '<%= root %>/',
|
||||
src: 'templates.json',
|
||||
src: 'templates.json',
|
||||
cwd: ''
|
||||
}
|
||||
]
|
||||
|
@ -185,7 +185,7 @@ function shell_buildBinaryOnDevOps(p, a) {
|
|||
function shell_run() {
|
||||
return [
|
||||
'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(';');
|
||||
}
|
||||
|
||||
|
|
|
@ -69,6 +69,7 @@
|
|||
"splitargs": "github:deviantony/splitargs#semver:~0.2.0",
|
||||
"toastr": "github:CodeSeven/toastr#semver:~2.1.3",
|
||||
"ui-select": "^0.19.8",
|
||||
"uuid": "^3.3.2",
|
||||
"xterm": "^3.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
Loading…
Reference in New Issue