mirror of https://github.com/portainer/portainer
commit
0b2a76d75a
|
@ -21,24 +21,18 @@ Also, be sure to check our FAQ and documentation first: https://portainer.readth
|
||||||
-->
|
-->
|
||||||
|
|
||||||
**Bug description**
|
**Bug description**
|
||||||
|
|
||||||
A clear and concise description of what the bug is.
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
**Expected behavior**
|
**Expected behavior**
|
||||||
A clear and concise description of what you expected to happen.
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
Briefly describe what you were expecting.
|
|
||||||
|
|
||||||
**Steps to reproduce the issue:**
|
**Steps to reproduce the issue:**
|
||||||
|
|
||||||
Steps to reproduce the behavior:
|
|
||||||
1. Go to '...'
|
1. Go to '...'
|
||||||
2. Click on '....'
|
2. Click on '....'
|
||||||
3. Scroll down to '....'
|
3. Scroll down to '....'
|
||||||
4. See error
|
4. See error
|
||||||
|
|
||||||
**Technical details:**
|
**Technical details:**
|
||||||
|
|
||||||
* Portainer version:
|
* Portainer version:
|
||||||
* Docker version (managed by Portainer):
|
* Docker version (managed by Portainer):
|
||||||
* Platform (windows/linux):
|
* Platform (windows/linux):
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
# Config for Stalebot, limited to only `issues`
|
||||||
|
only: issues
|
||||||
|
|
||||||
|
# Issues config
|
||||||
|
issues:
|
||||||
|
daysUntilStale: 60
|
||||||
|
daysUntilClose: 7
|
||||||
|
|
||||||
|
# Limit the number of actions per hour, from 1-30. Default is 30
|
||||||
|
limitPerRun: 30
|
||||||
|
|
||||||
|
# Issues with these labels will never be considered stale
|
||||||
|
exemptLabels:
|
||||||
|
- kind/enhancement
|
||||||
|
- kind/feature
|
||||||
|
- kind/question
|
||||||
|
- kind/style
|
||||||
|
- bug/need-confirmation
|
||||||
|
- bug/confirmed
|
||||||
|
- status/discuss
|
||||||
|
|
||||||
|
# Only issues with all of these labels are checked if stale. Defaults to `[]` (disabled)
|
||||||
|
onlyLabels: []
|
||||||
|
|
||||||
|
# Set to true to ignore issues in a project (defaults to false)
|
||||||
|
exemptProjects: true
|
||||||
|
# Set to true to ignore issues in a milestone (defaults to false)
|
||||||
|
exemptMilestones: true
|
||||||
|
# Set to true to ignore issues with an assignee (defaults to false)
|
||||||
|
exemptAssignees: true
|
||||||
|
|
||||||
|
# Label to use when marking an issue as stale
|
||||||
|
staleLabel: status/stale
|
||||||
|
|
||||||
|
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||||
|
markComment: >
|
||||||
|
This issue has been marked as stale as it has not had recent activity,
|
||||||
|
it will be closed if no further activity occurs in the next 7 days.
|
||||||
|
If you believe that it has been incorrectly labelled as stale,
|
||||||
|
leave a comment and the label will be removed.
|
||||||
|
|
||||||
|
# Comment to post when removing the stale label.
|
||||||
|
# unmarkComment: >
|
||||||
|
# Your comment here.
|
||||||
|
|
||||||
|
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||||
|
closeComment: >
|
||||||
|
Since no further activity has appeared on this issue it will be closed.
|
||||||
|
If you believe that it has been incorrectly closed, leave a comment
|
||||||
|
and mention @itsconquest. One of our staff will then review the issue.
|
||||||
|
|
||||||
|
Note - If it is an old bug report, make sure that it is reproduceable in the
|
||||||
|
latest version of Portainer as it may have already been fixed.
|
||||||
|
|
|
@ -5,6 +5,8 @@ import (
|
||||||
"path"
|
"path"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/portainer/portainer/api/bolt/tunnelserver"
|
||||||
|
|
||||||
"github.com/boltdb/bolt"
|
"github.com/boltdb/bolt"
|
||||||
"github.com/portainer/portainer/api"
|
"github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/bolt/dockerhub"
|
"github.com/portainer/portainer/api/bolt/dockerhub"
|
||||||
|
@ -51,6 +53,7 @@ type Store struct {
|
||||||
TeamMembershipService *teammembership.Service
|
TeamMembershipService *teammembership.Service
|
||||||
TeamService *team.Service
|
TeamService *team.Service
|
||||||
TemplateService *template.Service
|
TemplateService *template.Service
|
||||||
|
TunnelServerService *tunnelserver.Service
|
||||||
UserService *user.Service
|
UserService *user.Service
|
||||||
VersionService *version.Service
|
VersionService *version.Service
|
||||||
WebhookService *webhook.Service
|
WebhookService *webhook.Service
|
||||||
|
@ -220,6 +223,12 @@ func (store *Store) initServices() error {
|
||||||
}
|
}
|
||||||
store.TemplateService = templateService
|
store.TemplateService = templateService
|
||||||
|
|
||||||
|
tunnelServerService, err := tunnelserver.NewService(store.db)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
store.TunnelServerService = tunnelServerService
|
||||||
|
|
||||||
userService, err := user.NewService(store.db)
|
userService, err := user.NewService(store.db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
package endpoint
|
package endpoint
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/boltdb/bolt"
|
||||||
"github.com/portainer/portainer/api"
|
"github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/bolt/internal"
|
"github.com/portainer/portainer/api/bolt/internal"
|
||||||
|
|
||||||
"github.com/boltdb/bolt"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -64,7 +63,7 @@ func (service *Service) Endpoints() ([]portainer.Endpoint, error) {
|
||||||
cursor := bucket.Cursor()
|
cursor := bucket.Cursor()
|
||||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||||
var endpoint portainer.Endpoint
|
var endpoint portainer.Endpoint
|
||||||
err := internal.UnmarshalObject(v, &endpoint)
|
err := internal.UnmarshalObjectWithJsoniter(v, &endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,8 @@ package internal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
||||||
|
jsoniter "github.com/json-iterator/go"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MarshalObject encodes an object to binary format
|
// MarshalObject encodes an object to binary format
|
||||||
|
@ -13,3 +15,11 @@ func MarshalObject(object interface{}) ([]byte, error) {
|
||||||
func UnmarshalObject(data []byte, object interface{}) error {
|
func UnmarshalObject(data []byte, object interface{}) error {
|
||||||
return json.Unmarshal(data, object)
|
return json.Unmarshal(data, object)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UnmarshalObjectWithJsoniter decodes an object from binary data
|
||||||
|
// using the jsoniter library. It is mainly used to accelerate endpoint
|
||||||
|
// decoding at the moment.
|
||||||
|
func UnmarshalObjectWithJsoniter(data []byte, object interface{}) error {
|
||||||
|
var jsoni = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||||
|
return jsoni.Unmarshal(data, &object)
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
package migrator
|
||||||
|
|
||||||
|
import portainer "github.com/portainer/portainer/api"
|
||||||
|
|
||||||
|
func (m *Migrator) updateSettingsToDBVersion19() error {
|
||||||
|
legacySettings, err := m.settingsService.Settings()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if legacySettings.EdgeAgentCheckinInterval == 0 {
|
||||||
|
legacySettings.EdgeAgentCheckinInterval = portainer.DefaultEdgeAgentCheckinIntervalInSeconds
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.settingsService.UpdateSettings(legacySettings)
|
||||||
|
}
|
|
@ -249,5 +249,13 @@ func (m *Migrator) Migrate() error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Portainer 1.22.0
|
||||||
|
if m.currentDBVersion < 19 {
|
||||||
|
err := m.updateSettingsToDBVersion19()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return m.versionService.StoreDBVersion(portainer.DBVersion)
|
return m.versionService.StoreDBVersion(portainer.DBVersion)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
package tunnelserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/bolt/internal"
|
||||||
|
|
||||||
|
"github.com/boltdb/bolt"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// BucketName represents the name of the bucket where this service stores data.
|
||||||
|
BucketName = "tunnel_server"
|
||||||
|
infoKey = "INFO"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Service represents a service for managing endpoint data.
|
||||||
|
type Service struct {
|
||||||
|
db *bolt.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewService creates a new instance of a service.
|
||||||
|
func NewService(db *bolt.DB) (*Service, error) {
|
||||||
|
err := internal.CreateBucket(db, BucketName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Service{
|
||||||
|
db: db,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info retrieve the TunnelServerInfo object.
|
||||||
|
func (service *Service) Info() (*portainer.TunnelServerInfo, error) {
|
||||||
|
var info portainer.TunnelServerInfo
|
||||||
|
|
||||||
|
err := internal.GetObject(service.db, BucketName, []byte(infoKey), &info)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &info, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateInfo persists a TunnelServerInfo object.
|
||||||
|
func (service *Service) UpdateInfo(settings *portainer.TunnelServerInfo) error {
|
||||||
|
return internal.UpdateObject(service.db, BucketName, []byte(infoKey), settings)
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package chisel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GenerateEdgeKey will generate a key that can be used by an Edge agent to register with a Portainer instance.
|
||||||
|
// The key represents the following data in this particular format:
|
||||||
|
// portainer_instance_url|tunnel_server_addr|tunnel_server_fingerprint|endpoint_ID
|
||||||
|
// The key returned by this function is a base64 encoded version of the data.
|
||||||
|
func (service *Service) GenerateEdgeKey(url, host string, endpointIdentifier int) string {
|
||||||
|
keyInformation := []string{
|
||||||
|
url,
|
||||||
|
fmt.Sprintf("%s:%s", host, service.serverPort),
|
||||||
|
service.serverFingerprint,
|
||||||
|
strconv.Itoa(endpointIdentifier),
|
||||||
|
}
|
||||||
|
|
||||||
|
key := strings.Join(keyInformation, "|")
|
||||||
|
return base64.RawStdEncoding.EncodeToString([]byte(key))
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
package chisel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AddSchedule register a schedule inside the tunnel details associated to an endpoint.
|
||||||
|
func (service *Service) AddSchedule(endpointID portainer.EndpointID, schedule *portainer.EdgeSchedule) {
|
||||||
|
tunnel := service.GetTunnelDetails(endpointID)
|
||||||
|
|
||||||
|
existingScheduleIndex := -1
|
||||||
|
for idx, existingSchedule := range tunnel.Schedules {
|
||||||
|
if existingSchedule.ID == schedule.ID {
|
||||||
|
existingScheduleIndex = idx
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if existingScheduleIndex == -1 {
|
||||||
|
tunnel.Schedules = append(tunnel.Schedules, *schedule)
|
||||||
|
} else {
|
||||||
|
tunnel.Schedules[existingScheduleIndex] = *schedule
|
||||||
|
}
|
||||||
|
|
||||||
|
key := strconv.Itoa(int(endpointID))
|
||||||
|
service.tunnelDetailsMap.Set(key, tunnel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveSchedule will remove the specified schedule from each tunnel it was registered with.
|
||||||
|
func (service *Service) RemoveSchedule(scheduleID portainer.ScheduleID) {
|
||||||
|
for item := range service.tunnelDetailsMap.IterBuffered() {
|
||||||
|
tunnelDetails := item.Val.(*portainer.TunnelDetails)
|
||||||
|
|
||||||
|
updatedSchedules := make([]portainer.EdgeSchedule, 0)
|
||||||
|
for _, schedule := range tunnelDetails.Schedules {
|
||||||
|
if schedule.ID == scheduleID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
updatedSchedules = append(updatedSchedules, schedule)
|
||||||
|
}
|
||||||
|
|
||||||
|
tunnelDetails.Schedules = updatedSchedules
|
||||||
|
service.tunnelDetailsMap.Set(item.Key, tunnelDetails)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,191 @@
|
||||||
|
package chisel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/dchest/uniuri"
|
||||||
|
|
||||||
|
cmap "github.com/orcaman/concurrent-map"
|
||||||
|
|
||||||
|
chserver "github.com/jpillora/chisel/server"
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
tunnelCleanupInterval = 10 * time.Second
|
||||||
|
requiredTimeout = 15 * time.Second
|
||||||
|
activeTimeout = 4*time.Minute + 30*time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
// Service represents a service to manage the state of multiple reverse tunnels.
|
||||||
|
// It is used to start a reverse tunnel server and to manage the connection status of each tunnel
|
||||||
|
// connected to the tunnel server.
|
||||||
|
type Service struct {
|
||||||
|
serverFingerprint string
|
||||||
|
serverPort string
|
||||||
|
tunnelDetailsMap cmap.ConcurrentMap
|
||||||
|
endpointService portainer.EndpointService
|
||||||
|
tunnelServerService portainer.TunnelServerService
|
||||||
|
snapshotter portainer.Snapshotter
|
||||||
|
chiselServer *chserver.Server
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewService returns a pointer to a new instance of Service
|
||||||
|
func NewService(endpointService portainer.EndpointService, tunnelServerService portainer.TunnelServerService) *Service {
|
||||||
|
return &Service{
|
||||||
|
tunnelDetailsMap: cmap.New(),
|
||||||
|
endpointService: endpointService,
|
||||||
|
tunnelServerService: tunnelServerService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartTunnelServer starts a tunnel server on the specified addr and port.
|
||||||
|
// It uses a seed to generate a new private/public key pair. If the seed cannot
|
||||||
|
// be found inside the database, it will generate a new one randomly and persist it.
|
||||||
|
// It starts the tunnel status verification process in the background.
|
||||||
|
// The snapshotter is used in the tunnel status verification process.
|
||||||
|
func (service *Service) StartTunnelServer(addr, port string, snapshotter portainer.Snapshotter) error {
|
||||||
|
keySeed, err := service.retrievePrivateKeySeed()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
config := &chserver.Config{
|
||||||
|
Reverse: true,
|
||||||
|
KeySeed: keySeed,
|
||||||
|
}
|
||||||
|
|
||||||
|
chiselServer, err := chserver.NewServer(config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
service.serverFingerprint = chiselServer.GetFingerprint()
|
||||||
|
service.serverPort = port
|
||||||
|
|
||||||
|
err = chiselServer.Start(addr, port)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
service.chiselServer = chiselServer
|
||||||
|
|
||||||
|
// TODO: work-around Chisel default behavior.
|
||||||
|
// By default, Chisel will allow anyone to connect if no user exists.
|
||||||
|
username, password := generateRandomCredentials()
|
||||||
|
err = service.chiselServer.AddUser(username, password, "127.0.0.1")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
service.snapshotter = snapshotter
|
||||||
|
go service.startTunnelVerificationLoop()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *Service) retrievePrivateKeySeed() (string, error) {
|
||||||
|
var serverInfo *portainer.TunnelServerInfo
|
||||||
|
|
||||||
|
serverInfo, err := service.tunnelServerService.Info()
|
||||||
|
if err == portainer.ErrObjectNotFound {
|
||||||
|
keySeed := uniuri.NewLen(16)
|
||||||
|
|
||||||
|
serverInfo = &portainer.TunnelServerInfo{
|
||||||
|
PrivateKeySeed: keySeed,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := service.tunnelServerService.UpdateInfo(serverInfo)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
} else if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return serverInfo.PrivateKeySeed, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *Service) startTunnelVerificationLoop() {
|
||||||
|
log.Printf("[DEBUG] [chisel, monitoring] [check_interval_seconds: %f] [message: starting tunnel management process]", tunnelCleanupInterval.Seconds())
|
||||||
|
ticker := time.NewTicker(tunnelCleanupInterval)
|
||||||
|
stopSignal := make(chan struct{})
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
service.checkTunnels()
|
||||||
|
case <-stopSignal:
|
||||||
|
ticker.Stop()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *Service) checkTunnels() {
|
||||||
|
for item := range service.tunnelDetailsMap.IterBuffered() {
|
||||||
|
tunnel := item.Val.(*portainer.TunnelDetails)
|
||||||
|
|
||||||
|
if tunnel.LastActivity.IsZero() || tunnel.Status == portainer.EdgeAgentIdle {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
elapsed := time.Since(tunnel.LastActivity)
|
||||||
|
log.Printf("[DEBUG] [chisel,monitoring] [endpoint_id: %s] [status: %s] [status_time_seconds: %f] [message: endpoint tunnel monitoring]", item.Key, tunnel.Status, elapsed.Seconds())
|
||||||
|
|
||||||
|
if tunnel.Status == portainer.EdgeAgentManagementRequired && elapsed.Seconds() < requiredTimeout.Seconds() {
|
||||||
|
continue
|
||||||
|
} else if tunnel.Status == portainer.EdgeAgentManagementRequired && elapsed.Seconds() > requiredTimeout.Seconds() {
|
||||||
|
log.Printf("[DEBUG] [chisel,monitoring] [endpoint_id: %s] [status: %s] [status_time_seconds: %f] [timeout_seconds: %f] [message: REQUIRED state timeout exceeded]", item.Key, tunnel.Status, elapsed.Seconds(), requiredTimeout.Seconds())
|
||||||
|
}
|
||||||
|
|
||||||
|
if tunnel.Status == portainer.EdgeAgentActive && elapsed.Seconds() < activeTimeout.Seconds() {
|
||||||
|
continue
|
||||||
|
} else if tunnel.Status == portainer.EdgeAgentActive && elapsed.Seconds() > activeTimeout.Seconds() {
|
||||||
|
log.Printf("[DEBUG] [chisel,monitoring] [endpoint_id: %s] [status: %s] [status_time_seconds: %f] [timeout_seconds: %f] [message: ACTIVE state timeout exceeded]", item.Key, tunnel.Status, elapsed.Seconds(), activeTimeout.Seconds())
|
||||||
|
|
||||||
|
endpointID, err := strconv.Atoi(item.Key)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ERROR] [chisel,snapshot,conversion] Invalid endpoint identifier (id: %s): %s", item.Key, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = service.snapshotEnvironment(portainer.EndpointID(endpointID), tunnel.Port)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ERROR] [snapshot] Unable to snapshot Edge endpoint (id: %s): %s", item.Key, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tunnel.Schedules) > 0 {
|
||||||
|
endpointID, err := strconv.Atoi(item.Key)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ERROR] [chisel,conversion] Invalid endpoint identifier (id: %s): %s", item.Key, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
service.SetTunnelStatusToIdle(portainer.EndpointID(endpointID))
|
||||||
|
} else {
|
||||||
|
service.tunnelDetailsMap.Remove(item.Key)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service *Service) snapshotEnvironment(endpointID portainer.EndpointID, tunnelPort int) error {
|
||||||
|
endpoint, err := service.endpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
endpointURL := endpoint.URL
|
||||||
|
endpoint.URL = fmt.Sprintf("tcp://localhost:%d", tunnelPort)
|
||||||
|
snapshot, err := service.snapshotter.CreateSnapshot(endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint.Snapshots = []portainer.Snapshot{*snapshot}
|
||||||
|
endpoint.URL = endpointURL
|
||||||
|
return service.endpointService.UpdateEndpoint(endpoint.ID, endpoint)
|
||||||
|
}
|
|
@ -0,0 +1,144 @@
|
||||||
|
package chisel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/portainer/libcrypto"
|
||||||
|
|
||||||
|
"github.com/dchest/uniuri"
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
minAvailablePort = 49152
|
||||||
|
maxAvailablePort = 65535
|
||||||
|
)
|
||||||
|
|
||||||
|
// getUnusedPort is used to generate an unused random port in the dynamic port range.
|
||||||
|
// Dynamic ports (also called private ports) are 49152 to 65535.
|
||||||
|
func (service *Service) getUnusedPort() int {
|
||||||
|
port := randomInt(minAvailablePort, maxAvailablePort)
|
||||||
|
|
||||||
|
for item := range service.tunnelDetailsMap.IterBuffered() {
|
||||||
|
tunnel := item.Val.(*portainer.TunnelDetails)
|
||||||
|
if tunnel.Port == port {
|
||||||
|
return service.getUnusedPort()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return port
|
||||||
|
}
|
||||||
|
|
||||||
|
func randomInt(min, max int) int {
|
||||||
|
return min + rand.Intn(max-min)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTunnelDetails returns information about the tunnel associated to an endpoint.
|
||||||
|
func (service *Service) GetTunnelDetails(endpointID portainer.EndpointID) *portainer.TunnelDetails {
|
||||||
|
key := strconv.Itoa(int(endpointID))
|
||||||
|
|
||||||
|
if item, ok := service.tunnelDetailsMap.Get(key); ok {
|
||||||
|
tunnelDetails := item.(*portainer.TunnelDetails)
|
||||||
|
return tunnelDetails
|
||||||
|
}
|
||||||
|
|
||||||
|
schedules := make([]portainer.EdgeSchedule, 0)
|
||||||
|
return &portainer.TunnelDetails{
|
||||||
|
Status: portainer.EdgeAgentIdle,
|
||||||
|
Port: 0,
|
||||||
|
Schedules: schedules,
|
||||||
|
Credentials: "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTunnelStatusToActive update the status of the tunnel associated to the specified endpoint.
|
||||||
|
// It sets the status to ACTIVE.
|
||||||
|
func (service *Service) SetTunnelStatusToActive(endpointID portainer.EndpointID) {
|
||||||
|
tunnel := service.GetTunnelDetails(endpointID)
|
||||||
|
tunnel.Status = portainer.EdgeAgentActive
|
||||||
|
tunnel.Credentials = ""
|
||||||
|
tunnel.LastActivity = time.Now()
|
||||||
|
|
||||||
|
key := strconv.Itoa(int(endpointID))
|
||||||
|
service.tunnelDetailsMap.Set(key, tunnel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTunnelStatusToIdle update the status of the tunnel associated to the specified endpoint.
|
||||||
|
// It sets the status to IDLE.
|
||||||
|
// It removes any existing credentials associated to the tunnel.
|
||||||
|
func (service *Service) SetTunnelStatusToIdle(endpointID portainer.EndpointID) {
|
||||||
|
tunnel := service.GetTunnelDetails(endpointID)
|
||||||
|
|
||||||
|
tunnel.Status = portainer.EdgeAgentIdle
|
||||||
|
tunnel.Port = 0
|
||||||
|
tunnel.LastActivity = time.Now()
|
||||||
|
|
||||||
|
credentials := tunnel.Credentials
|
||||||
|
if credentials != "" {
|
||||||
|
tunnel.Credentials = ""
|
||||||
|
service.chiselServer.DeleteUser(strings.Split(credentials, ":")[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
key := strconv.Itoa(int(endpointID))
|
||||||
|
service.tunnelDetailsMap.Set(key, tunnel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTunnelStatusToRequired update the status of the tunnel associated to the specified endpoint.
|
||||||
|
// It sets the status to REQUIRED.
|
||||||
|
// If no port is currently associated to the tunnel, it will associate a random unused port to the tunnel
|
||||||
|
// and generate temporary credentials that can be used to establish a reverse tunnel on that port.
|
||||||
|
// Credentials are encrypted using the Edge ID associated to the endpoint.
|
||||||
|
func (service *Service) SetTunnelStatusToRequired(endpointID portainer.EndpointID) error {
|
||||||
|
tunnel := service.GetTunnelDetails(endpointID)
|
||||||
|
|
||||||
|
if tunnel.Port == 0 {
|
||||||
|
endpoint, err := service.endpointService.Endpoint(endpointID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
tunnel.Status = portainer.EdgeAgentManagementRequired
|
||||||
|
tunnel.Port = service.getUnusedPort()
|
||||||
|
tunnel.LastActivity = time.Now()
|
||||||
|
|
||||||
|
username, password := generateRandomCredentials()
|
||||||
|
authorizedRemote := fmt.Sprintf("^R:0.0.0.0:%d$", tunnel.Port)
|
||||||
|
err = service.chiselServer.AddUser(username, password, authorizedRemote)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
credentials, err := encryptCredentials(username, password, endpoint.EdgeID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
tunnel.Credentials = credentials
|
||||||
|
|
||||||
|
key := strconv.Itoa(int(endpointID))
|
||||||
|
service.tunnelDetailsMap.Set(key, tunnel)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateRandomCredentials() (string, string) {
|
||||||
|
username := uniuri.NewLen(8)
|
||||||
|
password := uniuri.NewLen(8)
|
||||||
|
return username, password
|
||||||
|
}
|
||||||
|
|
||||||
|
func encryptCredentials(username, password, key string) (string, error) {
|
||||||
|
credentials := fmt.Sprintf("%s:%s", username, password)
|
||||||
|
|
||||||
|
encryptedCredentials, err := libcrypto.Encrypt([]byte(credentials), []byte(key))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return base64.RawStdEncoding.EncodeToString(encryptedCredentials), nil
|
||||||
|
}
|
|
@ -33,6 +33,8 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
||||||
|
|
||||||
flags := &portainer.CLIFlags{
|
flags := &portainer.CLIFlags{
|
||||||
Addr: kingpin.Flag("bind", "Address and port to serve Portainer").Default(defaultBindAddress).Short('p').String(),
|
Addr: kingpin.Flag("bind", "Address and port to serve Portainer").Default(defaultBindAddress).Short('p').String(),
|
||||||
|
TunnelAddr: kingpin.Flag("tunnel-addr", "Address to serve the tunnel server").Default(defaultTunnelServerAddress).String(),
|
||||||
|
TunnelPort: kingpin.Flag("tunnel-port", "Port to serve the tunnel server").Default(defaultTunnelServerPort).String(),
|
||||||
Assets: kingpin.Flag("assets", "Path to the assets").Default(defaultAssetsDirectory).Short('a').String(),
|
Assets: kingpin.Flag("assets", "Path to the assets").Default(defaultAssetsDirectory).Short('a').String(),
|
||||||
Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(),
|
Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(),
|
||||||
EndpointURL: kingpin.Flag("host", "Endpoint URL").Short('H').String(),
|
EndpointURL: kingpin.Flag("host", "Endpoint URL").Short('H').String(),
|
||||||
|
|
|
@ -4,6 +4,8 @@ package cli
|
||||||
|
|
||||||
const (
|
const (
|
||||||
defaultBindAddress = ":9000"
|
defaultBindAddress = ":9000"
|
||||||
|
defaultTunnelServerAddress = "0.0.0.0"
|
||||||
|
defaultTunnelServerPort = "8000"
|
||||||
defaultDataDirectory = "/data"
|
defaultDataDirectory = "/data"
|
||||||
defaultAssetsDirectory = "./"
|
defaultAssetsDirectory = "./"
|
||||||
defaultNoAuth = "false"
|
defaultNoAuth = "false"
|
||||||
|
|
|
@ -2,6 +2,8 @@ package cli
|
||||||
|
|
||||||
const (
|
const (
|
||||||
defaultBindAddress = ":9000"
|
defaultBindAddress = ":9000"
|
||||||
|
defaultTunnelServerAddress = "0.0.0.0"
|
||||||
|
defaultTunnelServerPort = "8000"
|
||||||
defaultDataDirectory = "C:\\data"
|
defaultDataDirectory = "C:\\data"
|
||||||
defaultAssetsDirectory = "./"
|
defaultAssetsDirectory = "./"
|
||||||
defaultNoAuth = "false"
|
defaultNoAuth = "false"
|
||||||
|
|
|
@ -2,10 +2,13 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/portainer/portainer/api/chisel"
|
||||||
|
|
||||||
"github.com/portainer/portainer/api"
|
"github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/bolt"
|
"github.com/portainer/portainer/api/bolt"
|
||||||
"github.com/portainer/portainer/api/cli"
|
"github.com/portainer/portainer/api/cli"
|
||||||
|
@ -20,8 +23,6 @@ import (
|
||||||
"github.com/portainer/portainer/api/jwt"
|
"github.com/portainer/portainer/api/jwt"
|
||||||
"github.com/portainer/portainer/api/ldap"
|
"github.com/portainer/portainer/api/ldap"
|
||||||
"github.com/portainer/portainer/api/libcompose"
|
"github.com/portainer/portainer/api/libcompose"
|
||||||
|
|
||||||
"log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func initCLI() *portainer.CLIFlags {
|
func initCLI() *portainer.CLIFlags {
|
||||||
|
@ -69,12 +70,12 @@ func initStore(dataStorePath string, fileService portainer.FileService) *bolt.St
|
||||||
return store
|
return store
|
||||||
}
|
}
|
||||||
|
|
||||||
func initComposeStackManager(dataStorePath string) portainer.ComposeStackManager {
|
func initComposeStackManager(dataStorePath string, reverseTunnelService portainer.ReverseTunnelService) portainer.ComposeStackManager {
|
||||||
return libcompose.NewComposeStackManager(dataStorePath)
|
return libcompose.NewComposeStackManager(dataStorePath, reverseTunnelService)
|
||||||
}
|
}
|
||||||
|
|
||||||
func initSwarmStackManager(assetsPath string, dataStorePath string, signatureService portainer.DigitalSignatureService, fileService portainer.FileService) (portainer.SwarmStackManager, error) {
|
func initSwarmStackManager(assetsPath string, dataStorePath string, signatureService portainer.DigitalSignatureService, fileService portainer.FileService, reverseTunnelService portainer.ReverseTunnelService) (portainer.SwarmStackManager, error) {
|
||||||
return exec.NewSwarmStackManager(assetsPath, dataStorePath, signatureService, fileService)
|
return exec.NewSwarmStackManager(assetsPath, dataStorePath, signatureService, fileService, reverseTunnelService)
|
||||||
}
|
}
|
||||||
|
|
||||||
func initJWTService(authenticationEnabled bool) portainer.JWTService {
|
func initJWTService(authenticationEnabled bool) portainer.JWTService {
|
||||||
|
@ -104,8 +105,8 @@ func initGitService() portainer.GitService {
|
||||||
return &git.Service{}
|
return &git.Service{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func initClientFactory(signatureService portainer.DigitalSignatureService) *docker.ClientFactory {
|
func initClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService) *docker.ClientFactory {
|
||||||
return docker.NewClientFactory(signatureService)
|
return docker.NewClientFactory(signatureService, reverseTunnelService)
|
||||||
}
|
}
|
||||||
|
|
||||||
func initSnapshotter(clientFactory *docker.ClientFactory) portainer.Snapshotter {
|
func initSnapshotter(clientFactory *docker.ClientFactory) portainer.Snapshotter {
|
||||||
|
@ -196,7 +197,7 @@ func loadEndpointSyncSystemSchedule(jobScheduler portainer.JobScheduler, schedul
|
||||||
return scheduleService.CreateSchedule(endpointSyncSchedule)
|
return scheduleService.CreateSchedule(endpointSyncSchedule)
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadSchedulesFromDatabase(jobScheduler portainer.JobScheduler, jobService portainer.JobService, scheduleService portainer.ScheduleService, endpointService portainer.EndpointService, fileService portainer.FileService) error {
|
func loadSchedulesFromDatabase(jobScheduler portainer.JobScheduler, jobService portainer.JobService, scheduleService portainer.ScheduleService, endpointService portainer.EndpointService, fileService portainer.FileService, reverseTunnelService portainer.ReverseTunnelService) error {
|
||||||
schedules, err := scheduleService.Schedules()
|
schedules, err := scheduleService.Schedules()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -213,6 +214,13 @@ func loadSchedulesFromDatabase(jobScheduler portainer.JobScheduler, jobService p
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if schedule.EdgeSchedule != nil {
|
||||||
|
for _, endpointID := range schedule.EdgeSchedule.Endpoints {
|
||||||
|
reverseTunnelService.AddSchedule(endpointID, schedule.EdgeSchedule)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -265,6 +273,7 @@ func initSettings(settingsService portainer.SettingsService, flags *portainer.CL
|
||||||
AllowPrivilegedModeForRegularUsers: true,
|
AllowPrivilegedModeForRegularUsers: true,
|
||||||
EnableHostManagementFeatures: false,
|
EnableHostManagementFeatures: false,
|
||||||
SnapshotInterval: *flags.SnapshotInterval,
|
SnapshotInterval: *flags.SnapshotInterval,
|
||||||
|
EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds,
|
||||||
}
|
}
|
||||||
|
|
||||||
if *flags.Templates != "" {
|
if *flags.Templates != "" {
|
||||||
|
@ -540,7 +549,9 @@ func main() {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
clientFactory := initClientFactory(digitalSignatureService)
|
reverseTunnelService := chisel.NewService(store.EndpointService, store.TunnelServerService)
|
||||||
|
|
||||||
|
clientFactory := initClientFactory(digitalSignatureService, reverseTunnelService)
|
||||||
|
|
||||||
jobService := initJobService(clientFactory)
|
jobService := initJobService(clientFactory)
|
||||||
|
|
||||||
|
@ -551,12 +562,12 @@ func main() {
|
||||||
endpointManagement = false
|
endpointManagement = false
|
||||||
}
|
}
|
||||||
|
|
||||||
swarmStackManager, err := initSwarmStackManager(*flags.Assets, *flags.Data, digitalSignatureService, fileService)
|
swarmStackManager, err := initSwarmStackManager(*flags.Assets, *flags.Data, digitalSignatureService, fileService, reverseTunnelService)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
composeStackManager := initComposeStackManager(*flags.Data)
|
composeStackManager := initComposeStackManager(*flags.Data, reverseTunnelService)
|
||||||
|
|
||||||
err = initTemplates(store.TemplateService, fileService, *flags.Templates, *flags.TemplateFile)
|
err = initTemplates(store.TemplateService, fileService, *flags.Templates, *flags.TemplateFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -570,7 +581,7 @@ func main() {
|
||||||
|
|
||||||
jobScheduler := initJobScheduler()
|
jobScheduler := initJobScheduler()
|
||||||
|
|
||||||
err = loadSchedulesFromDatabase(jobScheduler, jobService, store.ScheduleService, store.EndpointService, fileService)
|
err = loadSchedulesFromDatabase(jobScheduler, jobService, store.ScheduleService, store.EndpointService, fileService, reverseTunnelService)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -658,7 +669,13 @@ func main() {
|
||||||
go terminateIfNoAdminCreated(store.UserService)
|
go terminateIfNoAdminCreated(store.UserService)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = reverseTunnelService.StartTunnelServer(*flags.TunnelAddr, *flags.TunnelPort, snapshotter)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
var server portainer.Server = &http.Server{
|
var server portainer.Server = &http.Server{
|
||||||
|
ReverseTunnelService: reverseTunnelService,
|
||||||
Status: applicationStatus,
|
Status: applicationStatus,
|
||||||
BindAddress: *flags.Addr,
|
BindAddress: *flags.Addr,
|
||||||
AssetsPath: *flags.Assets,
|
AssetsPath: *flags.Assets,
|
||||||
|
|
|
@ -53,7 +53,7 @@ func (runner *SnapshotJobRunner) Run() {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, endpoint := range endpoints {
|
for _, endpoint := range endpoints {
|
||||||
if endpoint.Type == portainer.AzureEnvironment {
|
if endpoint.Type == portainer.AzureEnvironment || endpoint.Type == portainer.EdgeAgentEnvironment {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,8 @@ import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"math/big"
|
"math/big"
|
||||||
|
|
||||||
|
"github.com/portainer/libcrypto"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -111,7 +113,7 @@ func (service *ECDSAService) CreateSignature(message string) (string, error) {
|
||||||
message = service.secret
|
message = service.secret
|
||||||
}
|
}
|
||||||
|
|
||||||
hash := HashFromBytes([]byte(message))
|
hash := libcrypto.HashFromBytes([]byte(message))
|
||||||
|
|
||||||
r := big.NewInt(0)
|
r := big.NewInt(0)
|
||||||
s := big.NewInt(0)
|
s := big.NewInt(0)
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
package crypto
|
|
||||||
|
|
||||||
import "crypto/md5"
|
|
||||||
|
|
||||||
// HashFromBytes returns the hash of the specified data
|
|
||||||
func HashFromBytes(data []byte) []byte {
|
|
||||||
digest := md5.New()
|
|
||||||
digest.Write(data)
|
|
||||||
return digest.Sum(nil)
|
|
||||||
}
|
|
|
@ -1,6 +1,7 @@
|
||||||
package docker
|
package docker
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
@ -12,17 +13,20 @@ import (
|
||||||
|
|
||||||
const (
|
const (
|
||||||
unsupportedEnvironmentType = portainer.Error("Environment not supported")
|
unsupportedEnvironmentType = portainer.Error("Environment not supported")
|
||||||
|
defaultDockerRequestTimeout = 60
|
||||||
)
|
)
|
||||||
|
|
||||||
// ClientFactory is used to create Docker clients
|
// ClientFactory is used to create Docker clients
|
||||||
type ClientFactory struct {
|
type ClientFactory struct {
|
||||||
signatureService portainer.DigitalSignatureService
|
signatureService portainer.DigitalSignatureService
|
||||||
|
reverseTunnelService portainer.ReverseTunnelService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClientFactory returns a new instance of a ClientFactory
|
// NewClientFactory returns a new instance of a ClientFactory
|
||||||
func NewClientFactory(signatureService portainer.DigitalSignatureService) *ClientFactory {
|
func NewClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService) *ClientFactory {
|
||||||
return &ClientFactory{
|
return &ClientFactory{
|
||||||
signatureService: signatureService,
|
signatureService: signatureService,
|
||||||
|
reverseTunnelService: reverseTunnelService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,6 +38,8 @@ func (factory *ClientFactory) CreateClient(endpoint *portainer.Endpoint, nodeNam
|
||||||
return nil, unsupportedEnvironmentType
|
return nil, unsupportedEnvironmentType
|
||||||
} else if endpoint.Type == portainer.AgentOnDockerEnvironment {
|
} else if endpoint.Type == portainer.AgentOnDockerEnvironment {
|
||||||
return createAgentClient(endpoint, factory.signatureService, nodeName)
|
return createAgentClient(endpoint, factory.signatureService, nodeName)
|
||||||
|
} else if endpoint.Type == portainer.EdgeAgentEnvironment {
|
||||||
|
return createEdgeClient(endpoint, factory.reverseTunnelService, nodeName)
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(endpoint.URL, "unix://") || strings.HasPrefix(endpoint.URL, "npipe://") {
|
if strings.HasPrefix(endpoint.URL, "unix://") || strings.HasPrefix(endpoint.URL, "npipe://") {
|
||||||
|
@ -62,6 +68,28 @@ func createTCPClient(endpoint *portainer.Endpoint) (*client.Client, error) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createEdgeClient(endpoint *portainer.Endpoint, reverseTunnelService portainer.ReverseTunnelService, nodeName string) (*client.Client, error) {
|
||||||
|
httpCli, err := httpClient(endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
headers := map[string]string{}
|
||||||
|
if nodeName != "" {
|
||||||
|
headers[portainer.PortainerAgentTargetHeader] = nodeName
|
||||||
|
}
|
||||||
|
|
||||||
|
tunnel := reverseTunnelService.GetTunnelDetails(endpoint.ID)
|
||||||
|
endpointURL := fmt.Sprintf("http://localhost:%d", tunnel.Port)
|
||||||
|
|
||||||
|
return client.NewClientWithOpts(
|
||||||
|
client.WithHost(endpointURL),
|
||||||
|
client.WithVersion(portainer.SupportedDockerAPIVersion),
|
||||||
|
client.WithHTTPClient(httpCli),
|
||||||
|
client.WithHTTPHeaders(headers),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func createAgentClient(endpoint *portainer.Endpoint, signatureService portainer.DigitalSignatureService, nodeName string) (*client.Client, error) {
|
func createAgentClient(endpoint *portainer.Endpoint, signatureService portainer.DigitalSignatureService, nodeName string) (*client.Client, error) {
|
||||||
httpCli, err := httpClient(endpoint)
|
httpCli, err := httpClient(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -103,6 +131,6 @@ func httpClient(endpoint *portainer.Endpoint) (*http.Client, error) {
|
||||||
|
|
||||||
return &http.Client{
|
return &http.Client{
|
||||||
Transport: transport,
|
Transport: transport,
|
||||||
Timeout: 30 * time.Second,
|
Timeout: defaultDockerRequestTimeout * time.Second,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package exec
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path"
|
"path"
|
||||||
|
@ -17,16 +18,18 @@ type SwarmStackManager struct {
|
||||||
dataPath string
|
dataPath string
|
||||||
signatureService portainer.DigitalSignatureService
|
signatureService portainer.DigitalSignatureService
|
||||||
fileService portainer.FileService
|
fileService portainer.FileService
|
||||||
|
reverseTunnelService portainer.ReverseTunnelService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSwarmStackManager initializes a new SwarmStackManager service.
|
// NewSwarmStackManager initializes a new SwarmStackManager service.
|
||||||
// It also updates the configuration of the Docker CLI binary.
|
// It also updates the configuration of the Docker CLI binary.
|
||||||
func NewSwarmStackManager(binaryPath, dataPath string, signatureService portainer.DigitalSignatureService, fileService portainer.FileService) (*SwarmStackManager, error) {
|
func NewSwarmStackManager(binaryPath, dataPath string, signatureService portainer.DigitalSignatureService, fileService portainer.FileService, reverseTunnelService portainer.ReverseTunnelService) (*SwarmStackManager, error) {
|
||||||
manager := &SwarmStackManager{
|
manager := &SwarmStackManager{
|
||||||
binaryPath: binaryPath,
|
binaryPath: binaryPath,
|
||||||
dataPath: dataPath,
|
dataPath: dataPath,
|
||||||
signatureService: signatureService,
|
signatureService: signatureService,
|
||||||
fileService: fileService,
|
fileService: fileService,
|
||||||
|
reverseTunnelService: reverseTunnelService,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := manager.updateDockerCLIConfiguration(dataPath)
|
err := manager.updateDockerCLIConfiguration(dataPath)
|
||||||
|
@ -39,7 +42,7 @@ func NewSwarmStackManager(binaryPath, dataPath string, signatureService portaine
|
||||||
|
|
||||||
// Login executes the docker login command against a list of registries (including DockerHub).
|
// Login executes the docker login command against a list of registries (including DockerHub).
|
||||||
func (manager *SwarmStackManager) Login(dockerhub *portainer.DockerHub, registries []portainer.Registry, endpoint *portainer.Endpoint) {
|
func (manager *SwarmStackManager) Login(dockerhub *portainer.DockerHub, registries []portainer.Registry, endpoint *portainer.Endpoint) {
|
||||||
command, args := prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint)
|
command, args := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint)
|
||||||
for _, registry := range registries {
|
for _, registry := range registries {
|
||||||
if registry.Authentication {
|
if registry.Authentication {
|
||||||
registryArgs := append(args, "login", "--username", registry.Username, "--password", registry.Password, registry.URL)
|
registryArgs := append(args, "login", "--username", registry.Username, "--password", registry.Password, registry.URL)
|
||||||
|
@ -55,7 +58,7 @@ func (manager *SwarmStackManager) Login(dockerhub *portainer.DockerHub, registri
|
||||||
|
|
||||||
// Logout executes the docker logout command.
|
// Logout executes the docker logout command.
|
||||||
func (manager *SwarmStackManager) Logout(endpoint *portainer.Endpoint) error {
|
func (manager *SwarmStackManager) Logout(endpoint *portainer.Endpoint) error {
|
||||||
command, args := prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint)
|
command, args := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint)
|
||||||
args = append(args, "logout")
|
args = append(args, "logout")
|
||||||
return runCommandAndCaptureStdErr(command, args, nil, "")
|
return runCommandAndCaptureStdErr(command, args, nil, "")
|
||||||
}
|
}
|
||||||
|
@ -63,7 +66,7 @@ func (manager *SwarmStackManager) Logout(endpoint *portainer.Endpoint) error {
|
||||||
// Deploy executes the docker stack deploy command.
|
// Deploy executes the docker stack deploy command.
|
||||||
func (manager *SwarmStackManager) Deploy(stack *portainer.Stack, prune bool, endpoint *portainer.Endpoint) error {
|
func (manager *SwarmStackManager) Deploy(stack *portainer.Stack, prune bool, endpoint *portainer.Endpoint) error {
|
||||||
stackFilePath := path.Join(stack.ProjectPath, stack.EntryPoint)
|
stackFilePath := path.Join(stack.ProjectPath, stack.EntryPoint)
|
||||||
command, args := prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint)
|
command, args := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint)
|
||||||
|
|
||||||
if prune {
|
if prune {
|
||||||
args = append(args, "stack", "deploy", "--prune", "--with-registry-auth", "--compose-file", stackFilePath, stack.Name)
|
args = append(args, "stack", "deploy", "--prune", "--with-registry-auth", "--compose-file", stackFilePath, stack.Name)
|
||||||
|
@ -82,7 +85,7 @@ func (manager *SwarmStackManager) Deploy(stack *portainer.Stack, prune bool, end
|
||||||
|
|
||||||
// Remove executes the docker stack rm command.
|
// Remove executes the docker stack rm command.
|
||||||
func (manager *SwarmStackManager) Remove(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
func (manager *SwarmStackManager) Remove(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||||
command, args := prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint)
|
command, args := manager.prepareDockerCommandAndArgs(manager.binaryPath, manager.dataPath, endpoint)
|
||||||
args = append(args, "stack", "rm", stack.Name)
|
args = append(args, "stack", "rm", stack.Name)
|
||||||
return runCommandAndCaptureStdErr(command, args, nil, "")
|
return runCommandAndCaptureStdErr(command, args, nil, "")
|
||||||
}
|
}
|
||||||
|
@ -106,7 +109,7 @@ func runCommandAndCaptureStdErr(command string, args []string, env []string, wor
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func prepareDockerCommandAndArgs(binaryPath, dataPath string, endpoint *portainer.Endpoint) (string, []string) {
|
func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, dataPath string, endpoint *portainer.Endpoint) (string, []string) {
|
||||||
// Assume Linux as a default
|
// Assume Linux as a default
|
||||||
command := path.Join(binaryPath, "docker")
|
command := path.Join(binaryPath, "docker")
|
||||||
|
|
||||||
|
@ -116,7 +119,14 @@ func prepareDockerCommandAndArgs(binaryPath, dataPath string, endpoint *portaine
|
||||||
|
|
||||||
args := make([]string, 0)
|
args := make([]string, 0)
|
||||||
args = append(args, "--config", dataPath)
|
args = append(args, "--config", dataPath)
|
||||||
args = append(args, "-H", endpoint.URL)
|
|
||||||
|
endpointURL := endpoint.URL
|
||||||
|
if endpoint.Type == portainer.EdgeAgentEnvironment {
|
||||||
|
tunnel := manager.reverseTunnelService.GetTunnelDetails(endpoint.ID)
|
||||||
|
endpointURL = fmt.Sprintf("tcp://localhost:%d", tunnel.Port)
|
||||||
|
}
|
||||||
|
|
||||||
|
args = append(args, "-H", endpointURL)
|
||||||
|
|
||||||
if endpoint.TLSConfig.TLS {
|
if endpoint.TLSConfig.TLS {
|
||||||
args = append(args, "--tls")
|
args = append(args, "--tls")
|
||||||
|
|
|
@ -53,12 +53,18 @@ func (handler *Handler) endpointGroupCreate(w http.ResponseWriter, r *http.Reque
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, id := range payload.AssociatedEndpoints {
|
||||||
for _, endpoint := range endpoints {
|
for _, endpoint := range endpoints {
|
||||||
if endpoint.GroupID == portainer.EndpointGroupID(1) {
|
if endpoint.ID == id {
|
||||||
err = handler.checkForGroupAssignment(endpoint, endpointGroup.ID, payload.AssociatedEndpoints)
|
endpoint.GroupID = endpointGroup.ID
|
||||||
|
|
||||||
|
err := handler.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
package endpointgroups
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
httperror "github.com/portainer/libhttp/error"
|
||||||
|
"github.com/portainer/libhttp/request"
|
||||||
|
"github.com/portainer/libhttp/response"
|
||||||
|
"github.com/portainer/portainer/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PUT request on /api/endpoint_groups/:id/endpoints/:endpointId
|
||||||
|
func (handler *Handler) endpointGroupAddEndpoint(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
endpointGroupID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint group identifier route variable", err}
|
||||||
|
}
|
||||||
|
|
||||||
|
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "endpointId")
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
|
||||||
|
}
|
||||||
|
|
||||||
|
endpointGroup, err := handler.EndpointGroupService.EndpointGroup(portainer.EndpointGroupID(endpointGroupID))
|
||||||
|
if err == portainer.ErrObjectNotFound {
|
||||||
|
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint group with the specified identifier inside the database", err}
|
||||||
|
} else if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint group with the specified identifier inside the database", 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}
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint.GroupID = endpointGroup.ID
|
||||||
|
|
||||||
|
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Empty(w)
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
package endpointgroups
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
httperror "github.com/portainer/libhttp/error"
|
||||||
|
"github.com/portainer/libhttp/request"
|
||||||
|
"github.com/portainer/libhttp/response"
|
||||||
|
"github.com/portainer/portainer/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DELETE request on /api/endpoint_groups/:id/endpoints/:endpointId
|
||||||
|
func (handler *Handler) endpointGroupDeleteEndpoint(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
endpointGroupID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint group identifier route variable", err}
|
||||||
|
}
|
||||||
|
|
||||||
|
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "endpointId")
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = handler.EndpointGroupService.EndpointGroup(portainer.EndpointGroupID(endpointGroupID))
|
||||||
|
if err == portainer.ErrObjectNotFound {
|
||||||
|
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint group with the specified identifier inside the database", err}
|
||||||
|
} else if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint group with the specified identifier inside the database", 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}
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoint.GroupID = portainer.EndpointGroupID(1)
|
||||||
|
|
||||||
|
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Empty(w)
|
||||||
|
}
|
|
@ -12,7 +12,6 @@ import (
|
||||||
type endpointGroupUpdatePayload struct {
|
type endpointGroupUpdatePayload struct {
|
||||||
Name string
|
Name string
|
||||||
Description string
|
Description string
|
||||||
AssociatedEndpoints []portainer.EndpointID
|
|
||||||
Tags []string
|
Tags []string
|
||||||
UserAccessPolicies portainer.UserAccessPolicies
|
UserAccessPolicies portainer.UserAccessPolicies
|
||||||
TeamAccessPolicies portainer.TeamAccessPolicies
|
TeamAccessPolicies portainer.TeamAccessPolicies
|
||||||
|
@ -67,19 +66,5 @@ func (handler *Handler) endpointGroupUpdate(w http.ResponseWriter, r *http.Reque
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint group changes inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint group changes inside the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
if payload.AssociatedEndpoints != nil {
|
|
||||||
endpoints, err := handler.EndpointService.Endpoints()
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, endpoint := range endpoints {
|
|
||||||
err = handler.updateEndpointGroup(endpoint, portainer.EndpointGroupID(endpointGroupID), payload.AssociatedEndpoints)
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint", err}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.JSON(w, endpointGroup)
|
return response.JSON(w, endpointGroup)
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,37 +31,9 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointGroupUpdate))).Methods(http.MethodPut)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointGroupUpdate))).Methods(http.MethodPut)
|
||||||
h.Handle("/endpoint_groups/{id}",
|
h.Handle("/endpoint_groups/{id}",
|
||||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointGroupDelete))).Methods(http.MethodDelete)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointGroupDelete))).Methods(http.MethodDelete)
|
||||||
|
h.Handle("/endpoint_groups/{id}/endpoints/{endpointId}",
|
||||||
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointGroupAddEndpoint))).Methods(http.MethodPut)
|
||||||
|
h.Handle("/endpoint_groups/{id}/endpoints/{endpointId}",
|
||||||
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointGroupDeleteEndpoint))).Methods(http.MethodDelete)
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *Handler) checkForGroupUnassignment(endpoint portainer.Endpoint, associatedEndpoints []portainer.EndpointID) error {
|
|
||||||
for _, id := range associatedEndpoints {
|
|
||||||
if id == endpoint.ID {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
endpoint.GroupID = portainer.EndpointGroupID(1)
|
|
||||||
return handler.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (handler *Handler) checkForGroupAssignment(endpoint portainer.Endpoint, groupID portainer.EndpointGroupID, associatedEndpoints []portainer.EndpointID) error {
|
|
||||||
for _, id := range associatedEndpoints {
|
|
||||||
|
|
||||||
if id == endpoint.ID {
|
|
||||||
endpoint.GroupID = groupID
|
|
||||||
return handler.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (handler *Handler) updateEndpointGroup(endpoint portainer.Endpoint, groupID portainer.EndpointGroupID, associatedEndpoints []portainer.EndpointID) error {
|
|
||||||
if endpoint.GroupID == groupID {
|
|
||||||
return handler.checkForGroupUnassignment(endpoint, associatedEndpoints)
|
|
||||||
} else if endpoint.GroupID == portainer.EndpointGroupID(1) {
|
|
||||||
return handler.checkForGroupAssignment(endpoint, groupID, associatedEndpoints)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -13,7 +13,9 @@ type Handler struct {
|
||||||
*mux.Router
|
*mux.Router
|
||||||
requestBouncer *security.RequestBouncer
|
requestBouncer *security.RequestBouncer
|
||||||
EndpointService portainer.EndpointService
|
EndpointService portainer.EndpointService
|
||||||
|
SettingsService portainer.SettingsService
|
||||||
ProxyManager *proxy.Manager
|
ProxyManager *proxy.Manager
|
||||||
|
ReverseTunnelService portainer.ReverseTunnelService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler creates a handler to proxy requests to external APIs.
|
// NewHandler creates a handler to proxy requests to external APIs.
|
||||||
|
|
|
@ -29,7 +29,7 @@ func (handler *Handler) proxyRequestsToAzureAPI(w http.ResponseWriter, r *http.R
|
||||||
}
|
}
|
||||||
|
|
||||||
var proxy http.Handler
|
var proxy http.Handler
|
||||||
proxy = handler.ProxyManager.GetProxy(string(endpointID))
|
proxy = handler.ProxyManager.GetProxy(endpoint)
|
||||||
if proxy == nil {
|
if proxy == nil {
|
||||||
proxy, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint)
|
proxy, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -3,6 +3,7 @@ package endpointproxy
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
"github.com/portainer/libhttp/request"
|
"github.com/portainer/libhttp/request"
|
||||||
|
@ -24,7 +25,7 @@ func (handler *Handler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
if endpoint.Status == portainer.EndpointStatusDown {
|
if endpoint.Type != portainer.EdgeAgentEnvironment && endpoint.Status == portainer.EndpointStatusDown {
|
||||||
return &httperror.HandlerError{http.StatusServiceUnavailable, "Unable to query endpoint", errors.New("Endpoint is down")}
|
return &httperror.HandlerError{http.StatusServiceUnavailable, "Unable to query endpoint", errors.New("Endpoint is down")}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,8 +34,32 @@ func (handler *Handler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if endpoint.Type == portainer.EdgeAgentEnvironment {
|
||||||
|
if endpoint.EdgeID == "" {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "No Edge agent registered with the endpoint", errors.New("No agent available")}
|
||||||
|
}
|
||||||
|
|
||||||
|
tunnel := handler.ReverseTunnelService.GetTunnelDetails(endpoint.ID)
|
||||||
|
if tunnel.Status == portainer.EdgeAgentIdle {
|
||||||
|
handler.ProxyManager.DeleteProxy(endpoint)
|
||||||
|
|
||||||
|
err := handler.ReverseTunnelService.SetTunnelStatusToRequired(endpoint.ID)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update tunnel status", err}
|
||||||
|
}
|
||||||
|
|
||||||
|
settings, err := handler.SettingsService.Settings()
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
|
||||||
|
}
|
||||||
|
|
||||||
|
waitForAgentToConnect := time.Duration(settings.EdgeAgentCheckinInterval) * time.Second
|
||||||
|
time.Sleep(waitForAgentToConnect * 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var proxy http.Handler
|
var proxy http.Handler
|
||||||
proxy = handler.ProxyManager.GetProxy(string(endpointID))
|
proxy = handler.ProxyManager.GetProxy(endpoint)
|
||||||
if proxy == nil {
|
if proxy == nil {
|
||||||
proxy, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint)
|
proxy, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
package endpoints
|
package endpoints
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"log"
|
"log"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
@ -41,7 +44,7 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error {
|
||||||
|
|
||||||
endpointType, err := request.RetrieveNumericMultiPartFormValue(r, "EndpointType", false)
|
endpointType, err := request.RetrieveNumericMultiPartFormValue(r, "EndpointType", false)
|
||||||
if err != nil || endpointType == 0 {
|
if err != nil || endpointType == 0 {
|
||||||
return portainer.Error("Invalid endpoint type value. Value must be one of: 1 (Docker environment), 2 (Agent environment) or 3 (Azure environment)")
|
return portainer.Error("Invalid endpoint type value. Value must be one of: 1 (Docker environment), 2 (Agent environment), 3 (Azure environment) or 4 (Edge Agent environment)")
|
||||||
}
|
}
|
||||||
payload.EndpointType = endpointType
|
payload.EndpointType = endpointType
|
||||||
|
|
||||||
|
@ -149,6 +152,8 @@ func (handler *Handler) endpointCreate(w http.ResponseWriter, r *http.Request) *
|
||||||
func (handler *Handler) createEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) {
|
func (handler *Handler) createEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) {
|
||||||
if portainer.EndpointType(payload.EndpointType) == portainer.AzureEnvironment {
|
if portainer.EndpointType(payload.EndpointType) == portainer.AzureEnvironment {
|
||||||
return handler.createAzureEndpoint(payload)
|
return handler.createAzureEndpoint(payload)
|
||||||
|
} else if portainer.EndpointType(payload.EndpointType) == portainer.EdgeAgentEnvironment {
|
||||||
|
return handler.createEdgeAgentEndpoint(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
if payload.TLS {
|
if payload.TLS {
|
||||||
|
@ -195,6 +200,52 @@ func (handler *Handler) createAzureEndpoint(payload *endpointCreatePayload) (*po
|
||||||
return endpoint, nil
|
return endpoint, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) {
|
||||||
|
endpointType := portainer.EdgeAgentEnvironment
|
||||||
|
endpointID := handler.EndpointService.GetNextIdentifier()
|
||||||
|
|
||||||
|
portainerURL, err := url.Parse(payload.URL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint URL", err}
|
||||||
|
}
|
||||||
|
|
||||||
|
portainerHost, _, err := net.SplitHostPort(portainerURL.Host)
|
||||||
|
if err != nil {
|
||||||
|
portainerHost = portainerURL.Host
|
||||||
|
}
|
||||||
|
|
||||||
|
if portainerHost == "localhost" {
|
||||||
|
return nil, &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint URL", errors.New("cannot use localhost as endpoint URL")}
|
||||||
|
}
|
||||||
|
|
||||||
|
edgeKey := handler.ReverseTunnelService.GenerateEdgeKey(payload.URL, portainerHost, endpointID)
|
||||||
|
|
||||||
|
endpoint := &portainer.Endpoint{
|
||||||
|
ID: portainer.EndpointID(endpointID),
|
||||||
|
Name: payload.Name,
|
||||||
|
URL: portainerHost,
|
||||||
|
Type: endpointType,
|
||||||
|
GroupID: portainer.EndpointGroupID(payload.GroupID),
|
||||||
|
TLSConfig: portainer.TLSConfiguration{
|
||||||
|
TLS: false,
|
||||||
|
},
|
||||||
|
AuthorizedUsers: []portainer.UserID{},
|
||||||
|
AuthorizedTeams: []portainer.TeamID{},
|
||||||
|
Extensions: []portainer.EndpointExtension{},
|
||||||
|
Tags: payload.Tags,
|
||||||
|
Status: portainer.EndpointStatusUp,
|
||||||
|
Snapshots: []portainer.Snapshot{},
|
||||||
|
EdgeKey: edgeKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = handler.EndpointService.CreateEndpoint(endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint inside the database", err}
|
||||||
|
}
|
||||||
|
|
||||||
|
return endpoint, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (handler *Handler) createUnsecuredEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) {
|
func (handler *Handler) createUnsecuredEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) {
|
||||||
endpointType := portainer.DockerEnvironment
|
endpointType := portainer.DockerEnvironment
|
||||||
|
|
||||||
|
|
|
@ -41,7 +41,7 @@ func (handler *Handler) endpointDelete(w http.ResponseWriter, r *http.Request) *
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove endpoint from the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove endpoint from the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
handler.ProxyManager.DeleteProxy(string(endpointID))
|
handler.ProxyManager.DeleteProxy(endpoint)
|
||||||
|
|
||||||
return response.Empty(w)
|
return response.Empty(w)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,24 +2,43 @@ package endpoints
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
|
||||||
|
"github.com/portainer/libhttp/request"
|
||||||
|
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
"github.com/portainer/libhttp/response"
|
"github.com/portainer/libhttp/response"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GET request on /api/endpoints
|
// GET request on /api/endpoints?(start=<start>)&(limit=<limit>)&(search=<search>)&(groupId=<groupId)
|
||||||
func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
endpoints, err := handler.EndpointService.Endpoints()
|
start, _ := request.RetrieveNumericQueryParameter(r, "start", true)
|
||||||
if err != nil {
|
if start != 0 {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err}
|
start--
|
||||||
}
|
}
|
||||||
|
|
||||||
|
search, _ := request.RetrieveQueryParameter(r, "search", true)
|
||||||
|
if search != "" {
|
||||||
|
search = strings.ToLower(search)
|
||||||
|
}
|
||||||
|
|
||||||
|
groupID, _ := request.RetrieveNumericQueryParameter(r, "groupId", true)
|
||||||
|
limit, _ := request.RetrieveNumericQueryParameter(r, "limit", true)
|
||||||
|
|
||||||
endpointGroups, err := handler.EndpointGroupService.EndpointGroups()
|
endpointGroups, err := handler.EndpointGroupService.EndpointGroups()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint groups from the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint groups from the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
endpoints, err := handler.EndpointService.Endpoints()
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err}
|
||||||
|
}
|
||||||
|
|
||||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||||
|
@ -27,9 +46,113 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht
|
||||||
|
|
||||||
filteredEndpoints := security.FilterEndpoints(endpoints, endpointGroups, securityContext)
|
filteredEndpoints := security.FilterEndpoints(endpoints, endpointGroups, securityContext)
|
||||||
|
|
||||||
for idx := range filteredEndpoints {
|
if groupID != 0 {
|
||||||
hideFields(&filteredEndpoints[idx])
|
filteredEndpoints = filterEndpointsByGroupID(filteredEndpoints, portainer.EndpointGroupID(groupID))
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.JSON(w, filteredEndpoints)
|
if search != "" {
|
||||||
|
filteredEndpoints = filterEndpointsBySearchCriteria(filteredEndpoints, endpointGroups, search)
|
||||||
|
}
|
||||||
|
|
||||||
|
filteredEndpointCount := len(filteredEndpoints)
|
||||||
|
|
||||||
|
paginatedEndpoints := paginateEndpoints(filteredEndpoints, start, limit)
|
||||||
|
|
||||||
|
for idx := range paginatedEndpoints {
|
||||||
|
hideFields(&paginatedEndpoints[idx])
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("X-Total-Count", strconv.Itoa(filteredEndpointCount))
|
||||||
|
return response.JSON(w, paginatedEndpoints)
|
||||||
|
}
|
||||||
|
|
||||||
|
func paginateEndpoints(endpoints []portainer.Endpoint, start, limit int) []portainer.Endpoint {
|
||||||
|
if limit == 0 {
|
||||||
|
return endpoints
|
||||||
|
}
|
||||||
|
|
||||||
|
endpointCount := len(endpoints)
|
||||||
|
|
||||||
|
if start > endpointCount {
|
||||||
|
start = endpointCount
|
||||||
|
}
|
||||||
|
|
||||||
|
end := start + limit
|
||||||
|
if end > endpointCount {
|
||||||
|
end = endpointCount
|
||||||
|
}
|
||||||
|
|
||||||
|
return endpoints[start:end]
|
||||||
|
}
|
||||||
|
|
||||||
|
func filterEndpointsByGroupID(endpoints []portainer.Endpoint, endpointGroupID portainer.EndpointGroupID) []portainer.Endpoint {
|
||||||
|
filteredEndpoints := make([]portainer.Endpoint, 0)
|
||||||
|
|
||||||
|
for _, endpoint := range endpoints {
|
||||||
|
if endpoint.GroupID == endpointGroupID {
|
||||||
|
filteredEndpoints = append(filteredEndpoints, endpoint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredEndpoints
|
||||||
|
}
|
||||||
|
|
||||||
|
func filterEndpointsBySearchCriteria(endpoints []portainer.Endpoint, endpointGroups []portainer.EndpointGroup, searchCriteria string) []portainer.Endpoint {
|
||||||
|
filteredEndpoints := make([]portainer.Endpoint, 0)
|
||||||
|
|
||||||
|
for _, endpoint := range endpoints {
|
||||||
|
|
||||||
|
if endpointMatchSearchCriteria(&endpoint, searchCriteria) {
|
||||||
|
filteredEndpoints = append(filteredEndpoints, endpoint)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if endpointGroupMatchSearchCriteria(&endpoint, endpointGroups, searchCriteria) {
|
||||||
|
filteredEndpoints = append(filteredEndpoints, endpoint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredEndpoints
|
||||||
|
}
|
||||||
|
|
||||||
|
func endpointMatchSearchCriteria(endpoint *portainer.Endpoint, searchCriteria string) bool {
|
||||||
|
if strings.Contains(strings.ToLower(endpoint.Name), searchCriteria) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(strings.ToLower(endpoint.URL), searchCriteria) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if endpoint.Status == portainer.EndpointStatusUp && searchCriteria == "up" {
|
||||||
|
return true
|
||||||
|
} else if endpoint.Status == portainer.EndpointStatusDown && searchCriteria == "down" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tag := range endpoint.Tags {
|
||||||
|
if strings.Contains(strings.ToLower(tag), searchCriteria) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func endpointGroupMatchSearchCriteria(endpoint *portainer.Endpoint, endpointGroups []portainer.EndpointGroup, searchCriteria string) bool {
|
||||||
|
for _, group := range endpointGroups {
|
||||||
|
if group.ID == endpoint.GroupID {
|
||||||
|
if strings.Contains(strings.ToLower(group.Name), searchCriteria) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tag := range group.Tags {
|
||||||
|
if strings.Contains(strings.ToLower(tag), searchCriteria) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -19,6 +19,9 @@ const (
|
||||||
|
|
||||||
func hideFields(endpoint *portainer.Endpoint) {
|
func hideFields(endpoint *portainer.Endpoint) {
|
||||||
endpoint.AzureCredentials = portainer.AzureCredentials{}
|
endpoint.AzureCredentials = portainer.AzureCredentials{}
|
||||||
|
if len(endpoint.Snapshots) > 0 {
|
||||||
|
endpoint.Snapshots[0].SnapshotRaw = portainer.SnapshotRaw{}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handler is the HTTP handler used to handle endpoint operations.
|
// Handler is the HTTP handler used to handle endpoint operations.
|
||||||
|
@ -32,6 +35,8 @@ type Handler struct {
|
||||||
ProxyManager *proxy.Manager
|
ProxyManager *proxy.Manager
|
||||||
Snapshotter portainer.Snapshotter
|
Snapshotter portainer.Snapshotter
|
||||||
JobService portainer.JobService
|
JobService portainer.JobService
|
||||||
|
ReverseTunnelService portainer.ReverseTunnelService
|
||||||
|
SettingsService portainer.SettingsService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler creates a handler to manage endpoint operations.
|
// NewHandler creates a handler to manage endpoint operations.
|
||||||
|
@ -62,5 +67,8 @@ func NewHandler(bouncer *security.RequestBouncer, authorizeEndpointManagement bo
|
||||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointJob))).Methods(http.MethodPost)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointJob))).Methods(http.MethodPost)
|
||||||
h.Handle("/endpoints/{id}/snapshot",
|
h.Handle("/endpoints/{id}/snapshot",
|
||||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointSnapshot))).Methods(http.MethodPost)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointSnapshot))).Methods(http.MethodPost)
|
||||||
|
h.Handle("/endpoints/{id}/status",
|
||||||
|
bouncer.PublicAccess(httperror.LoggerHandler(h.endpointStatusInspect))).Methods(http.MethodGet)
|
||||||
|
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,9 +5,9 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/portainer/libcrypto"
|
||||||
"github.com/portainer/libhttp/response"
|
"github.com/portainer/libhttp/response"
|
||||||
"github.com/portainer/portainer/api"
|
"github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/crypto"
|
|
||||||
"github.com/portainer/portainer/api/http/client"
|
"github.com/portainer/portainer/api/http/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ func (handler *Handler) motd(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
message := strings.Join(data.Message, "\n")
|
message := strings.Join(data.Message, "\n")
|
||||||
|
|
||||||
hash := crypto.HashFromBytes([]byte(message))
|
hash := libcrypto.HashFromBytes([]byte(message))
|
||||||
resp := motdResponse{
|
resp := motdResponse{
|
||||||
Title: data.Title,
|
Title: data.Title,
|
||||||
Message: message,
|
Message: message,
|
||||||
|
|
|
@ -18,6 +18,7 @@ type Handler struct {
|
||||||
FileService portainer.FileService
|
FileService portainer.FileService
|
||||||
JobService portainer.JobService
|
JobService portainer.JobService
|
||||||
JobScheduler portainer.JobScheduler
|
JobScheduler portainer.JobScheduler
|
||||||
|
ReverseTunnelService portainer.ReverseTunnelService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler creates a handler to manage schedule operations.
|
// NewHandler creates a handler to manage schedule operations.
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
package schedules
|
package schedules
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/asaskevich/govalidator"
|
"github.com/asaskevich/govalidator"
|
||||||
|
@ -113,7 +115,7 @@ func (payload *scheduleCreateFromFileContentPayload) Validate(r *http.Request) e
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /api/schedules?method=file/string
|
// POST /api/schedules?method=file|string
|
||||||
func (handler *Handler) scheduleCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
func (handler *Handler) scheduleCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
settings, err := handler.SettingsService.Settings()
|
settings, err := handler.SettingsService.Settings()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -219,6 +221,46 @@ func (handler *Handler) createScheduleObjectFromFileContentPayload(payload *sche
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *Handler) addAndPersistSchedule(schedule *portainer.Schedule, file []byte) error {
|
func (handler *Handler) addAndPersistSchedule(schedule *portainer.Schedule, file []byte) error {
|
||||||
|
nonEdgeEndpointIDs := make([]portainer.EndpointID, 0)
|
||||||
|
edgeEndpointIDs := make([]portainer.EndpointID, 0)
|
||||||
|
|
||||||
|
edgeCronExpression := strings.Split(schedule.CronExpression, " ")
|
||||||
|
if len(edgeCronExpression) == 6 {
|
||||||
|
edgeCronExpression = edgeCronExpression[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ID := range schedule.ScriptExecutionJob.Endpoints {
|
||||||
|
|
||||||
|
endpoint, err := handler.EndpointService.Endpoint(ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if endpoint.Type != portainer.EdgeAgentEnvironment {
|
||||||
|
nonEdgeEndpointIDs = append(nonEdgeEndpointIDs, endpoint.ID)
|
||||||
|
} else {
|
||||||
|
edgeEndpointIDs = append(edgeEndpointIDs, endpoint.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(edgeEndpointIDs) > 0 {
|
||||||
|
edgeSchedule := &portainer.EdgeSchedule{
|
||||||
|
ID: schedule.ID,
|
||||||
|
CronExpression: strings.Join(edgeCronExpression, " "),
|
||||||
|
Script: base64.RawStdEncoding.EncodeToString(file),
|
||||||
|
Endpoints: edgeEndpointIDs,
|
||||||
|
Version: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, endpointID := range edgeEndpointIDs {
|
||||||
|
handler.ReverseTunnelService.AddSchedule(endpointID, edgeSchedule)
|
||||||
|
}
|
||||||
|
|
||||||
|
schedule.EdgeSchedule = edgeSchedule
|
||||||
|
}
|
||||||
|
|
||||||
|
schedule.ScriptExecutionJob.Endpoints = nonEdgeEndpointIDs
|
||||||
|
|
||||||
scriptPath, err := handler.FileService.StoreScheduledJobFileFromBytes(strconv.Itoa(int(schedule.ID)), file)
|
scriptPath, err := handler.FileService.StoreScheduledJobFileFromBytes(strconv.Itoa(int(schedule.ID)), file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -42,6 +42,8 @@ func (handler *Handler) scheduleDelete(w http.ResponseWriter, r *http.Request) *
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the files associated to the schedule on the filesystem", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the files associated to the schedule on the filesystem", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handler.ReverseTunnelService.RemoveSchedule(schedule.ID)
|
||||||
|
|
||||||
handler.JobScheduler.UnscheduleJob(schedule.ID)
|
handler.JobScheduler.UnscheduleJob(schedule.ID)
|
||||||
|
|
||||||
err = handler.ScheduleService.DeleteSchedule(portainer.ScheduleID(scheduleID))
|
err = handler.ScheduleService.DeleteSchedule(portainer.ScheduleID(scheduleID))
|
||||||
|
|
|
@ -3,6 +3,7 @@ package schedules
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
@ -18,6 +19,7 @@ type taskContainer struct {
|
||||||
Status string `json:"Status"`
|
Status string `json:"Status"`
|
||||||
Created float64 `json:"Created"`
|
Created float64 `json:"Created"`
|
||||||
Labels map[string]string `json:"Labels"`
|
Labels map[string]string `json:"Labels"`
|
||||||
|
Edge bool `json:"Edge"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET request on /api/schedules/:id/tasks
|
// GET request on /api/schedules/:id/tasks
|
||||||
|
@ -64,6 +66,22 @@ func (handler *Handler) scheduleTasks(w http.ResponseWriter, r *http.Request) *h
|
||||||
tasks = append(tasks, endpointTasks...)
|
tasks = append(tasks, endpointTasks...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if schedule.EdgeSchedule != nil {
|
||||||
|
for _, endpointID := range schedule.EdgeSchedule.Endpoints {
|
||||||
|
|
||||||
|
cronTask := taskContainer{
|
||||||
|
ID: fmt.Sprintf("schedule_%d", schedule.EdgeSchedule.ID),
|
||||||
|
EndpointID: endpointID,
|
||||||
|
Edge: true,
|
||||||
|
Status: "",
|
||||||
|
Created: 0,
|
||||||
|
Labels: map[string]string{},
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks = append(tasks, cronTask)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return response.JSON(w, tasks)
|
return response.JSON(w, tasks)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,6 +105,7 @@ func extractTasksFromContainerSnasphot(endpoint *portainer.Endpoint, scheduleID
|
||||||
for _, container := range containers {
|
for _, container := range containers {
|
||||||
if container.Labels["io.portainer.schedule.id"] == strconv.Itoa(int(scheduleID)) {
|
if container.Labels["io.portainer.schedule.id"] == strconv.Itoa(int(scheduleID)) {
|
||||||
container.EndpointID = endpoint.ID
|
container.EndpointID = endpoint.ID
|
||||||
|
container.Edge = false
|
||||||
endpointTasks = append(endpointTasks, container)
|
endpointTasks = append(endpointTasks, container)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package schedules
|
package schedules
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -58,7 +59,15 @@ func (handler *Handler) scheduleUpdate(w http.ResponseWriter, r *http.Request) *
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a schedule with the specified identifier inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a schedule with the specified identifier inside the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateJobSchedule := updateSchedule(schedule, &payload)
|
updateJobSchedule := false
|
||||||
|
if schedule.EdgeSchedule != nil {
|
||||||
|
err := handler.updateEdgeSchedule(schedule, &payload)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update Edge schedule", err}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
updateJobSchedule = updateSchedule(schedule, &payload)
|
||||||
|
}
|
||||||
|
|
||||||
if payload.FileContent != nil {
|
if payload.FileContent != nil {
|
||||||
_, err := handler.FileService.StoreScheduledJobFileFromBytes(strconv.Itoa(scheduleID), []byte(*payload.FileContent))
|
_, err := handler.FileService.StoreScheduledJobFileFromBytes(strconv.Itoa(scheduleID), []byte(*payload.FileContent))
|
||||||
|
@ -85,6 +94,46 @@ func (handler *Handler) scheduleUpdate(w http.ResponseWriter, r *http.Request) *
|
||||||
return response.JSON(w, schedule)
|
return response.JSON(w, schedule)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (handler *Handler) updateEdgeSchedule(schedule *portainer.Schedule, payload *scheduleUpdatePayload) error {
|
||||||
|
if payload.Name != nil {
|
||||||
|
schedule.Name = *payload.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.Endpoints != nil {
|
||||||
|
|
||||||
|
edgeEndpointIDs := make([]portainer.EndpointID, 0)
|
||||||
|
|
||||||
|
for _, ID := range payload.Endpoints {
|
||||||
|
endpoint, err := handler.EndpointService.Endpoint(ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if endpoint.Type == portainer.EdgeAgentEnvironment {
|
||||||
|
edgeEndpointIDs = append(edgeEndpointIDs, endpoint.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
schedule.EdgeSchedule.Endpoints = edgeEndpointIDs
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.CronExpression != nil {
|
||||||
|
schedule.EdgeSchedule.CronExpression = *payload.CronExpression
|
||||||
|
schedule.EdgeSchedule.Version++
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.FileContent != nil {
|
||||||
|
schedule.EdgeSchedule.Script = base64.RawStdEncoding.EncodeToString([]byte(*payload.FileContent))
|
||||||
|
schedule.EdgeSchedule.Version++
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, endpointID := range schedule.EdgeSchedule.Endpoints {
|
||||||
|
handler.ReverseTunnelService.AddSchedule(endpointID, schedule.EdgeSchedule)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func updateSchedule(schedule *portainer.Schedule, payload *scheduleUpdatePayload) bool {
|
func updateSchedule(schedule *portainer.Schedule, payload *scheduleUpdatePayload) bool {
|
||||||
updateJobSchedule := false
|
updateJobSchedule := false
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,7 @@ type settingsUpdatePayload struct {
|
||||||
EnableHostManagementFeatures *bool
|
EnableHostManagementFeatures *bool
|
||||||
SnapshotInterval *string
|
SnapshotInterval *string
|
||||||
TemplatesURL *string
|
TemplatesURL *string
|
||||||
|
EdgeAgentCheckinInterval *int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (payload *settingsUpdatePayload) Validate(r *http.Request) error {
|
func (payload *settingsUpdatePayload) Validate(r *http.Request) error {
|
||||||
|
@ -103,6 +104,10 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if payload.EdgeAgentCheckinInterval != nil {
|
||||||
|
settings.EdgeAgentCheckinInterval = *payload.EdgeAgentCheckinInterval
|
||||||
|
}
|
||||||
|
|
||||||
tlsError := handler.updateTLS(settings)
|
tlsError := handler.updateTLS(settings)
|
||||||
if tlsError != nil {
|
if tlsError != nil {
|
||||||
return tlsError
|
return tlsError
|
||||||
|
|
|
@ -62,8 +62,10 @@ func (handler *Handler) handleAttachRequest(w http.ResponseWriter, r *http.Reque
|
||||||
|
|
||||||
r.Header.Del("Origin")
|
r.Header.Del("Origin")
|
||||||
|
|
||||||
if params.nodeName != "" || params.endpoint.Type == portainer.AgentOnDockerEnvironment {
|
if params.endpoint.Type == portainer.AgentOnDockerEnvironment {
|
||||||
return handler.proxyWebsocketRequest(w, r, params)
|
return handler.proxyAgentWebsocketRequest(w, r, params)
|
||||||
|
} else if params.endpoint.Type == portainer.EdgeAgentEnvironment {
|
||||||
|
return handler.proxyEdgeAgentWebsocketRequest(w, r, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
websocketConn, err := handler.connectionUpgrader.Upgrade(w, r, nil)
|
websocketConn, err := handler.connectionUpgrader.Upgrade(w, r, nil)
|
||||||
|
|
|
@ -68,8 +68,10 @@ func (handler *Handler) websocketExec(w http.ResponseWriter, r *http.Request) *h
|
||||||
func (handler *Handler) handleExecRequest(w http.ResponseWriter, r *http.Request, params *webSocketRequestParams) error {
|
func (handler *Handler) handleExecRequest(w http.ResponseWriter, r *http.Request, params *webSocketRequestParams) error {
|
||||||
r.Header.Del("Origin")
|
r.Header.Del("Origin")
|
||||||
|
|
||||||
if params.nodeName != "" || params.endpoint.Type == portainer.AgentOnDockerEnvironment {
|
if params.endpoint.Type == portainer.AgentOnDockerEnvironment {
|
||||||
return handler.proxyWebsocketRequest(w, r, params)
|
return handler.proxyAgentWebsocketRequest(w, r, params)
|
||||||
|
} else if params.endpoint.Type == portainer.EdgeAgentEnvironment {
|
||||||
|
return handler.proxyEdgeAgentWebsocketRequest(w, r, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
websocketConn, err := handler.connectionUpgrader.Upgrade(w, r, nil)
|
websocketConn, err := handler.connectionUpgrader.Upgrade(w, r, nil)
|
||||||
|
|
|
@ -13,6 +13,7 @@ type Handler struct {
|
||||||
*mux.Router
|
*mux.Router
|
||||||
EndpointService portainer.EndpointService
|
EndpointService portainer.EndpointService
|
||||||
SignatureService portainer.DigitalSignatureService
|
SignatureService portainer.DigitalSignatureService
|
||||||
|
ReverseTunnelService portainer.ReverseTunnelService
|
||||||
requestBouncer *security.RequestBouncer
|
requestBouncer *security.RequestBouncer
|
||||||
connectionUpgrader websocket.Upgrader
|
connectionUpgrader websocket.Upgrader
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,14 +2,37 @@ package websocket
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/koding/websocketproxy"
|
"github.com/koding/websocketproxy"
|
||||||
"github.com/portainer/portainer/api"
|
"github.com/portainer/portainer/api"
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (handler *Handler) proxyWebsocketRequest(w http.ResponseWriter, r *http.Request, params *webSocketRequestParams) error {
|
func (handler *Handler) proxyEdgeAgentWebsocketRequest(w http.ResponseWriter, r *http.Request, params *webSocketRequestParams) error {
|
||||||
|
tunnel := handler.ReverseTunnelService.GetTunnelDetails(params.endpoint.ID)
|
||||||
|
|
||||||
|
endpointURL, err := url.Parse(fmt.Sprintf("http://localhost:%d", tunnel.Port))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
endpointURL.Scheme = "ws"
|
||||||
|
proxy := websocketproxy.NewProxy(endpointURL)
|
||||||
|
|
||||||
|
proxy.Director = func(incoming *http.Request, out http.Header) {
|
||||||
|
out.Set(portainer.PortainerAgentTargetHeader, params.nodeName)
|
||||||
|
}
|
||||||
|
|
||||||
|
handler.ReverseTunnelService.SetTunnelStatusToActive(params.endpoint.ID)
|
||||||
|
proxy.ServeHTTP(w, r)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (handler *Handler) proxyAgentWebsocketRequest(w http.ResponseWriter, r *http.Request, params *webSocketRequestParams) error {
|
||||||
agentURL, err := url.Parse(params.endpoint.URL)
|
agentURL, err := url.Parse(params.endpoint.URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -24,7 +24,9 @@ type (
|
||||||
DockerHubService portainer.DockerHubService
|
DockerHubService portainer.DockerHubService
|
||||||
SettingsService portainer.SettingsService
|
SettingsService portainer.SettingsService
|
||||||
SignatureService portainer.DigitalSignatureService
|
SignatureService portainer.DigitalSignatureService
|
||||||
|
ReverseTunnelService portainer.ReverseTunnelService
|
||||||
endpointIdentifier portainer.EndpointID
|
endpointIdentifier portainer.EndpointID
|
||||||
|
endpointType portainer.EndpointType
|
||||||
}
|
}
|
||||||
restrictedDockerOperationContext struct {
|
restrictedDockerOperationContext struct {
|
||||||
isAdmin bool
|
isAdmin bool
|
||||||
|
@ -58,7 +60,19 @@ func (p *proxyTransport) RoundTrip(request *http.Request) (*http.Response, error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *proxyTransport) executeDockerRequest(request *http.Request) (*http.Response, error) {
|
func (p *proxyTransport) executeDockerRequest(request *http.Request) (*http.Response, error) {
|
||||||
return p.dockerTransport.RoundTrip(request)
|
response, err := p.dockerTransport.RoundTrip(request)
|
||||||
|
|
||||||
|
if p.endpointType != portainer.EdgeAgentEnvironment {
|
||||||
|
return response, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
p.ReverseTunnelService.SetTunnelStatusToActive(p.endpointIdentifier)
|
||||||
|
} else {
|
||||||
|
p.ReverseTunnelService.SetTunnelStatusToIdle(p.endpointIdentifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *proxyTransport) proxyDockerRequest(request *http.Request) (*http.Response, error) {
|
func (p *proxyTransport) proxyDockerRequest(request *http.Request) (*http.Response, error) {
|
||||||
|
|
|
@ -21,6 +21,7 @@ type proxyFactory struct {
|
||||||
RegistryService portainer.RegistryService
|
RegistryService portainer.RegistryService
|
||||||
DockerHubService portainer.DockerHubService
|
DockerHubService portainer.DockerHubService
|
||||||
SignatureService portainer.DigitalSignatureService
|
SignatureService portainer.DigitalSignatureService
|
||||||
|
ReverseTunnelService portainer.ReverseTunnelService
|
||||||
}
|
}
|
||||||
|
|
||||||
func (factory *proxyFactory) newHTTPProxy(u *url.URL) http.Handler {
|
func (factory *proxyFactory) newHTTPProxy(u *url.URL) http.Handler {
|
||||||
|
@ -29,21 +30,21 @@ func (factory *proxyFactory) newHTTPProxy(u *url.URL) http.Handler {
|
||||||
}
|
}
|
||||||
|
|
||||||
func newAzureProxy(credentials *portainer.AzureCredentials) (http.Handler, error) {
|
func newAzureProxy(credentials *portainer.AzureCredentials) (http.Handler, error) {
|
||||||
url, err := url.Parse(AzureAPIBaseURL)
|
remoteURL, err := url.Parse(AzureAPIBaseURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
proxy := newSingleHostReverseProxyWithHostHeader(url)
|
proxy := newSingleHostReverseProxyWithHostHeader(remoteURL)
|
||||||
proxy.Transport = NewAzureTransport(credentials)
|
proxy.Transport = NewAzureTransport(credentials)
|
||||||
|
|
||||||
return proxy, nil
|
return proxy, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (factory *proxyFactory) newDockerHTTPSProxy(u *url.URL, tlsConfig *portainer.TLSConfiguration, enableSignature bool, endpointID portainer.EndpointID) (http.Handler, error) {
|
func (factory *proxyFactory) newDockerHTTPSProxy(u *url.URL, tlsConfig *portainer.TLSConfiguration, endpoint *portainer.Endpoint) (http.Handler, error) {
|
||||||
u.Scheme = "https"
|
u.Scheme = "https"
|
||||||
|
|
||||||
proxy := factory.createDockerReverseProxy(u, enableSignature, endpointID)
|
proxy := factory.createDockerReverseProxy(u, endpoint)
|
||||||
config, err := crypto.CreateTLSConfigurationFromDisk(tlsConfig.TLSCACertPath, tlsConfig.TLSCertPath, tlsConfig.TLSKeyPath, tlsConfig.TLSSkipVerify)
|
config, err := crypto.CreateTLSConfigurationFromDisk(tlsConfig.TLSCACertPath, tlsConfig.TLSCertPath, tlsConfig.TLSKeyPath, tlsConfig.TLSSkipVerify)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -53,13 +54,19 @@ func (factory *proxyFactory) newDockerHTTPSProxy(u *url.URL, tlsConfig *portaine
|
||||||
return proxy, nil
|
return proxy, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (factory *proxyFactory) newDockerHTTPProxy(u *url.URL, enableSignature bool, endpointID portainer.EndpointID) http.Handler {
|
func (factory *proxyFactory) newDockerHTTPProxy(u *url.URL, endpoint *portainer.Endpoint) http.Handler {
|
||||||
u.Scheme = "http"
|
u.Scheme = "http"
|
||||||
return factory.createDockerReverseProxy(u, enableSignature, endpointID)
|
return factory.createDockerReverseProxy(u, endpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (factory *proxyFactory) createDockerReverseProxy(u *url.URL, enableSignature bool, endpointID portainer.EndpointID) *httputil.ReverseProxy {
|
func (factory *proxyFactory) createDockerReverseProxy(u *url.URL, endpoint *portainer.Endpoint) *httputil.ReverseProxy {
|
||||||
proxy := newSingleHostReverseProxyWithHostHeader(u)
|
proxy := newSingleHostReverseProxyWithHostHeader(u)
|
||||||
|
|
||||||
|
enableSignature := false
|
||||||
|
if endpoint.Type == portainer.AgentOnDockerEnvironment {
|
||||||
|
enableSignature = true
|
||||||
|
}
|
||||||
|
|
||||||
transport := &proxyTransport{
|
transport := &proxyTransport{
|
||||||
enableSignature: enableSignature,
|
enableSignature: enableSignature,
|
||||||
ResourceControlService: factory.ResourceControlService,
|
ResourceControlService: factory.ResourceControlService,
|
||||||
|
@ -67,8 +74,10 @@ func (factory *proxyFactory) createDockerReverseProxy(u *url.URL, enableSignatur
|
||||||
SettingsService: factory.SettingsService,
|
SettingsService: factory.SettingsService,
|
||||||
RegistryService: factory.RegistryService,
|
RegistryService: factory.RegistryService,
|
||||||
DockerHubService: factory.DockerHubService,
|
DockerHubService: factory.DockerHubService,
|
||||||
|
ReverseTunnelService: factory.ReverseTunnelService,
|
||||||
dockerTransport: &http.Transport{},
|
dockerTransport: &http.Transport{},
|
||||||
endpointIdentifier: endpointID,
|
endpointIdentifier: endpoint.ID,
|
||||||
|
endpointType: endpoint.Type,
|
||||||
}
|
}
|
||||||
|
|
||||||
if enableSignature {
|
if enableSignature {
|
||||||
|
|
|
@ -8,7 +8,7 @@ import (
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (factory *proxyFactory) newLocalProxy(path string, endpointID portainer.EndpointID) http.Handler {
|
func (factory *proxyFactory) newLocalProxy(path string, endpoint *portainer.Endpoint) http.Handler {
|
||||||
proxy := &localProxy{}
|
proxy := &localProxy{}
|
||||||
transport := &proxyTransport{
|
transport := &proxyTransport{
|
||||||
enableSignature: false,
|
enableSignature: false,
|
||||||
|
@ -18,7 +18,9 @@ func (factory *proxyFactory) newLocalProxy(path string, endpointID portainer.End
|
||||||
RegistryService: factory.RegistryService,
|
RegistryService: factory.RegistryService,
|
||||||
DockerHubService: factory.DockerHubService,
|
DockerHubService: factory.DockerHubService,
|
||||||
dockerTransport: newSocketTransport(path),
|
dockerTransport: newSocketTransport(path),
|
||||||
endpointIdentifier: endpointID,
|
ReverseTunnelService: factory.ReverseTunnelService,
|
||||||
|
endpointIdentifier: endpoint.ID,
|
||||||
|
endpointType: endpoint.Type,
|
||||||
}
|
}
|
||||||
proxy.Transport = transport
|
proxy.Transport = transport
|
||||||
return proxy
|
return proxy
|
||||||
|
|
|
@ -5,12 +5,11 @@ package proxy
|
||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"github.com/Microsoft/go-winio"
|
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (factory *proxyFactory) newLocalProxy(path string, endpointID portainer.EndpointID) http.Handler {
|
func (factory *proxyFactory) newLocalProxy(path string, endpoint *portainer.Endpoint) http.Handler {
|
||||||
proxy := &localProxy{}
|
proxy := &localProxy{}
|
||||||
transport := &proxyTransport{
|
transport := &proxyTransport{
|
||||||
enableSignature: false,
|
enableSignature: false,
|
||||||
|
@ -19,8 +18,10 @@ func (factory *proxyFactory) newLocalProxy(path string, endpointID portainer.End
|
||||||
SettingsService: factory.SettingsService,
|
SettingsService: factory.SettingsService,
|
||||||
RegistryService: factory.RegistryService,
|
RegistryService: factory.RegistryService,
|
||||||
DockerHubService: factory.DockerHubService,
|
DockerHubService: factory.DockerHubService,
|
||||||
|
ReverseTunnelService: factory.ReverseTunnelService,
|
||||||
dockerTransport: newNamedPipeTransport(path),
|
dockerTransport: newNamedPipeTransport(path),
|
||||||
endpointIdentifier: endpointID,
|
endpointIdentifier: endpoint.ID,
|
||||||
|
endpointType: endpoint.Type,
|
||||||
}
|
}
|
||||||
proxy.Transport = transport
|
proxy.Transport = transport
|
||||||
return proxy
|
return proxy
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package proxy
|
package proxy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -21,6 +22,7 @@ type (
|
||||||
// Manager represents a service used to manage Docker proxies.
|
// Manager represents a service used to manage Docker proxies.
|
||||||
Manager struct {
|
Manager struct {
|
||||||
proxyFactory *proxyFactory
|
proxyFactory *proxyFactory
|
||||||
|
reverseTunnelService portainer.ReverseTunnelService
|
||||||
proxies cmap.ConcurrentMap
|
proxies cmap.ConcurrentMap
|
||||||
extensionProxies cmap.ConcurrentMap
|
extensionProxies cmap.ConcurrentMap
|
||||||
legacyExtensionProxies cmap.ConcurrentMap
|
legacyExtensionProxies cmap.ConcurrentMap
|
||||||
|
@ -34,6 +36,7 @@ type (
|
||||||
RegistryService portainer.RegistryService
|
RegistryService portainer.RegistryService
|
||||||
DockerHubService portainer.DockerHubService
|
DockerHubService portainer.DockerHubService
|
||||||
SignatureService portainer.DigitalSignatureService
|
SignatureService portainer.DigitalSignatureService
|
||||||
|
ReverseTunnelService portainer.ReverseTunnelService
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -50,13 +53,15 @@ func NewManager(parameters *ManagerParams) *Manager {
|
||||||
RegistryService: parameters.RegistryService,
|
RegistryService: parameters.RegistryService,
|
||||||
DockerHubService: parameters.DockerHubService,
|
DockerHubService: parameters.DockerHubService,
|
||||||
SignatureService: parameters.SignatureService,
|
SignatureService: parameters.SignatureService,
|
||||||
|
ReverseTunnelService: parameters.ReverseTunnelService,
|
||||||
},
|
},
|
||||||
|
reverseTunnelService: parameters.ReverseTunnelService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetProxy returns the proxy associated to a key
|
// GetProxy returns the proxy associated to a key
|
||||||
func (manager *Manager) GetProxy(key string) http.Handler {
|
func (manager *Manager) GetProxy(endpoint *portainer.Endpoint) http.Handler {
|
||||||
proxy, ok := manager.proxies.Get(key)
|
proxy, ok := manager.proxies.Get(string(endpoint.ID))
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -76,8 +81,8 @@ func (manager *Manager) CreateAndRegisterProxy(endpoint *portainer.Endpoint) (ht
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteProxy deletes the proxy associated to a key
|
// DeleteProxy deletes the proxy associated to a key
|
||||||
func (manager *Manager) DeleteProxy(key string) {
|
func (manager *Manager) DeleteProxy(endpoint *portainer.Endpoint) {
|
||||||
manager.proxies.Remove(key)
|
manager.proxies.Remove(string(endpoint.ID))
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetExtensionProxy returns an extension proxy associated to an extension identifier
|
// GetExtensionProxy returns an extension proxy associated to an extension identifier
|
||||||
|
@ -92,7 +97,7 @@ func (manager *Manager) GetExtensionProxy(extensionID portainer.ExtensionID) htt
|
||||||
// CreateExtensionProxy creates a new HTTP reverse proxy for an extension and
|
// CreateExtensionProxy creates a new HTTP reverse proxy for an extension and
|
||||||
// registers it in the extension map associated to the specified extension identifier
|
// registers it in the extension map associated to the specified extension identifier
|
||||||
func (manager *Manager) CreateExtensionProxy(extensionID portainer.ExtensionID) (http.Handler, error) {
|
func (manager *Manager) CreateExtensionProxy(extensionID portainer.ExtensionID) (http.Handler, error) {
|
||||||
address := "http://localhost:" + extensionPorts[extensionID]
|
address := "http://" + portainer.ExtensionServer + ":" + extensionPorts[extensionID]
|
||||||
|
|
||||||
extensionURL, err := url.Parse(address)
|
extensionURL, err := url.Parse(address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -136,28 +141,40 @@ func (manager *Manager) CreateLegacyExtensionProxy(key, extensionAPIURL string)
|
||||||
return proxy, nil
|
return proxy, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (manager *Manager) createDockerProxy(endpointURL *url.URL, tlsConfig *portainer.TLSConfiguration, endpointID portainer.EndpointID) (http.Handler, error) {
|
func (manager *Manager) createDockerProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
|
||||||
if endpointURL.Scheme == "tcp" {
|
baseURL := endpoint.URL
|
||||||
if tlsConfig.TLS || tlsConfig.TLSSkipVerify {
|
if endpoint.Type == portainer.EdgeAgentEnvironment {
|
||||||
return manager.proxyFactory.newDockerHTTPSProxy(endpointURL, tlsConfig, false, endpointID)
|
tunnel := manager.reverseTunnelService.GetTunnelDetails(endpoint.ID)
|
||||||
}
|
baseURL = fmt.Sprintf("http://localhost:%d", tunnel.Port)
|
||||||
return manager.proxyFactory.newDockerHTTPProxy(endpointURL, false, endpointID), nil
|
|
||||||
}
|
|
||||||
return manager.proxyFactory.newLocalProxy(endpointURL.Path, endpointID), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (manager *Manager) createProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
|
endpointURL, err := url.Parse(baseURL)
|
||||||
endpointURL, err := url.Parse(endpoint.URL)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
switch endpoint.Type {
|
switch endpoint.Type {
|
||||||
case portainer.AgentOnDockerEnvironment:
|
case portainer.AgentOnDockerEnvironment:
|
||||||
return manager.proxyFactory.newDockerHTTPSProxy(endpointURL, &endpoint.TLSConfig, true, endpoint.ID)
|
return manager.proxyFactory.newDockerHTTPSProxy(endpointURL, &endpoint.TLSConfig, endpoint)
|
||||||
case portainer.AzureEnvironment:
|
case portainer.EdgeAgentEnvironment:
|
||||||
|
return 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 newAzureProxy(&endpoint.AzureCredentials)
|
||||||
default:
|
|
||||||
return manager.createDockerProxy(endpointURL, &endpoint.TLSConfig, endpoint.ID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return manager.createDockerProxy(endpoint)
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,6 +44,7 @@ type Server struct {
|
||||||
AuthDisabled bool
|
AuthDisabled bool
|
||||||
EndpointManagement bool
|
EndpointManagement bool
|
||||||
Status *portainer.Status
|
Status *portainer.Status
|
||||||
|
ReverseTunnelService portainer.ReverseTunnelService
|
||||||
ExtensionManager portainer.ExtensionManager
|
ExtensionManager portainer.ExtensionManager
|
||||||
ComposeStackManager portainer.ComposeStackManager
|
ComposeStackManager portainer.ComposeStackManager
|
||||||
CryptoService portainer.CryptoService
|
CryptoService portainer.CryptoService
|
||||||
|
@ -88,6 +89,7 @@ func (server *Server) Start() error {
|
||||||
RegistryService: server.RegistryService,
|
RegistryService: server.RegistryService,
|
||||||
DockerHubService: server.DockerHubService,
|
DockerHubService: server.DockerHubService,
|
||||||
SignatureService: server.SignatureService,
|
SignatureService: server.SignatureService,
|
||||||
|
ReverseTunnelService: server.ReverseTunnelService,
|
||||||
}
|
}
|
||||||
proxyManager := proxy.NewManager(proxyManagerParameters)
|
proxyManager := proxy.NewManager(proxyManagerParameters)
|
||||||
|
|
||||||
|
@ -132,6 +134,8 @@ func (server *Server) Start() error {
|
||||||
endpointHandler.ProxyManager = proxyManager
|
endpointHandler.ProxyManager = proxyManager
|
||||||
endpointHandler.Snapshotter = server.Snapshotter
|
endpointHandler.Snapshotter = server.Snapshotter
|
||||||
endpointHandler.JobService = server.JobService
|
endpointHandler.JobService = server.JobService
|
||||||
|
endpointHandler.ReverseTunnelService = server.ReverseTunnelService
|
||||||
|
endpointHandler.SettingsService = server.SettingsService
|
||||||
|
|
||||||
var endpointGroupHandler = endpointgroups.NewHandler(requestBouncer)
|
var endpointGroupHandler = endpointgroups.NewHandler(requestBouncer)
|
||||||
endpointGroupHandler.EndpointGroupService = server.EndpointGroupService
|
endpointGroupHandler.EndpointGroupService = server.EndpointGroupService
|
||||||
|
@ -140,6 +144,8 @@ func (server *Server) Start() error {
|
||||||
var endpointProxyHandler = endpointproxy.NewHandler(requestBouncer)
|
var endpointProxyHandler = endpointproxy.NewHandler(requestBouncer)
|
||||||
endpointProxyHandler.EndpointService = server.EndpointService
|
endpointProxyHandler.EndpointService = server.EndpointService
|
||||||
endpointProxyHandler.ProxyManager = proxyManager
|
endpointProxyHandler.ProxyManager = proxyManager
|
||||||
|
endpointProxyHandler.SettingsService = server.SettingsService
|
||||||
|
endpointProxyHandler.ReverseTunnelService = server.ReverseTunnelService
|
||||||
|
|
||||||
var fileHandler = file.NewHandler(filepath.Join(server.AssetsPath, "public"))
|
var fileHandler = file.NewHandler(filepath.Join(server.AssetsPath, "public"))
|
||||||
|
|
||||||
|
@ -168,6 +174,7 @@ func (server *Server) Start() error {
|
||||||
schedulesHandler.JobService = server.JobService
|
schedulesHandler.JobService = server.JobService
|
||||||
schedulesHandler.JobScheduler = server.JobScheduler
|
schedulesHandler.JobScheduler = server.JobScheduler
|
||||||
schedulesHandler.SettingsService = server.SettingsService
|
schedulesHandler.SettingsService = server.SettingsService
|
||||||
|
schedulesHandler.ReverseTunnelService = server.ReverseTunnelService
|
||||||
|
|
||||||
var settingsHandler = settings.NewHandler(requestBouncer)
|
var settingsHandler = settings.NewHandler(requestBouncer)
|
||||||
settingsHandler.SettingsService = server.SettingsService
|
settingsHandler.SettingsService = server.SettingsService
|
||||||
|
@ -216,6 +223,7 @@ func (server *Server) Start() error {
|
||||||
var websocketHandler = websocket.NewHandler(requestBouncer)
|
var websocketHandler = websocket.NewHandler(requestBouncer)
|
||||||
websocketHandler.EndpointService = server.EndpointService
|
websocketHandler.EndpointService = server.EndpointService
|
||||||
websocketHandler.SignatureService = server.SignatureService
|
websocketHandler.SignatureService = server.SignatureService
|
||||||
|
websocketHandler.ReverseTunnelService = server.ReverseTunnelService
|
||||||
|
|
||||||
var webhookHandler = webhooks.NewHandler(requestBouncer)
|
var webhookHandler = webhooks.NewHandler(requestBouncer)
|
||||||
webhookHandler.WebhookService = server.WebhookService
|
webhookHandler.WebhookService = server.WebhookService
|
||||||
|
|
|
@ -2,6 +2,7 @@ package libcompose
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
@ -18,18 +19,27 @@ import (
|
||||||
// ComposeStackManager represents a service for managing compose stacks.
|
// ComposeStackManager represents a service for managing compose stacks.
|
||||||
type ComposeStackManager struct {
|
type ComposeStackManager struct {
|
||||||
dataPath string
|
dataPath string
|
||||||
|
reverseTunnelService portainer.ReverseTunnelService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewComposeStackManager initializes a new ComposeStackManager service.
|
// NewComposeStackManager initializes a new ComposeStackManager service.
|
||||||
func NewComposeStackManager(dataPath string) *ComposeStackManager {
|
func NewComposeStackManager(dataPath string, reverseTunnelService portainer.ReverseTunnelService) *ComposeStackManager {
|
||||||
return &ComposeStackManager{
|
return &ComposeStackManager{
|
||||||
dataPath: dataPath,
|
dataPath: dataPath,
|
||||||
|
reverseTunnelService: reverseTunnelService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func createClient(endpoint *portainer.Endpoint) (client.Factory, error) {
|
func (manager *ComposeStackManager) createClient(endpoint *portainer.Endpoint) (client.Factory, error) {
|
||||||
|
|
||||||
|
endpointURL := endpoint.URL
|
||||||
|
if endpoint.Type == portainer.EdgeAgentEnvironment {
|
||||||
|
tunnel := manager.reverseTunnelService.GetTunnelDetails(endpoint.ID)
|
||||||
|
endpointURL = fmt.Sprintf("tcp://localhost:%d", tunnel.Port)
|
||||||
|
}
|
||||||
|
|
||||||
clientOpts := client.Options{
|
clientOpts := client.Options{
|
||||||
Host: endpoint.URL,
|
Host: endpointURL,
|
||||||
APIVersion: portainer.SupportedDockerAPIVersion,
|
APIVersion: portainer.SupportedDockerAPIVersion,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,7 +57,7 @@ func createClient(endpoint *portainer.Endpoint) (client.Factory, error) {
|
||||||
// Up will deploy a compose stack (equivalent of docker-compose up)
|
// Up will deploy a compose stack (equivalent of docker-compose up)
|
||||||
func (manager *ComposeStackManager) Up(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
func (manager *ComposeStackManager) Up(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||||
|
|
||||||
clientFactory, err := createClient(endpoint)
|
clientFactory, err := manager.createClient(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -85,7 +95,7 @@ func (manager *ComposeStackManager) Up(stack *portainer.Stack, endpoint *portain
|
||||||
|
|
||||||
// Down will shutdown a compose stack (equivalent of docker-compose down)
|
// Down will shutdown a compose stack (equivalent of docker-compose down)
|
||||||
func (manager *ComposeStackManager) Down(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
func (manager *ComposeStackManager) Down(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||||
clientFactory, err := createClient(endpoint)
|
clientFactory, err := manager.createClient(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package portainer
|
package portainer
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
type (
|
type (
|
||||||
// Pair defines a key/value string pair
|
// Pair defines a key/value string pair
|
||||||
Pair struct {
|
Pair struct {
|
||||||
|
@ -10,6 +12,8 @@ type (
|
||||||
// CLIFlags represents the available flags on the CLI
|
// CLIFlags represents the available flags on the CLI
|
||||||
CLIFlags struct {
|
CLIFlags struct {
|
||||||
Addr *string
|
Addr *string
|
||||||
|
TunnelAddr *string
|
||||||
|
TunnelPort *string
|
||||||
AdminPassword *string
|
AdminPassword *string
|
||||||
AdminPasswordFile *string
|
AdminPasswordFile *string
|
||||||
Assets *string
|
Assets *string
|
||||||
|
@ -105,6 +109,7 @@ type (
|
||||||
SnapshotInterval string `json:"SnapshotInterval"`
|
SnapshotInterval string `json:"SnapshotInterval"`
|
||||||
TemplatesURL string `json:"TemplatesURL"`
|
TemplatesURL string `json:"TemplatesURL"`
|
||||||
EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"`
|
EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"`
|
||||||
|
EdgeAgentCheckinInterval int `json:"EdgeAgentCheckinInterval"`
|
||||||
|
|
||||||
// Deprecated fields
|
// Deprecated fields
|
||||||
DisplayDonationHeader bool
|
DisplayDonationHeader bool
|
||||||
|
@ -250,7 +255,8 @@ type (
|
||||||
Snapshots []Snapshot `json:"Snapshots"`
|
Snapshots []Snapshot `json:"Snapshots"`
|
||||||
UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"`
|
UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"`
|
||||||
TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies"`
|
TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies"`
|
||||||
|
EdgeID string `json:"EdgeID,omitempty"`
|
||||||
|
EdgeKey string `json:"EdgeKey"`
|
||||||
// Deprecated fields
|
// Deprecated fields
|
||||||
// Deprecated in DBVersion == 4
|
// Deprecated in DBVersion == 4
|
||||||
TLS bool `json:"TLS,omitempty"`
|
TLS bool `json:"TLS,omitempty"`
|
||||||
|
@ -333,11 +339,21 @@ type (
|
||||||
Recurring bool
|
Recurring bool
|
||||||
Created int64
|
Created int64
|
||||||
JobType JobType
|
JobType JobType
|
||||||
|
EdgeSchedule *EdgeSchedule
|
||||||
ScriptExecutionJob *ScriptExecutionJob
|
ScriptExecutionJob *ScriptExecutionJob
|
||||||
SnapshotJob *SnapshotJob
|
SnapshotJob *SnapshotJob
|
||||||
EndpointSyncJob *EndpointSyncJob
|
EndpointSyncJob *EndpointSyncJob
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EdgeSchedule represents a scheduled job that can run on Edge environments.
|
||||||
|
EdgeSchedule struct {
|
||||||
|
ID ScheduleID `json:"Id"`
|
||||||
|
CronExpression string `json:"CronExpression"`
|
||||||
|
Script string `json:"Script"`
|
||||||
|
Version int `json:"Version"`
|
||||||
|
Endpoints []EndpointID `json:"Endpoints"`
|
||||||
|
}
|
||||||
|
|
||||||
// WebhookID represents a webhook identifier.
|
// WebhookID represents a webhook identifier.
|
||||||
WebhookID int
|
WebhookID int
|
||||||
|
|
||||||
|
@ -575,6 +591,20 @@ type (
|
||||||
Valid bool `json:"Valid,omitempty"`
|
Valid bool `json:"Valid,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TunnelDetails represents information associated to a tunnel
|
||||||
|
TunnelDetails struct {
|
||||||
|
Status string
|
||||||
|
LastActivity time.Time
|
||||||
|
Port int
|
||||||
|
Schedules []EdgeSchedule
|
||||||
|
Credentials string
|
||||||
|
}
|
||||||
|
|
||||||
|
// TunnelServerInfo represents information associated to the tunnel server
|
||||||
|
TunnelServerInfo struct {
|
||||||
|
PrivateKeySeed string `json:"PrivateKeySeed"`
|
||||||
|
}
|
||||||
|
|
||||||
// CLIService represents a service for managing CLI
|
// CLIService represents a service for managing CLI
|
||||||
CLIService interface {
|
CLIService interface {
|
||||||
ParseFlags(version string) (*CLIFlags, error)
|
ParseFlags(version string) (*CLIFlags, error)
|
||||||
|
@ -692,6 +722,12 @@ type (
|
||||||
StoreDBVersion(version int) error
|
StoreDBVersion(version int) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TunnelServerService represents a service for managing data associated to the tunnel server
|
||||||
|
TunnelServerService interface {
|
||||||
|
Info() (*TunnelServerInfo, error)
|
||||||
|
UpdateInfo(info *TunnelServerInfo) error
|
||||||
|
}
|
||||||
|
|
||||||
// WebhookService represents a service for managing webhook data.
|
// WebhookService represents a service for managing webhook data.
|
||||||
WebhookService interface {
|
WebhookService interface {
|
||||||
Webhooks() ([]Webhook, error)
|
Webhooks() ([]Webhook, error)
|
||||||
|
@ -850,21 +886,35 @@ type (
|
||||||
DisableExtension(extension *Extension) error
|
DisableExtension(extension *Extension) error
|
||||||
UpdateExtension(extension *Extension, version string) error
|
UpdateExtension(extension *Extension, version string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReverseTunnelService represensts a service used to manage reverse tunnel connections.
|
||||||
|
ReverseTunnelService interface {
|
||||||
|
StartTunnelServer(addr, port string, snapshotter Snapshotter) error
|
||||||
|
GenerateEdgeKey(url, host string, endpointIdentifier int) string
|
||||||
|
SetTunnelStatusToActive(endpointID EndpointID)
|
||||||
|
SetTunnelStatusToRequired(endpointID EndpointID) error
|
||||||
|
SetTunnelStatusToIdle(endpointID EndpointID)
|
||||||
|
GetTunnelDetails(endpointID EndpointID) *TunnelDetails
|
||||||
|
AddSchedule(endpointID EndpointID, schedule *EdgeSchedule)
|
||||||
|
RemoveSchedule(scheduleID ScheduleID)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// APIVersion is the version number of the Portainer API
|
// APIVersion is the version number of the Portainer API
|
||||||
APIVersion = "1.21.0"
|
APIVersion = "1.22.0"
|
||||||
// DBVersion is the version number of the Portainer database
|
// DBVersion is the version number of the Portainer database
|
||||||
DBVersion = 18
|
DBVersion = 19
|
||||||
// AssetsServerURL represents the URL of the Portainer asset server
|
// AssetsServerURL represents the URL of the Portainer asset server
|
||||||
AssetsServerURL = "https://portainer-io-assets.sfo2.digitaloceanspaces.com"
|
AssetsServerURL = "https://portainer-io-assets.sfo2.digitaloceanspaces.com"
|
||||||
// MessageOfTheDayURL represents the URL where Portainer MOTD message can be retrieved
|
// MessageOfTheDayURL represents the URL where Portainer MOTD message can be retrieved
|
||||||
MessageOfTheDayURL = AssetsServerURL + "/motd.json"
|
MessageOfTheDayURL = AssetsServerURL + "/motd.json"
|
||||||
// ExtensionDefinitionsURL represents the URL where Portainer extension definitions can be retrieved
|
// ExtensionDefinitionsURL represents the URL where Portainer extension definitions can be retrieved
|
||||||
ExtensionDefinitionsURL = AssetsServerURL + "/extensions-1.21.0.json"
|
ExtensionDefinitionsURL = AssetsServerURL + "/extensions-1.22.0.json"
|
||||||
// PortainerAgentHeader represents the name of the header available in any agent response
|
// PortainerAgentHeader represents the name of the header available in any agent response
|
||||||
PortainerAgentHeader = "Portainer-Agent"
|
PortainerAgentHeader = "Portainer-Agent"
|
||||||
|
// PortainerAgentEdgeIDHeader represent the name of the header containing the Edge ID associated to an agent/agent cluster
|
||||||
|
PortainerAgentEdgeIDHeader = "X-PortainerAgent-EdgeID"
|
||||||
// PortainerAgentTargetHeader represent the name of the header containing the target node name
|
// PortainerAgentTargetHeader represent the name of the header containing the target node name
|
||||||
PortainerAgentTargetHeader = "X-PortainerAgent-Target"
|
PortainerAgentTargetHeader = "X-PortainerAgent-Target"
|
||||||
// PortainerAgentSignatureHeader represent the name of the header containing the digital signature
|
// PortainerAgentSignatureHeader represent the name of the header containing the digital signature
|
||||||
|
@ -876,6 +926,10 @@ const (
|
||||||
PortainerAgentSignatureMessage = "Portainer-App"
|
PortainerAgentSignatureMessage = "Portainer-App"
|
||||||
// SupportedDockerAPIVersion is the minimum Docker API version supported by Portainer
|
// SupportedDockerAPIVersion is the minimum Docker API version supported by Portainer
|
||||||
SupportedDockerAPIVersion = "1.24"
|
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 (
|
const (
|
||||||
|
@ -951,6 +1005,8 @@ const (
|
||||||
AgentOnDockerEnvironment
|
AgentOnDockerEnvironment
|
||||||
// AzureEnvironment represents an endpoint connected to an Azure environment
|
// AzureEnvironment represents an endpoint connected to an Azure environment
|
||||||
AzureEnvironment
|
AzureEnvironment
|
||||||
|
// EdgeAgentEnvironment represents an endpoint connected to an Edge agent
|
||||||
|
EdgeAgentEnvironment
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -1017,6 +1073,15 @@ const (
|
||||||
CustomRegistry
|
CustomRegistry
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// EdgeAgentIdle represents an idle state for a tunnel connected to an Edge endpoint.
|
||||||
|
EdgeAgentIdle string = "IDLE"
|
||||||
|
// EdgeAgentManagementRequired represents a required state for a tunnel connected to an Edge endpoint
|
||||||
|
EdgeAgentManagementRequired string = "REQUIRED"
|
||||||
|
// EdgeAgentActive represents an active state for a tunnel connected to an Edge endpoint
|
||||||
|
EdgeAgentActive string = "ACTIVE"
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
OperationDockerContainerArchiveInfo Authorization = "DockerContainerArchiveInfo"
|
OperationDockerContainerArchiveInfo Authorization = "DockerContainerArchiveInfo"
|
||||||
OperationDockerContainerList Authorization = "DockerContainerList"
|
OperationDockerContainerList Authorization = "DockerContainerList"
|
||||||
|
|
459
api/swagger.yaml
459
api/swagger.yaml
|
@ -54,7 +54,7 @@ info:
|
||||||
|
|
||||||
**NOTE**: You can find more information on how to query the Docker API in the [Docker official documentation](https://docs.docker.com/engine/api/v1.30/) as well as in [this Portainer example](https://gist.github.com/deviantony/77026d402366b4b43fa5918d41bc42f8).
|
**NOTE**: You can find more information on how to query the Docker API in the [Docker official documentation](https://docs.docker.com/engine/api/v1.30/) as well as in [this Portainer example](https://gist.github.com/deviantony/77026d402366b4b43fa5918d41bc42f8).
|
||||||
|
|
||||||
version: "1.21.0"
|
version: "1.22.0"
|
||||||
title: "Portainer API"
|
title: "Portainer API"
|
||||||
contact:
|
contact:
|
||||||
email: "info@portainer.io"
|
email: "info@portainer.io"
|
||||||
|
@ -69,10 +69,14 @@ tags:
|
||||||
description: "Manage Docker environments"
|
description: "Manage Docker environments"
|
||||||
- name: "endpoint_groups"
|
- name: "endpoint_groups"
|
||||||
description: "Manage endpoint groups"
|
description: "Manage endpoint groups"
|
||||||
|
- name: "extensions"
|
||||||
|
description: "Manage extensions"
|
||||||
- name: "registries"
|
- name: "registries"
|
||||||
description: "Manage Docker registries"
|
description: "Manage Docker registries"
|
||||||
- name: "resource_controls"
|
- name: "resource_controls"
|
||||||
description: "Manage access control on Docker resources"
|
description: "Manage access control on Docker resources"
|
||||||
|
- name: "roles"
|
||||||
|
description: "Manage roles"
|
||||||
- name: "settings"
|
- name: "settings"
|
||||||
description: "Manage Portainer settings"
|
description: "Manage Portainer settings"
|
||||||
- name: "status"
|
- name: "status"
|
||||||
|
@ -741,6 +745,285 @@ paths:
|
||||||
examples:
|
examples:
|
||||||
application/json:
|
application/json:
|
||||||
err: "EndpointGroup management is disabled"
|
err: "EndpointGroup management is disabled"
|
||||||
|
/endpoint_groups/{id}/endpoints/{endpointId}:
|
||||||
|
put:
|
||||||
|
tags:
|
||||||
|
- "endpoint_groups"
|
||||||
|
summary: "Add an endpoint to an endpoint group"
|
||||||
|
description: |
|
||||||
|
Add an endpoint to an endpoint group
|
||||||
|
**Access policy**: administrator
|
||||||
|
operationId: "EndpointGroupAddEndpoint"
|
||||||
|
consumes:
|
||||||
|
- "application/json"
|
||||||
|
produces:
|
||||||
|
- "application/json"
|
||||||
|
security:
|
||||||
|
- jwt: []
|
||||||
|
parameters:
|
||||||
|
- name: "id"
|
||||||
|
in: "path"
|
||||||
|
description: "EndpointGroup identifier"
|
||||||
|
required: true
|
||||||
|
type: "integer"
|
||||||
|
- name: "endpointId"
|
||||||
|
in: "path"
|
||||||
|
description: "Endpoint identifier"
|
||||||
|
required: true
|
||||||
|
type: "integer"
|
||||||
|
responses:
|
||||||
|
204:
|
||||||
|
description: "Success"
|
||||||
|
400:
|
||||||
|
description: "Invalid request"
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/GenericError"
|
||||||
|
examples:
|
||||||
|
application/json:
|
||||||
|
err: "Invalid request data format"
|
||||||
|
404:
|
||||||
|
description: "EndpointGroup not found"
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/GenericError"
|
||||||
|
examples:
|
||||||
|
application/json:
|
||||||
|
err: "EndpointGroup not found"
|
||||||
|
500:
|
||||||
|
description: "Server error"
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/GenericError"
|
||||||
|
delete:
|
||||||
|
tags:
|
||||||
|
- "endpoint_groups"
|
||||||
|
summary: "Remove an endpoint group"
|
||||||
|
description: |
|
||||||
|
Remove an endpoint group.
|
||||||
|
**Access policy**: administrator
|
||||||
|
operationId: "EndpointGroupDeleteEndpoint"
|
||||||
|
security:
|
||||||
|
- jwt: []
|
||||||
|
parameters:
|
||||||
|
- name: "id"
|
||||||
|
in: "path"
|
||||||
|
description: "EndpointGroup identifier"
|
||||||
|
required: true
|
||||||
|
type: "integer"
|
||||||
|
- name: "endpointId"
|
||||||
|
in: "path"
|
||||||
|
description: "Endpoint identifier"
|
||||||
|
required: true
|
||||||
|
type: "integer"
|
||||||
|
responses:
|
||||||
|
204:
|
||||||
|
description: "Success"
|
||||||
|
400:
|
||||||
|
description: "Invalid request"
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/GenericError"
|
||||||
|
examples:
|
||||||
|
application/json:
|
||||||
|
err: "Invalid request"
|
||||||
|
404:
|
||||||
|
description: "EndpointGroup not found"
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/GenericError"
|
||||||
|
examples:
|
||||||
|
application/json:
|
||||||
|
err: "EndpointGroup not found"
|
||||||
|
500:
|
||||||
|
description: "Server error"
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/GenericError"
|
||||||
|
/extensions:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- "extensions"
|
||||||
|
summary: "List extensions"
|
||||||
|
description: |
|
||||||
|
List all extensions registered inside Portainer. If the store parameter is set to true,
|
||||||
|
will retrieve extensions details from the online repository.
|
||||||
|
**Access policy**: administrator
|
||||||
|
operationId: "ExtensionList"
|
||||||
|
produces:
|
||||||
|
- "application/json"
|
||||||
|
security:
|
||||||
|
- jwt: []
|
||||||
|
parameters:
|
||||||
|
- name: "store"
|
||||||
|
in: "query"
|
||||||
|
description: "Retrieve online information about extensions. Possible values: true or false."
|
||||||
|
required: false
|
||||||
|
type: "boolean"
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: "Success"
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/ExtensionListResponse"
|
||||||
|
500:
|
||||||
|
description: "Server error"
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/GenericError"
|
||||||
|
post:
|
||||||
|
tags:
|
||||||
|
- "extensions"
|
||||||
|
summary: "Enable an extension"
|
||||||
|
description: |
|
||||||
|
Enable an extension.
|
||||||
|
**Access policy**: administrator
|
||||||
|
operationId: "ExtensionCreate"
|
||||||
|
consumes:
|
||||||
|
- "application/json"
|
||||||
|
produces:
|
||||||
|
- "application/json"
|
||||||
|
security:
|
||||||
|
- jwt: []
|
||||||
|
parameters:
|
||||||
|
- in: "body"
|
||||||
|
name: "body"
|
||||||
|
description: "Extension details"
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/ExtensionCreateRequest"
|
||||||
|
responses:
|
||||||
|
204:
|
||||||
|
description: "Success"
|
||||||
|
400:
|
||||||
|
description: "Invalid request"
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/GenericError"
|
||||||
|
examples:
|
||||||
|
application/json:
|
||||||
|
err: "Invalid request data format"
|
||||||
|
500:
|
||||||
|
description: "Server error"
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/GenericError"
|
||||||
|
/extensions/{id}:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- "extensions"
|
||||||
|
summary: "Inspect an extension"
|
||||||
|
description: |
|
||||||
|
Retrieve details abount an extension.
|
||||||
|
**Access policy**: administrator
|
||||||
|
operationId: "ExtensionInspect"
|
||||||
|
produces:
|
||||||
|
- "application/json"
|
||||||
|
security:
|
||||||
|
- jwt: []
|
||||||
|
parameters:
|
||||||
|
- name: "id"
|
||||||
|
in: "path"
|
||||||
|
description: "extension identifier"
|
||||||
|
required: true
|
||||||
|
type: "integer"
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: "Success"
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/Extension"
|
||||||
|
400:
|
||||||
|
description: "Invalid request"
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/GenericError"
|
||||||
|
examples:
|
||||||
|
application/json:
|
||||||
|
err: "Invalid request"
|
||||||
|
404:
|
||||||
|
description: "Extension not found"
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/GenericError"
|
||||||
|
examples:
|
||||||
|
application/json:
|
||||||
|
err: "Extension not found"
|
||||||
|
500:
|
||||||
|
description: "Server error"
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/GenericError"
|
||||||
|
put:
|
||||||
|
tags:
|
||||||
|
- "extensions"
|
||||||
|
summary: "Update an extension"
|
||||||
|
description: |
|
||||||
|
Update an extension to a specific version of the extension.
|
||||||
|
**Access policy**: administrator
|
||||||
|
operationId: "ExtensionUpdate"
|
||||||
|
consumes:
|
||||||
|
- "application/json"
|
||||||
|
produces:
|
||||||
|
- "application/json"
|
||||||
|
security:
|
||||||
|
- jwt: []
|
||||||
|
parameters:
|
||||||
|
- name: "id"
|
||||||
|
in: "path"
|
||||||
|
description: "Extension identifier"
|
||||||
|
required: true
|
||||||
|
type: "integer"
|
||||||
|
- in: "body"
|
||||||
|
name: "body"
|
||||||
|
description: "Extension details"
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/ExtensionUpdateRequest"
|
||||||
|
responses:
|
||||||
|
204:
|
||||||
|
description: "Success"
|
||||||
|
400:
|
||||||
|
description: "Invalid request"
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/GenericError"
|
||||||
|
examples:
|
||||||
|
application/json:
|
||||||
|
err: "Invalid request data format"
|
||||||
|
404:
|
||||||
|
description: "Extension not found"
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/GenericError"
|
||||||
|
examples:
|
||||||
|
application/json:
|
||||||
|
err: "Extension not found"
|
||||||
|
500:
|
||||||
|
description: "Server error"
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/GenericError"
|
||||||
|
delete:
|
||||||
|
tags:
|
||||||
|
- "extensions"
|
||||||
|
summary: "Disable an extension"
|
||||||
|
description: |
|
||||||
|
Disable an extension.
|
||||||
|
**Access policy**: administrator
|
||||||
|
operationId: "ExtensionDelete"
|
||||||
|
security:
|
||||||
|
- jwt: []
|
||||||
|
parameters:
|
||||||
|
- name: "id"
|
||||||
|
in: "path"
|
||||||
|
description: "Extension identifier"
|
||||||
|
required: true
|
||||||
|
type: "integer"
|
||||||
|
responses:
|
||||||
|
204:
|
||||||
|
description: "Success"
|
||||||
|
400:
|
||||||
|
description: "Invalid request"
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/GenericError"
|
||||||
|
examples:
|
||||||
|
application/json:
|
||||||
|
err: "Invalid request"
|
||||||
|
404:
|
||||||
|
description: "Extension not found"
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/GenericError"
|
||||||
|
examples:
|
||||||
|
application/json:
|
||||||
|
err: "Extension not found"
|
||||||
|
500:
|
||||||
|
description: "Server error"
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/GenericError"
|
||||||
/registries:
|
/registries:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
|
@ -1098,6 +1381,29 @@ paths:
|
||||||
description: "Server error"
|
description: "Server error"
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/definitions/GenericError"
|
$ref: "#/definitions/GenericError"
|
||||||
|
/roles:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- "roles"
|
||||||
|
summary: "List roles"
|
||||||
|
description: |
|
||||||
|
List all roles available for use with the RBAC extension.
|
||||||
|
**Access policy**: administrator
|
||||||
|
operationId: "RoleList"
|
||||||
|
produces:
|
||||||
|
- "application/json"
|
||||||
|
security:
|
||||||
|
- jwt: []
|
||||||
|
parameters: []
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: "Success"
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/RoleListResponse"
|
||||||
|
500:
|
||||||
|
description: "Server error"
|
||||||
|
schema:
|
||||||
|
$ref: "#/definitions/GenericError"
|
||||||
/settings:
|
/settings:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
|
@ -2868,7 +3174,7 @@ definitions:
|
||||||
description: "Is analytics enabled"
|
description: "Is analytics enabled"
|
||||||
Version:
|
Version:
|
||||||
type: "string"
|
type: "string"
|
||||||
example: "1.21.0"
|
example: "1.22.0"
|
||||||
description: "Portainer API version"
|
description: "Portainer API version"
|
||||||
PublicSettingsInspectResponse:
|
PublicSettingsInspectResponse:
|
||||||
type: "object"
|
type: "object"
|
||||||
|
@ -3649,17 +3955,13 @@ definitions:
|
||||||
type: "string"
|
type: "string"
|
||||||
example: "Endpoint group description"
|
example: "Endpoint group description"
|
||||||
description: "Endpoint group description"
|
description: "Endpoint group description"
|
||||||
Labels:
|
Tags:
|
||||||
type: "array"
|
type: "array"
|
||||||
|
description: "List of tags associated to the endpoint group"
|
||||||
items:
|
items:
|
||||||
$ref: "#/definitions/Pair"
|
type: "string"
|
||||||
AssociatedEndpoints:
|
example: "zone/east-coast"
|
||||||
type: "array"
|
description: "Tag"
|
||||||
description: "List of endpoint identifiers that will be part of this group"
|
|
||||||
items:
|
|
||||||
type: "integer"
|
|
||||||
example: 1
|
|
||||||
description: "Endpoint identifier"
|
|
||||||
UserAccessPolicies:
|
UserAccessPolicies:
|
||||||
$ref: "#/definitions/UserAccessPolicies"
|
$ref: "#/definitions/UserAccessPolicies"
|
||||||
TeamAccessPolicies:
|
TeamAccessPolicies:
|
||||||
|
@ -4335,3 +4637,138 @@ definitions:
|
||||||
type: "string"
|
type: "string"
|
||||||
example: "version: 3\n services:\n web:\n image:nginx"
|
example: "version: 3\n services:\n web:\n image:nginx"
|
||||||
description: "Content of the Stack file."
|
description: "Content of the Stack file."
|
||||||
|
LicenseInformation:
|
||||||
|
type: "object"
|
||||||
|
properties:
|
||||||
|
LicenseKey:
|
||||||
|
type: "string"
|
||||||
|
description: "License key"
|
||||||
|
example: "1-uKmVwboSWVIZv5URmE0VRkpbPX0rrCVeDxJl97LZ0piltw2SU28DSrNwPZAHCEAwB2SeKm6BCFcVwzGMBEixKQ"
|
||||||
|
Company:
|
||||||
|
type: "string"
|
||||||
|
description: "Company associated to the license"
|
||||||
|
example: "Portainer.io"
|
||||||
|
Expiration:
|
||||||
|
type: "string"
|
||||||
|
description: "License expiry date"
|
||||||
|
example: "2077-07-07"
|
||||||
|
Valid:
|
||||||
|
type: "boolean"
|
||||||
|
description: "Is the license valid"
|
||||||
|
example: "true"
|
||||||
|
Extension:
|
||||||
|
type: "object"
|
||||||
|
properties:
|
||||||
|
Id:
|
||||||
|
type: "integer"
|
||||||
|
example: 1
|
||||||
|
description: "Extension identifier"
|
||||||
|
Name:
|
||||||
|
type: "string"
|
||||||
|
example: "Registry Manager"
|
||||||
|
description: "Extension name"
|
||||||
|
Enabled:
|
||||||
|
type: "boolean"
|
||||||
|
example: "true"
|
||||||
|
description: "Is the extension enabled"
|
||||||
|
ShortDescription:
|
||||||
|
type: "string"
|
||||||
|
description: "Short description about the extension"
|
||||||
|
example: "Enable in-app registry management"
|
||||||
|
DescriptionURL:
|
||||||
|
type: "string"
|
||||||
|
description: "URL to the file containing the extension description"
|
||||||
|
example: https://portainer-io-assets.sfo2.digitaloceanspaces.com/description_registry_manager.html"
|
||||||
|
Available:
|
||||||
|
type: "boolean"
|
||||||
|
description: "Is the extension available for download and activation"
|
||||||
|
example: "true"
|
||||||
|
Images:
|
||||||
|
type: "array"
|
||||||
|
description: "List of screenshot URLs"
|
||||||
|
items:
|
||||||
|
type: "string"
|
||||||
|
example: "https://portainer-io-assets.sfo2.digitaloceanspaces.com/screenshots/rm01.png"
|
||||||
|
description: "Screenshot URL"
|
||||||
|
Logo:
|
||||||
|
type: "string"
|
||||||
|
description: "Icon associated to the extension"
|
||||||
|
example: "fa-database"
|
||||||
|
Price:
|
||||||
|
type: "string"
|
||||||
|
description: "Extension price"
|
||||||
|
example: "US$9.95"
|
||||||
|
PriceDescription:
|
||||||
|
type: "string"
|
||||||
|
description: "Details about extension pricing"
|
||||||
|
example: "Price per instance per year"
|
||||||
|
ShopURL:
|
||||||
|
type: "string"
|
||||||
|
description: "URL used to buy the extension"
|
||||||
|
example: "https://portainer.io/checkout/?add-to-cart=1164"
|
||||||
|
UpdateAvailable:
|
||||||
|
type: "boolean"
|
||||||
|
description: "Is an update available for this extension"
|
||||||
|
example: "true"
|
||||||
|
Version:
|
||||||
|
type: "string"
|
||||||
|
description: "Extension version"
|
||||||
|
example: "1.0.0"
|
||||||
|
License:
|
||||||
|
$ref: "#/definitions/LicenseInformation"
|
||||||
|
ExtensionListResponse:
|
||||||
|
type: "array"
|
||||||
|
items:
|
||||||
|
$ref: "#/definitions/Extension"
|
||||||
|
ExtensionCreateRequest:
|
||||||
|
type: "object"
|
||||||
|
required:
|
||||||
|
- "License"
|
||||||
|
properties:
|
||||||
|
License:
|
||||||
|
type: "string"
|
||||||
|
example: "1-uKmVwboSWVIZv5URmE0VRkpbPX0rrCVeDxJl97LZ0piltw2SU28DSrNwPZAHCEAwB2SeKm6BCFcVwzGMBEixKQ"
|
||||||
|
description: "License key"
|
||||||
|
ExtensionUpdateRequest:
|
||||||
|
type: "object"
|
||||||
|
required:
|
||||||
|
- "Version"
|
||||||
|
properties:
|
||||||
|
Version:
|
||||||
|
type: "string"
|
||||||
|
example: "1.1.0"
|
||||||
|
description: "New version of the extension"
|
||||||
|
RoleListResponse:
|
||||||
|
type: "array"
|
||||||
|
items:
|
||||||
|
$ref: "#/definitions/Role"
|
||||||
|
Role:
|
||||||
|
type: "object"
|
||||||
|
properties:
|
||||||
|
Id:
|
||||||
|
type: "integer"
|
||||||
|
description: "Role identifier"
|
||||||
|
example: 2
|
||||||
|
Name:
|
||||||
|
type: "string"
|
||||||
|
description: "Role name"
|
||||||
|
example: "HelpDesk"
|
||||||
|
Description:
|
||||||
|
type: "string"
|
||||||
|
description: "Role description"
|
||||||
|
example: "Read-only access of all resources in an endpoint"
|
||||||
|
Authorizations:
|
||||||
|
$ref: "#/definitions/Authorizations"
|
||||||
|
Authorizations:
|
||||||
|
type: "object"
|
||||||
|
description: "Authorizations associated to a role"
|
||||||
|
additionalProperties:
|
||||||
|
type: "object"
|
||||||
|
properties:
|
||||||
|
authorization:
|
||||||
|
type: "string"
|
||||||
|
value:
|
||||||
|
type: "boolean"
|
||||||
|
example:
|
||||||
|
"DockerContainerList": true
|
||||||
|
"DockerVolumeList": true
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"packageName": "portainer",
|
"packageName": "portainer",
|
||||||
"packageVersion": "1.21.0",
|
"packageVersion": "1.22.0",
|
||||||
"projectName": "portainer"
|
"projectName": "portainer"
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ import angular from 'angular';
|
||||||
import './agent/_module';
|
import './agent/_module';
|
||||||
import './azure/_module';
|
import './azure/_module';
|
||||||
import './docker/__module';
|
import './docker/__module';
|
||||||
import './extensions/storidge/__module';
|
|
||||||
import './portainer/__module';
|
import './portainer/__module';
|
||||||
|
|
||||||
angular.module('portainer', [
|
angular.module('portainer', [
|
||||||
|
@ -31,7 +30,7 @@ angular.module('portainer', [
|
||||||
'portainer.azure',
|
'portainer.azure',
|
||||||
'portainer.docker',
|
'portainer.docker',
|
||||||
'portainer.extensions',
|
'portainer.extensions',
|
||||||
'extension.storidge',
|
'portainer.integrations',
|
||||||
'rzModule',
|
'rzModule',
|
||||||
'moment-picker'
|
'moment-picker'
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -42,7 +42,7 @@ function (HttpRequestHelper, VolumeBrowserService, FileSaver, Blob, ModalService
|
||||||
var filePath = this.state.path === '/' ? file : this.state.path + '/' + file;
|
var filePath = this.state.path === '/' ? file : this.state.path + '/' + file;
|
||||||
VolumeBrowserService.get(this.volumeId, filePath)
|
VolumeBrowserService.get(this.volumeId, filePath)
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
var downloadData = new Blob([data.file], { type: 'text/plain;charset=utf-8' });
|
var downloadData = new Blob([data.file]);
|
||||||
FileSaver.saveAs(downloadData, file);
|
FileSaver.saveAs(downloadData, file);
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
|
|
|
@ -14,7 +14,8 @@ angular.module('portainer.agent')
|
||||||
},
|
},
|
||||||
get: {
|
get: {
|
||||||
method: 'GET', params: { action: 'get' },
|
method: 'GET', params: { action: 'get' },
|
||||||
transformResponse: browseGetResponse
|
transformResponse: browseGetResponse,
|
||||||
|
responseType: 'arraybuffer'
|
||||||
},
|
},
|
||||||
delete: {
|
delete: {
|
||||||
method: 'DELETE', params: { action: 'delete' }
|
method: 'DELETE', params: { action: 'delete' }
|
||||||
|
|
|
@ -12,7 +12,8 @@ angular.module('portainer.agent')
|
||||||
},
|
},
|
||||||
get: {
|
get: {
|
||||||
method: 'GET', params: { action: 'get' },
|
method: 'GET', params: { action: 'get' },
|
||||||
transformResponse: browseGetResponse
|
transformResponse: browseGetResponse,
|
||||||
|
responseType: 'arraybuffer'
|
||||||
},
|
},
|
||||||
delete: {
|
delete: {
|
||||||
method: 'DELETE', params: { action: 'delete' }
|
method: 'DELETE', params: { action: 'delete' }
|
||||||
|
|
18
app/app.js
18
app/app.js
|
@ -1,8 +1,8 @@
|
||||||
import _ from 'lodash-es';
|
import _ from 'lodash-es';
|
||||||
|
|
||||||
angular.module('portainer')
|
angular.module('portainer')
|
||||||
.run(['$rootScope', '$state', 'Authentication', 'authManager', 'StateManager', 'EndpointProvider', 'Notifications', 'Analytics', 'cfpLoadingBar', '$transitions', 'HttpRequestHelper',
|
.run(['$rootScope', '$state', '$interval', 'Authentication', 'authManager', 'StateManager', 'EndpointProvider', 'Notifications', 'Analytics', 'SystemService', 'cfpLoadingBar', '$transitions', 'HttpRequestHelper',
|
||||||
function ($rootScope, $state, Authentication, authManager, StateManager, EndpointProvider, Notifications, Analytics, cfpLoadingBar, $transitions, HttpRequestHelper) {
|
function ($rootScope, $state, $interval, Authentication, authManager, StateManager, EndpointProvider, Notifications, Analytics, SystemService, cfpLoadingBar, $transitions, HttpRequestHelper) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
EndpointProvider.initialize();
|
EndpointProvider.initialize();
|
||||||
|
@ -31,11 +31,23 @@ function ($rootScope, $state, Authentication, authManager, StateManager, Endpoin
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
$transitions.onBefore({ to: 'docker.**' }, function() {
|
$transitions.onBefore({}, function() {
|
||||||
HttpRequestHelper.resetAgentHeaders();
|
HttpRequestHelper.resetAgentHeaders();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Keep-alive Edge endpoints by sending a ping request every minute
|
||||||
|
$interval(function() {
|
||||||
|
ping(EndpointProvider, SystemService);
|
||||||
|
}, 60 * 1000)
|
||||||
|
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
|
function ping(EndpointProvider, SystemService) {
|
||||||
|
let endpoint = EndpointProvider.currentEndpoint();
|
||||||
|
if (endpoint !== undefined && endpoint.Type === 4) {
|
||||||
|
SystemService.ping(endpoint.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function initAuthentication(authManager, Authentication, $rootScope, $state) {
|
function initAuthentication(authManager, Authentication, $rootScope, $state) {
|
||||||
authManager.checkAuthOnRefresh();
|
authManager.checkAuthOnRefresh();
|
||||||
|
|
|
@ -50,7 +50,7 @@
|
||||||
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
|
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
|
||||||
<td>
|
<td>
|
||||||
<span class="md-checkbox">
|
<span class="md-checkbox">
|
||||||
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
|
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-click="$ctrl.selectItem(item, $event)"/>
|
||||||
<label for="select_{{ $index }}"></label>
|
<label for="select_{{ $index }}"></label>
|
||||||
</span>
|
</span>
|
||||||
<a ui-sref="azure.containerinstances.container({ id: item.Id })">{{ item.Name | truncate:50 }}</a>
|
<a ui-sref="azure.containerinstances.container({ id: item.Id })">{{ item.Name | truncate:50 }}</a>
|
||||||
|
|
|
@ -5,6 +5,44 @@
|
||||||
<div class="toolBarTitle">
|
<div class="toolBarTitle">
|
||||||
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }}
|
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="settings">
|
||||||
|
<span class="setting" ng-class="{ 'setting-active': $ctrl.settings.open }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.settings.open">
|
||||||
|
<span uib-dropdown-toggle><i class="fa fa-cog" aria-hidden="true"></i> Settings</span>
|
||||||
|
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
|
||||||
|
<div class="tableMenu">
|
||||||
|
<div class="menuHeader">
|
||||||
|
Table settings
|
||||||
|
</div>
|
||||||
|
<div class="menuContent">
|
||||||
|
<div>
|
||||||
|
<div class="md-checkbox">
|
||||||
|
<input id="setting_auto_refresh" type="checkbox" ng-model="$ctrl.settings.repeater.autoRefresh" ng-change="$ctrl.onSettingsRepeaterChange()"/>
|
||||||
|
<label for="setting_auto_refresh">Auto refresh</label>
|
||||||
|
</div>
|
||||||
|
<div ng-if="$ctrl.settings.repeater.autoRefresh">
|
||||||
|
<label for="settings_refresh_rate">
|
||||||
|
Refresh rate
|
||||||
|
</label>
|
||||||
|
<select id="settings_refresh_rate" ng-model="$ctrl.settings.repeater.refreshRate" ng-change="$ctrl.onSettingsRepeaterChange()" class="small-select">
|
||||||
|
<option value="10">10s</option>
|
||||||
|
<option value="30">30s</option>
|
||||||
|
<option value="60">1min</option>
|
||||||
|
<option value="120">2min</option>
|
||||||
|
<option value="300">5min</option>
|
||||||
|
</select>
|
||||||
|
<span>
|
||||||
|
<i id="refreshRateChange" class="fa fa-check green-icon" aria-hidden="true" style="margin-top: 7px; display: none;"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.settings.open = false;">Close</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="actionBar" authorization="DockerConfigDelete, DockerConfigCreate">
|
<div class="actionBar" authorization="DockerConfigDelete, DockerConfigCreate">
|
||||||
<button type="button" class="btn btn-sm btn-danger" authorization="DockerConfigDelete"
|
<button type="button" class="btn btn-sm btn-danger" authorization="DockerConfigDelete"
|
||||||
|
@ -54,7 +92,7 @@
|
||||||
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
|
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
|
||||||
<td>
|
<td>
|
||||||
<span class="md-checkbox" authorization="DockerConfigDelete, DockerConfigCreate">
|
<span class="md-checkbox" authorization="DockerConfigDelete, DockerConfigCreate">
|
||||||
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
|
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-click="$ctrl.selectItem(item, $event)"/>
|
||||||
<label for="select_{{ $index }}"></label>
|
<label for="select_{{ $index }}"></label>
|
||||||
</span>
|
</span>
|
||||||
<a ui-sref="docker.configs.config({id: item.Id})">{{ item.Name }}</a>
|
<a ui-sref="docker.configs.config({id: item.Id})">{{ item.Name }}</a>
|
||||||
|
|
|
@ -9,6 +9,7 @@ angular.module('portainer.docker').component('configsDatatable', {
|
||||||
orderBy: '@',
|
orderBy: '@',
|
||||||
reverseOrder: '<',
|
reverseOrder: '<',
|
||||||
showOwnershipColumn: '<',
|
showOwnershipColumn: '<',
|
||||||
removeAction: '<'
|
removeAction: '<',
|
||||||
|
refreshCallback: '<'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -69,6 +69,27 @@
|
||||||
<input id="setting_container_trunc" type="checkbox" ng-model="$ctrl.settings.truncateContainerName" ng-change="$ctrl.onSettingsContainerNameTruncateChange()"/>
|
<input id="setting_container_trunc" type="checkbox" ng-model="$ctrl.settings.truncateContainerName" ng-change="$ctrl.onSettingsContainerNameTruncateChange()"/>
|
||||||
<label for="setting_container_trunc">Truncate container name</label>
|
<label for="setting_container_trunc">Truncate container name</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="md-checkbox">
|
||||||
|
<input id="setting_auto_refresh" type="checkbox" ng-model="$ctrl.settings.repeater.autoRefresh" ng-change="$ctrl.onSettingsRepeaterChange()"/>
|
||||||
|
<label for="setting_auto_refresh">Auto refresh</label>
|
||||||
|
</div>
|
||||||
|
<div ng-if="$ctrl.settings.repeater.autoRefresh">
|
||||||
|
<label for="settings_refresh_rate">
|
||||||
|
Refresh rate
|
||||||
|
</label>
|
||||||
|
<select id="settings_refresh_rate" ng-model="$ctrl.settings.repeater.refreshRate" ng-change="$ctrl.onSettingsRepeaterChange()" class="small-select">
|
||||||
|
<option value="10">10s</option>
|
||||||
|
<option value="30">30s</option>
|
||||||
|
<option value="60">1min</option>
|
||||||
|
<option value="120">2min</option>
|
||||||
|
<option value="300">5min</option>
|
||||||
|
</select>
|
||||||
|
<span>
|
||||||
|
<i id="refreshRateChange" class="fa fa-check green-icon" aria-hidden="true" style="margin-top: 7px; display: none;"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div authorization="DockerContainerStats, DockerContainerLogs, DockerExecStart, DockerContainerInspect, DockerTaskInspect, DockerTaskLogs, DockerContainerAttach">
|
<div authorization="DockerContainerStats, DockerContainerLogs, DockerExecStart, DockerContainerInspect, DockerTaskInspect, DockerTaskLogs, DockerContainerAttach">
|
||||||
<div class="menuHeader">
|
<div class="menuHeader">
|
||||||
|
@ -217,7 +238,7 @@
|
||||||
<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))" ng-class="{active: item.Checked}">
|
<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))" ng-class="{active: item.Checked}">
|
||||||
<td>
|
<td>
|
||||||
<span class="md-checkbox" ng-if="!$ctrl.offlineMode" authorization="DockerContainerStart, DockerContainerStop, DockerContainerKill, DockerContainerRestart, DockerContainerPause, DockerContainerUnpause, DockerContainerDelete, DockerContainerCreate">
|
<span class="md-checkbox" ng-if="!$ctrl.offlineMode" authorization="DockerContainerStart, DockerContainerStop, DockerContainerKill, DockerContainerRestart, DockerContainerPause, DockerContainerUnpause, DockerContainerDelete, DockerContainerCreate">
|
||||||
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
|
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-click="$ctrl.selectItem(item, $event)"/>
|
||||||
<label for="select_{{ $index }}"></label>
|
<label for="select_{{ $index }}"></label>
|
||||||
</span>
|
</span>
|
||||||
<a ng-if="!$ctrl.offlineMode" ui-sref="docker.containers.container({ id: item.Id, nodeName: item.NodeName })" title="{{ item | containername }}">{{ item | containername | truncate: $ctrl.settings.containerNameTruncateSize }}</a>
|
<a ng-if="!$ctrl.offlineMode" ui-sref="docker.containers.container({ id: item.Id, nodeName: item.NodeName })" title="{{ item | containername }}">{{ item | containername | truncate: $ctrl.settings.containerNameTruncateSize }}</a>
|
||||||
|
|
|
@ -11,6 +11,7 @@ angular.module('portainer.docker').component('containersDatatable', {
|
||||||
showOwnershipColumn: '<',
|
showOwnershipColumn: '<',
|
||||||
showHostColumn: '<',
|
showHostColumn: '<',
|
||||||
showAddAction: '<',
|
showAddAction: '<',
|
||||||
offlineMode: '<'
|
offlineMode: '<',
|
||||||
|
refreshCallback: '<'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,25 +1,21 @@
|
||||||
import _ from 'lodash-es';
|
import _ from 'lodash-es';
|
||||||
|
|
||||||
angular.module('portainer.docker')
|
angular.module('portainer.docker')
|
||||||
.controller('ContainersDatatableController', ['PaginationService', 'DatatableService', 'EndpointProvider',
|
.controller('ContainersDatatableController', ['$scope', '$controller', 'DatatableService', 'EndpointProvider',
|
||||||
function (PaginationService, DatatableService, EndpointProvider) {
|
function ($scope, $controller, DatatableService, EndpointProvider) {
|
||||||
|
|
||||||
|
angular.extend(this, $controller('GenericDatatableController', {$scope: $scope}));
|
||||||
|
|
||||||
var ctrl = this;
|
var ctrl = this;
|
||||||
|
|
||||||
this.state = {
|
this.state = Object.assign(this.state, {
|
||||||
selectAll: false,
|
|
||||||
orderBy: this.orderBy,
|
|
||||||
paginatedItemLimit: PaginationService.getPaginationLimit(this.tableKey),
|
|
||||||
displayTextFilter: false,
|
|
||||||
selectedItemCount: 0,
|
|
||||||
selectedItems: [],
|
|
||||||
noStoppedItemsSelected: true,
|
noStoppedItemsSelected: true,
|
||||||
noRunningItemsSelected: true,
|
noRunningItemsSelected: true,
|
||||||
noPausedItemsSelected: true,
|
noPausedItemsSelected: true,
|
||||||
publicURL: EndpointProvider.endpointPublicURL()
|
publicURL: EndpointProvider.endpointPublicURL()
|
||||||
};
|
});
|
||||||
|
|
||||||
this.settings = {
|
this.settings = Object.assign(this.settings, {
|
||||||
open: false,
|
|
||||||
truncateContainerName: true,
|
truncateContainerName: true,
|
||||||
containerNameTruncateSize: 32,
|
containerNameTruncateSize: 32,
|
||||||
showQuickActionStats: true,
|
showQuickActionStats: true,
|
||||||
|
@ -27,7 +23,7 @@ function (PaginationService, DatatableService, EndpointProvider) {
|
||||||
showQuickActionExec: true,
|
showQuickActionExec: true,
|
||||||
showQuickActionInspect: true,
|
showQuickActionInspect: true,
|
||||||
showQuickActionAttach: false
|
showQuickActionAttach: false
|
||||||
};
|
});
|
||||||
|
|
||||||
this.filters = {
|
this.filters = {
|
||||||
state: {
|
state: {
|
||||||
|
@ -81,45 +77,13 @@ function (PaginationService, DatatableService, EndpointProvider) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.onTextFilterChange = function() {
|
|
||||||
DatatableService.setDataTableTextFilters(this.tableKey, this.state.textFilter);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.onColumnVisibilityChange = function() {
|
this.onColumnVisibilityChange = function() {
|
||||||
DatatableService.setColumnVisibilitySettings(this.tableKey, this.columnVisibility);
|
DatatableService.setColumnVisibilitySettings(this.tableKey, this.columnVisibility);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.changeOrderBy = function(orderField) {
|
this.onSelectionChanged = function() {
|
||||||
this.state.reverseOrder = this.state.orderBy === orderField ? !this.state.reverseOrder : false;
|
|
||||||
this.state.orderBy = orderField;
|
|
||||||
DatatableService.setDataTableOrder(this.tableKey, orderField, this.state.reverseOrder);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.toggleItemSelection = function(item) {
|
|
||||||
if (item.Checked) {
|
|
||||||
this.state.selectedItemCount++;
|
|
||||||
this.state.selectedItems.push(item);
|
|
||||||
} else {
|
|
||||||
this.state.selectedItems.splice(this.state.selectedItems.indexOf(item), 1);
|
|
||||||
this.state.selectedItemCount--;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.selectItem = function(item) {
|
|
||||||
this.toggleItemSelection(item);
|
|
||||||
this.updateSelectionState();
|
this.updateSelectionState();
|
||||||
};
|
|
||||||
|
|
||||||
this.selectAll = function() {
|
|
||||||
for (var i = 0; i < this.state.filteredDataSet.length; i++) {
|
|
||||||
var item = this.state.filteredDataSet[i];
|
|
||||||
if (item.Checked !== this.state.selectAll) {
|
|
||||||
item.Checked = this.state.selectAll;
|
|
||||||
this.toggleItemSelection(item);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
this.updateSelectionState();
|
|
||||||
};
|
|
||||||
|
|
||||||
this.updateSelectionState = function() {
|
this.updateSelectionState = function() {
|
||||||
this.state.noStoppedItemsSelected = true;
|
this.state.noStoppedItemsSelected = true;
|
||||||
|
@ -144,10 +108,6 @@ function (PaginationService, DatatableService, EndpointProvider) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.changePaginationLimit = function() {
|
|
||||||
PaginationService.setPaginationLimit(this.tableKey, this.state.paginatedItemLimit);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.applyFilters = function(value) {
|
this.applyFilters = function(value) {
|
||||||
var container = value;
|
var container = value;
|
||||||
var filters = ctrl.filters;
|
var filters = ctrl.filters;
|
||||||
|
@ -209,40 +169,40 @@ function (PaginationService, DatatableService, EndpointProvider) {
|
||||||
};
|
};
|
||||||
|
|
||||||
this.$onInit = function() {
|
this.$onInit = function() {
|
||||||
setDefaults(this);
|
this.setDefaults();
|
||||||
this.prepareTableFromDataset();
|
this.prepareTableFromDataset();
|
||||||
|
|
||||||
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
|
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
|
||||||
if (storedOrder !== null) {
|
if (storedOrder !== null) {
|
||||||
this.state.reverseOrder = storedOrder.reverse;
|
this.state.reverseOrder = storedOrder.reverse;
|
||||||
this.state.orderBy = storedOrder.orderBy;
|
this.state.orderBy = storedOrder.orderBy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var textFilter = DatatableService.getDataTableTextFilters(this.tableKey);
|
||||||
|
if (textFilter !== null) {
|
||||||
|
this.state.textFilter = textFilter;
|
||||||
|
this.onTextFilterChange();
|
||||||
|
}
|
||||||
|
|
||||||
var storedFilters = DatatableService.getDataTableFilters(this.tableKey);
|
var storedFilters = DatatableService.getDataTableFilters(this.tableKey);
|
||||||
if (storedFilters !== null) {
|
if (storedFilters !== null) {
|
||||||
|
this.filters = storedFilters;
|
||||||
|
this.filters.state.open = false;
|
||||||
this.updateStoredFilters(storedFilters.state.values);
|
this.updateStoredFilters(storedFilters.state.values);
|
||||||
}
|
}
|
||||||
this.filters.state.open = false;
|
|
||||||
|
|
||||||
var storedSettings = DatatableService.getDataTableSettings(this.tableKey);
|
var storedSettings = DatatableService.getDataTableSettings(this.tableKey);
|
||||||
if (storedSettings !== null) {
|
if (storedSettings !== null) {
|
||||||
this.settings = storedSettings;
|
this.settings = storedSettings;
|
||||||
}
|
|
||||||
this.settings.open = false;
|
this.settings.open = false;
|
||||||
|
}
|
||||||
|
this.onSettingsRepeaterChange();
|
||||||
|
|
||||||
var storedColumnVisibility = DatatableService.getColumnVisibilitySettings(this.tableKey);
|
var storedColumnVisibility = DatatableService.getColumnVisibilitySettings(this.tableKey);
|
||||||
if (storedColumnVisibility !== null) {
|
if (storedColumnVisibility !== null) {
|
||||||
this.columnVisibility = storedColumnVisibility;
|
this.columnVisibility = storedColumnVisibility;
|
||||||
}
|
|
||||||
this.columnVisibility.state.open = false;
|
this.columnVisibility.state.open = false;
|
||||||
|
|
||||||
var textFilter = DatatableService.getDataTableTextFilters(this.tableKey);
|
|
||||||
if (textFilter !== null) {
|
|
||||||
this.state.textFilter = textFilter;
|
|
||||||
}
|
}
|
||||||
|
this.state.orderBy = this.orderBy;
|
||||||
};
|
};
|
||||||
|
|
||||||
function setDefaults(ctrl) {
|
|
||||||
ctrl.showTextFilter = ctrl.showTextFilter ? ctrl.showTextFilter : false;
|
|
||||||
ctrl.state.reverseOrder = ctrl.reverseOrder ? ctrl.reverseOrder : false;
|
|
||||||
}
|
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -1,15 +1,12 @@
|
||||||
import _ from 'lodash-es';
|
import _ from 'lodash-es';
|
||||||
|
|
||||||
angular.module('portainer.docker')
|
angular.module('portainer.docker')
|
||||||
.controller('JobsDatatableController', ['$q', '$state', 'PaginationService', 'DatatableService', 'ContainerService', 'ModalService', 'Notifications',
|
.controller('JobsDatatableController', ['$scope', '$controller', '$q', '$state', 'PaginationService', 'DatatableService', 'ContainerService', 'ModalService', 'Notifications',
|
||||||
function ($q, $state, PaginationService, DatatableService, ContainerService, ModalService, Notifications) {
|
function ($scope, $controller, $q, $state, PaginationService, DatatableService, ContainerService, ModalService, Notifications) {
|
||||||
var ctrl = this;
|
|
||||||
|
|
||||||
this.state = {
|
angular.extend(this, $controller('GenericDatatableController', {$scope: $scope}));
|
||||||
orderBy: this.orderBy,
|
|
||||||
paginatedItemLimit: PaginationService.getPaginationLimit(this.tableKey),
|
var ctrl = this;
|
||||||
displayTextFilter: false
|
|
||||||
};
|
|
||||||
|
|
||||||
this.filters = {
|
this.filters = {
|
||||||
state: {
|
state: {
|
||||||
|
@ -19,20 +16,6 @@ angular.module('portainer.docker')
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.onTextFilterChange = function() {
|
|
||||||
DatatableService.setDataTableTextFilters(this.tableKey, this.state.textFilter);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.changeOrderBy = function (orderField) {
|
|
||||||
this.state.reverseOrder = this.state.orderBy === orderField ? !this.state.reverseOrder : false;
|
|
||||||
this.state.orderBy = orderField;
|
|
||||||
DatatableService.setDataTableOrder(this.tableKey, orderField, this.state.reverseOrder);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.changePaginationLimit = function () {
|
|
||||||
PaginationService.setPaginationLimit(this.tableKey, this.state.paginatedItemLimit);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.applyFilters = function (value) {
|
this.applyFilters = function (value) {
|
||||||
var container = value;
|
var container = value;
|
||||||
var filters = ctrl.filters;
|
var filters = ctrl.filters;
|
||||||
|
@ -122,7 +105,7 @@ angular.module('portainer.docker')
|
||||||
};
|
};
|
||||||
|
|
||||||
this.$onInit = function() {
|
this.$onInit = function() {
|
||||||
setDefaults(this);
|
this.setDefaults();
|
||||||
this.prepareTableFromDataset();
|
this.prepareTableFromDataset();
|
||||||
|
|
||||||
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
|
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
|
||||||
|
@ -131,21 +114,28 @@ angular.module('portainer.docker')
|
||||||
this.state.orderBy = storedOrder.orderBy;
|
this.state.orderBy = storedOrder.orderBy;
|
||||||
}
|
}
|
||||||
|
|
||||||
var storedFilters = DatatableService.getDataTableFilters(this.tableKey);
|
|
||||||
if (storedFilters !== null) {
|
|
||||||
this.updateStoredFilters(storedFilters.state.values);
|
|
||||||
}
|
|
||||||
this.filters.state.open = false;
|
|
||||||
|
|
||||||
var textFilter = DatatableService.getDataTableTextFilters(this.tableKey);
|
var textFilter = DatatableService.getDataTableTextFilters(this.tableKey);
|
||||||
if (textFilter !== null) {
|
if (textFilter !== null) {
|
||||||
this.state.textFilter = textFilter;
|
this.state.textFilter = textFilter;
|
||||||
|
this.onTextFilterChange();
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
function setDefaults(ctrl) {
|
var storedFilters = DatatableService.getDataTableFilters(this.tableKey);
|
||||||
ctrl.showTextFilter = ctrl.showTextFilter ? ctrl.showTextFilter : false;
|
if (storedFilters !== null) {
|
||||||
ctrl.state.reverseOrder = ctrl.reverseOrder ? ctrl.reverseOrder : false;
|
this.filters = storedFilters;
|
||||||
|
this.updateStoredFilters(storedFilters.state.values);
|
||||||
}
|
}
|
||||||
|
if (this.filters && this.filters.state) {
|
||||||
|
this.filters.state.open = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var storedSettings = DatatableService.getDataTableSettings(this.tableKey);
|
||||||
|
if (storedSettings !== null) {
|
||||||
|
this.settings = storedSettings;
|
||||||
|
this.settings.open = false;
|
||||||
|
}
|
||||||
|
this.onSettingsRepeaterChange();
|
||||||
|
this.state.orderBy = this.orderBy;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -5,6 +5,44 @@
|
||||||
<div class="toolBarTitle">
|
<div class="toolBarTitle">
|
||||||
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }}
|
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="settings">
|
||||||
|
<span class="setting" ng-class="{ 'setting-active': $ctrl.settings.open }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.settings.open">
|
||||||
|
<span uib-dropdown-toggle><i class="fa fa-cog" aria-hidden="true"></i> Settings</span>
|
||||||
|
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
|
||||||
|
<div class="tableMenu">
|
||||||
|
<div class="menuHeader">
|
||||||
|
Table settings
|
||||||
|
</div>
|
||||||
|
<div class="menuContent">
|
||||||
|
<div>
|
||||||
|
<div class="md-checkbox">
|
||||||
|
<input id="setting_auto_refresh" type="checkbox" ng-model="$ctrl.settings.repeater.autoRefresh" ng-change="$ctrl.onSettingsRepeaterChange()"/>
|
||||||
|
<label for="setting_auto_refresh">Auto refresh</label>
|
||||||
|
</div>
|
||||||
|
<div ng-if="$ctrl.settings.repeater.autoRefresh">
|
||||||
|
<label for="settings_refresh_rate">
|
||||||
|
Refresh rate
|
||||||
|
</label>
|
||||||
|
<select id="settings_refresh_rate" ng-model="$ctrl.settings.repeater.refreshRate" ng-change="$ctrl.onSettingsRepeaterChange()" class="small-select">
|
||||||
|
<option value="10">10s</option>
|
||||||
|
<option value="30">30s</option>
|
||||||
|
<option value="60">1min</option>
|
||||||
|
<option value="120">2min</option>
|
||||||
|
<option value="300">5min</option>
|
||||||
|
</select>
|
||||||
|
<span>
|
||||||
|
<i id="refreshRateChange" class="fa fa-check green-icon" aria-hidden="true" style="margin-top: 7px; display: none;"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.settings.open = false;">Close</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="actionBar" ng-if="!$ctrl.offlineMode" authorization="DockerImageDelete, DockerImageBuild, DockerImageLoad, DockerImageGet">
|
<div class="actionBar" ng-if="!$ctrl.offlineMode" authorization="DockerImageDelete, DockerImageBuild, DockerImageLoad, DockerImageGet">
|
||||||
<div class="btn-group" authorization="DockerImageDelete">
|
<div class="btn-group" authorization="DockerImageDelete">
|
||||||
|
@ -43,7 +81,7 @@
|
||||||
<table class="table table-hover table-filters nowrap-cells">
|
<table class="table table-hover table-filters nowrap-cells">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th uib-dropdown dropdown-append-to-body auto-close="disabled" popover-placement="bottom-left" is-open="$ctrl.filters.usage.open">
|
<th uib-dropdown dropdown-append-to-body auto-close="disabled" popover-placement="bottom-left" is-open="$ctrl.filters.state.open">
|
||||||
<span class="md-checkbox" ng-if="!$ctrl.offlineMode">
|
<span class="md-checkbox" ng-if="!$ctrl.offlineMode">
|
||||||
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
||||||
<label for="select_all"></label>
|
<label for="select_all"></label>
|
||||||
|
@ -54,8 +92,8 @@
|
||||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Id' && $ctrl.state.reverseOrder"></i>
|
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Id' && $ctrl.state.reverseOrder"></i>
|
||||||
</a>
|
</a>
|
||||||
<div>
|
<div>
|
||||||
<span uib-dropdown-toggle class="table-filter" ng-if="!$ctrl.filters.usage.enabled">Filter <i class="fa fa-filter" aria-hidden="true"></i></span>
|
<span uib-dropdown-toggle class="table-filter" ng-if="!$ctrl.filters.state.enabled">Filter <i class="fa fa-filter" aria-hidden="true"></i></span>
|
||||||
<span uib-dropdown-toggle class="table-filter filter-active" ng-if="$ctrl.filters.usage.enabled">Filter <i class="fa fa-check" aria-hidden="true"></i></span>
|
<span uib-dropdown-toggle class="table-filter filter-active" ng-if="$ctrl.filters.state.enabled">Filter <i class="fa fa-check" aria-hidden="true"></i></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="dropdown-menu" uib-dropdown-menu>
|
<div class="dropdown-menu" uib-dropdown-menu>
|
||||||
<div class="tableMenu">
|
<div class="tableMenu">
|
||||||
|
@ -64,16 +102,16 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="menuContent">
|
<div class="menuContent">
|
||||||
<div class="md-checkbox">
|
<div class="md-checkbox">
|
||||||
<input id="filter_usage_usedImages" type="checkbox" ng-model="$ctrl.filters.usage.showUsedImages" ng-change="$ctrl.onUsageFilterChange()"/>
|
<input id="filter_usage_usedImages" type="checkbox" ng-model="$ctrl.filters.state.showUsedImages" ng-change="$ctrl.onUsageFilterChange()"/>
|
||||||
<label for="filter_usage_usedImages">Used images</label>
|
<label for="filter_usage_usedImages">Used images</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="md-checkbox">
|
<div class="md-checkbox">
|
||||||
<input id="filter_usage_unusedImages" type="checkbox" ng-model="$ctrl.filters.usage.showUnusedImages" ng-change="$ctrl.onUsageFilterChange()"/>
|
<input id="filter_usage_unusedImages" type="checkbox" ng-model="$ctrl.filters.state.showUnusedImages" ng-change="$ctrl.onUsageFilterChange()"/>
|
||||||
<label for="filter_usage_unusedImages">Unused images</label>
|
<label for="filter_usage_unusedImages">Unused images</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.filters.usage.open = false;">Close</a>
|
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.filters.state.open = false;">Close</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -112,7 +150,7 @@
|
||||||
<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))" ng-class="{active: item.Checked}">
|
<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))" ng-class="{active: item.Checked}">
|
||||||
<td>
|
<td>
|
||||||
<span class="md-checkbox" ng-if="!$ctrl.offlineMode">
|
<span class="md-checkbox" ng-if="!$ctrl.offlineMode">
|
||||||
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
|
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-click="$ctrl.selectItem(item, $event)"/>
|
||||||
<label for="select_{{ $index }}"></label>
|
<label for="select_{{ $index }}"></label>
|
||||||
</span>
|
</span>
|
||||||
<a ng-if="!$ctrl.offlineMode" ui-sref="docker.images.image({ id: item.Id, nodeName: item.NodeName })" class="monospaced" title="{{ item.Id }}">{{ item.Id | truncate:40 }}</a>
|
<a ng-if="!$ctrl.offlineMode" ui-sref="docker.images.image({ id: item.Id, nodeName: item.NodeName })" class="monospaced" title="{{ item.Id }}">{{ item.Id | truncate:40 }}</a>
|
||||||
|
|
|
@ -13,6 +13,7 @@ angular.module('portainer.docker').component('imagesDatatable', {
|
||||||
downloadAction: '<',
|
downloadAction: '<',
|
||||||
forceRemoveAction: '<',
|
forceRemoveAction: '<',
|
||||||
exportInProgress: '<',
|
exportInProgress: '<',
|
||||||
offlineMode: '<'
|
offlineMode: '<',
|
||||||
|
refreshCallback: '<'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,20 +1,13 @@
|
||||||
angular.module('portainer.docker')
|
angular.module('portainer.docker')
|
||||||
.controller('ImagesDatatableController', ['PaginationService', 'DatatableService',
|
.controller('ImagesDatatableController', ['$scope', '$controller', 'DatatableService',
|
||||||
function (PaginationService, DatatableService) {
|
function ($scope, $controller, DatatableService) {
|
||||||
|
|
||||||
|
angular.extend(this, $controller('GenericDatatableController', {$scope: $scope}));
|
||||||
|
|
||||||
var ctrl = this;
|
var ctrl = this;
|
||||||
|
|
||||||
this.state = {
|
|
||||||
selectAll: false,
|
|
||||||
orderBy: this.orderBy,
|
|
||||||
paginatedItemLimit: PaginationService.getPaginationLimit(this.tableKey),
|
|
||||||
displayTextFilter: false,
|
|
||||||
selectedItemCount: 0,
|
|
||||||
selectedItems: []
|
|
||||||
};
|
|
||||||
|
|
||||||
this.filters = {
|
this.filters = {
|
||||||
usage: {
|
state: {
|
||||||
open: false,
|
open: false,
|
||||||
enabled: false,
|
enabled: false,
|
||||||
showUsedImages: true,
|
showUsedImages: true,
|
||||||
|
@ -22,62 +15,29 @@ function (PaginationService, DatatableService) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.onTextFilterChange = function() {
|
|
||||||
DatatableService.setDataTableTextFilters(this.tableKey, this.state.textFilter);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.changeOrderBy = function(orderField) {
|
|
||||||
this.state.reverseOrder = this.state.orderBy === orderField ? !this.state.reverseOrder : false;
|
|
||||||
this.state.orderBy = orderField;
|
|
||||||
DatatableService.setDataTableOrder(this.tableKey, orderField, this.state.reverseOrder);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.selectItem = function(item) {
|
|
||||||
if (item.Checked) {
|
|
||||||
this.state.selectedItemCount++;
|
|
||||||
this.state.selectedItems.push(item);
|
|
||||||
} else {
|
|
||||||
this.state.selectedItems.splice(this.state.selectedItems.indexOf(item), 1);
|
|
||||||
this.state.selectedItemCount--;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.selectAll = function() {
|
|
||||||
for (var i = 0; i < this.state.filteredDataSet.length; i++) {
|
|
||||||
var item = this.state.filteredDataSet[i];
|
|
||||||
if (item.Checked !== this.state.selectAll) {
|
|
||||||
item.Checked = this.state.selectAll;
|
|
||||||
this.selectItem(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.changePaginationLimit = function() {
|
|
||||||
PaginationService.setPaginationLimit(this.tableKey, this.state.paginatedItemLimit);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.applyFilters = function(value) {
|
this.applyFilters = function(value) {
|
||||||
var image = value;
|
var image = value;
|
||||||
var filters = ctrl.filters;
|
var filters = ctrl.filters;
|
||||||
if ((image.ContainerCount === 0 && filters.usage.showUnusedImages)
|
if ((image.ContainerCount === 0 && filters.state.showUnusedImages)
|
||||||
|| (image.ContainerCount !== 0 && filters.usage.showUsedImages)) {
|
|| (image.ContainerCount !== 0 && filters.state.showUsedImages)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
this.onUsageFilterChange = function() {
|
this.onstateFilterChange = function() {
|
||||||
var filters = this.filters.usage;
|
var filters = this.filters.state;
|
||||||
var filtered = false;
|
var filtered = false;
|
||||||
if (!filters.showUsedImages || !filters.showUnusedImages) {
|
if (!filters.showUsedImages || !filters.showUnusedImages) {
|
||||||
filtered = true;
|
filtered = true;
|
||||||
}
|
}
|
||||||
this.filters.usage.enabled = filtered;
|
this.filters.state.enabled = filtered;
|
||||||
DatatableService.setDataTableFilters(this.tableKey, this.filters);
|
DatatableService.setDataTableFilters(this.tableKey, this.filters);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.$onInit = function() {
|
this.$onInit = function() {
|
||||||
setDefaults(this);
|
this.setDefaults();
|
||||||
|
this.prepareTableFromDataset();
|
||||||
|
|
||||||
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
|
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
|
||||||
if (storedOrder !== null) {
|
if (storedOrder !== null) {
|
||||||
|
@ -85,20 +45,27 @@ function (PaginationService, DatatableService) {
|
||||||
this.state.orderBy = storedOrder.orderBy;
|
this.state.orderBy = storedOrder.orderBy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var textFilter = DatatableService.getDataTableTextFilters(this.tableKey);
|
||||||
|
if (textFilter !== null) {
|
||||||
|
this.state.textFilter = textFilter;
|
||||||
|
this.onTextFilterChange();
|
||||||
|
}
|
||||||
|
|
||||||
var storedFilters = DatatableService.getDataTableFilters(this.tableKey);
|
var storedFilters = DatatableService.getDataTableFilters(this.tableKey);
|
||||||
if (storedFilters !== null) {
|
if (storedFilters !== null) {
|
||||||
this.filters = storedFilters;
|
this.filters = storedFilters;
|
||||||
}
|
}
|
||||||
this.filters.usage.open = false;
|
if (this.filters && this.filters.state) {
|
||||||
|
this.filters.state.open = false;
|
||||||
var textFilter = DatatableService.getDataTableTextFilters(this.tableKey);
|
|
||||||
if (textFilter !== null) {
|
|
||||||
this.state.textFilter = textFilter;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var storedSettings = DatatableService.getDataTableSettings(this.tableKey);
|
||||||
|
if (storedSettings !== null) {
|
||||||
|
this.settings = storedSettings;
|
||||||
|
this.settings.open = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.onSettingsRepeaterChange();
|
||||||
|
this.state.orderBy = this.orderBy;
|
||||||
};
|
};
|
||||||
|
|
||||||
function setDefaults(ctrl) {
|
|
||||||
ctrl.showTextFilter = ctrl.showTextFilter ? ctrl.showTextFilter : false;
|
|
||||||
ctrl.state.reverseOrder = ctrl.reverseOrder ? ctrl.reverseOrder : false;
|
|
||||||
}
|
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -60,7 +60,7 @@
|
||||||
ng-class="{active: item.Checked}">
|
ng-class="{active: item.Checked}">
|
||||||
<td>
|
<td>
|
||||||
<span class="md-checkbox">
|
<span class="md-checkbox">
|
||||||
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)" />
|
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-click="$ctrl.selectItem(item, $event)" />
|
||||||
<label for="select_{{ $index }}"></label>
|
<label for="select_{{ $index }}"></label>
|
||||||
</span>
|
</span>
|
||||||
<a ui-sref="docker.nodes.node({id: item.Id})" ng-if="$ctrl.accessToNodeDetails">{{ item.Hostname }}</a>
|
<a ui-sref="docker.nodes.node({id: item.Id})" ng-if="$ctrl.accessToNodeDetails">{{ item.Hostname }}</a>
|
||||||
|
|
|
@ -5,6 +5,44 @@
|
||||||
<div class="toolBarTitle">
|
<div class="toolBarTitle">
|
||||||
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }}
|
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="settings">
|
||||||
|
<span class="setting" ng-class="{ 'setting-active': $ctrl.settings.open }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.settings.open">
|
||||||
|
<span uib-dropdown-toggle><i class="fa fa-cog" aria-hidden="true"></i> Settings</span>
|
||||||
|
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
|
||||||
|
<div class="tableMenu">
|
||||||
|
<div class="menuHeader">
|
||||||
|
Table settings
|
||||||
|
</div>
|
||||||
|
<div class="menuContent">
|
||||||
|
<div>
|
||||||
|
<div class="md-checkbox">
|
||||||
|
<input id="setting_auto_refresh" type="checkbox" ng-model="$ctrl.settings.repeater.autoRefresh" ng-change="$ctrl.onSettingsRepeaterChange()"/>
|
||||||
|
<label for="setting_auto_refresh">Auto refresh</label>
|
||||||
|
</div>
|
||||||
|
<div ng-if="$ctrl.settings.repeater.autoRefresh">
|
||||||
|
<label for="settings_refresh_rate">
|
||||||
|
Refresh rate
|
||||||
|
</label>
|
||||||
|
<select id="settings_refresh_rate" ng-model="$ctrl.settings.repeater.refreshRate" ng-change="$ctrl.onSettingsRepeaterChange()" class="small-select">
|
||||||
|
<option value="10">10s</option>
|
||||||
|
<option value="30">30s</option>
|
||||||
|
<option value="60">1min</option>
|
||||||
|
<option value="120">2min</option>
|
||||||
|
<option value="300">5min</option>
|
||||||
|
</select>
|
||||||
|
<span>
|
||||||
|
<i id="refreshRateChange" class="fa fa-check green-icon" aria-hidden="true" style="margin-top: 7px; display: none;"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.settings.open = false;">Close</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="actionBar" ng-if="!$ctrl.offlineMode" authorization="DockerNetworkDelete, DockerNetworkCreate">
|
<div class="actionBar" ng-if="!$ctrl.offlineMode" authorization="DockerNetworkDelete, DockerNetworkCreate">
|
||||||
<button type="button" class="btn btn-sm btn-danger" authorization="DockerNetworkDelete"
|
<button type="button" class="btn btn-sm btn-danger" authorization="DockerNetworkDelete"
|
||||||
|
@ -110,7 +148,7 @@
|
||||||
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
|
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
|
||||||
<td>
|
<td>
|
||||||
<span class="md-checkbox" ng-if="!$ctrl.offlineMode">
|
<span class="md-checkbox" ng-if="!$ctrl.offlineMode">
|
||||||
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)" ng-disabled="$ctrl.disableRemove(item)"/>
|
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-click="$ctrl.selectItem(item, $event)" ng-disabled="$ctrl.disableRemove(item)"/>
|
||||||
<label for="select_{{ $index }}"></label>
|
<label for="select_{{ $index }}"></label>
|
||||||
</span>
|
</span>
|
||||||
<a ng-if="!$ctrl.offlineMode" ui-sref="docker.networks.network({ id: item.Id, nodeName: item.NodeName })" title="{{ item.Name }}">{{ item.Name | truncate:40 }}</a>
|
<a ng-if="!$ctrl.offlineMode" ui-sref="docker.networks.network({ id: item.Id, nodeName: item.NodeName })" title="{{ item.Name }}">{{ item.Name | truncate:40 }}</a>
|
||||||
|
|
|
@ -11,6 +11,7 @@ angular.module('portainer.docker').component('networksDatatable', {
|
||||||
showOwnershipColumn: '<',
|
showOwnershipColumn: '<',
|
||||||
showHostColumn: '<',
|
showHostColumn: '<',
|
||||||
removeAction: '<',
|
removeAction: '<',
|
||||||
offlineMode: '<'
|
offlineMode: '<',
|
||||||
|
refreshCallback: '<'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,20 +1,51 @@
|
||||||
angular.module('portainer.docker')
|
angular.module('portainer.docker')
|
||||||
.controller('NetworksDatatableController', ['$scope', '$controller', 'PREDEFINED_NETWORKS',
|
.controller('NetworksDatatableController', ['$scope', '$controller', 'PREDEFINED_NETWORKS', 'DatatableService',
|
||||||
function ($scope, $controller, PREDEFINED_NETWORKS) {
|
function ($scope, $controller, PREDEFINED_NETWORKS, DatatableService) {
|
||||||
|
|
||||||
angular.extend(this, $controller('GenericDatatableController', {$scope: $scope}));
|
angular.extend(this, $controller('GenericDatatableController', {$scope: $scope}));
|
||||||
|
|
||||||
this.disableRemove = function(item) {
|
this.disableRemove = function(item) {
|
||||||
return PREDEFINED_NETWORKS.includes(item.Name);
|
return PREDEFINED_NETWORKS.includes(item.Name);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.selectAll = function() {
|
/**
|
||||||
for (var i = 0; i < this.state.filteredDataSet.length; i++) {
|
* Do not allow PREDEFINED_NETWORKS to be selected
|
||||||
var item = this.state.filteredDataSet[i];
|
*/
|
||||||
if (!this.disableRemove(item) && item.Checked !== this.state.selectAll) {
|
this.allowSelection = function(item) {
|
||||||
item.Checked = this.state.selectAll;
|
return !this.disableRemove(item);
|
||||||
this.selectItem(item);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.$onInit = function() {
|
||||||
|
this.setDefaults();
|
||||||
|
this.prepareTableFromDataset();
|
||||||
|
|
||||||
|
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
|
||||||
|
if (storedOrder !== null) {
|
||||||
|
this.state.reverseOrder = storedOrder.reverse;
|
||||||
|
this.state.orderBy = storedOrder.orderBy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var textFilter = DatatableService.getDataTableTextFilters(this.tableKey);
|
||||||
|
if (textFilter !== null) {
|
||||||
|
this.state.textFilter = textFilter;
|
||||||
|
this.onTextFilterChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
var storedFilters = DatatableService.getDataTableFilters(this.tableKey);
|
||||||
|
if (storedFilters !== null) {
|
||||||
|
this.filters = storedFilters;
|
||||||
|
}
|
||||||
|
if (this.filters && this.filters.state) {
|
||||||
|
this.filters.state.open = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var storedSettings = DatatableService.getDataTableSettings(this.tableKey);
|
||||||
|
if (storedSettings !== null) {
|
||||||
|
this.settings = storedSettings;
|
||||||
|
this.settings.open = false;
|
||||||
|
}
|
||||||
|
this.onSettingsRepeaterChange();
|
||||||
|
this.state.orderBy = this.orderBy;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
]);
|
]);
|
|
@ -5,6 +5,44 @@
|
||||||
<div class="toolBarTitle">
|
<div class="toolBarTitle">
|
||||||
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }}
|
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="settings">
|
||||||
|
<span class="setting" ng-class="{ 'setting-active': $ctrl.settings.open }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.settings.open">
|
||||||
|
<span uib-dropdown-toggle><i class="fa fa-cog" aria-hidden="true"></i> Settings</span>
|
||||||
|
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
|
||||||
|
<div class="tableMenu">
|
||||||
|
<div class="menuHeader">
|
||||||
|
Table settings
|
||||||
|
</div>
|
||||||
|
<div class="menuContent">
|
||||||
|
<div>
|
||||||
|
<div class="md-checkbox">
|
||||||
|
<input id="setting_auto_refresh" type="checkbox" ng-model="$ctrl.settings.repeater.autoRefresh" ng-change="$ctrl.onSettingsRepeaterChange()"/>
|
||||||
|
<label for="setting_auto_refresh">Auto refresh</label>
|
||||||
|
</div>
|
||||||
|
<div ng-if="$ctrl.settings.repeater.autoRefresh">
|
||||||
|
<label for="settings_refresh_rate">
|
||||||
|
Refresh rate
|
||||||
|
</label>
|
||||||
|
<select id="settings_refresh_rate" ng-model="$ctrl.settings.repeater.refreshRate" ng-change="$ctrl.onSettingsRepeaterChange()" class="small-select">
|
||||||
|
<option value="10">10s</option>
|
||||||
|
<option value="30">30s</option>
|
||||||
|
<option value="60">1min</option>
|
||||||
|
<option value="120">2min</option>
|
||||||
|
<option value="300">5min</option>
|
||||||
|
</select>
|
||||||
|
<span>
|
||||||
|
<i id="refreshRateChange" class="fa fa-check green-icon" aria-hidden="true" style="margin-top: 7px; display: none;"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.settings.open = false;">Close</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="searchBar">
|
<div class="searchBar">
|
||||||
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
||||||
|
|
|
@ -9,6 +9,7 @@ angular.module('portainer.docker').component('nodesDatatable', {
|
||||||
orderBy: '@',
|
orderBy: '@',
|
||||||
reverseOrder: '<',
|
reverseOrder: '<',
|
||||||
showIpAddressColumn: '<',
|
showIpAddressColumn: '<',
|
||||||
accessToNodeDetails: '<'
|
accessToNodeDetails: '<',
|
||||||
|
refreshCallback: '<'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,6 +5,44 @@
|
||||||
<div class="toolBarTitle">
|
<div class="toolBarTitle">
|
||||||
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }}
|
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="settings">
|
||||||
|
<span class="setting" ng-class="{ 'setting-active': $ctrl.settings.open }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.settings.open">
|
||||||
|
<span uib-dropdown-toggle><i class="fa fa-cog" aria-hidden="true"></i> Settings</span>
|
||||||
|
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
|
||||||
|
<div class="tableMenu">
|
||||||
|
<div class="menuHeader">
|
||||||
|
Table settings
|
||||||
|
</div>
|
||||||
|
<div class="menuContent">
|
||||||
|
<div>
|
||||||
|
<div class="md-checkbox">
|
||||||
|
<input id="setting_auto_refresh" type="checkbox" ng-model="$ctrl.settings.repeater.autoRefresh" ng-change="$ctrl.onSettingsRepeaterChange()"/>
|
||||||
|
<label for="setting_auto_refresh">Auto refresh</label>
|
||||||
|
</div>
|
||||||
|
<div ng-if="$ctrl.settings.repeater.autoRefresh">
|
||||||
|
<label for="settings_refresh_rate">
|
||||||
|
Refresh rate
|
||||||
|
</label>
|
||||||
|
<select id="settings_refresh_rate" ng-model="$ctrl.settings.repeater.refreshRate" ng-change="$ctrl.onSettingsRepeaterChange()" class="small-select">
|
||||||
|
<option value="10">10s</option>
|
||||||
|
<option value="30">30s</option>
|
||||||
|
<option value="60">1min</option>
|
||||||
|
<option value="120">2min</option>
|
||||||
|
<option value="300">5min</option>
|
||||||
|
</select>
|
||||||
|
<span>
|
||||||
|
<i id="refreshRateChange" class="fa fa-check green-icon" aria-hidden="true" style="margin-top: 7px; display: none;"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.settings.open = false;">Close</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="actionBar" authorization="DockerSecretDelete, DockerSecretCreate">
|
<div class="actionBar" authorization="DockerSecretDelete, DockerSecretCreate">
|
||||||
<button type="button" class="btn btn-sm btn-danger" authorization="DockerSecretDelete"
|
<button type="button" class="btn btn-sm btn-danger" authorization="DockerSecretDelete"
|
||||||
|
@ -54,7 +92,7 @@
|
||||||
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
|
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
|
||||||
<td>
|
<td>
|
||||||
<span class="md-checkbox" authorization="DockerSecretDelete, DockerSecretCreate">
|
<span class="md-checkbox" authorization="DockerSecretDelete, DockerSecretCreate">
|
||||||
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
|
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-click="$ctrl.selectItem(item, $event)"/>
|
||||||
<label for="select_{{ $index }}"></label>
|
<label for="select_{{ $index }}"></label>
|
||||||
</span>
|
</span>
|
||||||
<a ui-sref="docker.secrets.secret({id: item.Id})">{{ item.Name }}</a>
|
<a ui-sref="docker.secrets.secret({id: item.Id})">{{ item.Name }}</a>
|
||||||
|
|
|
@ -9,6 +9,7 @@ angular.module('portainer.docker').component('secretsDatatable', {
|
||||||
orderBy: '@',
|
orderBy: '@',
|
||||||
reverseOrder: '<',
|
reverseOrder: '<',
|
||||||
showOwnershipColumn: '<',
|
showOwnershipColumn: '<',
|
||||||
removeAction: '<'
|
removeAction: '<',
|
||||||
|
refreshCallback: '<'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,18 +1,21 @@
|
||||||
import _ from 'lodash-es';
|
import _ from 'lodash-es';
|
||||||
|
|
||||||
angular.module('portainer.docker')
|
angular.module('portainer.docker')
|
||||||
.controller('ServiceTasksDatatableController', ['DatatableService',
|
.controller('ServiceTasksDatatableController', ['$scope', '$controller', 'DatatableService',
|
||||||
function (DatatableService) {
|
function ($scope, $controller, DatatableService) {
|
||||||
|
|
||||||
|
angular.extend(this, $controller('GenericDatatableController', {$scope: $scope}));
|
||||||
|
|
||||||
var ctrl = this;
|
var ctrl = this;
|
||||||
|
|
||||||
this.state = {
|
this.state = Object.assign(this.state, {
|
||||||
orderBy: this.orderBy,
|
|
||||||
showQuickActionStats: true,
|
showQuickActionStats: true,
|
||||||
showQuickActionLogs: true,
|
showQuickActionLogs: true,
|
||||||
showQuickActionExec: true,
|
showQuickActionConsole: true,
|
||||||
showQuickActionInspect: true,
|
showQuickActionInspect: true,
|
||||||
|
showQuickActionExec: true,
|
||||||
showQuickActionAttach: false
|
showQuickActionAttach: false
|
||||||
};
|
});
|
||||||
|
|
||||||
this.filters = {
|
this.filters = {
|
||||||
state: {
|
state: {
|
||||||
|
@ -45,12 +48,6 @@ function (DatatableService) {
|
||||||
this.filters.state.enabled = filtered;
|
this.filters.state.enabled = filtered;
|
||||||
};
|
};
|
||||||
|
|
||||||
this.changeOrderBy = function(orderField) {
|
|
||||||
this.state.reverseOrder = this.state.orderBy === orderField ? !this.state.reverseOrder : false;
|
|
||||||
this.state.orderBy = orderField;
|
|
||||||
DatatableService.setDataTableOrder(this.tableKey, orderField, this.state.reverseOrder);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.prepareTableFromDataset = function() {
|
this.prepareTableFromDataset = function() {
|
||||||
var availableStateFilters = [];
|
var availableStateFilters = [];
|
||||||
for (var i = 0; i < this.dataset.length; i++) {
|
for (var i = 0; i < this.dataset.length; i++) {
|
||||||
|
@ -61,7 +58,7 @@ function (DatatableService) {
|
||||||
};
|
};
|
||||||
|
|
||||||
this.$onInit = function() {
|
this.$onInit = function() {
|
||||||
setDefaults(this);
|
this.setDefaults();
|
||||||
this.prepareTableFromDataset();
|
this.prepareTableFromDataset();
|
||||||
|
|
||||||
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
|
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
|
||||||
|
@ -69,9 +66,28 @@ function (DatatableService) {
|
||||||
this.state.reverseOrder = storedOrder.reverse;
|
this.state.reverseOrder = storedOrder.reverse;
|
||||||
this.state.orderBy = storedOrder.orderBy;
|
this.state.orderBy = storedOrder.orderBy;
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
function setDefaults(ctrl) {
|
var textFilter = DatatableService.getDataTableTextFilters(this.tableKey);
|
||||||
ctrl.state.reverseOrder = ctrl.reverseOrder ? ctrl.reverseOrder : false;
|
if (textFilter !== null) {
|
||||||
|
this.state.textFilter = textFilter;
|
||||||
|
this.onTextFilterChange();
|
||||||
}
|
}
|
||||||
}]);
|
|
||||||
|
var storedFilters = DatatableService.getDataTableFilters(this.tableKey);
|
||||||
|
if (storedFilters !== null) {
|
||||||
|
this.filters = storedFilters;
|
||||||
|
}
|
||||||
|
if (this.filters && this.filters.state) {
|
||||||
|
this.filters.state.open = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var storedSettings = DatatableService.getDataTableSettings(this.tableKey);
|
||||||
|
if (storedSettings !== null) {
|
||||||
|
this.settings = storedSettings;
|
||||||
|
this.settings.open = false;
|
||||||
|
}
|
||||||
|
this.onSettingsRepeaterChange();
|
||||||
|
this.state.orderBy = this.orderBy;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
|
@ -5,6 +5,44 @@
|
||||||
<div class="toolBarTitle">
|
<div class="toolBarTitle">
|
||||||
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }}
|
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="settings">
|
||||||
|
<span class="setting" ng-class="{ 'setting-active': $ctrl.settings.open }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.settings.open">
|
||||||
|
<span uib-dropdown-toggle><i class="fa fa-cog" aria-hidden="true"></i> Settings</span>
|
||||||
|
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
|
||||||
|
<div class="tableMenu">
|
||||||
|
<div class="menuHeader">
|
||||||
|
Table settings
|
||||||
|
</div>
|
||||||
|
<div class="menuContent">
|
||||||
|
<div>
|
||||||
|
<div class="md-checkbox">
|
||||||
|
<input id="setting_auto_refresh" type="checkbox" ng-model="$ctrl.settings.repeater.autoRefresh" ng-change="$ctrl.onSettingsRepeaterChange()"/>
|
||||||
|
<label for="setting_auto_refresh">Auto refresh</label>
|
||||||
|
</div>
|
||||||
|
<div ng-if="$ctrl.settings.repeater.autoRefresh">
|
||||||
|
<label for="settings_refresh_rate">
|
||||||
|
Refresh rate
|
||||||
|
</label>
|
||||||
|
<select id="settings_refresh_rate" ng-model="$ctrl.settings.repeater.refreshRate" ng-change="$ctrl.onSettingsRepeaterChange()" class="small-select">
|
||||||
|
<option value="10">10s</option>
|
||||||
|
<option value="30">30s</option>
|
||||||
|
<option value="60">1min</option>
|
||||||
|
<option value="120">2min</option>
|
||||||
|
<option value="300">5min</option>
|
||||||
|
</select>
|
||||||
|
<span>
|
||||||
|
<i id="refreshRateChange" class="fa fa-check green-icon" aria-hidden="true" style="margin-top: 7px; display: none;"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.settings.open = false;">Close</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<services-datatable-actions
|
<services-datatable-actions
|
||||||
selected-items="$ctrl.state.selectedItems"
|
selected-items="$ctrl.state.selectedItems"
|
||||||
|
@ -84,7 +122,7 @@
|
||||||
<tr ng-click="$ctrl.expandItem(item, !item.Expanded)" dir-paginate-start="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}" class="interactive">
|
<tr ng-click="$ctrl.expandItem(item, !item.Expanded)" dir-paginate-start="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}" class="interactive">
|
||||||
<td>
|
<td>
|
||||||
<span class="md-checkbox" authorization="DockerServiceUpdate, DockerServiceDelete, DockerServiceCreate">
|
<span class="md-checkbox" authorization="DockerServiceUpdate, DockerServiceDelete, DockerServiceCreate">
|
||||||
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
|
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-click="$ctrl.selectItem(item, $event)"/>
|
||||||
<label for="select_{{ $index }}"></label>
|
<label for="select_{{ $index }}"></label>
|
||||||
</span>
|
</span>
|
||||||
<a><i ng-class="{ 'fas fa-angle-down': item.Expanded, 'fas fa-angle-right': !item.Expanded }" class="space-right" aria-hidden="true"></i></a>
|
<a><i ng-class="{ 'fas fa-angle-down': item.Expanded, 'fas fa-angle-right': !item.Expanded }" class="space-right" aria-hidden="true"></i></a>
|
||||||
|
|
|
@ -14,6 +14,7 @@ angular.module('portainer.docker').component('servicesDatatable', {
|
||||||
showUpdateAction: '<',
|
showUpdateAction: '<',
|
||||||
showAddAction: '<',
|
showAddAction: '<',
|
||||||
showStackColumn: '<',
|
showStackColumn: '<',
|
||||||
showTaskLogsButton: '<'
|
showTaskLogsButton: '<',
|
||||||
|
refreshCallback: '<'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,52 +1,18 @@
|
||||||
import _ from 'lodash-es';
|
import _ from 'lodash-es';
|
||||||
|
|
||||||
angular.module('portainer.docker')
|
angular.module('portainer.docker')
|
||||||
.controller('ServicesDatatableController', ['PaginationService', 'DatatableService', 'EndpointProvider',
|
.controller('ServicesDatatableController', ['$scope', '$controller', 'DatatableService', 'EndpointProvider',
|
||||||
function (PaginationService, DatatableService, EndpointProvider) {
|
function ($scope, $controller, DatatableService, EndpointProvider) {
|
||||||
|
|
||||||
|
angular.extend(this, $controller('GenericDatatableController', {$scope: $scope}));
|
||||||
|
|
||||||
var ctrl = this;
|
var ctrl = this;
|
||||||
|
|
||||||
this.state = {
|
this.state = Object.assign(this.state,{
|
||||||
selectAll: false,
|
|
||||||
expandAll: false,
|
expandAll: false,
|
||||||
orderBy: this.orderBy,
|
|
||||||
paginatedItemLimit: PaginationService.getPaginationLimit(this.tableKey),
|
|
||||||
displayTextFilter: false,
|
|
||||||
selectedItemCount: 0,
|
|
||||||
selectedItems: [],
|
|
||||||
expandedItems: [],
|
expandedItems: [],
|
||||||
publicURL: EndpointProvider.endpointPublicURL()
|
publicURL: EndpointProvider.endpointPublicURL()
|
||||||
};
|
});
|
||||||
|
|
||||||
this.onTextFilterChange = function() {
|
|
||||||
DatatableService.setDataTableTextFilters(this.tableKey, this.state.textFilter);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.changeOrderBy = function(orderField) {
|
|
||||||
this.state.reverseOrder = this.state.orderBy === orderField ? !this.state.reverseOrder : false;
|
|
||||||
this.state.orderBy = orderField;
|
|
||||||
DatatableService.setDataTableOrder(this.tableKey, orderField, this.state.reverseOrder);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.selectItem = function(item) {
|
|
||||||
if (item.Checked) {
|
|
||||||
this.state.selectedItemCount++;
|
|
||||||
this.state.selectedItems.push(item);
|
|
||||||
} else {
|
|
||||||
this.state.selectedItems.splice(this.state.selectedItems.indexOf(item), 1);
|
|
||||||
this.state.selectedItemCount--;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.selectAll = function() {
|
|
||||||
for (var i = 0; i < this.state.filteredDataSet.length; i++) {
|
|
||||||
var item = this.state.filteredDataSet[i];
|
|
||||||
if (item.Checked !== this.state.selectAll) {
|
|
||||||
item.Checked = this.state.selectAll;
|
|
||||||
this.selectItem(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.expandAll = function() {
|
this.expandAll = function() {
|
||||||
this.state.expandAll = !this.state.expandAll;
|
this.state.expandAll = !this.state.expandAll;
|
||||||
|
@ -56,10 +22,6 @@ function (PaginationService, DatatableService, EndpointProvider) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.changePaginationLimit = function() {
|
|
||||||
PaginationService.setPaginationLimit(this.tableKey, this.state.paginatedItemLimit);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.expandItem = function(item, expanded) {
|
this.expandItem = function(item, expanded) {
|
||||||
item.Expanded = expanded;
|
item.Expanded = expanded;
|
||||||
if (item.Expanded) {
|
if (item.Expanded) {
|
||||||
|
@ -103,7 +65,8 @@ function (PaginationService, DatatableService, EndpointProvider) {
|
||||||
};
|
};
|
||||||
|
|
||||||
this.$onInit = function() {
|
this.$onInit = function() {
|
||||||
setDefaults(this);
|
this.setDefaults();
|
||||||
|
this.prepareTableFromDataset();
|
||||||
|
|
||||||
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
|
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
|
||||||
if (storedOrder !== null) {
|
if (storedOrder !== null) {
|
||||||
|
@ -111,19 +74,31 @@ function (PaginationService, DatatableService, EndpointProvider) {
|
||||||
this.state.orderBy = storedOrder.orderBy;
|
this.state.orderBy = storedOrder.orderBy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var textFilter = DatatableService.getDataTableTextFilters(this.tableKey);
|
||||||
|
if (textFilter !== null) {
|
||||||
|
this.state.textFilter = textFilter;
|
||||||
|
this.onTextFilterChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
var storedFilters = DatatableService.getDataTableFilters(this.tableKey);
|
||||||
|
if (storedFilters !== null) {
|
||||||
|
this.filters = storedFilters;
|
||||||
|
}
|
||||||
|
if (this.filters && this.filters.state) {
|
||||||
|
this.filters.state.open = false;
|
||||||
|
}
|
||||||
|
|
||||||
var storedExpandedItems = DatatableService.getDataTableExpandedItems(this.tableKey);
|
var storedExpandedItems = DatatableService.getDataTableExpandedItems(this.tableKey);
|
||||||
if (storedExpandedItems !== null) {
|
if (storedExpandedItems !== null) {
|
||||||
this.expandItems(storedExpandedItems);
|
this.expandItems(storedExpandedItems);
|
||||||
}
|
}
|
||||||
|
|
||||||
var textFilter = DatatableService.getDataTableTextFilters(this.tableKey);
|
var storedSettings = DatatableService.getDataTableSettings(this.tableKey);
|
||||||
if (textFilter !== null) {
|
if (storedSettings !== null) {
|
||||||
this.state.textFilter = textFilter;
|
this.settings = storedSettings;
|
||||||
|
this.settings.open = false;
|
||||||
}
|
}
|
||||||
|
this.onSettingsRepeaterChange();
|
||||||
|
this.state.orderBy = this.orderBy;
|
||||||
};
|
};
|
||||||
|
|
||||||
function setDefaults(ctrl) {
|
|
||||||
ctrl.showTextFilter = ctrl.showTextFilter ? ctrl.showTextFilter : false;
|
|
||||||
ctrl.state.reverseOrder = ctrl.reverseOrder ? ctrl.reverseOrder : false;
|
|
||||||
}
|
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -1,56 +1,20 @@
|
||||||
angular.module('portainer.docker')
|
angular.module('portainer.docker')
|
||||||
.controller('TasksDatatableController', ['PaginationService', 'DatatableService',
|
.controller('TasksDatatableController', ['$scope', '$controller', 'DatatableService',
|
||||||
function (PaginationService, DatatableService) {
|
function ($scope, $controller, DatatableService) {
|
||||||
this.state = {
|
|
||||||
|
angular.extend(this, $controller('GenericDatatableController', {$scope: $scope}));
|
||||||
|
|
||||||
|
this.state = Object.assign(this.state, {
|
||||||
showQuickActionStats: true,
|
showQuickActionStats: true,
|
||||||
showQuickActionLogs: true,
|
showQuickActionLogs: true,
|
||||||
showQuickActionExec: true,
|
showQuickActionExec: true,
|
||||||
showQuickActionInspect: true,
|
showQuickActionInspect: true,
|
||||||
showQuickActionAttach: false,
|
showQuickActionAttach: false
|
||||||
selectAll: false,
|
});
|
||||||
orderBy: this.orderBy,
|
|
||||||
paginatedItemLimit: PaginationService.getPaginationLimit(this.tableKey),
|
|
||||||
displayTextFilter: false,
|
|
||||||
selectedItemCount: 0,
|
|
||||||
selectedItems: []
|
|
||||||
};
|
|
||||||
|
|
||||||
this.onTextFilterChange = function() {
|
|
||||||
DatatableService.setDataTableTextFilters(this.tableKey, this.state.textFilter);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.changeOrderBy = function(orderField) {
|
|
||||||
this.state.reverseOrder = this.state.orderBy === orderField ? !this.state.reverseOrder : false;
|
|
||||||
this.state.orderBy = orderField;
|
|
||||||
DatatableService.setDataTableOrder(this.tableKey, orderField, this.state.reverseOrder);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.selectItem = function(item) {
|
|
||||||
if (item.Checked) {
|
|
||||||
this.state.selectedItemCount++;
|
|
||||||
this.state.selectedItems.push(item);
|
|
||||||
} else {
|
|
||||||
this.state.selectedItems.splice(this.state.selectedItems.indexOf(item), 1);
|
|
||||||
this.state.selectedItemCount--;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.selectAll = function() {
|
|
||||||
for (var i = 0; i < this.state.filteredDataSet.length; i++) {
|
|
||||||
var item = this.state.filteredDataSet[i];
|
|
||||||
if (item.Checked !== this.state.selectAll) {
|
|
||||||
item.Checked = this.state.selectAll;
|
|
||||||
this.selectItem(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.changePaginationLimit = function() {
|
|
||||||
PaginationService.setPaginationLimit(this.tableKey, this.state.paginatedItemLimit);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.$onInit = function() {
|
this.$onInit = function() {
|
||||||
setDefaults(this);
|
this.setDefaults();
|
||||||
|
this.prepareTableFromDataset();
|
||||||
|
|
||||||
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
|
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
|
||||||
if (storedOrder !== null) {
|
if (storedOrder !== null) {
|
||||||
|
@ -61,11 +25,23 @@ function (PaginationService, DatatableService) {
|
||||||
var textFilter = DatatableService.getDataTableTextFilters(this.tableKey);
|
var textFilter = DatatableService.getDataTableTextFilters(this.tableKey);
|
||||||
if (textFilter !== null) {
|
if (textFilter !== null) {
|
||||||
this.state.textFilter = textFilter;
|
this.state.textFilter = textFilter;
|
||||||
|
this.onTextFilterChange();
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
function setDefaults(ctrl) {
|
var storedFilters = DatatableService.getDataTableFilters(this.tableKey);
|
||||||
ctrl.showTextFilter = ctrl.showTextFilter ? ctrl.showTextFilter : false;
|
if (storedFilters !== null) {
|
||||||
ctrl.state.reverseOrder = ctrl.reverseOrder ? ctrl.reverseOrder : false;
|
this.filters = storedFilters;
|
||||||
}
|
}
|
||||||
|
if (this.filters && this.filters.state) {
|
||||||
|
this.filters.state.open = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var storedSettings = DatatableService.getDataTableSettings(this.tableKey);
|
||||||
|
if (storedSettings !== null) {
|
||||||
|
this.settings = storedSettings;
|
||||||
|
this.settings.open = false;
|
||||||
|
}
|
||||||
|
this.onSettingsRepeaterChange();
|
||||||
|
this.state.orderBy = this.orderBy;
|
||||||
|
};
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -5,6 +5,44 @@
|
||||||
<div class="toolBarTitle">
|
<div class="toolBarTitle">
|
||||||
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }}
|
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="settings">
|
||||||
|
<span class="setting" ng-class="{ 'setting-active': $ctrl.settings.open }" uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.settings.open">
|
||||||
|
<span uib-dropdown-toggle><i class="fa fa-cog" aria-hidden="true"></i> Settings</span>
|
||||||
|
<div class="dropdown-menu dropdown-menu-right" uib-dropdown-menu>
|
||||||
|
<div class="tableMenu">
|
||||||
|
<div class="menuHeader">
|
||||||
|
Table settings
|
||||||
|
</div>
|
||||||
|
<div class="menuContent">
|
||||||
|
<div>
|
||||||
|
<div class="md-checkbox">
|
||||||
|
<input id="setting_auto_refresh" type="checkbox" ng-model="$ctrl.settings.repeater.autoRefresh" ng-change="$ctrl.onSettingsRepeaterChange()"/>
|
||||||
|
<label for="setting_auto_refresh">Auto refresh</label>
|
||||||
|
</div>
|
||||||
|
<div ng-if="$ctrl.settings.repeater.autoRefresh">
|
||||||
|
<label for="settings_refresh_rate">
|
||||||
|
Refresh rate
|
||||||
|
</label>
|
||||||
|
<select id="settings_refresh_rate" ng-model="$ctrl.settings.repeater.refreshRate" ng-change="$ctrl.onSettingsRepeaterChange()" class="small-select">
|
||||||
|
<option value="10">10s</option>
|
||||||
|
<option value="30">30s</option>
|
||||||
|
<option value="60">1min</option>
|
||||||
|
<option value="120">2min</option>
|
||||||
|
<option value="300">5min</option>
|
||||||
|
</select>
|
||||||
|
<span>
|
||||||
|
<i id="refreshRateChange" class="fa fa-check green-icon" aria-hidden="true" style="margin-top: 7px; display: none;"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.settings.open = false;">Close</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="actionBar" ng-if="!$ctrl.offlineMode" authorization="DockerVolumeDelete, DockerVolumeCreate">
|
<div class="actionBar" ng-if="!$ctrl.offlineMode" authorization="DockerVolumeDelete, DockerVolumeCreate">
|
||||||
<button type="button" class="btn btn-sm btn-danger" authorization="DockerVolumeDelete"
|
<button type="button" class="btn btn-sm btn-danger" authorization="DockerVolumeDelete"
|
||||||
|
@ -23,7 +61,7 @@
|
||||||
<table class="table table-hover table-filters nowrap-cells">
|
<table class="table table-hover table-filters nowrap-cells">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.filters.usage.open">
|
<th uib-dropdown dropdown-append-to-body auto-close="disabled" is-open="$ctrl.filters.state.open">
|
||||||
<span class="md-checkbox" ng-if="!$ctrl.offlineMode" authorization="DockerVolumeDelete, DockerVolumeCreate">
|
<span class="md-checkbox" ng-if="!$ctrl.offlineMode" authorization="DockerVolumeDelete, DockerVolumeCreate">
|
||||||
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
||||||
<label for="select_all"></label>
|
<label for="select_all"></label>
|
||||||
|
@ -34,8 +72,8 @@
|
||||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Id' && $ctrl.state.reverseOrder"></i>
|
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Id' && $ctrl.state.reverseOrder"></i>
|
||||||
</a>
|
</a>
|
||||||
<div>
|
<div>
|
||||||
<span uib-dropdown-toggle class="table-filter" ng-if="!$ctrl.filters.usage.enabled">Filter <i class="fa fa-filter" aria-hidden="true"></i></span>
|
<span uib-dropdown-toggle class="table-filter" ng-if="!$ctrl.filters.state.enabled">Filter <i class="fa fa-filter" aria-hidden="true"></i></span>
|
||||||
<span uib-dropdown-toggle class="table-filter filter-active" ng-if="$ctrl.filters.usage.enabled">Filter <i class="fa fa-check" aria-hidden="true"></i></span>
|
<span uib-dropdown-toggle class="table-filter filter-active" ng-if="$ctrl.filters.state.enabled">Filter <i class="fa fa-check" aria-hidden="true"></i></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="dropdown-menu" uib-dropdown-menu>
|
<div class="dropdown-menu" uib-dropdown-menu>
|
||||||
<div class="tableMenu">
|
<div class="tableMenu">
|
||||||
|
@ -44,16 +82,16 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="menuContent">
|
<div class="menuContent">
|
||||||
<div class="md-checkbox">
|
<div class="md-checkbox">
|
||||||
<input id="filter_usage_usedImages" type="checkbox" ng-model="$ctrl.filters.usage.showUsedVolumes" ng-change="$ctrl.onUsageFilterChange()"/>
|
<input id="filter_usage_usedImages" type="checkbox" ng-model="$ctrl.filters.state.showUsedVolumes" ng-change="$ctrl.onUsageFilterChange()"/>
|
||||||
<label for="filter_usage_usedImages">Used volumes</label>
|
<label for="filter_usage_usedImages">Used volumes</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="md-checkbox">
|
<div class="md-checkbox">
|
||||||
<input id="filter_usage_unusedImages" type="checkbox" ng-model="$ctrl.filters.usage.showUnusedVolumes" ng-change="$ctrl.onUsageFilterChange()"/>
|
<input id="filter_usage_unusedImages" type="checkbox" ng-model="$ctrl.filters.state.showUnusedVolumes" ng-change="$ctrl.onUsageFilterChange()"/>
|
||||||
<label for="filter_usage_unusedImages">Unused volumes</label>
|
<label for="filter_usage_unusedImages">Unused volumes</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.filters.usage.open = false;">Close</a>
|
<a type="button" class="btn btn-default btn-sm" ng-click="$ctrl.filters.state.open = false;">Close</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -106,7 +144,7 @@
|
||||||
<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))" ng-class="{active: item.Checked}">
|
<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))" ng-class="{active: item.Checked}">
|
||||||
<td>
|
<td>
|
||||||
<span class="md-checkbox" ng-if="!$ctrl.offlineMode" authorization="DockerVolumeDelete, DockerVolumeCreate">
|
<span class="md-checkbox" ng-if="!$ctrl.offlineMode" authorization="DockerVolumeDelete, DockerVolumeCreate">
|
||||||
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
|
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-click="$ctrl.selectItem(item, $event)"/>
|
||||||
<label for="select_{{ $index }}"></label>
|
<label for="select_{{ $index }}"></label>
|
||||||
</span>
|
</span>
|
||||||
<a ng-if="!$ctrl.offlineMode" ui-sref="docker.volumes.volume({ id: item.Id, nodeName: item.NodeName })" class="monospaced" title="{{ item.Id }}">{{ item.Id | truncate:40 }}</a>
|
<a ng-if="!$ctrl.offlineMode" ui-sref="docker.volumes.volume({ id: item.Id, nodeName: item.NodeName })" class="monospaced" title="{{ item.Id }}">{{ item.Id | truncate:40 }}</a>
|
||||||
|
|
|
@ -12,6 +12,7 @@ angular.module('portainer.docker').component('volumesDatatable', {
|
||||||
showHostColumn: '<',
|
showHostColumn: '<',
|
||||||
removeAction: '<',
|
removeAction: '<',
|
||||||
showBrowseAction: '<',
|
showBrowseAction: '<',
|
||||||
offlineMode: '<'
|
offlineMode: '<',
|
||||||
|
refreshCallback: '<'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,20 +1,13 @@
|
||||||
angular.module('portainer.docker')
|
angular.module('portainer.docker')
|
||||||
.controller('VolumesDatatableController', ['PaginationService', 'DatatableService',
|
.controller('VolumesDatatableController', ['$scope', '$controller', 'DatatableService',
|
||||||
function (PaginationService, DatatableService) {
|
function ($scope, $controller, DatatableService) {
|
||||||
|
|
||||||
|
angular.extend(this, $controller('GenericDatatableController', {$scope: $scope}));
|
||||||
|
|
||||||
var ctrl = this;
|
var ctrl = this;
|
||||||
|
|
||||||
this.state = {
|
|
||||||
selectAll: false,
|
|
||||||
orderBy: this.orderBy,
|
|
||||||
paginatedItemLimit: PaginationService.getPaginationLimit(this.tableKey),
|
|
||||||
displayTextFilter: false,
|
|
||||||
selectedItemCount: 0,
|
|
||||||
selectedItems: []
|
|
||||||
};
|
|
||||||
|
|
||||||
this.filters = {
|
this.filters = {
|
||||||
usage: {
|
state: {
|
||||||
open: false,
|
open: false,
|
||||||
enabled: false,
|
enabled: false,
|
||||||
showUsedVolumes: true,
|
showUsedVolumes: true,
|
||||||
|
@ -22,62 +15,29 @@ function (PaginationService, DatatableService) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.onTextFilterChange = function() {
|
|
||||||
DatatableService.setDataTableTextFilters(this.tableKey, this.state.textFilter);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.changeOrderBy = function(orderField) {
|
|
||||||
this.state.reverseOrder = this.state.orderBy === orderField ? !this.state.reverseOrder : false;
|
|
||||||
this.state.orderBy = orderField;
|
|
||||||
DatatableService.setDataTableOrder(this.tableKey, orderField, this.state.reverseOrder);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.selectItem = function(item) {
|
|
||||||
if (item.Checked) {
|
|
||||||
this.state.selectedItemCount++;
|
|
||||||
this.state.selectedItems.push(item);
|
|
||||||
} else {
|
|
||||||
this.state.selectedItems.splice(this.state.selectedItems.indexOf(item), 1);
|
|
||||||
this.state.selectedItemCount--;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.selectAll = function() {
|
|
||||||
for (var i = 0; i < this.state.filteredDataSet.length; i++) {
|
|
||||||
var item = this.state.filteredDataSet[i];
|
|
||||||
if (item.Checked !== this.state.selectAll) {
|
|
||||||
item.Checked = this.state.selectAll;
|
|
||||||
this.selectItem(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.changePaginationLimit = function() {
|
|
||||||
PaginationService.setPaginationLimit(this.tableKey, this.state.paginatedItemLimit);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.applyFilters = function(value) {
|
this.applyFilters = function(value) {
|
||||||
var volume = value;
|
var volume = value;
|
||||||
var filters = ctrl.filters;
|
var filters = ctrl.filters;
|
||||||
if ((volume.dangling && filters.usage.showUnusedVolumes)
|
if ((volume.dangling && filters.state.showUnusedVolumes)
|
||||||
|| (!volume.dangling && filters.usage.showUsedVolumes)) {
|
|| (!volume.dangling && filters.state.showUsedVolumes)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
this.onUsageFilterChange = function() {
|
this.onstateFilterChange = function() {
|
||||||
var filters = this.filters.usage;
|
var filters = this.filters.state;
|
||||||
var filtered = false;
|
var filtered = false;
|
||||||
if (!filters.showUsedVolumes || !filters.showUnusedVolumes) {
|
if (!filters.showUsedVolumes || !filters.showUnusedVolumes) {
|
||||||
filtered = true;
|
filtered = true;
|
||||||
}
|
}
|
||||||
this.filters.usage.enabled = filtered;
|
this.filters.state.enabled = filtered;
|
||||||
DatatableService.setDataTableFilters(this.tableKey, this.filters);
|
DatatableService.setDataTableFilters(this.tableKey, this.filters);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.$onInit = function() {
|
this.$onInit = function() {
|
||||||
setDefaults(this);
|
this.setDefaults();
|
||||||
|
this.prepareTableFromDataset();
|
||||||
|
|
||||||
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
|
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
|
||||||
if (storedOrder !== null) {
|
if (storedOrder !== null) {
|
||||||
|
@ -85,20 +45,26 @@ function (PaginationService, DatatableService) {
|
||||||
this.state.orderBy = storedOrder.orderBy;
|
this.state.orderBy = storedOrder.orderBy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var textFilter = DatatableService.getDataTableTextFilters(this.tableKey);
|
||||||
|
if (textFilter !== null) {
|
||||||
|
this.state.textFilter = textFilter;
|
||||||
|
this.onTextFilterChange();
|
||||||
|
}
|
||||||
|
|
||||||
var storedFilters = DatatableService.getDataTableFilters(this.tableKey);
|
var storedFilters = DatatableService.getDataTableFilters(this.tableKey);
|
||||||
if (storedFilters !== null) {
|
if (storedFilters !== null) {
|
||||||
this.filters = storedFilters;
|
this.filters = storedFilters;
|
||||||
}
|
}
|
||||||
this.filters.usage.open = false;
|
if (this.filters && this.filters.state) {
|
||||||
|
this.filters.state.open = false;
|
||||||
var textFilter = DatatableService.getDataTableTextFilters(this.tableKey);
|
|
||||||
if (textFilter !== null) {
|
|
||||||
this.state.textFilter = textFilter;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var storedSettings = DatatableService.getDataTableSettings(this.tableKey);
|
||||||
|
if (storedSettings !== null) {
|
||||||
|
this.settings = storedSettings;
|
||||||
|
this.settings.open = false;
|
||||||
|
}
|
||||||
|
this.onSettingsRepeaterChange();
|
||||||
|
this.state.orderBy = this.orderBy;
|
||||||
};
|
};
|
||||||
|
|
||||||
function setDefaults(ctrl) {
|
|
||||||
ctrl.showTextFilter = ctrl.showTextFilter ? ctrl.showTextFilter : false;
|
|
||||||
ctrl.state.reverseOrder = ctrl.reverseOrder ? ctrl.reverseOrder : false;
|
|
||||||
}
|
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -29,6 +29,10 @@
|
||||||
<td>Network Plugins</td>
|
<td>Network Plugins</td>
|
||||||
<td>{{ $ctrl.engine.networkPlugins | arraytostr: ', ' }}</td>
|
<td>{{ $ctrl.engine.networkPlugins | arraytostr: ', ' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr ng-if="$ctrl.engine.engineLabels.length">
|
||||||
|
<td>Engine Labels</td>
|
||||||
|
<td>{{ $ctrl.engine.engineLabels | labelsToStr:', ' }}</td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</rd-widget-body>
|
</rd-widget-body>
|
||||||
|
|
|
@ -26,10 +26,6 @@
|
||||||
<td><span class="label label-{{ $ctrl.details.status | nodestatusbadge }}">{{
|
<td><span class="label label-{{ $ctrl.details.status | nodestatusbadge }}">{{
|
||||||
$ctrl.details.status }}</span></td>
|
$ctrl.details.status }}</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr ng-if=" $ctrl.details.engineLabels.length">
|
|
||||||
<td>Engine Labels</td>
|
|
||||||
<td>{{ $ctrl.details.engineLabels | labelsToStr:', ' }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<div class="nopadding">
|
<div class="nopadding">
|
||||||
|
|
|
@ -13,7 +13,7 @@ angular.module('portainer.docker')
|
||||||
agentProxy: false
|
agentProxy: false
|
||||||
};
|
};
|
||||||
|
|
||||||
if (type === 2) {
|
if (type === 2 || type === 4) {
|
||||||
mode.agentProxy = true;
|
mode.agentProxy = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { logsHandler, genericHandler } from "./response/handlers";
|
import {genericHandler, logsHandler} from './response/handlers';
|
||||||
|
|
||||||
angular.module('portainer.docker')
|
angular.module('portainer.docker')
|
||||||
.factory('Container', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', 'ContainersInterceptor',
|
.factory('Container', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', 'ContainersInterceptor',
|
||||||
|
@ -11,7 +11,7 @@ function ContainerFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, C
|
||||||
{
|
{
|
||||||
query: {
|
query: {
|
||||||
method: 'GET', params: { all: 0, action: 'json', filters: '@filters' },
|
method: 'GET', params: { all: 0, action: 'json', filters: '@filters' },
|
||||||
isArray: true, interceptor: ContainersInterceptor, timeout: 10000
|
isArray: true, interceptor: ContainersInterceptor, timeout: 15000
|
||||||
},
|
},
|
||||||
get: {
|
get: {
|
||||||
method: 'GET', params: { action: 'json' }
|
method: 'GET', params: { action: 'json' }
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { jsonObjectsToArrayHandler, deleteImageHandler } from './response/handlers';
|
import {deleteImageHandler, jsonObjectsToArrayHandler} from './response/handlers';
|
||||||
import {imageGetResponse} from './response/image';
|
import {imageGetResponse} from './response/image';
|
||||||
|
|
||||||
angular.module('portainer.docker')
|
angular.module('portainer.docker')
|
||||||
|
@ -10,7 +10,7 @@ function ImageFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, HttpR
|
||||||
endpointId: EndpointProvider.endpointID
|
endpointId: EndpointProvider.endpointID
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
query: {method: 'GET', params: {all: 0, action: 'json'}, isArray: true, interceptor: ImagesInterceptor, timeout: 10000},
|
query: {method: 'GET', params: {all: 0, action: 'json'}, isArray: true, interceptor: ImagesInterceptor, timeout: 15000},
|
||||||
get: {method: 'GET', params: {action: 'json'}},
|
get: {method: 'GET', params: {action: 'json'}},
|
||||||
search: {method: 'GET', params: {action: 'search'}},
|
search: {method: 'GET', params: {action: 'search'}},
|
||||||
history: {method: 'GET', params: {action: 'history'}, isArray: true},
|
history: {method: 'GET', params: {action: 'history'}, isArray: true},
|
||||||
|
|
|
@ -10,7 +10,7 @@ function NetworkFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, Net
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
query: {
|
query: {
|
||||||
method: 'GET', isArray: true, interceptor: NetworksInterceptor, timeout: 10000
|
method: 'GET', isArray: true, interceptor: NetworksInterceptor, timeout: 15000
|
||||||
},
|
},
|
||||||
get: {
|
get: {
|
||||||
method: 'GET'
|
method: 'GET'
|
||||||
|
|
|
@ -10,7 +10,7 @@ angular.module('portainer.docker')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
info: {
|
info: {
|
||||||
method: 'GET', params: { action: 'info' }, timeout: 10000, interceptor: InfoInterceptor
|
method: 'GET', params: { action: 'info' }, timeout: 15000, interceptor: InfoInterceptor
|
||||||
},
|
},
|
||||||
version: { method: 'GET', params: { action: 'version' }, timeout: 4500, interceptor: VersionInterceptor },
|
version: { method: 'GET', params: { action: 'version' }, timeout: 4500, interceptor: VersionInterceptor },
|
||||||
events: {
|
events: {
|
||||||
|
|
|
@ -7,7 +7,7 @@ angular.module('portainer.docker')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ping: {
|
ping: {
|
||||||
method: 'GET', params: { action: '_ping', endpointId: '@endpointId' }, timeout: 10000
|
method: 'GET', params: { action: '_ping', endpointId: '@endpointId' }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -9,7 +9,7 @@ angular.module('portainer.docker')
|
||||||
endpointId: EndpointProvider.endpointID
|
endpointId: EndpointProvider.endpointID
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
query: { method: 'GET', interceptor: VolumesInterceptor, timeout: 10000},
|
query: { method: 'GET', interceptor: VolumesInterceptor, timeout: 15000},
|
||||||
get: { method: 'GET', params: {id: '@id'} },
|
get: { method: 'GET', params: {id: '@id'} },
|
||||||
create: {method: 'POST', params: {action: 'create'}, transformResponse: genericHandler, ignoreLoadingBar: true},
|
create: {method: 'POST', params: {action: 'create'}, transformResponse: genericHandler, ignoreLoadingBar: true},
|
||||||
remove: {
|
remove: {
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
order-by="Name"
|
order-by="Name"
|
||||||
show-ownership-column="applicationState.application.authentication"
|
show-ownership-column="applicationState.application.authentication"
|
||||||
remove-action="ctrl.removeAction"
|
remove-action="ctrl.removeAction"
|
||||||
|
refresh-callback="ctrl.getConfigs"
|
||||||
></configs-datatable>
|
></configs-datatable>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,16 +3,23 @@ import angular from 'angular';
|
||||||
class ConfigsController {
|
class ConfigsController {
|
||||||
|
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
constructor($state, ConfigService, Notifications) {
|
constructor($state, ConfigService, Notifications, $async) {
|
||||||
this.$state = $state;
|
this.$state = $state;
|
||||||
this.ConfigService = ConfigService;
|
this.ConfigService = ConfigService;
|
||||||
this.Notifications = Notifications;
|
this.Notifications = Notifications;
|
||||||
|
this.$async = $async;
|
||||||
|
|
||||||
this.removeAction = this.removeAction.bind(this);
|
this.removeAction = this.removeAction.bind(this);
|
||||||
|
this.removeActionAsync = this.removeActionAsync.bind(this);
|
||||||
|
this.getConfigs = this.getConfigs.bind(this);
|
||||||
|
this.getConfigsAsync = this.getConfigsAsync.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
async $onInit() {
|
getConfigs() {
|
||||||
this.configs = [];
|
return this.$async(this.getConfigsAsync);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getConfigsAsync() {
|
||||||
try {
|
try {
|
||||||
this.configs = await this.ConfigService.configs();
|
this.configs = await this.ConfigService.configs();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -20,7 +27,16 @@ class ConfigsController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeAction(selectedItems) {
|
async $onInit() {
|
||||||
|
this.configs = [];
|
||||||
|
this.getConfigs();
|
||||||
|
}
|
||||||
|
|
||||||
|
removeAction(selectedItems) {
|
||||||
|
return this.$async(this.removeActionAsync, selectedItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeActionAsync(selectedItems) {
|
||||||
let actionCount = selectedItems.length;
|
let actionCount = selectedItems.length;
|
||||||
for (const config of selectedItems) {
|
for (const config of selectedItems) {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -5,7 +5,7 @@ import angular from "angular";
|
||||||
|
|
||||||
class CreateConfigController {
|
class CreateConfigController {
|
||||||
/* @ngInject */
|
/* @ngInject */
|
||||||
constructor($state, $transition$, Notifications, ConfigService, Authentication, FormValidator, ResourceControlService) {
|
constructor($async, $state, $transition$, Notifications, ConfigService, Authentication, FormValidator, ResourceControlService) {
|
||||||
this.$state = $state;
|
this.$state = $state;
|
||||||
this.$transition$ = $transition$;
|
this.$transition$ = $transition$;
|
||||||
this.Notifications = Notifications;
|
this.Notifications = Notifications;
|
||||||
|
@ -13,6 +13,7 @@ class CreateConfigController {
|
||||||
this.Authentication = Authentication;
|
this.Authentication = Authentication;
|
||||||
this.FormValidator = FormValidator;
|
this.FormValidator = FormValidator;
|
||||||
this.ResourceControlService = ResourceControlService;
|
this.ResourceControlService = ResourceControlService;
|
||||||
|
this.$async = $async;
|
||||||
|
|
||||||
this.formValues = {
|
this.formValues = {
|
||||||
Name: "",
|
Name: "",
|
||||||
|
@ -26,6 +27,30 @@ class CreateConfigController {
|
||||||
};
|
};
|
||||||
|
|
||||||
this.editorUpdate = this.editorUpdate.bind(this);
|
this.editorUpdate = this.editorUpdate.bind(this);
|
||||||
|
this.createAsync = this.createAsync.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
async $onInit() {
|
||||||
|
if (!this.$transition$.params().id) {
|
||||||
|
this.formValues.displayCodeEditor = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let data = await this.ConfigService.config(this.$transition$.params().id);
|
||||||
|
this.formValues.Name = data.Name + "_copy";
|
||||||
|
this.formValues.Data = data.Data;
|
||||||
|
let labels = _.keys(data.Labels);
|
||||||
|
for (let i = 0; i < labels.length; i++) {
|
||||||
|
let labelName = labels[i];
|
||||||
|
let labelValue = data.Labels[labelName];
|
||||||
|
this.formValues.Labels.push({ name: labelName, value: labelValue });
|
||||||
|
}
|
||||||
|
this.formValues.displayCodeEditor = true;
|
||||||
|
} catch (err) {
|
||||||
|
this.formValues.displayCodeEditor = true;
|
||||||
|
this.Notifications.error("Failure", err, "Unable to clone config");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addLabel() {
|
addLabel() {
|
||||||
|
@ -74,7 +99,11 @@ class CreateConfigController {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async create() {
|
create() {
|
||||||
|
return this.$async(this.createAsync);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createAsync() {
|
||||||
let accessControlData = this.formValues.AccessControlData;
|
let accessControlData = this.formValues.AccessControlData;
|
||||||
let userDetails = this.Authentication.getUserDetails();
|
let userDetails = this.Authentication.getUserDetails();
|
||||||
let isAdmin = this.Authentication.isAdmin();
|
let isAdmin = this.Authentication.isAdmin();
|
||||||
|
@ -111,29 +140,6 @@ class CreateConfigController {
|
||||||
editorUpdate(cm) {
|
editorUpdate(cm) {
|
||||||
this.formValues.ConfigContent = cm.getValue();
|
this.formValues.ConfigContent = cm.getValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
async $onInit() {
|
|
||||||
if (!this.$transition$.params().id) {
|
|
||||||
this.formValues.displayCodeEditor = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
let data = await this.ConfigService.config(this.$transition$.params().id);
|
|
||||||
this.formValues.Name = data.Name + "_copy";
|
|
||||||
this.formValues.Data = data.Data;
|
|
||||||
let labels = _.keys(data.Labels);
|
|
||||||
for (let i = 0; i < labels.length; i++) {
|
|
||||||
let labelName = labels[i];
|
|
||||||
let labelValue = data.Labels[labelName];
|
|
||||||
this.formValues.Labels.push({ name: labelName, value: labelValue });
|
|
||||||
}
|
|
||||||
this.formValues.displayCodeEditor = true;
|
|
||||||
} catch (err) {
|
|
||||||
this.formValues.displayCodeEditor = true;
|
|
||||||
this.Notifications.error("Failure", err, "Unable to clone config");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CreateConfigController;
|
export default CreateConfigController;
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
show-host-column="applicationState.endpoint.mode.agentProxy && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'"
|
show-host-column="applicationState.endpoint.mode.agentProxy && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE'"
|
||||||
show-add-action="true"
|
show-add-action="true"
|
||||||
offline-mode="offlineMode"
|
offline-mode="offlineMode"
|
||||||
|
refresh-callback="getContainers"
|
||||||
></containers-datatable>
|
></containers-datatable>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue