mirror of
https://github.com/portainer/portainer.git
synced 2025-07-23 15:29:42 +02:00
feat(home): add a new home view (#2033)
This commit is contained in:
parent
a94f2ee7b8
commit
b6792461a4
46 changed files with 994 additions and 399 deletions
|
@ -21,6 +21,7 @@ const (
|
||||||
errEndpointsFileNotFound = portainer.Error("Unable to locate external endpoints file")
|
errEndpointsFileNotFound = portainer.Error("Unable to locate external endpoints file")
|
||||||
errTemplateFileNotFound = portainer.Error("Unable to locate template file on disk")
|
errTemplateFileNotFound = portainer.Error("Unable to locate template file on disk")
|
||||||
errInvalidSyncInterval = portainer.Error("Invalid synchronization interval")
|
errInvalidSyncInterval = portainer.Error("Invalid synchronization interval")
|
||||||
|
errInvalidSnapshotInterval = portainer.Error("Invalid snapshot interval")
|
||||||
errEndpointExcludeExternal = portainer.Error("Cannot use the -H flag mutually with --external-endpoints")
|
errEndpointExcludeExternal = portainer.Error("Cannot use the -H flag mutually with --external-endpoints")
|
||||||
errNoAuthExcludeAdminPassword = portainer.Error("Cannot use --no-auth with --admin-password or --admin-password-file")
|
errNoAuthExcludeAdminPassword = portainer.Error("Cannot use --no-auth with --admin-password or --admin-password-file")
|
||||||
errAdminPassExcludeAdminPassFile = portainer.Error("Cannot use --admin-password with --admin-password-file")
|
errAdminPassExcludeAdminPassFile = portainer.Error("Cannot use --admin-password with --admin-password-file")
|
||||||
|
@ -47,6 +48,8 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
||||||
SSLCert: kingpin.Flag("sslcert", "Path to the SSL certificate used to secure the Portainer instance").Default(defaultSSLCertPath).String(),
|
SSLCert: kingpin.Flag("sslcert", "Path to the SSL certificate used to secure the Portainer instance").Default(defaultSSLCertPath).String(),
|
||||||
SSLKey: kingpin.Flag("sslkey", "Path to the SSL key used to secure the Portainer instance").Default(defaultSSLKeyPath).String(),
|
SSLKey: kingpin.Flag("sslkey", "Path to the SSL key used to secure the Portainer instance").Default(defaultSSLKeyPath).String(),
|
||||||
SyncInterval: kingpin.Flag("sync-interval", "Duration between each synchronization via the external endpoints source").Default(defaultSyncInterval).String(),
|
SyncInterval: kingpin.Flag("sync-interval", "Duration between each synchronization via the external endpoints source").Default(defaultSyncInterval).String(),
|
||||||
|
Snapshot: kingpin.Flag("snapshot", "Start a background job to create endpoint snapshots").Default(defaultSnapshot).Bool(),
|
||||||
|
SnapshotInterval: kingpin.Flag("snapshot-interval", "Duration between each endpoint snapshot job").Default(defaultSnapshotInterval).String(),
|
||||||
AdminPassword: kingpin.Flag("admin-password", "Hashed admin password").String(),
|
AdminPassword: kingpin.Flag("admin-password", "Hashed admin password").String(),
|
||||||
AdminPasswordFile: kingpin.Flag("admin-password-file", "Path to the file containing the password for the admin user").String(),
|
AdminPasswordFile: kingpin.Flag("admin-password-file", "Path to the file containing the password for the admin user").String(),
|
||||||
Labels: pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')),
|
Labels: pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')),
|
||||||
|
@ -95,6 +98,11 @@ func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = validateSnapshotInterval(*flags.SnapshotInterval)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if *flags.NoAuth && (*flags.AdminPassword != "" || *flags.AdminPasswordFile != "") {
|
if *flags.NoAuth && (*flags.AdminPassword != "" || *flags.AdminPasswordFile != "") {
|
||||||
return errNoAuthExcludeAdminPassword
|
return errNoAuthExcludeAdminPassword
|
||||||
}
|
}
|
||||||
|
@ -156,3 +164,13 @@ func validateSyncInterval(syncInterval string) error {
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateSnapshotInterval(snapshotInterval string) error {
|
||||||
|
if snapshotInterval != defaultSnapshotInterval {
|
||||||
|
_, err := time.ParseDuration(snapshotInterval)
|
||||||
|
if err != nil {
|
||||||
|
return errInvalidSnapshotInterval
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -3,19 +3,21 @@
|
||||||
package cli
|
package cli
|
||||||
|
|
||||||
const (
|
const (
|
||||||
defaultBindAddress = ":9000"
|
defaultBindAddress = ":9000"
|
||||||
defaultDataDirectory = "/data"
|
defaultDataDirectory = "/data"
|
||||||
defaultAssetsDirectory = "./"
|
defaultAssetsDirectory = "./"
|
||||||
defaultNoAuth = "false"
|
defaultNoAuth = "false"
|
||||||
defaultNoAnalytics = "false"
|
defaultNoAnalytics = "false"
|
||||||
defaultTLS = "false"
|
defaultTLS = "false"
|
||||||
defaultTLSSkipVerify = "false"
|
defaultTLSSkipVerify = "false"
|
||||||
defaultTLSCACertPath = "/certs/ca.pem"
|
defaultTLSCACertPath = "/certs/ca.pem"
|
||||||
defaultTLSCertPath = "/certs/cert.pem"
|
defaultTLSCertPath = "/certs/cert.pem"
|
||||||
defaultTLSKeyPath = "/certs/key.pem"
|
defaultTLSKeyPath = "/certs/key.pem"
|
||||||
defaultSSL = "false"
|
defaultSSL = "false"
|
||||||
defaultSSLCertPath = "/certs/portainer.crt"
|
defaultSSLCertPath = "/certs/portainer.crt"
|
||||||
defaultSSLKeyPath = "/certs/portainer.key"
|
defaultSSLKeyPath = "/certs/portainer.key"
|
||||||
defaultSyncInterval = "60s"
|
defaultSyncInterval = "60s"
|
||||||
defaultTemplateFile = "/templates.json"
|
defaultSnapshot = "true"
|
||||||
|
defaultSnapshotInterval = "5m"
|
||||||
|
defaultTemplateFile = "/templates.json"
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,19 +1,21 @@
|
||||||
package cli
|
package cli
|
||||||
|
|
||||||
const (
|
const (
|
||||||
defaultBindAddress = ":9000"
|
defaultBindAddress = ":9000"
|
||||||
defaultDataDirectory = "C:\\data"
|
defaultDataDirectory = "C:\\data"
|
||||||
defaultAssetsDirectory = "./"
|
defaultAssetsDirectory = "./"
|
||||||
defaultNoAuth = "false"
|
defaultNoAuth = "false"
|
||||||
defaultNoAnalytics = "false"
|
defaultNoAnalytics = "false"
|
||||||
defaultTLS = "false"
|
defaultTLS = "false"
|
||||||
defaultTLSSkipVerify = "false"
|
defaultTLSSkipVerify = "false"
|
||||||
defaultTLSCACertPath = "C:\\certs\\ca.pem"
|
defaultTLSCACertPath = "C:\\certs\\ca.pem"
|
||||||
defaultTLSCertPath = "C:\\certs\\cert.pem"
|
defaultTLSCertPath = "C:\\certs\\cert.pem"
|
||||||
defaultTLSKeyPath = "C:\\certs\\key.pem"
|
defaultTLSKeyPath = "C:\\certs\\key.pem"
|
||||||
defaultSSL = "false"
|
defaultSSL = "false"
|
||||||
defaultSSLCertPath = "C:\\certs\\portainer.crt"
|
defaultSSLCertPath = "C:\\certs\\portainer.crt"
|
||||||
defaultSSLKeyPath = "C:\\certs\\portainer.key"
|
defaultSSLKeyPath = "C:\\certs\\portainer.key"
|
||||||
defaultSyncInterval = "60s"
|
defaultSyncInterval = "60s"
|
||||||
defaultTemplateFile = "/templates.json"
|
defaultSnapshot = "true"
|
||||||
|
defaultSnapshotInterval = "5m"
|
||||||
|
defaultTemplateFile = "/templates.json"
|
||||||
)
|
)
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"github.com/portainer/portainer/cli"
|
"github.com/portainer/portainer/cli"
|
||||||
"github.com/portainer/portainer/cron"
|
"github.com/portainer/portainer/cron"
|
||||||
"github.com/portainer/portainer/crypto"
|
"github.com/portainer/portainer/crypto"
|
||||||
|
"github.com/portainer/portainer/docker"
|
||||||
"github.com/portainer/portainer/exec"
|
"github.com/portainer/portainer/exec"
|
||||||
"github.com/portainer/portainer/filesystem"
|
"github.com/portainer/portainer/filesystem"
|
||||||
"github.com/portainer/portainer/git"
|
"github.com/portainer/portainer/git"
|
||||||
|
@ -101,25 +102,37 @@ func initGitService() portainer.GitService {
|
||||||
return &git.Service{}
|
return &git.Service{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func initEndpointWatcher(endpointService portainer.EndpointService, externalEnpointFile string, syncInterval string) bool {
|
func initClientFactory(signatureService portainer.DigitalSignatureService) *docker.ClientFactory {
|
||||||
authorizeEndpointMgmt := true
|
return docker.NewClientFactory(signatureService)
|
||||||
if externalEnpointFile != "" {
|
|
||||||
authorizeEndpointMgmt = false
|
|
||||||
log.Println("Using external endpoint definition. Endpoint management via the API will be disabled.")
|
|
||||||
endpointWatcher := cron.NewWatcher(endpointService, syncInterval)
|
|
||||||
err := endpointWatcher.WatchEndpointFile(externalEnpointFile)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return authorizeEndpointMgmt
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func initStatus(authorizeEndpointMgmt bool, flags *portainer.CLIFlags) *portainer.Status {
|
func initJobScheduler(endpointService portainer.EndpointService, clientFactory *docker.ClientFactory, flags *portainer.CLIFlags) (portainer.JobScheduler, error) {
|
||||||
|
jobScheduler := cron.NewJobScheduler(endpointService, clientFactory)
|
||||||
|
|
||||||
|
if *flags.ExternalEndpoints != "" {
|
||||||
|
log.Println("Using external endpoint definition. Endpoint management via the API will be disabled.")
|
||||||
|
err := jobScheduler.ScheduleEndpointSyncJob(*flags.ExternalEndpoints, *flags.SyncInterval)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if *flags.Snapshot {
|
||||||
|
err := jobScheduler.ScheduleSnapshotJob(*flags.SnapshotInterval)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return jobScheduler, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func initStatus(endpointManagement, snapshot bool, flags *portainer.CLIFlags) *portainer.Status {
|
||||||
return &portainer.Status{
|
return &portainer.Status{
|
||||||
Analytics: !*flags.NoAnalytics,
|
Analytics: !*flags.NoAnalytics,
|
||||||
Authentication: !*flags.NoAuth,
|
Authentication: !*flags.NoAuth,
|
||||||
EndpointManagement: authorizeEndpointMgmt,
|
EndpointManagement: endpointManagement,
|
||||||
|
Snapshot: snapshot,
|
||||||
Version: portainer.APIVersion,
|
Version: portainer.APIVersion,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -154,6 +167,7 @@ func initSettings(settingsService portainer.SettingsService, flags *portainer.CL
|
||||||
},
|
},
|
||||||
AllowBindMountsForRegularUsers: true,
|
AllowBindMountsForRegularUsers: true,
|
||||||
AllowPrivilegedModeForRegularUsers: true,
|
AllowPrivilegedModeForRegularUsers: true,
|
||||||
|
SnapshotInterval: *flags.SnapshotInterval,
|
||||||
}
|
}
|
||||||
|
|
||||||
if *flags.Labels != nil {
|
if *flags.Labels != nil {
|
||||||
|
@ -283,6 +297,8 @@ func createTLSSecuredEndpoint(flags *portainer.CLIFlags, endpointService portain
|
||||||
AuthorizedTeams: []portainer.TeamID{},
|
AuthorizedTeams: []portainer.TeamID{},
|
||||||
Extensions: []portainer.EndpointExtension{},
|
Extensions: []portainer.EndpointExtension{},
|
||||||
Tags: []string{},
|
Tags: []string{},
|
||||||
|
Status: portainer.EndpointStatusUp,
|
||||||
|
Snapshots: []portainer.Snapshot{},
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(endpoint.URL, "tcp://") {
|
if strings.HasPrefix(endpoint.URL, "tcp://") {
|
||||||
|
@ -322,6 +338,8 @@ func createUnsecuredEndpoint(endpointURL string, endpointService portainer.Endpo
|
||||||
AuthorizedTeams: []portainer.TeamID{},
|
AuthorizedTeams: []portainer.TeamID{},
|
||||||
Extensions: []portainer.EndpointExtension{},
|
Extensions: []portainer.EndpointExtension{},
|
||||||
Tags: []string{},
|
Tags: []string{},
|
||||||
|
Status: portainer.EndpointStatusUp,
|
||||||
|
Snapshots: []portainer.Snapshot{},
|
||||||
}
|
}
|
||||||
|
|
||||||
return endpointService.CreateEndpoint(endpoint)
|
return endpointService.CreateEndpoint(endpoint)
|
||||||
|
@ -366,9 +384,21 @@ func main() {
|
||||||
|
|
||||||
gitService := initGitService()
|
gitService := initGitService()
|
||||||
|
|
||||||
authorizeEndpointMgmt := initEndpointWatcher(store.EndpointService, *flags.ExternalEndpoints, *flags.SyncInterval)
|
clientFactory := initClientFactory(digitalSignatureService)
|
||||||
|
|
||||||
err := initKeyPair(fileService, digitalSignatureService)
|
jobScheduler, err := initJobScheduler(store.EndpointService, clientFactory, flags)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
jobScheduler.Start()
|
||||||
|
|
||||||
|
endpointManagement := true
|
||||||
|
if *flags.ExternalEndpoints != "" {
|
||||||
|
endpointManagement = false
|
||||||
|
}
|
||||||
|
|
||||||
|
err = initKeyPair(fileService, digitalSignatureService)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -395,7 +425,7 @@ func main() {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
applicationStatus := initStatus(authorizeEndpointMgmt, flags)
|
applicationStatus := initStatus(endpointManagement, *flags.Snapshot, flags)
|
||||||
|
|
||||||
err = initEndpoint(flags, store.EndpointService)
|
err = initEndpoint(flags, store.EndpointService)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -443,7 +473,7 @@ func main() {
|
||||||
BindAddress: *flags.Addr,
|
BindAddress: *flags.Addr,
|
||||||
AssetsPath: *flags.Assets,
|
AssetsPath: *flags.Assets,
|
||||||
AuthDisabled: *flags.NoAuth,
|
AuthDisabled: *flags.NoAuth,
|
||||||
EndpointManagement: authorizeEndpointMgmt,
|
EndpointManagement: endpointManagement,
|
||||||
UserService: store.UserService,
|
UserService: store.UserService,
|
||||||
TeamService: store.TeamService,
|
TeamService: store.TeamService,
|
||||||
TeamMembershipService: store.TeamMembershipService,
|
TeamMembershipService: store.TeamMembershipService,
|
||||||
|
@ -464,6 +494,7 @@ func main() {
|
||||||
LDAPService: ldapService,
|
LDAPService: ldapService,
|
||||||
GitService: gitService,
|
GitService: gitService,
|
||||||
SignatureService: digitalSignatureService,
|
SignatureService: digitalSignatureService,
|
||||||
|
JobScheduler: jobScheduler,
|
||||||
SSL: *flags.SSL,
|
SSL: *flags.SSL,
|
||||||
SSLCert: *flags.SSLCert,
|
SSLCert: *flags.SSLCert,
|
||||||
SSLKey: *flags.SSLKey,
|
SSLKey: *flags.SSLKey,
|
||||||
|
|
60
api/cron/job_endpoint_snapshot.go
Normal file
60
api/cron/job_endpoint_snapshot.go
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
package cron
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/portainer/portainer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
endpointSnapshotJob struct {
|
||||||
|
endpointService portainer.EndpointService
|
||||||
|
snapshotter portainer.Snapshotter
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func newEndpointSnapshotJob(endpointService portainer.EndpointService, snapshotter portainer.Snapshotter) endpointSnapshotJob {
|
||||||
|
return endpointSnapshotJob{
|
||||||
|
endpointService: endpointService,
|
||||||
|
snapshotter: snapshotter,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (job endpointSnapshotJob) Snapshot() error {
|
||||||
|
|
||||||
|
endpoints, err := job.endpointService.Endpoints()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, endpoint := range endpoints {
|
||||||
|
if endpoint.Type == portainer.AzureEnvironment {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshot, err := job.snapshotter.CreateSnapshot(&endpoint)
|
||||||
|
endpoint.Status = portainer.EndpointStatusUp
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("cron error: endpoint snapshot error (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err)
|
||||||
|
endpoint.Status = portainer.EndpointStatusDown
|
||||||
|
}
|
||||||
|
|
||||||
|
if snapshot != nil {
|
||||||
|
endpoint.Snapshots = []portainer.Snapshot{*snapshot}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = job.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (job endpointSnapshotJob) Run() {
|
||||||
|
err := job.Snapshot()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("cron error: snapshot job error (err=%s)\n", err)
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/portainer/portainer"
|
"github.com/portainer/portainer"
|
||||||
|
@ -12,7 +11,6 @@ import (
|
||||||
|
|
||||||
type (
|
type (
|
||||||
endpointSyncJob struct {
|
endpointSyncJob struct {
|
||||||
logger *log.Logger
|
|
||||||
endpointService portainer.EndpointService
|
endpointService portainer.EndpointService
|
||||||
endpointFilePath string
|
endpointFilePath string
|
||||||
}
|
}
|
||||||
|
@ -41,15 +39,14 @@ const (
|
||||||
|
|
||||||
func newEndpointSyncJob(endpointFilePath string, endpointService portainer.EndpointService) endpointSyncJob {
|
func newEndpointSyncJob(endpointFilePath string, endpointService portainer.EndpointService) endpointSyncJob {
|
||||||
return endpointSyncJob{
|
return endpointSyncJob{
|
||||||
logger: log.New(os.Stderr, "", log.LstdFlags),
|
|
||||||
endpointService: endpointService,
|
endpointService: endpointService,
|
||||||
endpointFilePath: endpointFilePath,
|
endpointFilePath: endpointFilePath,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func endpointSyncError(err error, logger *log.Logger) bool {
|
func endpointSyncError(err error) bool {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Printf("Endpoint synchronization error: %s", err)
|
log.Printf("cron error: synchronization job error (err=%s)\n", err)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
@ -140,23 +137,23 @@ func (job endpointSyncJob) prepareSyncData(storedEndpoints, fileEndpoints []port
|
||||||
if fidx != -1 {
|
if fidx != -1 {
|
||||||
endpoint := mergeEndpointIfRequired(&storedEndpoints[idx], &fileEndpoints[fidx])
|
endpoint := mergeEndpointIfRequired(&storedEndpoints[idx], &fileEndpoints[fidx])
|
||||||
if endpoint != nil {
|
if endpoint != nil {
|
||||||
job.logger.Printf("New definition for a stored endpoint found in file, updating database. [name: %v] [url: %v]\n", endpoint.Name, endpoint.URL)
|
log.Printf("New definition for a stored endpoint found in file, updating database. [name: %v] [url: %v]\n", endpoint.Name, endpoint.URL)
|
||||||
endpointsToUpdate = append(endpointsToUpdate, endpoint)
|
endpointsToUpdate = append(endpointsToUpdate, endpoint)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
job.logger.Printf("Stored endpoint not found in file (definition might be invalid), removing from database. [name: %v] [url: %v]", storedEndpoints[idx].Name, storedEndpoints[idx].URL)
|
log.Printf("Stored endpoint not found in file (definition might be invalid), removing from database. [name: %v] [url: %v]", storedEndpoints[idx].Name, storedEndpoints[idx].URL)
|
||||||
endpointsToDelete = append(endpointsToDelete, &storedEndpoints[idx])
|
endpointsToDelete = append(endpointsToDelete, &storedEndpoints[idx])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for idx, endpoint := range fileEndpoints {
|
for idx, endpoint := range fileEndpoints {
|
||||||
if !isValidEndpoint(&endpoint) {
|
if !isValidEndpoint(&endpoint) {
|
||||||
job.logger.Printf("Invalid file endpoint definition, skipping. [name: %v] [url: %v]", endpoint.Name, endpoint.URL)
|
log.Printf("Invalid file endpoint definition, skipping. [name: %v] [url: %v]", endpoint.Name, endpoint.URL)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
sidx := endpointExists(&fileEndpoints[idx], storedEndpoints)
|
sidx := endpointExists(&fileEndpoints[idx], storedEndpoints)
|
||||||
if sidx == -1 {
|
if sidx == -1 {
|
||||||
job.logger.Printf("File endpoint not found in database, adding to database. [name: %v] [url: %v]", fileEndpoints[idx].Name, fileEndpoints[idx].URL)
|
log.Printf("File endpoint not found in database, adding to database. [name: %v] [url: %v]", fileEndpoints[idx].Name, fileEndpoints[idx].URL)
|
||||||
endpointsToCreate = append(endpointsToCreate, &fileEndpoints[idx])
|
endpointsToCreate = append(endpointsToCreate, &fileEndpoints[idx])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -170,13 +167,13 @@ func (job endpointSyncJob) prepareSyncData(storedEndpoints, fileEndpoints []port
|
||||||
|
|
||||||
func (job endpointSyncJob) Sync() error {
|
func (job endpointSyncJob) Sync() error {
|
||||||
data, err := ioutil.ReadFile(job.endpointFilePath)
|
data, err := ioutil.ReadFile(job.endpointFilePath)
|
||||||
if endpointSyncError(err, job.logger) {
|
if endpointSyncError(err) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var fileEndpoints []fileEndpoint
|
var fileEndpoints []fileEndpoint
|
||||||
err = json.Unmarshal(data, &fileEndpoints)
|
err = json.Unmarshal(data, &fileEndpoints)
|
||||||
if endpointSyncError(err, job.logger) {
|
if endpointSyncError(err) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -185,7 +182,7 @@ func (job endpointSyncJob) Sync() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
storedEndpoints, err := job.endpointService.Endpoints()
|
storedEndpoints, err := job.endpointService.Endpoints()
|
||||||
if endpointSyncError(err, job.logger) {
|
if endpointSyncError(err) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -194,16 +191,16 @@ func (job endpointSyncJob) Sync() error {
|
||||||
sync := job.prepareSyncData(storedEndpoints, convertedFileEndpoints)
|
sync := job.prepareSyncData(storedEndpoints, convertedFileEndpoints)
|
||||||
if sync.requireSync() {
|
if sync.requireSync() {
|
||||||
err = job.endpointService.Synchronize(sync.endpointsToCreate, sync.endpointsToUpdate, sync.endpointsToDelete)
|
err = job.endpointService.Synchronize(sync.endpointsToCreate, sync.endpointsToUpdate, sync.endpointsToDelete)
|
||||||
if endpointSyncError(err, job.logger) {
|
if endpointSyncError(err) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
job.logger.Printf("Endpoint synchronization ended. [created: %v] [updated: %v] [deleted: %v]", len(sync.endpointsToCreate), len(sync.endpointsToUpdate), len(sync.endpointsToDelete))
|
log.Printf("Endpoint synchronization ended. [created: %v] [updated: %v] [deleted: %v]", len(sync.endpointsToCreate), len(sync.endpointsToUpdate), len(sync.endpointsToDelete))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (job endpointSyncJob) Run() {
|
func (job endpointSyncJob) Run() {
|
||||||
job.logger.Println("Endpoint synchronization job started.")
|
log.Println("cron: synchronization job started")
|
||||||
err := job.Sync()
|
err := job.Sync()
|
||||||
endpointSyncError(err, job.logger)
|
endpointSyncError(err)
|
||||||
}
|
}
|
87
api/cron/scheduler.go
Normal file
87
api/cron/scheduler.go
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
package cron
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/portainer/portainer"
|
||||||
|
"github.com/portainer/portainer/docker"
|
||||||
|
"github.com/robfig/cron"
|
||||||
|
)
|
||||||
|
|
||||||
|
// JobScheduler represents a service for managing crons.
|
||||||
|
type JobScheduler struct {
|
||||||
|
cron *cron.Cron
|
||||||
|
endpointService portainer.EndpointService
|
||||||
|
snapshotter portainer.Snapshotter
|
||||||
|
|
||||||
|
endpointFilePath string
|
||||||
|
endpointSyncInterval string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewJobScheduler initializes a new service.
|
||||||
|
func NewJobScheduler(endpointService portainer.EndpointService, clientFactory *docker.ClientFactory) *JobScheduler {
|
||||||
|
return &JobScheduler{
|
||||||
|
cron: cron.New(),
|
||||||
|
endpointService: endpointService,
|
||||||
|
snapshotter: docker.NewSnapshotter(clientFactory),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScheduleEndpointSyncJob schedules a cron job to synchronize the endpoints from a file
|
||||||
|
func (scheduler *JobScheduler) ScheduleEndpointSyncJob(endpointFilePath string, interval string) error {
|
||||||
|
|
||||||
|
scheduler.endpointFilePath = endpointFilePath
|
||||||
|
scheduler.endpointSyncInterval = interval
|
||||||
|
|
||||||
|
job := newEndpointSyncJob(endpointFilePath, scheduler.endpointService)
|
||||||
|
|
||||||
|
err := job.Sync()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return scheduler.cron.AddJob("@every "+interval, job)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScheduleSnapshotJob schedules a cron job to create endpoint snapshots
|
||||||
|
func (scheduler *JobScheduler) ScheduleSnapshotJob(interval string) error {
|
||||||
|
job := newEndpointSnapshotJob(scheduler.endpointService, scheduler.snapshotter)
|
||||||
|
|
||||||
|
err := job.Snapshot()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return scheduler.cron.AddJob("@every "+interval, job)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateSnapshotJob will update the schedules to match the new snapshot interval
|
||||||
|
func (scheduler *JobScheduler) UpdateSnapshotJob(interval string) {
|
||||||
|
// TODO: the cron library do not support removing/updating schedules.
|
||||||
|
// As a work-around we need to re-create the cron and reschedule the jobs.
|
||||||
|
// We should update the library.
|
||||||
|
jobs := scheduler.cron.Entries()
|
||||||
|
scheduler.cron.Stop()
|
||||||
|
|
||||||
|
scheduler.cron = cron.New()
|
||||||
|
|
||||||
|
for _, job := range jobs {
|
||||||
|
switch job.Job.(type) {
|
||||||
|
case endpointSnapshotJob:
|
||||||
|
scheduler.ScheduleSnapshotJob(interval)
|
||||||
|
case endpointSyncJob:
|
||||||
|
scheduler.ScheduleEndpointSyncJob(scheduler.endpointFilePath, scheduler.endpointSyncInterval)
|
||||||
|
default:
|
||||||
|
log.Println("Unsupported job")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduler.cron.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start starts the scheduled jobs
|
||||||
|
func (scheduler *JobScheduler) Start() {
|
||||||
|
if len(scheduler.cron.Entries()) > 0 {
|
||||||
|
scheduler.cron.Start()
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,40 +0,0 @@
|
||||||
package cron
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/portainer/portainer"
|
|
||||||
"github.com/robfig/cron"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Watcher represents a service for managing crons.
|
|
||||||
type Watcher struct {
|
|
||||||
Cron *cron.Cron
|
|
||||||
EndpointService portainer.EndpointService
|
|
||||||
syncInterval string
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewWatcher initializes a new service.
|
|
||||||
func NewWatcher(endpointService portainer.EndpointService, syncInterval string) *Watcher {
|
|
||||||
return &Watcher{
|
|
||||||
Cron: cron.New(),
|
|
||||||
EndpointService: endpointService,
|
|
||||||
syncInterval: syncInterval,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WatchEndpointFile starts a cron job to synchronize the endpoints from a file
|
|
||||||
func (watcher *Watcher) WatchEndpointFile(endpointFilePath string) error {
|
|
||||||
job := newEndpointSyncJob(endpointFilePath, watcher.EndpointService)
|
|
||||||
|
|
||||||
err := job.Sync()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = watcher.Cron.AddJob("@every "+watcher.syncInterval, job)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
watcher.Cron.Start()
|
|
||||||
return nil
|
|
||||||
}
|
|
103
api/docker/client.go
Normal file
103
api/docker/client.go
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
"github.com/portainer/portainer"
|
||||||
|
"github.com/portainer/portainer/crypto"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
unsupportedEnvironmentType = portainer.Error("Environment not supported")
|
||||||
|
)
|
||||||
|
|
||||||
|
// ClientFactory is used to create Docker clients
|
||||||
|
type ClientFactory struct {
|
||||||
|
signatureService portainer.DigitalSignatureService
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClientFactory returns a new instance of a ClientFactory
|
||||||
|
func NewClientFactory(signatureService portainer.DigitalSignatureService) *ClientFactory {
|
||||||
|
return &ClientFactory{
|
||||||
|
signatureService: signatureService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateClient is a generic function to create a Docker client based on
|
||||||
|
// a specific endpoint configuration
|
||||||
|
func (factory *ClientFactory) CreateClient(endpoint *portainer.Endpoint) (*client.Client, error) {
|
||||||
|
if endpoint.Type == portainer.AzureEnvironment {
|
||||||
|
return nil, unsupportedEnvironmentType
|
||||||
|
} else if endpoint.Type == portainer.AgentOnDockerEnvironment {
|
||||||
|
return createAgentClient(endpoint, factory.signatureService)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(endpoint.URL, "unix://") {
|
||||||
|
return createUnixSocketClient(endpoint)
|
||||||
|
}
|
||||||
|
return createTCPClient(endpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createUnixSocketClient(endpoint *portainer.Endpoint) (*client.Client, error) {
|
||||||
|
return client.NewClientWithOpts(
|
||||||
|
client.WithHost(endpoint.URL),
|
||||||
|
client.WithVersion(portainer.SupportedDockerAPIVersion),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createTCPClient(endpoint *portainer.Endpoint) (*client.Client, error) {
|
||||||
|
httpCli, err := httpClient(endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return client.NewClientWithOpts(
|
||||||
|
client.WithHost(endpoint.URL),
|
||||||
|
client.WithVersion(portainer.SupportedDockerAPIVersion),
|
||||||
|
client.WithHTTPClient(httpCli),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createAgentClient(endpoint *portainer.Endpoint, signatureService portainer.DigitalSignatureService) (*client.Client, error) {
|
||||||
|
httpCli, err := httpClient(endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
signature, err := signatureService.Sign(portainer.PortainerAgentSignatureMessage)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
headers := map[string]string{
|
||||||
|
portainer.PortainerAgentPublicKeyHeader: signatureService.EncodedPublicKey(),
|
||||||
|
portainer.PortainerAgentSignatureHeader: signature,
|
||||||
|
}
|
||||||
|
|
||||||
|
return client.NewClientWithOpts(
|
||||||
|
client.WithHost(endpoint.URL),
|
||||||
|
client.WithVersion(portainer.SupportedDockerAPIVersion),
|
||||||
|
client.WithHTTPClient(httpCli),
|
||||||
|
client.WithHTTPHeaders(headers),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func httpClient(endpoint *portainer.Endpoint) (*http.Client, error) {
|
||||||
|
transport := &http.Transport{}
|
||||||
|
|
||||||
|
if endpoint.TLSConfig.TLS {
|
||||||
|
tlsConfig, err := crypto.CreateTLSConfigurationFromDisk(endpoint.TLSConfig.TLSCACertPath, endpoint.TLSConfig.TLSCertPath, endpoint.TLSConfig.TLSKeyPath, endpoint.TLSConfig.TLSSkipVerify)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
transport.TLSClientConfig = tlsConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
return &http.Client{
|
||||||
|
Timeout: time.Second * 10,
|
||||||
|
Transport: transport,
|
||||||
|
}, nil
|
||||||
|
}
|
135
api/docker/snapshot.go
Normal file
135
api/docker/snapshot.go
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/filters"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
"github.com/portainer/portainer"
|
||||||
|
)
|
||||||
|
|
||||||
|
func snapshot(cli *client.Client) (*portainer.Snapshot, error) {
|
||||||
|
_, err := cli.Ping(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshot := &portainer.Snapshot{
|
||||||
|
StackCount: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = snapshotInfo(snapshot, cli)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if snapshot.Swarm {
|
||||||
|
err = snapshotSwarmServices(snapshot, cli)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = snapshotContainers(snapshot, cli)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = snapshotImages(snapshot, cli)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = snapshotVolumes(snapshot, cli)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshot.Time = time.Now().Unix()
|
||||||
|
return snapshot, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func snapshotInfo(snapshot *portainer.Snapshot, cli *client.Client) error {
|
||||||
|
info, err := cli.Info(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshot.Swarm = info.Swarm.ControlAvailable
|
||||||
|
snapshot.DockerVersion = info.ServerVersion
|
||||||
|
snapshot.TotalCPU = info.NCPU
|
||||||
|
snapshot.TotalMemory = info.MemTotal
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func snapshotSwarmServices(snapshot *portainer.Snapshot, cli *client.Client) error {
|
||||||
|
stacks := make(map[string]struct{})
|
||||||
|
|
||||||
|
services, err := cli.ServiceList(context.Background(), types.ServiceListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, service := range services {
|
||||||
|
for k, v := range service.Spec.Labels {
|
||||||
|
if k == "com.docker.stack.namespace" {
|
||||||
|
stacks[v] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshot.ServiceCount = len(services)
|
||||||
|
snapshot.StackCount += len(stacks)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func snapshotContainers(snapshot *portainer.Snapshot, cli *client.Client) error {
|
||||||
|
containers, err := cli.ContainerList(context.Background(), types.ContainerListOptions{All: true})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
runningContainers := 0
|
||||||
|
stoppedContainers := 0
|
||||||
|
stacks := make(map[string]struct{})
|
||||||
|
for _, container := range containers {
|
||||||
|
if container.State == "exited" {
|
||||||
|
stoppedContainers++
|
||||||
|
} else if container.State == "running" {
|
||||||
|
runningContainers++
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range container.Labels {
|
||||||
|
if k == "com.docker.compose.project" {
|
||||||
|
stacks[v] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshot.RunningContainerCount = runningContainers
|
||||||
|
snapshot.StoppedContainerCount = stoppedContainers
|
||||||
|
snapshot.StackCount += len(stacks)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func snapshotImages(snapshot *portainer.Snapshot, cli *client.Client) error {
|
||||||
|
images, err := cli.ImageList(context.Background(), types.ImageListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshot.ImageCount = len(images)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func snapshotVolumes(snapshot *portainer.Snapshot, cli *client.Client) error {
|
||||||
|
volumes, err := cli.VolumeList(context.Background(), filters.Args{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshot.VolumeCount = len(volumes.Volumes)
|
||||||
|
return nil
|
||||||
|
}
|
27
api/docker/snapshotter.go
Normal file
27
api/docker/snapshotter.go
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/portainer/portainer"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Snapshotter represents a service used to create endpoint snapshots
|
||||||
|
type Snapshotter struct {
|
||||||
|
clientFactory *ClientFactory
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSnapshotter returns a new Snapshotter instance
|
||||||
|
func NewSnapshotter(clientFactory *ClientFactory) *Snapshotter {
|
||||||
|
return &Snapshotter{
|
||||||
|
clientFactory: clientFactory,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateSnapshot creates a snapshot of a specific endpoint
|
||||||
|
func (snapshotter *Snapshotter) CreateSnapshot(endpoint *portainer.Endpoint) (*portainer.Snapshot, error) {
|
||||||
|
cli, err := snapshotter.clientFactory.CreateClient(endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return snapshot(cli)
|
||||||
|
}
|
|
@ -177,6 +177,8 @@ func (handler *Handler) createAzureEndpoint(payload *endpointCreatePayload) (*po
|
||||||
Extensions: []portainer.EndpointExtension{},
|
Extensions: []portainer.EndpointExtension{},
|
||||||
AzureCredentials: credentials,
|
AzureCredentials: credentials,
|
||||||
Tags: payload.Tags,
|
Tags: payload.Tags,
|
||||||
|
Status: portainer.EndpointStatusUp,
|
||||||
|
Snapshots: []portainer.Snapshot{},
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.EndpointService.CreateEndpoint(endpoint)
|
err = handler.EndpointService.CreateEndpoint(endpoint)
|
||||||
|
@ -213,6 +215,8 @@ func (handler *Handler) createUnsecuredEndpoint(payload *endpointCreatePayload)
|
||||||
AuthorizedTeams: []portainer.TeamID{},
|
AuthorizedTeams: []portainer.TeamID{},
|
||||||
Extensions: []portainer.EndpointExtension{},
|
Extensions: []portainer.EndpointExtension{},
|
||||||
Tags: payload.Tags,
|
Tags: payload.Tags,
|
||||||
|
Status: portainer.EndpointStatusUp,
|
||||||
|
Snapshots: []portainer.Snapshot{},
|
||||||
}
|
}
|
||||||
|
|
||||||
err := handler.EndpointService.CreateEndpoint(endpoint)
|
err := handler.EndpointService.CreateEndpoint(endpoint)
|
||||||
|
@ -253,6 +257,8 @@ func (handler *Handler) createTLSSecuredEndpoint(payload *endpointCreatePayload)
|
||||||
AuthorizedTeams: []portainer.TeamID{},
|
AuthorizedTeams: []portainer.TeamID{},
|
||||||
Extensions: []portainer.EndpointExtension{},
|
Extensions: []portainer.EndpointExtension{},
|
||||||
Tags: payload.Tags,
|
Tags: payload.Tags,
|
||||||
|
Status: portainer.EndpointStatusUp,
|
||||||
|
Snapshots: []portainer.Snapshot{},
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.EndpointService.CreateEndpoint(endpoint)
|
err = handler.EndpointService.CreateEndpoint(endpoint)
|
||||||
|
|
|
@ -15,6 +15,7 @@ type Handler struct {
|
||||||
SettingsService portainer.SettingsService
|
SettingsService portainer.SettingsService
|
||||||
LDAPService portainer.LDAPService
|
LDAPService portainer.LDAPService
|
||||||
FileService portainer.FileService
|
FileService portainer.FileService
|
||||||
|
JobScheduler portainer.JobScheduler
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler creates a handler to manage settings operations.
|
// NewHandler creates a handler to manage settings operations.
|
||||||
|
|
|
@ -13,6 +13,7 @@ type publicSettingsResponse struct {
|
||||||
AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod"`
|
AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod"`
|
||||||
AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"`
|
AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"`
|
||||||
AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"`
|
AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"`
|
||||||
|
SnapshotInterval string `json:"SnapshotInterval"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET request on /api/settings/public
|
// GET request on /api/settings/public
|
||||||
|
@ -27,6 +28,7 @@ func (handler *Handler) settingsPublic(w http.ResponseWriter, r *http.Request) *
|
||||||
AuthenticationMethod: settings.AuthenticationMethod,
|
AuthenticationMethod: settings.AuthenticationMethod,
|
||||||
AllowBindMountsForRegularUsers: settings.AllowBindMountsForRegularUsers,
|
AllowBindMountsForRegularUsers: settings.AllowBindMountsForRegularUsers,
|
||||||
AllowPrivilegedModeForRegularUsers: settings.AllowPrivilegedModeForRegularUsers,
|
AllowPrivilegedModeForRegularUsers: settings.AllowPrivilegedModeForRegularUsers,
|
||||||
|
SnapshotInterval: settings.SnapshotInterval,
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.JSON(w, publicSettings)
|
return response.JSON(w, publicSettings)
|
||||||
|
|
|
@ -12,22 +12,20 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type settingsUpdatePayload struct {
|
type settingsUpdatePayload struct {
|
||||||
LogoURL string
|
LogoURL *string
|
||||||
BlackListedLabels []portainer.Pair
|
BlackListedLabels []portainer.Pair
|
||||||
AuthenticationMethod int
|
AuthenticationMethod *int
|
||||||
LDAPSettings portainer.LDAPSettings
|
LDAPSettings *portainer.LDAPSettings
|
||||||
AllowBindMountsForRegularUsers bool
|
AllowBindMountsForRegularUsers *bool
|
||||||
AllowPrivilegedModeForRegularUsers bool
|
AllowPrivilegedModeForRegularUsers *bool
|
||||||
|
SnapshotInterval *string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (payload *settingsUpdatePayload) Validate(r *http.Request) error {
|
func (payload *settingsUpdatePayload) Validate(r *http.Request) error {
|
||||||
if payload.AuthenticationMethod == 0 {
|
if *payload.AuthenticationMethod != 1 && *payload.AuthenticationMethod != 2 {
|
||||||
return portainer.Error("Invalid authentication method")
|
|
||||||
}
|
|
||||||
if payload.AuthenticationMethod != 1 && payload.AuthenticationMethod != 2 {
|
|
||||||
return portainer.Error("Invalid authentication method value. Value must be one of: 1 (internal) or 2 (LDAP/AD)")
|
return portainer.Error("Invalid authentication method value. Value must be one of: 1 (internal) or 2 (LDAP/AD)")
|
||||||
}
|
}
|
||||||
if !govalidator.IsNull(payload.LogoURL) && !govalidator.IsURL(payload.LogoURL) {
|
if payload.LogoURL != nil && *payload.LogoURL != "" && !govalidator.IsURL(*payload.LogoURL) {
|
||||||
return portainer.Error("Invalid logo URL. Must correspond to a valid URL format")
|
return portainer.Error("Invalid logo URL. Must correspond to a valid URL format")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -41,15 +39,40 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
|
||||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
settings := &portainer.Settings{
|
settings, err := handler.SettingsService.Settings()
|
||||||
LogoURL: payload.LogoURL,
|
if err != nil {
|
||||||
BlackListedLabels: payload.BlackListedLabels,
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve the settings from the database", err}
|
||||||
LDAPSettings: payload.LDAPSettings,
|
}
|
||||||
AllowBindMountsForRegularUsers: payload.AllowBindMountsForRegularUsers,
|
|
||||||
AllowPrivilegedModeForRegularUsers: payload.AllowPrivilegedModeForRegularUsers,
|
if payload.AuthenticationMethod != nil {
|
||||||
|
settings.AuthenticationMethod = portainer.AuthenticationMethod(*payload.AuthenticationMethod)
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.LogoURL != nil {
|
||||||
|
settings.LogoURL = *payload.LogoURL
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.BlackListedLabels != nil {
|
||||||
|
settings.BlackListedLabels = payload.BlackListedLabels
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.LDAPSettings != nil {
|
||||||
|
settings.LDAPSettings = *payload.LDAPSettings
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.AllowBindMountsForRegularUsers != nil {
|
||||||
|
settings.AllowBindMountsForRegularUsers = *payload.AllowBindMountsForRegularUsers
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.AllowPrivilegedModeForRegularUsers != nil {
|
||||||
|
settings.AllowPrivilegedModeForRegularUsers = *payload.AllowPrivilegedModeForRegularUsers
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.SnapshotInterval != nil && *payload.SnapshotInterval != settings.SnapshotInterval {
|
||||||
|
settings.SnapshotInterval = *payload.SnapshotInterval
|
||||||
|
handler.JobScheduler.UpdateSnapshotJob(settings.SnapshotInterval)
|
||||||
}
|
}
|
||||||
|
|
||||||
settings.AuthenticationMethod = portainer.AuthenticationMethod(payload.AuthenticationMethod)
|
|
||||||
tlsError := handler.updateTLS(settings)
|
tlsError := handler.updateTLS(settings)
|
||||||
if tlsError != nil {
|
if tlsError != nil {
|
||||||
return tlsError
|
return tlsError
|
||||||
|
|
|
@ -40,6 +40,7 @@ type Server struct {
|
||||||
ComposeStackManager portainer.ComposeStackManager
|
ComposeStackManager portainer.ComposeStackManager
|
||||||
CryptoService portainer.CryptoService
|
CryptoService portainer.CryptoService
|
||||||
SignatureService portainer.DigitalSignatureService
|
SignatureService portainer.DigitalSignatureService
|
||||||
|
JobScheduler portainer.JobScheduler
|
||||||
DockerHubService portainer.DockerHubService
|
DockerHubService portainer.DockerHubService
|
||||||
EndpointService portainer.EndpointService
|
EndpointService portainer.EndpointService
|
||||||
EndpointGroupService portainer.EndpointGroupService
|
EndpointGroupService portainer.EndpointGroupService
|
||||||
|
@ -120,6 +121,7 @@ func (server *Server) Start() error {
|
||||||
settingsHandler.SettingsService = server.SettingsService
|
settingsHandler.SettingsService = server.SettingsService
|
||||||
settingsHandler.LDAPService = server.LDAPService
|
settingsHandler.LDAPService = server.LDAPService
|
||||||
settingsHandler.FileService = server.FileService
|
settingsHandler.FileService = server.FileService
|
||||||
|
settingsHandler.JobScheduler = server.JobScheduler
|
||||||
|
|
||||||
var stackHandler = stacks.NewHandler(requestBouncer)
|
var stackHandler = stacks.NewHandler(requestBouncer)
|
||||||
stackHandler.FileService = server.FileService
|
stackHandler.FileService = server.FileService
|
||||||
|
|
|
@ -31,12 +31,15 @@ type (
|
||||||
SSLCert *string
|
SSLCert *string
|
||||||
SSLKey *string
|
SSLKey *string
|
||||||
SyncInterval *string
|
SyncInterval *string
|
||||||
|
Snapshot *bool
|
||||||
|
SnapshotInterval *string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Status represents the application status.
|
// Status represents the application status.
|
||||||
Status struct {
|
Status struct {
|
||||||
Authentication bool `json:"Authentication"`
|
Authentication bool `json:"Authentication"`
|
||||||
EndpointManagement bool `json:"EndpointManagement"`
|
EndpointManagement bool `json:"EndpointManagement"`
|
||||||
|
Snapshot bool `json:"Snapshot"`
|
||||||
Analytics bool `json:"Analytics"`
|
Analytics bool `json:"Analytics"`
|
||||||
Version string `json:"Version"`
|
Version string `json:"Version"`
|
||||||
}
|
}
|
||||||
|
@ -75,6 +78,8 @@ type (
|
||||||
LDAPSettings LDAPSettings `json:"LDAPSettings"`
|
LDAPSettings LDAPSettings `json:"LDAPSettings"`
|
||||||
AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"`
|
AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"`
|
||||||
AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"`
|
AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"`
|
||||||
|
SnapshotInterval string `json:"SnapshotInterval"`
|
||||||
|
|
||||||
// Deprecated fields
|
// Deprecated fields
|
||||||
DisplayDonationHeader bool
|
DisplayDonationHeader bool
|
||||||
DisplayExternalContributors bool
|
DisplayExternalContributors bool
|
||||||
|
@ -177,6 +182,9 @@ type (
|
||||||
// EndpointType represents the type of an endpoint.
|
// EndpointType represents the type of an endpoint.
|
||||||
EndpointType int
|
EndpointType int
|
||||||
|
|
||||||
|
// EndpointStatus represents the status of an endpoint
|
||||||
|
EndpointStatus int
|
||||||
|
|
||||||
// Endpoint represents a Docker endpoint with all the info required
|
// Endpoint represents a Docker endpoint with all the info required
|
||||||
// to connect to it.
|
// to connect to it.
|
||||||
Endpoint struct {
|
Endpoint struct {
|
||||||
|
@ -192,6 +200,8 @@ type (
|
||||||
Extensions []EndpointExtension `json:"Extensions"`
|
Extensions []EndpointExtension `json:"Extensions"`
|
||||||
AzureCredentials AzureCredentials `json:"AzureCredentials,omitempty"`
|
AzureCredentials AzureCredentials `json:"AzureCredentials,omitempty"`
|
||||||
Tags []string `json:"Tags"`
|
Tags []string `json:"Tags"`
|
||||||
|
Status EndpointStatus `json:"Status"`
|
||||||
|
Snapshots []Snapshot `json:"Snapshots"`
|
||||||
|
|
||||||
// Deprecated fields
|
// Deprecated fields
|
||||||
// Deprecated in DBVersion == 4
|
// Deprecated in DBVersion == 4
|
||||||
|
@ -209,6 +219,21 @@ type (
|
||||||
AuthenticationKey string `json:"AuthenticationKey"`
|
AuthenticationKey string `json:"AuthenticationKey"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Snapshot represents a snapshot of a specific endpoint at a specific time
|
||||||
|
Snapshot struct {
|
||||||
|
Time int64 `json:"Time"`
|
||||||
|
DockerVersion string `json:"DockerVersion"`
|
||||||
|
Swarm bool `json:"Swarm"`
|
||||||
|
TotalCPU int `json:"TotalCPU"`
|
||||||
|
TotalMemory int64 `json:"TotalMemory"`
|
||||||
|
RunningContainerCount int `json:"RunningContainerCount"`
|
||||||
|
StoppedContainerCount int `json:"StoppedContainerCount"`
|
||||||
|
VolumeCount int `json:"VolumeCount"`
|
||||||
|
ImageCount int `json:"ImageCount"`
|
||||||
|
ServiceCount int `json:"ServiceCount"`
|
||||||
|
StackCount int `json:"StackCount"`
|
||||||
|
}
|
||||||
|
|
||||||
// EndpointGroupID represents an endpoint group identifier.
|
// EndpointGroupID represents an endpoint group identifier.
|
||||||
EndpointGroupID int
|
EndpointGroupID int
|
||||||
|
|
||||||
|
@ -539,9 +564,17 @@ type (
|
||||||
ClonePrivateRepositoryWithBasicAuth(repositoryURL, destination, username, password string) error
|
ClonePrivateRepositoryWithBasicAuth(repositoryURL, destination, username, password string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// EndpointWatcher represents a service to synchronize the endpoints via an external source.
|
// JobScheduler represents a service to run jobs on a periodic basis.
|
||||||
EndpointWatcher interface {
|
JobScheduler interface {
|
||||||
WatchEndpointFile(endpointFilePath string) error
|
ScheduleEndpointSyncJob(endpointFilePath, interval string) error
|
||||||
|
ScheduleSnapshotJob(interval string) error
|
||||||
|
UpdateSnapshotJob(interval string)
|
||||||
|
Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snapshotter represents a service used to create endpoint snapshots.
|
||||||
|
Snapshotter interface {
|
||||||
|
CreateSnapshot(endpoint *Endpoint) (*Snapshot, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// LDAPService represents a service used to authenticate users against a LDAP/AD.
|
// LDAPService represents a service used to authenticate users against a LDAP/AD.
|
||||||
|
@ -581,6 +614,8 @@ const (
|
||||||
// PortainerAgentSignatureMessage represents the message used to create a digital signature
|
// PortainerAgentSignatureMessage represents the message used to create a digital signature
|
||||||
// to be used when communicating with an agent
|
// to be used when communicating with an agent
|
||||||
PortainerAgentSignatureMessage = "Portainer-App"
|
PortainerAgentSignatureMessage = "Portainer-App"
|
||||||
|
// SupportedDockerAPIVersion is the minimum Docker API version supported by Portainer.
|
||||||
|
SupportedDockerAPIVersion = "1.24"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -673,3 +708,11 @@ const (
|
||||||
// ComposeStackTemplate represents a template used to deploy a Compose stack
|
// ComposeStackTemplate represents a template used to deploy a Compose stack
|
||||||
ComposeStackTemplate
|
ComposeStackTemplate
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
_ EndpointStatus = iota
|
||||||
|
// EndpointStatusUp is used to represent an available endpoint
|
||||||
|
EndpointStatusUp
|
||||||
|
// EndpointStatusDown is used to represent an unavailable endpoint
|
||||||
|
EndpointStatusDown
|
||||||
|
)
|
||||||
|
|
|
@ -4,9 +4,6 @@ angular.module('portainer.docker').component('dockerSidebarContent', {
|
||||||
'endpointApiVersion': '<',
|
'endpointApiVersion': '<',
|
||||||
'swarmManagement': '<',
|
'swarmManagement': '<',
|
||||||
'standaloneManagement': '<',
|
'standaloneManagement': '<',
|
||||||
'adminAccess': '<',
|
'adminAccess': '<'
|
||||||
'externalContributions': '<',
|
|
||||||
'sidebarToggledOn': '<',
|
|
||||||
'currentState': '<'
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<rd-header>
|
<rd-header>
|
||||||
<rd-header-title title-text="Home"></rd-header-title>
|
<rd-header-title title-text="Dashboard"></rd-header-title>
|
||||||
<rd-header-content>Dashboard</rd-header-content>
|
<rd-header-content>Endpoint summary</rd-header-content>
|
||||||
</rd-header>
|
</rd-header>
|
||||||
|
|
||||||
<div class="row" ng-if="applicationState.endpoint.mode.agentProxy">
|
<div class="row" ng-if="applicationState.endpoint.mode.agentProxy">
|
||||||
|
|
|
@ -187,6 +187,17 @@ angular.module('portainer.app', [])
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var home = {
|
||||||
|
name: 'portainer.home',
|
||||||
|
url: '/home',
|
||||||
|
views: {
|
||||||
|
'content@': {
|
||||||
|
templateUrl: 'app/portainer/views/home/home.html',
|
||||||
|
controller: 'HomeController'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
var registries = {
|
var registries = {
|
||||||
name: 'portainer.registries',
|
name: 'portainer.registries',
|
||||||
url: '/registries',
|
url: '/registries',
|
||||||
|
@ -404,6 +415,7 @@ angular.module('portainer.app', [])
|
||||||
$stateRegistryProvider.register(group);
|
$stateRegistryProvider.register(group);
|
||||||
$stateRegistryProvider.register(groupAccess);
|
$stateRegistryProvider.register(groupAccess);
|
||||||
$stateRegistryProvider.register(groupCreation);
|
$stateRegistryProvider.register(groupCreation);
|
||||||
|
$stateRegistryProvider.register(home);
|
||||||
$stateRegistryProvider.register(registries);
|
$stateRegistryProvider.register(registries);
|
||||||
$stateRegistryProvider.register(registry);
|
$stateRegistryProvider.register(registry);
|
||||||
$stateRegistryProvider.register(registryAccess);
|
$stateRegistryProvider.register(registryAccess);
|
||||||
|
|
|
@ -30,8 +30,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.datatable .searchBar {
|
.datatable .searchBar {
|
||||||
border-top: 1px solid #f6f6f6;
|
border-top: 1px solid #d2d1d1;
|
||||||
padding: 10px;
|
border-bottom: 1px solid #d2d1d1;
|
||||||
|
padding: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.datatable .searchInput {
|
.datatable .searchInput {
|
||||||
|
|
|
@ -0,0 +1,113 @@
|
||||||
|
<div class="datatable">
|
||||||
|
<rd-widget>
|
||||||
|
<rd-widget-body classes="no-padding">
|
||||||
|
<div class="toolBar">
|
||||||
|
<div class="toolBarTitle">
|
||||||
|
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.titleText }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="searchBar">
|
||||||
|
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
||||||
|
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search by name, group, tag..." auto-focus>
|
||||||
|
</div>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<a ng-click="$ctrl.changeOrderBy('Name')">
|
||||||
|
Name
|
||||||
|
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && !$ctrl.state.reverseOrder"></i>
|
||||||
|
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"></i>
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<a ng-click="$ctrl.changeOrderBy('GroupName')">
|
||||||
|
Group
|
||||||
|
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'GroupName' && !$ctrl.state.reverseOrder"></i>
|
||||||
|
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'GroupName' && $ctrl.state.reverseOrder"></i>
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<a ng-click="$ctrl.changeOrderBy('Status')">
|
||||||
|
Status
|
||||||
|
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Status' && !$ctrl.state.reverseOrder"></i>
|
||||||
|
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Status' && $ctrl.state.reverseOrder"></i>
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<a ng-click="$ctrl.changeOrderBy('Type')">
|
||||||
|
Type
|
||||||
|
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Type' && !$ctrl.state.reverseOrder"></i>
|
||||||
|
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Type' && $ctrl.state.reverseOrder"></i>
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<a ng-click="$ctrl.changeOrderBy('Snapshots[0].Time')">
|
||||||
|
Last snapshot
|
||||||
|
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Snapshots[0].Time' && !$ctrl.state.reverseOrder"></i>
|
||||||
|
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Snapshots[0].Time' && $ctrl.state.reverseOrder"></i>
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr 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}">
|
||||||
|
<td>
|
||||||
|
<a ng-click="$ctrl.dashboardAction(item)"><i class="fa fa-sign-in-alt" aria-hidden="true"></i> {{ item.Name }}</a>
|
||||||
|
</td>
|
||||||
|
<td>{{ item.GroupName }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="label label-{{ item.Status|endpointstatusbadge }}">{{ item.Status === 1 ? 'up' : 'down' }}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span>
|
||||||
|
<i ng-class="item.Type | endpointtypeicon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
|
{{ item.Type | endpointtypename }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span ng-if="item.Snapshots.length > 0">
|
||||||
|
{{ item.Snapshots[0].Time | getisodatefromtimestamp }}
|
||||||
|
</span>
|
||||||
|
<span ng-if="item.Snapshots.length === 0">-</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr dir-paginate-end class="text-muted" ng-if="item.Snapshots.length > 0">
|
||||||
|
<td colspan="5" style="border: 0; text-align:center;">
|
||||||
|
<snapshot-details
|
||||||
|
snapshot="item.Snapshots[0]"
|
||||||
|
></snapshot-details
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr ng-if="!$ctrl.dataset">
|
||||||
|
<td colspan="5" class="text-center text-muted">Loading...</td>
|
||||||
|
</tr>
|
||||||
|
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
|
||||||
|
<td colspan="5" class="text-center text-muted">No endpoint available.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="footer" ng-if="$ctrl.dataset">
|
||||||
|
<div class="paginationControls">
|
||||||
|
<form class="form-inline">
|
||||||
|
<span class="limitSelector">
|
||||||
|
<span style="margin-right: 5px;">
|
||||||
|
Items per page
|
||||||
|
</span>
|
||||||
|
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
|
||||||
|
<option value="0">All</option>
|
||||||
|
<option value="10">10</option>
|
||||||
|
<option value="25">25</option>
|
||||||
|
<option value="50">50</option>
|
||||||
|
<option value="100">100</option>
|
||||||
|
</select>
|
||||||
|
</span>
|
||||||
|
<dir-pagination-controls max-size="5"></dir-pagination-controls>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</rd-widget-body>
|
||||||
|
</rd-widget>
|
||||||
|
</div>
|
|
@ -0,0 +1,13 @@
|
||||||
|
angular.module('portainer.app').component('endpointsSnapshotDatatable', {
|
||||||
|
templateUrl: 'app/portainer/components/datatables/endpoints-snapshot-datatable/endpointsSnapshotDatatable.html',
|
||||||
|
controller: 'GenericDatatableController',
|
||||||
|
bindings: {
|
||||||
|
titleText: '@',
|
||||||
|
titleIcon: '@',
|
||||||
|
dataset: '<',
|
||||||
|
tableKey: '@',
|
||||||
|
orderBy: '@',
|
||||||
|
reverseOrder: '<',
|
||||||
|
dashboardAction: '<'
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,29 @@
|
||||||
|
<span style="border-top: 2px solid #e2e2e2; padding: 7px;">
|
||||||
|
<span style="padding: 0 7px 0 7px;">
|
||||||
|
<i class="fa fa-th-list space-right" aria-hidden="true"></i>{{ $ctrl.snapshot.StackCount }} stacks
|
||||||
|
</span>
|
||||||
|
<span style="padding: 0 7px 0 7px;" ng-if="$ctrl.snapshot.Swarm">
|
||||||
|
<i class="fa fa-list-alt space-right" aria-hidden="true"></i>{{ $ctrl.snapshot.ServiceCount }} services
|
||||||
|
</span>
|
||||||
|
<span style="padding: 0 7px 0 7px;">
|
||||||
|
<i class="fa fa-server space-right" aria-hidden="true"></i>{{ $ctrl.snapshot.RunningContainerCount + $ctrl.snapshot.StoppedContainerCount }} containers
|
||||||
|
<span ng-if="$ctrl.snapshot.RunningContainerCount > 0 || $ctrl.snapshot.StoppedContainerCount > 0">
|
||||||
|
-
|
||||||
|
<i class="fa fa-heartbeat green-icon" aria-hidden="true"></i> {{ $ctrl.snapshot.RunningContainerCount }}
|
||||||
|
<i class="fa fa-heartbeat red-icon" aria-hidden="true"></i> {{ $ctrl.snapshot.StoppedContainerCount }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span style="padding: 0 7px 0 7px;">
|
||||||
|
<i class="fa fa-cubes space-right" aria-hidden="true"></i>{{ $ctrl.snapshot.VolumeCount }} volumes
|
||||||
|
</span>
|
||||||
|
<span style="padding: 0 7px 0 7px;">
|
||||||
|
<i class="fa fa-clone space-right" aria-hidden="true"></i>{{ $ctrl.snapshot.ImageCount }} images
|
||||||
|
</span>
|
||||||
|
<span style="padding: 0 7px 0 7px; border-left: 2px solid #e2e2e2;">
|
||||||
|
<i class="fa fa-memory" aria-hidden="true"></i> {{ $ctrl.snapshot.TotalMemory | humansize }}
|
||||||
|
<i class="fa fa-microchip space-left" aria-hidden="true"></i> {{ $ctrl.snapshot.TotalCPU }}
|
||||||
|
</span>
|
||||||
|
<span style="padding: 0 7px 0 7px; border-left: 2px solid #e2e2e2;">
|
||||||
|
{{ $ctrl.snapshot.Swarm ? 'Swarm' : 'Standalone' }} {{ $ctrl.snapshot.DockerVersion }}
|
||||||
|
</span>
|
||||||
|
</span>
|
|
@ -0,0 +1,6 @@
|
||||||
|
angular.module('portainer.app').component('snapshotDetails', {
|
||||||
|
templateUrl: 'app/portainer/components/datatables/endpoints-snapshot-datatable/snapshot-details/snapshotDetails.html',
|
||||||
|
bindings: {
|
||||||
|
snapshot: '<'
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,7 @@
|
||||||
|
angular.module('portainer.app').component('informationPanel', {
|
||||||
|
templateUrl: 'app/portainer/components/information-panel/informationPanel.html',
|
||||||
|
bindings: {
|
||||||
|
titleText: '@'
|
||||||
|
},
|
||||||
|
transclude: true
|
||||||
|
});
|
|
@ -0,0 +1,14 @@
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<rd-widget>
|
||||||
|
<rd-widget-body>
|
||||||
|
<div class="col-sm-12 form-section-title">
|
||||||
|
{{ $ctrl.titleText }}
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<ng-transclude></ng-transclude>
|
||||||
|
</div>
|
||||||
|
</rd-widget-body>
|
||||||
|
</rd-widget>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -1,9 +0,0 @@
|
||||||
angular.module('portainer.app').component('sidebarEndpointSelector', {
|
|
||||||
templateUrl: 'app/portainer/components/sidebar-endpoint-selector/sidebarEndpointSelector.html',
|
|
||||||
controller: 'SidebarEndpointSelectorController',
|
|
||||||
bindings: {
|
|
||||||
'endpoints': '<',
|
|
||||||
'groups': '<',
|
|
||||||
'selectEndpoint': '<'
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -1,27 +0,0 @@
|
||||||
<div ng-if="$ctrl.endpoints.length > 1">
|
|
||||||
<div ng-if="!$ctrl.state.show">
|
|
||||||
<li class="sidebar-title">
|
|
||||||
<span class="interactive" style="color: #fff;" ng-click="$ctrl.state.show = true;">
|
|
||||||
<span class="fa fa-plug space-right"></span>Change environment
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
</div>
|
|
||||||
<div ng-if="$ctrl.state.show">
|
|
||||||
<div ng-if="$ctrl.availableGroups.length > 1">
|
|
||||||
<li class="sidebar-title"><span>Group</span></li>
|
|
||||||
<li class="sidebar-title">
|
|
||||||
<select class="select-endpoint form-control" ng-options="group.Name for group in $ctrl.availableGroups" ng-model="$ctrl.state.selectedGroup" ng-change="$ctrl.selectGroup()">
|
|
||||||
<option value="" disabled selected>Select a group</option>
|
|
||||||
</select>
|
|
||||||
</li>
|
|
||||||
</div>
|
|
||||||
<div ng-if="$ctrl.state.selectedGroup || $ctrl.availableGroups.length <= 1">
|
|
||||||
<li class="sidebar-title"><span>Endpoint</span></li>
|
|
||||||
<li class="sidebar-title">
|
|
||||||
<select class="select-endpoint form-control" ng-options="endpoint.Name for endpoint in $ctrl.availableEndpoints" ng-model="$ctrl.state.selectedEndpoint" ng-change="$ctrl.selectEndpoint($ctrl.state.selectedEndpoint)">
|
|
||||||
<option value="" disabled selected>Select an endpoint</option>
|
|
||||||
</select>
|
|
||||||
</li>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
|
@ -1,34 +0,0 @@
|
||||||
angular.module('portainer.app')
|
|
||||||
.controller('SidebarEndpointSelectorController', function () {
|
|
||||||
var ctrl = this;
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
show: false,
|
|
||||||
selectedGroup: null,
|
|
||||||
selectedEndpoint: null
|
|
||||||
};
|
|
||||||
|
|
||||||
this.selectGroup = function() {
|
|
||||||
this.availableEndpoints = this.endpoints.filter(function f(endpoint) {
|
|
||||||
return endpoint.GroupId === ctrl.state.selectedGroup.Id;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
this.$onInit = function() {
|
|
||||||
this.availableGroups = filterEmptyGroups(this.groups, this.endpoints);
|
|
||||||
this.availableEndpoints = this.endpoints;
|
|
||||||
};
|
|
||||||
|
|
||||||
function filterEmptyGroups(groups, endpoints) {
|
|
||||||
return groups.filter(function f(group) {
|
|
||||||
for (var i = 0; i < endpoints.length; i++) {
|
|
||||||
|
|
||||||
var endpoint = endpoints[i];
|
|
||||||
if (endpoint.GroupId === group.Id) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -138,4 +138,13 @@ angular.module('portainer.app')
|
||||||
return 'fa fa-eye';
|
return 'fa fa-eye';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
})
|
||||||
|
.filter('endpointstatusbadge', function () {
|
||||||
|
'use strict';
|
||||||
|
return function (status) {
|
||||||
|
if (status === 2) {
|
||||||
|
return 'danger';
|
||||||
|
}
|
||||||
|
return 'success';
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,4 +5,5 @@ function SettingsViewModel(data) {
|
||||||
this.LDAPSettings = data.LDAPSettings;
|
this.LDAPSettings = data.LDAPSettings;
|
||||||
this.AllowBindMountsForRegularUsers = data.AllowBindMountsForRegularUsers;
|
this.AllowBindMountsForRegularUsers = data.AllowBindMountsForRegularUsers;
|
||||||
this.AllowPrivilegedModeForRegularUsers = data.AllowPrivilegedModeForRegularUsers;
|
this.AllowPrivilegedModeForRegularUsers = data.AllowPrivilegedModeForRegularUsers;
|
||||||
|
this.SnapshotInterval = data.SnapshotInterval;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
function StatusViewModel(data) {
|
function StatusViewModel(data) {
|
||||||
this.Authentication = data.Authentication;
|
this.Authentication = data.Authentication;
|
||||||
|
this.Snapshot = data.Snapshot;
|
||||||
this.EndpointManagement = data.EndpointManagement;
|
this.EndpointManagement = data.EndpointManagement;
|
||||||
this.Analytics = data.Analytics;
|
this.Analytics = data.Analytics;
|
||||||
this.Version = data.Version;
|
this.Version = data.Version;
|
||||||
|
|
|
@ -25,12 +25,19 @@ function StateManagerFactory($q, SystemService, InfoHelper, LocalStorage, Settin
|
||||||
LocalStorage.storeApplicationState(state.application);
|
LocalStorage.storeApplicationState(state.application);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
manager.updateSnapshotInterval = function(interval) {
|
||||||
|
state.application.snapshotInterval = interval;
|
||||||
|
LocalStorage.storeApplicationState(state.application);
|
||||||
|
};
|
||||||
|
|
||||||
function assignStateFromStatusAndSettings(status, settings) {
|
function assignStateFromStatusAndSettings(status, settings) {
|
||||||
state.application.authentication = status.Authentication;
|
state.application.authentication = status.Authentication;
|
||||||
state.application.analytics = status.Analytics;
|
state.application.analytics = status.Analytics;
|
||||||
state.application.endpointManagement = status.EndpointManagement;
|
state.application.endpointManagement = status.EndpointManagement;
|
||||||
|
state.application.snapshot = status.Snapshot;
|
||||||
state.application.version = status.Version;
|
state.application.version = status.Version;
|
||||||
state.application.logo = settings.LogoURL;
|
state.application.logo = settings.LogoURL;
|
||||||
|
state.application.snapshotInterval = settings.SnapshotInterval;
|
||||||
state.application.validity = moment().unix();
|
state.application.validity = moment().unix();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,14 +117,11 @@ function StateManagerFactory($q, SystemService, InfoHelper, LocalStorage, Settin
|
||||||
return extensions;
|
return extensions;
|
||||||
}
|
}
|
||||||
|
|
||||||
manager.updateEndpointState = function(loading, type, extensions) {
|
manager.updateEndpointState = function(name, type, extensions) {
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
state.loading = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === 3) {
|
if (type === 3) {
|
||||||
|
state.endpoint.name = name;
|
||||||
state.endpoint.mode = { provider: 'AZURE' };
|
state.endpoint.mode = { provider: 'AZURE' };
|
||||||
LocalStorage.storeEndpointState(state.endpoint);
|
LocalStorage.storeEndpointState(state.endpoint);
|
||||||
deferred.resolve();
|
deferred.resolve();
|
||||||
|
@ -132,6 +136,7 @@ function StateManagerFactory($q, SystemService, InfoHelper, LocalStorage, Settin
|
||||||
var endpointMode = InfoHelper.determineEndpointMode(data.info, type);
|
var endpointMode = InfoHelper.determineEndpointMode(data.info, type);
|
||||||
var endpointAPIVersion = parseFloat(data.version.ApiVersion);
|
var endpointAPIVersion = parseFloat(data.version.ApiVersion);
|
||||||
state.endpoint.mode = endpointMode;
|
state.endpoint.mode = endpointMode;
|
||||||
|
state.endpoint.name = name;
|
||||||
state.endpoint.apiVersion = endpointAPIVersion;
|
state.endpoint.apiVersion = endpointAPIVersion;
|
||||||
state.endpoint.extensions = assignExtensions(extensions);
|
state.endpoint.extensions = assignExtensions(extensions);
|
||||||
LocalStorage.storeEndpointState(state.endpoint);
|
LocalStorage.storeEndpointState(state.endpoint);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
angular.module('portainer.app')
|
angular.module('portainer.app')
|
||||||
.controller('AuthenticationController', ['$scope', '$state', '$transition$', '$window', '$timeout', '$sanitize', 'Authentication', 'Users', 'UserService', 'EndpointService', 'StateManager', 'EndpointProvider', 'Notifications', 'SettingsService', 'ExtensionManager',
|
.controller('AuthenticationController', ['$scope', '$state', '$transition$', '$sanitize', 'Authentication', 'UserService', 'EndpointService', 'StateManager', 'Notifications', 'SettingsService',
|
||||||
function ($scope, $state, $transition$, $window, $timeout, $sanitize, Authentication, Users, UserService, EndpointService, StateManager, EndpointProvider, Notifications, SettingsService, ExtensionManager) {
|
function ($scope, $state, $transition$, $sanitize, Authentication, UserService, EndpointService, StateManager, Notifications, SettingsService) {
|
||||||
|
|
||||||
$scope.logo = StateManager.getState().application.logo;
|
$scope.logo = StateManager.getState().application.logo;
|
||||||
|
|
||||||
|
@ -13,47 +13,13 @@ function ($scope, $state, $transition$, $window, $timeout, $sanitize, Authentica
|
||||||
AuthenticationError: ''
|
AuthenticationError: ''
|
||||||
};
|
};
|
||||||
|
|
||||||
function redirectToDockerDashboard(endpoint) {
|
|
||||||
ExtensionManager.initEndpointExtensions(endpoint.Id)
|
|
||||||
.then(function success(data) {
|
|
||||||
var extensions = data;
|
|
||||||
return StateManager.updateEndpointState(true, endpoint.Type, extensions);
|
|
||||||
})
|
|
||||||
.then(function success(data) {
|
|
||||||
$state.go('docker.dashboard');
|
|
||||||
})
|
|
||||||
.catch(function error(err) {
|
|
||||||
Notifications.error('Failure', err, 'Unable to connect to the Docker endpoint');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function redirectToAzureDashboard(endpoint) {
|
|
||||||
StateManager.updateEndpointState(false, endpoint.Type, [])
|
|
||||||
.then(function success(data) {
|
|
||||||
$state.go('azure.dashboard');
|
|
||||||
})
|
|
||||||
.catch(function error(err) {
|
|
||||||
Notifications.error('Failure', err, 'Unable to connect to the Docker endpoint');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function redirectToDashboard(endpoint) {
|
|
||||||
EndpointProvider.setEndpointID(endpoint.Id);
|
|
||||||
|
|
||||||
if (endpoint.Type === 3) {
|
|
||||||
return redirectToAzureDashboard(endpoint);
|
|
||||||
}
|
|
||||||
redirectToDockerDashboard(endpoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
function unauthenticatedFlow() {
|
function unauthenticatedFlow() {
|
||||||
EndpointService.endpoints()
|
EndpointService.endpoints()
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
var endpoints = data;
|
if (endpoints.length === 0) {
|
||||||
if (endpoints.length > 0) {
|
|
||||||
redirectToDashboard(endpoints[0]);
|
|
||||||
} else {
|
|
||||||
$state.go('portainer.init.endpoint');
|
$state.go('portainer.init.endpoint');
|
||||||
|
} else {
|
||||||
|
$state.go('portainer.home');
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
|
@ -92,13 +58,10 @@ function ($scope, $state, $transition$, $window, $timeout, $sanitize, Authentica
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
var endpoints = data;
|
var endpoints = data;
|
||||||
var userDetails = Authentication.getUserDetails();
|
var userDetails = Authentication.getUserDetails();
|
||||||
if (endpoints.length > 0) {
|
if (endpoints.length === 0 && userDetails.role === 1) {
|
||||||
redirectToDashboard(endpoints[0]);
|
|
||||||
} else if (endpoints.length === 0 && userDetails.role === 1) {
|
|
||||||
$state.go('portainer.init.endpoint');
|
$state.go('portainer.init.endpoint');
|
||||||
} else if (endpoints.length === 0 && userDetails.role === 2) {
|
} else {
|
||||||
Authentication.logout();
|
$state.go('portainer.home');
|
||||||
$scope.state.AuthenticationError = 'User not allowed. Please contact your administrator.';
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(function error() {
|
.catch(function error() {
|
||||||
|
@ -114,7 +77,7 @@ function ($scope, $state, $transition$, $window, $timeout, $sanitize, Authentica
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Authentication.isAuthenticated()) {
|
if (Authentication.isAuthenticated()) {
|
||||||
$state.go('docker.dashboard');
|
$state.go('portainer.home');
|
||||||
}
|
}
|
||||||
|
|
||||||
var authenticationEnabled = $scope.applicationState.application.authentication;
|
var authenticationEnabled = $scope.applicationState.application.authentication;
|
||||||
|
|
37
app/portainer/views/home/home.html
Normal file
37
app/portainer/views/home/home.html
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
<rd-header>
|
||||||
|
<rd-header-title title-text="Home">
|
||||||
|
<a data-toggle="tooltip" title="Refresh" ui-sref="portainer.home" ui-sref-opts="{reload: true}">
|
||||||
|
<i class="fa fa-sync" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
|
</rd-header-title>
|
||||||
|
<rd-header-content>Endpoints</rd-header-content>
|
||||||
|
</rd-header>
|
||||||
|
|
||||||
|
<information-panel title-text="Information" ng-if="!isAdmin && endpoints.length === 0">
|
||||||
|
<span class="small">
|
||||||
|
<p class="text-muted">
|
||||||
|
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
|
You do not have access to any environment. Please contact your administrator.
|
||||||
|
</p>
|
||||||
|
</span>
|
||||||
|
</information-panel>
|
||||||
|
|
||||||
|
<information-panel title-text="Information" ng-if="isAdmin && !applicationState.application.snapshot">
|
||||||
|
<span class="small">
|
||||||
|
<p class="text-muted">
|
||||||
|
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||||
|
Endpoint snapshot is disabled.
|
||||||
|
</p>
|
||||||
|
</span>
|
||||||
|
</information-panel>
|
||||||
|
|
||||||
|
<div class="row" ng-if="endpoints.length > 0">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<endpoints-snapshot-datatable
|
||||||
|
title-text="Endpoints" title-icon="fa-plug"
|
||||||
|
dataset="endpoints" table-key="endpoints"
|
||||||
|
order-by="Name"
|
||||||
|
dashboard-action="goToDashboard"
|
||||||
|
></endpoints-snapshot-datatable>
|
||||||
|
</div>
|
||||||
|
</div>
|
58
app/portainer/views/home/homeController.js
Normal file
58
app/portainer/views/home/homeController.js
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
angular.module('portainer.app')
|
||||||
|
.controller('HomeController', ['$q', '$scope', '$state', 'Authentication', 'EndpointService', 'EndpointHelper', 'GroupService', 'Notifications', 'EndpointProvider', 'StateManager', 'ExtensionManager',
|
||||||
|
function ($q, $scope, $state, Authentication, EndpointService, EndpointHelper, GroupService, Notifications, EndpointProvider, StateManager, ExtensionManager) {
|
||||||
|
|
||||||
|
$scope.goToDashboard = function(endpoint) {
|
||||||
|
EndpointProvider.setEndpointID(endpoint.Id);
|
||||||
|
EndpointProvider.setEndpointPublicURL(endpoint.PublicURL);
|
||||||
|
if (endpoint.Type === 3) {
|
||||||
|
switchToAzureEndpoint(endpoint);
|
||||||
|
} else {
|
||||||
|
switchToDockerEndpoint(endpoint);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function switchToAzureEndpoint(endpoint) {
|
||||||
|
StateManager.updateEndpointState(endpoint.Name, endpoint.Type, [])
|
||||||
|
.then(function success() {
|
||||||
|
$state.go('azure.dashboard');
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
Notifications.error('Failure', err, 'Unable to connect to the Azure endpoint');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchToDockerEndpoint(endpoint) {
|
||||||
|
ExtensionManager.initEndpointExtensions(endpoint.Id)
|
||||||
|
.then(function success(data) {
|
||||||
|
var extensions = data;
|
||||||
|
return StateManager.updateEndpointState(endpoint.Name, endpoint.Type, extensions);
|
||||||
|
})
|
||||||
|
.then(function success() {
|
||||||
|
$state.go('docker.dashboard');
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
Notifications.error('Failure', err, 'Unable to connect to the Docker endpoint');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initView() {
|
||||||
|
$scope.isAdmin = Authentication.getUserDetails().role === 1;
|
||||||
|
|
||||||
|
$q.all({
|
||||||
|
endpoints: EndpointService.endpoints(),
|
||||||
|
groups: GroupService.groups()
|
||||||
|
})
|
||||||
|
.then(function success(data) {
|
||||||
|
var endpoints = data.endpoints;
|
||||||
|
var groups = data.groups;
|
||||||
|
EndpointHelper.mapGroupNameToEndpoint(endpoints, groups);
|
||||||
|
$scope.endpoints = endpoints;
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
Notifications.error('Failure', err, 'Unable to retrieve endpoint information');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
initView();
|
||||||
|
}]);
|
|
@ -1,6 +1,6 @@
|
||||||
angular.module('portainer.app')
|
angular.module('portainer.app')
|
||||||
.controller('InitAdminController', ['$scope', '$state', '$sanitize', 'Notifications', 'Authentication', 'StateManager', 'UserService', 'EndpointService', 'EndpointProvider', 'ExtensionManager',
|
.controller('InitAdminController', ['$scope', '$state', '$sanitize', 'Notifications', 'Authentication', 'StateManager', 'UserService', 'EndpointService',
|
||||||
function ($scope, $state, $sanitize, Notifications, Authentication, StateManager, UserService, EndpointService, EndpointProvider, ExtensionManager) {
|
function ($scope, $state, $sanitize, Notifications, Authentication, StateManager, UserService, EndpointService) {
|
||||||
|
|
||||||
$scope.logo = StateManager.getState().application.logo;
|
$scope.logo = StateManager.getState().application.logo;
|
||||||
|
|
||||||
|
@ -30,20 +30,7 @@ function ($scope, $state, $sanitize, Notifications, Authentication, StateManager
|
||||||
if (data.length === 0) {
|
if (data.length === 0) {
|
||||||
$state.go('portainer.init.endpoint');
|
$state.go('portainer.init.endpoint');
|
||||||
} else {
|
} else {
|
||||||
var endpoint = data[0];
|
$state.go('portainer.home');
|
||||||
endpointID = endpoint.Id;
|
|
||||||
EndpointProvider.setEndpointID(endpointID);
|
|
||||||
ExtensionManager.initEndpointExtensions(endpointID)
|
|
||||||
.then(function success(data) {
|
|
||||||
var extensions = data;
|
|
||||||
return StateManager.updateEndpointState(false, endpoint.Type, extensions);
|
|
||||||
})
|
|
||||||
.then(function success() {
|
|
||||||
$state.go('docker.dashboard');
|
|
||||||
})
|
|
||||||
.catch(function error(err) {
|
|
||||||
Notifications.error('Failure', err, 'Unable to connect to Docker environment');
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
angular.module('portainer.app')
|
angular.module('portainer.app')
|
||||||
.controller('InitEndpointController', ['$scope', '$state', 'EndpointService', 'StateManager', 'EndpointProvider', 'Notifications', 'ExtensionManager',
|
.controller('InitEndpointController', ['$scope', '$state', 'EndpointService', 'StateManager', 'Notifications',
|
||||||
function ($scope, $state, EndpointService, StateManager, EndpointProvider, Notifications, ExtensionManager) {
|
function ($scope, $state, EndpointService, StateManager, Notifications) {
|
||||||
|
|
||||||
if (!_.isEmpty($scope.applicationState.endpoint)) {
|
if (!_.isEmpty($scope.applicationState.endpoint)) {
|
||||||
$state.go('docker.dashboard');
|
$state.go('portainer.home');
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.logo = StateManager.getState().application.logo;
|
$scope.logo = StateManager.getState().application.logo;
|
||||||
|
@ -36,16 +36,7 @@ function ($scope, $state, EndpointService, StateManager, EndpointProvider, Notif
|
||||||
$scope.state.actionInProgress = true;
|
$scope.state.actionInProgress = true;
|
||||||
EndpointService.createLocalEndpoint()
|
EndpointService.createLocalEndpoint()
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
endpoint = data;
|
$state.go('portainer.home');
|
||||||
EndpointProvider.setEndpointID(endpoint.Id);
|
|
||||||
return ExtensionManager.initEndpointExtensions(endpoint.Id);
|
|
||||||
})
|
|
||||||
.then(function success(data) {
|
|
||||||
var extensions = data;
|
|
||||||
return StateManager.updateEndpointState(false, endpoint.Type, extensions);
|
|
||||||
})
|
|
||||||
.then(function success(data) {
|
|
||||||
$state.go('docker.dashboard');
|
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
Notifications.error('Failure', err, 'Unable to connect to the Docker environment');
|
Notifications.error('Failure', err, 'Unable to connect to the Docker environment');
|
||||||
|
@ -92,12 +83,7 @@ function ($scope, $state, EndpointService, StateManager, EndpointProvider, Notif
|
||||||
$scope.state.actionInProgress = true;
|
$scope.state.actionInProgress = true;
|
||||||
EndpointService.createAzureEndpoint(name, applicationId, tenantId, authenticationKey, 1, [])
|
EndpointService.createAzureEndpoint(name, applicationId, tenantId, authenticationKey, 1, [])
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
endpoint = data;
|
$state.go('portainer.home');
|
||||||
EndpointProvider.setEndpointID(endpoint.Id);
|
|
||||||
return StateManager.updateEndpointState(false, endpoint.Type, []);
|
|
||||||
})
|
|
||||||
.then(function success(data) {
|
|
||||||
$state.go('azure.dashboard');
|
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
Notifications.error('Failure', err, 'Unable to connect to the Azure environment');
|
Notifications.error('Failure', err, 'Unable to connect to the Azure environment');
|
||||||
|
@ -112,16 +98,7 @@ function ($scope, $state, EndpointService, StateManager, EndpointProvider, Notif
|
||||||
$scope.state.actionInProgress = true;
|
$scope.state.actionInProgress = true;
|
||||||
EndpointService.createRemoteEndpoint(name, type, URL, PublicURL, 1, [], TLS, TLSSkipVerify, TLSSKipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile)
|
EndpointService.createRemoteEndpoint(name, type, URL, PublicURL, 1, [], TLS, TLSSkipVerify, TLSSKipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile)
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
endpoint = data;
|
$state.go('portainer.home');
|
||||||
EndpointProvider.setEndpointID(endpoint.Id);
|
|
||||||
return ExtensionManager.initEndpointExtensions(endpoint.Id);
|
|
||||||
})
|
|
||||||
.then(function success(data) {
|
|
||||||
var extensions = data;
|
|
||||||
return StateManager.updateEndpointState(false, endpoint.Type, extensions);
|
|
||||||
})
|
|
||||||
.then(function success(data) {
|
|
||||||
$state.go('docker.dashboard');
|
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
Notifications.error('Failure', err, 'Unable to connect to the Docker environment');
|
Notifications.error('Failure', err, 'Unable to connect to the Docker environment');
|
||||||
|
|
|
@ -9,6 +9,14 @@
|
||||||
<rd-widget-header icon="fa-cogs" title-text="Application settings"></rd-widget-header>
|
<rd-widget-header icon="fa-cogs" title-text="Application settings"></rd-widget-header>
|
||||||
<rd-widget-body>
|
<rd-widget-body>
|
||||||
<form class="form-horizontal">
|
<form class="form-horizontal">
|
||||||
|
<!-- snapshot-interval -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="snapshot_interval" class="col-sm-2 control-label text-left">Snapshot interval</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input type="text" class="form-control" ng-model="settings.SnapshotInterval" id="snapshot_interval" placeholder="e.g. 15m">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- !snapshot-interval -->
|
||||||
<!-- logo -->
|
<!-- logo -->
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
|
|
|
@ -51,6 +51,7 @@ function ($scope, $state, Notifications, SettingsService, StateManager, DEFAULT_
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
Notifications.success('Settings updated');
|
Notifications.success('Settings updated');
|
||||||
StateManager.updateLogo(settings.LogoURL);
|
StateManager.updateLogo(settings.LogoURL);
|
||||||
|
StateManager.updateSnapshotInterval(settings.SnapshotInterval);
|
||||||
$state.reload();
|
$state.reload();
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
|
|
|
@ -1,35 +1,32 @@
|
||||||
<!-- Sidebar -->
|
<!-- Sidebar -->
|
||||||
<div id="sidebar-wrapper">
|
<div id="sidebar-wrapper">
|
||||||
<div class="sidebar-header">
|
<div class="sidebar-header">
|
||||||
<a ng-click="toggleSidebar()" class="interactive">
|
<a ui-sref="portainer.home">
|
||||||
<img ng-if="logo" ng-src="{{ logo }}" class="img-responsive logo">
|
<img ng-if="logo" ng-src="{{ logo }}" class="img-responsive logo">
|
||||||
<img ng-if="!logo" src="images/logo.png" class="img-responsive logo" alt="Portainer">
|
<img ng-if="!logo" src="images/logo.png" class="img-responsive logo" alt="Portainer">
|
||||||
<span class="menu-icon glyphicon glyphicon-transfer"></span>
|
|
||||||
</a>
|
</a>
|
||||||
|
<a ng-click="toggleSidebar()"><span class="menu-icon glyphicon glyphicon-transfer"></span></a>
|
||||||
</div>
|
</div>
|
||||||
<div class="sidebar-content">
|
<div class="sidebar-content">
|
||||||
<ul class="sidebar">
|
<ul class="sidebar">
|
||||||
<sidebar-endpoint-selector ng-if="endpoints && groups"
|
<li class="sidebar-list">
|
||||||
endpoints="endpoints"
|
<a ui-sref="portainer.home" ui-sref-active="active">Home <span class="menu-icon fa fa-home fa-fw"></span></a>
|
||||||
groups="groups"
|
</li>
|
||||||
select-endpoint="switchEndpoint"
|
<li class="sidebar-title endpoint-name" ng-if="applicationState.endpoint.name">
|
||||||
></sidebar-endpoint-selector>
|
<span class="fa fa-plug space-right"></span>{{ applicationState.endpoint.name }}
|
||||||
<li class="sidebar-title"><span class="endpoint-name">{{ activeEndpoint.Name }}</span></li>
|
</li>
|
||||||
<azure-sidebar-content ng-if="applicationState.endpoint.mode.provider === 'AZURE'">
|
<azure-sidebar-content ng-if="applicationState.endpoint.mode && applicationState.endpoint.mode.provider === 'AZURE'">
|
||||||
</azure-sidebar-content>
|
</azure-sidebar-content>
|
||||||
<docker-sidebar-content ng-if="applicationState.endpoint.mode.provider !== 'AZURE'"
|
<docker-sidebar-content ng-if="applicationState.endpoint.mode && applicationState.endpoint.mode.provider !== 'AZURE'"
|
||||||
endpoint-api-version="applicationState.endpoint.apiVersion"
|
endpoint-api-version="applicationState.endpoint.apiVersion"
|
||||||
swarm-management="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER'"
|
swarm-management="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER'"
|
||||||
standalone-management="applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE' || applicationState.endpoint.mode.provider === 'VMWARE_VIC'"
|
standalone-management="applicationState.endpoint.mode.provider === 'DOCKER_STANDALONE' || applicationState.endpoint.mode.provider === 'VMWARE_VIC'"
|
||||||
admin-access="!applicationState.application.authentication || isAdmin"
|
admin-access="!applicationState.application.authentication || isAdmin"
|
||||||
external-contributions="displayExternalContributors"
|
|
||||||
sidebar-toggled-on="toggle"
|
|
||||||
current-state="$state.current.name"
|
|
||||||
></docker-sidebar-content>
|
></docker-sidebar-content>
|
||||||
<li class="sidebar-title" ng-if="applicationState.endpoint.extensions.length > 0">
|
<li class="sidebar-title" ng-if="applicationState.endpoint.mode && applicationState.endpoint.extensions.length > 0">
|
||||||
<span>Extensions</span>
|
<span>Extensions</span>
|
||||||
</li>
|
</li>
|
||||||
<li class="sidebar-list" ng-if="applicationState.endpoint.extensions.indexOf('storidge') !== -1 && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER'">
|
<li class="sidebar-list" ng-if="applicationState.endpoint.mode && applicationState.endpoint.extensions.indexOf('storidge') !== -1 && applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER'">
|
||||||
<a ui-sref="storidge.cluster" ui-sref-active="active">Storidge <span class="menu-icon fa fa-bolt fa-fw"></span></a>
|
<a ui-sref="storidge.cluster" ui-sref-active="active">Storidge <span class="menu-icon fa fa-bolt fa-fw"></span></a>
|
||||||
<div class="sidebar-sublist" ng-if="toggle && ($state.current.name === 'storidge.cluster' || $state.current.name === 'storidge.profiles' || $state.current.name === 'storidge.monitor' || $state.current.name === 'storidge.profiles.new' || $state.current.name === 'storidge.profiles.profile')">
|
<div class="sidebar-sublist" ng-if="toggle && ($state.current.name === 'storidge.cluster' || $state.current.name === 'storidge.profiles' || $state.current.name === 'storidge.monitor' || $state.current.name === 'storidge.profiles.new' || $state.current.name === 'storidge.profiles.profile')">
|
||||||
<a ui-sref="storidge.monitor" ui-sref-active="active">Monitor</a>
|
<a ui-sref="storidge.monitor" ui-sref-active="active">Monitor</a>
|
||||||
|
@ -39,10 +36,10 @@
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li class="sidebar-title" ng-if="!applicationState.application.authentication || isAdmin || isTeamLeader">
|
<li class="sidebar-title" ng-if="!applicationState.application.authentication || isAdmin || isTeamLeader">
|
||||||
<span>Portainer settings</span>
|
<span>Settings</span>
|
||||||
</li>
|
</li>
|
||||||
<li class="sidebar-list" ng-if="applicationState.application.authentication && (isAdmin || isTeamLeader)">
|
<li class="sidebar-list" ng-if="applicationState.application.authentication && (isAdmin || isTeamLeader)">
|
||||||
<a ui-sref="portainer.users" ui-sref-active="active">User management <span class="menu-icon fa fa-users fa-fw"></span></a>
|
<a ui-sref="portainer.users" ui-sref-active="active">Users <span class="menu-icon fa fa-users fa-fw"></span></a>
|
||||||
<div class="sidebar-sublist" ng-if="toggle && ($state.current.name === 'portainer.users' || $state.current.name === 'portainer.users.user' || $state.current.name === 'portainer.teams' || $state.current.name === 'portainer.teams.team')">
|
<div class="sidebar-sublist" ng-if="toggle && ($state.current.name === 'portainer.users' || $state.current.name === 'portainer.users.user' || $state.current.name === 'portainer.teams' || $state.current.name === 'portainer.teams.team')">
|
||||||
<a ui-sref="portainer.teams" ui-sref-active="active">Teams</a>
|
<a ui-sref="portainer.teams" ui-sref-active="active">Teams</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,64 +1,6 @@
|
||||||
angular.module('portainer.app')
|
angular.module('portainer.app')
|
||||||
.controller('SidebarController', ['$q', '$scope', '$state', 'EndpointService', 'GroupService', 'StateManager', 'EndpointProvider', 'Notifications', 'Authentication', 'UserService', 'ExtensionManager',
|
.controller('SidebarController', ['$q', '$scope', 'StateManager', 'Notifications', 'Authentication', 'UserService',
|
||||||
function ($q, $scope, $state, EndpointService, GroupService, StateManager, EndpointProvider, Notifications, Authentication, UserService, ExtensionManager) {
|
function ($q, $scope, StateManager, Notifications, Authentication, UserService) {
|
||||||
|
|
||||||
$scope.switchEndpoint = function(endpoint) {
|
|
||||||
EndpointProvider.setEndpointID(endpoint.Id);
|
|
||||||
EndpointProvider.setEndpointPublicURL(endpoint.PublicURL);
|
|
||||||
if (endpoint.Type === 3) {
|
|
||||||
switchToAzureEndpoint(endpoint);
|
|
||||||
} else {
|
|
||||||
switchToDockerEndpoint(endpoint);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function switchToAzureEndpoint(endpoint) {
|
|
||||||
StateManager.updateEndpointState(false, endpoint.Type, [])
|
|
||||||
.then(function success() {
|
|
||||||
$scope.currentEndpoint = endpoint;
|
|
||||||
$state.go('azure.dashboard');
|
|
||||||
})
|
|
||||||
.catch(function error(err) {
|
|
||||||
Notifications.error('Failure', err, 'Unable to connect to the Docker endpoint');
|
|
||||||
var currentEndpoint = $scope.currentEndpoint;
|
|
||||||
EndpointProvider.setEndpointID(currentEndpoint.Id);
|
|
||||||
EndpointProvider.setEndpointPublicURL(currentEndpoint.PublicURL);
|
|
||||||
return StateManager.updateEndpointState(true, currentEndpoint.Type, currentEndpoint.Extensions);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function switchToDockerEndpoint(endpoint) {
|
|
||||||
ExtensionManager.initEndpointExtensions(endpoint.Id)
|
|
||||||
.then(function success(data) {
|
|
||||||
var extensions = data;
|
|
||||||
return StateManager.updateEndpointState(true, endpoint.Type, extensions);
|
|
||||||
})
|
|
||||||
.then(function success() {
|
|
||||||
$scope.currentEndpoint = endpoint;
|
|
||||||
$state.go('docker.dashboard');
|
|
||||||
})
|
|
||||||
.catch(function error(err) {
|
|
||||||
Notifications.error('Failure', err, 'Unable to connect to the Docker endpoint');
|
|
||||||
var currentEndpoint = $scope.currentEndpoint;
|
|
||||||
EndpointProvider.setEndpointID(currentEndpoint.Id);
|
|
||||||
EndpointProvider.setEndpointPublicURL(currentEndpoint.PublicURL);
|
|
||||||
return StateManager.updateEndpointState(true, currentEndpoint.Type, currentEndpoint.Extensions);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function setActiveEndpoint(endpoints) {
|
|
||||||
var activeEndpointID = EndpointProvider.endpointID();
|
|
||||||
|
|
||||||
for (var i = 0; i < endpoints.length; i++) {
|
|
||||||
var endpoint = endpoints[i];
|
|
||||||
if (endpoint.Id === activeEndpointID) {
|
|
||||||
$scope.activeEndpoint = endpoint;
|
|
||||||
$scope.currentEndpoint = endpoint;
|
|
||||||
EndpointProvider.setEndpointPublicURL(endpoint.PublicURL);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkPermissions(memberships) {
|
function checkPermissions(memberships) {
|
||||||
var isLeader = false;
|
var isLeader = false;
|
||||||
|
@ -72,32 +14,18 @@ function ($q, $scope, $state, EndpointService, GroupService, StateManager, Endpo
|
||||||
|
|
||||||
function initView() {
|
function initView() {
|
||||||
$scope.uiVersion = StateManager.getState().application.version;
|
$scope.uiVersion = StateManager.getState().application.version;
|
||||||
$scope.displayExternalContributors = StateManager.getState().application.displayExternalContributors;
|
|
||||||
$scope.logo = StateManager.getState().application.logo;
|
$scope.logo = StateManager.getState().application.logo;
|
||||||
|
|
||||||
$q.all({
|
var userDetails = Authentication.getUserDetails();
|
||||||
endpoints: EndpointService.endpoints(),
|
var isAdmin = userDetails.role === 1;
|
||||||
groups: GroupService.groups()
|
$scope.isAdmin = isAdmin;
|
||||||
})
|
|
||||||
.then(function success(data) {
|
|
||||||
var endpoints = data.endpoints;
|
|
||||||
$scope.groups = _.sortBy(data.groups, ['Name']);
|
|
||||||
$scope.endpoints = _.sortBy(endpoints, ['Name']);
|
|
||||||
|
|
||||||
setActiveEndpoint(endpoints);
|
$q.when(!isAdmin ? UserService.userMemberships(userDetails.ID) : [])
|
||||||
|
|
||||||
if (StateManager.getState().application.authentication) {
|
|
||||||
var userDetails = Authentication.getUserDetails();
|
|
||||||
var isAdmin = userDetails.role === 1 ? true: false;
|
|
||||||
$scope.isAdmin = isAdmin;
|
|
||||||
return $q.when(!isAdmin ? UserService.userMemberships(userDetails.ID) : []);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
checkPermissions(data);
|
checkPermissions(data);
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
Notifications.error('Failure', err, 'Unable to retrieve endpoints');
|
Notifications.error('Failure', err, 'Unable to retrieve user memberships');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -311,8 +311,10 @@ ul.sidebar .sidebar-title {
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul.sidebar .sidebar-title .endpoint-name {
|
ul.sidebar .sidebar-title.endpoint-name {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
text-align: center;
|
||||||
|
text-indent: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul.sidebar .sidebar-list a {
|
ul.sidebar .sidebar-list a {
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
"node": ">= 0.8.4"
|
"node": ">= 0.8.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free-webfonts": "^1.0.4",
|
"@fortawesome/fontawesome-free-webfonts": "^1.0.9",
|
||||||
"@uirouter/angularjs": "~1.0.6",
|
"@uirouter/angularjs": "~1.0.6",
|
||||||
"angular": "~1.5.0",
|
"angular": "~1.5.0",
|
||||||
"angular-clipboard": "^1.6.2",
|
"angular-clipboard": "^1.6.2",
|
||||||
|
|
|
@ -2,9 +2,9 @@
|
||||||
# yarn lockfile v1
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
"@fortawesome/fontawesome-free-webfonts@^1.0.4":
|
"@fortawesome/fontawesome-free-webfonts@^1.0.9":
|
||||||
version "1.0.4"
|
version "1.0.9"
|
||||||
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free-webfonts/-/fontawesome-free-webfonts-1.0.4.tgz#bac5d89755bf3bc2d2b4deee47d92febf641bb1f"
|
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free-webfonts/-/fontawesome-free-webfonts-1.0.9.tgz#72f2c10453422aba0d338fa6a9cb761b50ba24d5"
|
||||||
|
|
||||||
"@uirouter/angularjs@~1.0.6":
|
"@uirouter/angularjs@~1.0.6":
|
||||||
version "1.0.11"
|
version "1.0.11"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue